diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b1ea21b..6a51c4d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,10 @@ "mcp__maa-mcp__connect_adb_device", "mcp__maa-mcp__swipe", "mcp__maa-mcp__find_adb_device_list", - "mcp__maa-mcp__test_sweep" + "mcp__maa-mcp__test_sweep", + "mcp__maa-mcp__find_window_list", + "mcp__maa-mcp__get_current_datetime", + "mcp__maa-mcp__run_pipeline" ], "defaultMode": "bypassPermissions" }, @@ -28,4 +31,4 @@ "cwd": "${workspaceFolder}" } } -} \ No newline at end of file +} diff --git a/.mcp.json b/.mcp.json index 9f1ffae..2f2be01 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,7 +4,7 @@ "command": "python", "args": [ "-m", - "maa_mcp.pipeline_server" + "maa_mcp" ] } } diff --git a/maa_mcp/pipeline_server.py b/maa_mcp/pipeline_server.py index 6f9629c..5b01e71 100644 --- a/maa_mcp/pipeline_server.py +++ b/maa_mcp/pipeline_server.py @@ -24,6 +24,7 @@ import maa_mcp.control import maa_mcp.utils import maa_mcp.resource +import maa_mcp.pipeline_tools # 导入 Pipeline 子模块 from dataclasses import dataclass diff --git a/maa_mcp/pipeline_tools.py b/maa_mcp/pipeline_tools.py index 55f6b02..989a83b 100644 --- a/maa_mcp/pipeline_tools.py +++ b/maa_mcp/pipeline_tools.py @@ -14,11 +14,10 @@ from typing import Optional from lzstring import LZString -from maa.tasker import TaskDetail from maa_mcp.core import mcp from maa_mcp.paths import get_data_dir -from maa_mcp.resource import get_or_create_resource, get_or_create_tasker +from maa_mcp.resource import get_or_create_resource, get_or_create_tasker, add_resource_path # Pipeline 协议文档(精简版,包含 AI 生成 Pipeline 所需的关键信息) @@ -391,11 +390,52 @@ def save_pipeline( - controller_id: 控制器 ID,由 connect_adb_device() 或 connect_window() 返回 - pipeline_path: Pipeline JSON 文件路径 - entry: 入口节点名称(可选),不指定则使用 Pipeline 中的第一个节点 + - resource_path: 资源目录路径(可选),用于指定 Pipeline 所需的资源文件路径。 + 如果不指定,则使用 MaaMCP 默认的资源目录。 + 当 Pipeline 需要使用外部资源(如 MaaGC 的资源)时,需要指定此参数。 返回值: - - 成功:返回 TaskDetail 对象,包含 task_id、entry、status、nodes 等执行信息 + - 成功:返回 dict 对象,包含以下字段: + - task_id: 任务 ID + - entry: 入口节点名称 + - status: 执行状态字符串("succeeded" | "failed" | "running" | "pending" | "done") + - node_count: 执行的节点数量 + - nodes: 节点详情列表,每个节点包含: + - name: 节点名称 + - recognition: 识别结果(如果有),包含 all_results 列表 + - all_results: 识别到的目标列表,每项包含 box(坐标)和 score(置信度) - 失败:返回错误信息字符串 + 判断识别结果: + - 检查 nodes 是否包含 recognition.all_results: + - 有内容 = 识别成功,找到了目标 + - 无内容或 nodes 为空 = 识别失败,未找到目标 + - box 格式:[x, y, width, height] + - score 范围:0-1,越高越准确 + + 示例返回值(识别成功): + { + "task_id": 200000001, + "entry": "BackButton_500ms", + "status": "succeeded", + "node_count": 1, + "nodes": [{ + "name": "BackButton_500ms", + "recognition": { + "all_results": [{"box": [653, 7, 46, 40], "score": 0.999726}] + } + }] + } + + 示例返回值(识别失败): + { + "task_id": 200000002, + "entry": "BackButton_500ms", + "status": "failed", + "node_count": 1, + "nodes": [{"name": ""}] + } + 说明: 此函数会先加载 Pipeline 文件到 Resource,然后通过 Tasker 执行任务。 ⚠️ 重要:run_pipeline 不会自动把界面恢复到入口节点所假设的起始状态。 @@ -406,7 +446,12 @@ def run_pipeline( controller_id: str, pipeline_path: str, entry: Optional[str] = None, -) -> TaskDetail | str: + resource_path: Optional[str] = None, +) -> dict | str: + # 如果传入了 resource_path,添加它以便 get_or_create_resource 加载该路径 + if resource_path: + add_resource_path(resource_path) + # 检查文件是否存在 path = Path(pipeline_path) if not path.exists(): @@ -450,7 +495,44 @@ def run_pipeline( if not task_detail: return "任务执行失败,无法获取执行详情" - return task_detail + # 解析状态为易读文字 + status = task_detail.status + if status.succeeded: + status_text = "succeeded" + elif status.failed: + status_text = "failed" + elif status.running: + status_text = "running" + elif status.pending: + status_text = "pending" + elif status.done: + status_text = "done" + else: + status_text = str(status) + + # 获取节点详情 + nodes_info = [] + if hasattr(task_detail, 'nodes') and task_detail.nodes: + for node in task_detail.nodes: + node_info = {} + if hasattr(node, 'recognition') and node.recognition: + node_info["recognition"] = { + "all_results": getattr(node.recognition, 'all_results', None) + } + if hasattr(node, 'name'): + node_info["name"] = node.name + nodes_info.append(node_info) + + result = { + "task_id": task_detail.task_id, + "entry": task_detail.entry, + "status": status_text, + "node_count": len(task_detail.node_id_list) if hasattr(task_detail, 'node_id_list') else 0, + } + if nodes_info: + result["nodes"] = nodes_info + + return result """ diff --git a/maa_mcp/resource.py b/maa_mcp/resource.py index 0b5e5f7..7d8e97f 100644 --- a/maa_mcp/resource.py +++ b/maa_mcp/resource.py @@ -1,5 +1,6 @@ from typing import Optional +from loguru import logger from maa.controller import Controller from maa.resource import Resource from maa.tasker import Tasker @@ -11,23 +12,77 @@ # 全局资源 ID 的固定键名 _GLOBAL_RESOURCE_KEY = "_global_resource" +# 资源路径列表(按加载顺序,后加载的会覆盖先加载的同名资源) +_resource_paths: list[str] = [] +# 记录默认路径是否已加载 +_default_loaded: bool = False +# 记录已加载的自定义路径(用于去重) +_loaded_paths: list[str] = [] + + +def add_resource_path(path: str): + """ + 添加资源路径,后加载的会覆盖先加载的同名资源。 + 添加后会立即加载该路径到已创建的 Resource。 + """ + global _resource_paths, _default_loaded, _loaded_paths + + if path not in _resource_paths: + _resource_paths.append(path) + + resource: Resource | None = object_registry.get(_GLOBAL_RESOURCE_KEY) + if not resource: + # Resource 还不存在,等 get_or_create_resource 时一起加载 + return + + # Resource 已存在,立即加载新路径 + # 先确保默认路径已加载 + if not _default_loaded: + default_path = str(get_resource_dir()) + if not resource.post_bundle(default_path).wait().succeeded: + logger.warning(f"加载默认资源包失败: {default_path}") + _default_loaded = True + + # 只加载新增的自定义路径(去重) + if path not in _loaded_paths: + if not resource.post_bundle(str(path)).wait().succeeded: + logger.warning(f"加载自定义资源包失败: {path}") + _loaded_paths.append(path) + + +def clear_resource(): + """清除全局 Resource 缓存,强制重新创建(保留已配置的资源路径)。""" + global _default_loaded, _loaded_paths + _default_loaded = False + _loaded_paths.clear() + object_registry.unregister(_GLOBAL_RESOURCE_KEY) + def get_or_create_resource() -> Optional[Resource]: """ 获取或创建全局唯一的 Resource 实例。 注意:调用此函数前应确保 OCR 资源已存在,否则可能加载失败。 + + Resource 会按顺序加载多个资源路径,后加载的会覆盖先加载的同名资源。 """ + global _resource_paths, _default_loaded, _loaded_paths + resource: Resource | None = object_registry.get(_GLOBAL_RESOURCE_KEY) - if resource: - return resource + if not resource: + resource = Resource() + object_registry.register_by_name(_GLOBAL_RESOURCE_KEY, resource) - resource_path = get_resource_dir() + # 首次创建时加载所有路径 + default_path = str(get_resource_dir()) + if not resource.post_bundle(default_path).wait().succeeded: + logger.warning(f"加载默认资源包失败: {default_path}") + _default_loaded = True - resource = Resource() - if not resource.post_bundle(str(resource_path)).wait().succeeded: - return None + for path in _resource_paths: + if not resource.post_bundle(str(path)).wait().succeeded: + logger.warning(f"加载自定义资源包失败: {path}") + _loaded_paths.append(path) - object_registry.register_by_name(_GLOBAL_RESOURCE_KEY, resource) return resource