Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -28,4 +31,4 @@
"cwd": "${workspaceFolder}"
}
}
}
}
2 changes: 1 addition & 1 deletion .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"command": "python",
"args": [
"-m",
"maa_mcp.pipeline_server"
"maa_mcp"
]
}
}
Expand Down
1 change: 1 addition & 0 deletions maa_mcp/pipeline_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 87 additions & 5 deletions maa_mcp/pipeline_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 所需的关键信息)
Expand Down Expand Up @@ -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 不会自动把界面恢复到入口节点所假设的起始状态。
Expand All @@ -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():
Expand Down Expand Up @@ -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


"""
Expand Down
69 changes: 62 additions & 7 deletions maa_mcp/resource.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.


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


Expand Down
Loading