--- name: "pipeline-generate" description: "Generate MaaFramework Pipeline nodes and recognition snippets from screenshots or observed UI state. Use for OCR node generation, ROI sweep, choosing TemplateMatch/OCR/ColorMatch/CustomRecognition, preserving target-file schema style, and designing `next`/`[JumpBack]` links before merging generated nodes into pipeline JSON." --- # pipeline-generate ## 概念 Pipeline 由 Node 组成。本 skill 针对**OCR 文本识别节点**,按 Pipeline 协议生成节点 JSON 并合并到目标 pipeline 文件。 **核心流程**:连接设备 → `ocr()` 拿 box → 扩大 ROI → 合并节点 **自带脚本**(与本 SKILL.md 同目录): | 脚本 | 用途 | |------|------| | `generate_node.py` | 单节点生成(默认 `expand=20`) | | `generate_sweep.py` | 多 expand 变体扫描,找最佳 ROI | ## MCP 工具绑定 依赖 `maa-mcp` MCP 服务。 | 工具 | 说明 | |------|------| | `find_adb_device_list` / `connect_adb_device` | 连接设备 | | `ocr` | **截图 + OCR 一步完成**(内部已调 screencap,外部不要再调) | | `load_pipeline` / `save_pipeline` | 读/写 pipeline JSON | | `check_and_download_ocr` | 首次需下载 OCR 模型 | | `run_pipeline` | 测试 pipeline 节点 | ## 输入参数 | 参数 | 必填 | 默认 | 说明 | |------|------|------|------| | `target_text` | ✅ | — | 要识别的目标中文文字 | | `node_name` | ✅ | — | 节点名(PascalCase) | | `pipeline_file` | ✅ | — | 目标 pipeline 路径(相对 `assets/resource/base/pipeline/xxx.json` 或绝对路径) | | `action_type` | ❌ | `Click` | Click / DoNothing / LongPress / Swipe / ClickKey / InputText | | `expand_offset` | ❌ | `20` | ROI 扩边像素(**推荐先用 sweep 找最佳**) | | `post_delay` | ❌ | `500` | | | `timeout` | ❌ | `2000` | | | `overwrite` | ❌ | `False` | 节点名冲突时是否覆盖 | ## 3 步工作流(伪代码) ```python # === Step 1: 连接设备 === from maa_mcp.adb import find_adb_device_list, connect_adb_device controller_id = connect_adb_device(find_adb_device_list()[0]) # === Step 2: OCR 拿 box + 算 ROI === from maa_mcp.vision import ocr from maa_mcp.download import check_and_download_ocr ocr_results = ocr(controller_id) if isinstance(ocr_results, str) and "OCR 模型文件不存在" in ocr_results: check_and_download_ocr() ocr_results = ocr(controller_id) matched = [r for r in ocr_results if target_text in (r.text if hasattr(r, "text") else r["text"])] best = max(matched, key=lambda r: r.score if hasattr(r, "score") else r["score"]) box = best.box if hasattr(best, "box") else best["box"] # 扩大 ROI(720p 硬编码 + 4 边裁剪) SCREEN_W, SCREEN_H = 720, 1280 x, y, w, h = box E = expand_offset roi = [ max(0, x - E), max(0, y - E), min(SCREEN_W - max(0, x - E), w + 2 * E), min(SCREEN_H - max(0, y - E), h + 2 * E), ] # === Step 3: 合并到目标 pipeline === from maa_mcp.pipeline_tools import load_pipeline, save_pipeline from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[3] pipeline_path = Path(pipeline_file) if not pipeline_path.is_absolute(): pipeline_path = PROJECT_ROOT / "assets" / "resource" / "base" / "pipeline" / pipeline_file existing = load_pipeline(str(pipeline_path)) or {} if node_name in existing and not overwrite: raise RuntimeError(f"节点 '{node_name}' 已存在") existing[node_name] = { "recognition": "OCR", "expected": [target_text], "roi": roi, "action": action_type, "post_delay": post_delay, "timeout": timeout, } save_pipeline( pipeline_json=json.dumps(existing, ensure_ascii=False, indent=4), output_path=str(pipeline_path), overwrite=True, ) ``` ## ROI 扩大示意 ``` 原始 box: ┌────┐ │ 文字│ └────┘ 扩大后 roi: ┌──────────┐ │ ┌────┐ │ │ │文字│ │ │ └────┘ │ └──────────┘ ``` ## 使用流程 > 脚本位于 `.claude/skills/pipeline-generate/`,所有命令从**项目根目录** `f:\workspace\MAAGC` 运行。 ### 步骤 1: Sweep 找最佳 expand ```bash # 生成多个 expand 变体的测试 pipeline python .claude/skills/pipeline-generate/generate_sweep.py "角色" "46,1248,50,30" 0,5,10,15,20,25,30 ``` 然后用 `run_pipeline` 逐个测试每个 `Sweep__eN` 节点,**用 `BackButton_500ms` 返回大地图**(详见 [pipeline-testing](../pipeline-testing/SKILL.md))。记录成功的 expand 值(score ≥ 0.99 为佳)。 ### 步骤 2: 正式生成节点 ```bash python .claude/skills/pipeline-generate/generate_node.py "角色" UI_RoleListPage main_ui.json --expand 20 --overwrite ``` ## 关键经验 ### 历史审查后的生成策略 - 先判断节点类型,不要默认所有问题都是 OCR:稳定图标/按钮优先 TemplateMatch,颜色状态可用 ColorMatch,动态文本用 OCR,列表/复杂图像后处理用 CustomRecognition。 - MaaGumballs 的历史文件多为平铺字段风格;M9A HEAD 多为 v5 object-form:`action: { type, param }`、`recognition: { type, param }`。生成时沿用目标文件的既有风格,不要在同一局部混用两套格式。 - 生成链路时先画父级 `next` 状态机:稳定页面、成功态、弹窗 `[JumpBack]`、加载 `[JumpBack]`、危险确认分支分开建节点。 - 对会消耗资源或改变账号状态的节点,默认生成 `DoNothing` 或单独验证节点;只有用户明确要执行时才生成直接点击确认。 - 如果需要 Python,先决定是 CustomAction 还是 CustomRecognition:动作/控制流用 CustomAction;识别后处理和动态 box 返回用 CustomRecognition。 1. **`ocr()` 自动截图**:MaaMCP 的 `ocr()` 工具会自行获取当前画面,调用前不要重复 `screencap()`;如果换了 MCP provider,先读该工具的参数说明确认截图语义。 2. **ROI 不是越大越好**:默认 `expand=75` 会失败(OCR 把"角色"拆成"电"+"色")。多数节点 sweet spot 是 `expand=20-30`。 3. **特殊节点需要小 ROI**:"城堡" expand≥20 全失败,**只接受 0-15**(上方有图标 M/3.9m/1077/👍 干扰)。 4. **`expected` 必须匹配当前资源实际显示文本**:在 MaaGumballs 中文资源里 `["角色"]` 正确、`["Role"]` 找不到;跨语言项目要按目标资源/locale 写实际 OCR 文本或项目约定的 i18n 形式。 5. **OCR 非确定性**:同一 ROI 不同次结果可能不同,`timeout: 2000` 期间会重试。 6. **OCR 失败不要立刻换 TemplateMatch**:先看截图、扫 ROI、检查 `expected` 与颜色干扰;如果目标本质是稳定图标/按钮,TemplateMatch 本来就是正确选择,不必死守 OCR。 7. **可滚动 UI 用大 ROI + 父级 orchestrator**(**重要**): - **不要**在 Click 节点的 `next` 里放 `[JumpBack]CastleSwipeDown/Up` —— 找不到文字时会**死循环滑动**! - 正确模式参考 marry.json 里的 `CastleHall` 节点:父级 orchestrator 节点的 `next` 列表里放 `[JumpBack]XXXEntry` + `[JumpBack]XXXSwipeDown` + `[JumpBack]XXXSwipeUp` 等 - 滚动容错 ROI 范围参考 `CastleHallEntry`: `[60, 391, 609, 795]` 8. **`run_pipeline` 必须有手动超时意识**:超过 ~10 秒不返回要主动停止,可能 ROI/expected 配错或 OCR 引擎卡住。 9. **改完 pipeline 文件后调 `load_pipeline(path)` 即可**:**不需要重启 server**。`run_pipeline` 每次都按 `pipeline_path` 从磁盘读最新内容,reload 后立即生效。 10. **可滚动 UI 用统一大 ROI**:当多个目标在同一个可滚动列表(如城堡建筑列表)时,**所有节点共用同一 ROI** `[x, top_y, w, full_h]`,覆盖整个滚动区域。避免每个节点各自 ROI 滚动后失效。前提:每个节点的 `expected` 文字是唯一的(OCR 按 expected 匹配不会冲突)。 11. **ROI 上边界 ≤ 元素最小 y**:目标元素在 y=424 时,ROI y 起点必须 ≤ 424,否则切掉顶部导致 OCR 失败。例:原 ROI `[100, 450, ...]` 把"城堡管理"切掉 26px → 改为 `[100, 400, ...]` 通过。 12. **卡住时截图查看**:节点超时、OCR 找不到、行为异常时,调 `screencap` 看当前屏幕实际状态。可能界面已不在预期页、可能位置已被遮挡。 13. **跨页面流程用 `next` 状态机而非 Python orchestration**:当一个流程涉及多个页面跳转(如:大地图 → 活动入口 → 难度选择 → 队伍 → 战斗),用 MaaFramework 的 `next` + `[JumpBack]` 串节点。**不要**写 Python `for/while` 调 `context.run_task()` 模拟状态机。详见 [.claude/skills/pipeline-option/SKILL.md](../pipeline-option/SKILL.md) 的「不要做 #10」和 [.claude/skills/pipeline-guide/SKILL.md](../pipeline-guide/SKILL.md) 的「跨页面状态机」。 14. **跨文件节点引用在 `run_pipeline` 测试中会失败**:MaaFramework 全局加载时所有 `assets/resource/base/pipeline/*.json` 合并到同一命名空间,`[JumpBack]OtherFileNode` 能解析。但 `run_pipeline` **只加载单文件**,跨文件引用会报"加载 Pipeline 失败"。**应对**: - 单元测试每个节点用 `run_pipeline`(无跨文件依赖的子流程)是 OK 的 - 含跨文件引用的状态机流程,集成测试必须用 MaaFramework GUI/CLI 触发 - 调试时可考虑 `MaaCli` 命令行运行全 bundle ### 已验证最优 expand(5 节点实测) | 节点 | expand | score | 备注 | |------|--------|-------|------| | `UI_RoleListPage` | **20** | 0.9997 | 中部偏左 | | `UI_RoleFormationPage` | **20** | 0.998 | 角色右边 | | `UI_CastlePage` | **3** | 0.997 | ⚠️ 仅 0-15 | | `UI_TeamPage` | **20** | 0.997 | 城堡右边 | | `ClickGoToArchipelago` | **20** | 0.991 | 中间大地图按钮 | ### 用 `color_filter` 减少 OCR 干扰(实战技巧) **场景**:ROI 里同时有**目标文字 + 周边装饰**(如"0/31"绿色能量条 vs "0/23"绿色节点数),OCR 可能误识别装饰色块。 **方案**:先建一个 `ColorMatch` 节点(限定像素颜色范围),然后在其他 OCR 节点上加 `color_filter` 字段引用它。 ```jsonc "AutoSky_GreenCheck": { "recognition": "ColorMatch", "roi": [558, 802, 157, 45], "method": 4, "lower": [22, 123, 57], // RGB 下界(暗绿) "upper": [55, 215, 102], // RGB 上界(亮绿) "action": "DoNothing", "post_delay": 200, "timeout": 2000 }, "AutoSky_CheckEnergyZero": { "recognition": "OCR", "expected": ["0/\\d+"], "roi": [850, 1280, 220, 50], "color_filter": "AutoSky_GreenCheck", // ← 只在绿色区域 OCR "action": "DoNothing" } ``` **取色技巧**(用截图工具): - 目标区域:取目标**装饰/边框**色(非文字色,文字一般会变色) - RGB 范围要**宽松**一些(±20),覆盖光照变化 - method=4 是 RGB(0=HSV) **实战案例**:本项目(MaaGumballs)用这个方法区分"能量条 0/31"vs"节点数 0/23",两者都是绿色 OCR 文本,周围装饰色也不同。 ### 已验证:可滚动 UI 统一 ROI(10 城堡建筑) | 节点 | 统一 ROI | score | 备注 | |------|----------|-------|------| | `CastleManage` | `[100, 400, 520, 880]` | 0.999 | 顶部 | | `Market` | 同上 | 0.999 | 顶部 | | `Blacksmith` | 同上 | 0.998 | 顶部 | | `AlchemyWorkshop` | 同上 | 0.999 | 顶部 | | `TrainingCenter` | 同上 | 1.000 | 顶部 | | `CastleMainHall` | 同上 | 0.876 | 顶部只露 25px | | `Shrine` | 同上 | 0.999 | 中段 | | `Family` | 同上 | 0.999 | 中段 | | `Museum` | 同上 | 0.999 | 底部 | | `Manor` | 同上 | 0.999 | 底部 | **关键设计**: - 所有节点 ROI 完全相同(`[100, 400, 520, 880]`,覆盖 y=400-1280) - 不靠 expand 微调,靠 `expected` 文字差异让 OCR 区分 - 不放 `next` 链(避免死循环) --- ## 跨页面状态机流程(用 `next` + `[JumpBack]`) 当生成的活动流程需要**跨多个页面跳转**(如:大地图 → 活动入口 → 难度选择 → 队伍 → 战斗),用 MaaFramework 的 `next` + `[JumpBack]` 机制串接各页面节点,**不要写 Python orchestration**。 ### 模式:状态机入口节点 ```jsonc { "MyActivity_Start": { "next": [ "MyActivity_TeamReady", // 已在队伍配置页 → 点击"进入战斗" "[JumpBack]MyActivity_Difficulty_Select", // 在难度选择页 → 选难度 "[JumpBack]MyActivity_Enter" // 在大地图 → 找入口 ], "timeout": 10000 }, "MyActivity_Enter": { "next": [ "MyActivity_Enter_Click", // 找到图标 → 点击 "[JumpBack]BigMap_Activity_Resident", // 切"常驻"tab "[JumpBack]BigMap_Activity" // 打开活动页 ], "timeout": 10000 }, "MyActivity_EnterBattle": { "recognition": "OCR", "expected": ["进入战斗"], "action": "Click", "next": [ "MyActivity_FightStart", // 战斗开始 "[JumpBack]MyActivity_TravelSelect_Boat", // 乘船 "[JumpBack]MyActivity_TravelSelect_Walk" // 步行 fallback ] }, "MyActivity_TravelSelect_Boat": { "recognition": "OCR", "expected": ["确定"], "roi": [490, 740, 100, 80], // 窄 ROI 限定乘船行 "action": "Click" }, "MyActivity_TravelSelect_Walk": { "recognition": "OCR", "expected": ["确定"], "roi": [490, 590, 100, 80], // 窄 ROI 限定步行行 "action": "Click" } } ``` ### 关键设计要点 1. **`[JumpBack]` 是状态回退的关键**:命中后执行完节点链,自动返回父节点的 `next` 继续。 2. **窄 ROI 区分同名字段**:用 y 范围 [490, 740, 100, 80] vs [490, 590, 100, 80] 区分两个"确定"按钮行(y 范围不重叠)。 3. **`target_offset` 偏移点击**:识别难度文字后用 `target_offset: [270, 0, 0, 0]` 把点击位置右移到"确定"按钮上。 4. **跨文件节点引用**:MaaFramework 全局加载会合并所有 `pipeline/*.json`,所以 `[JumpBack]BigMap_Activity`(在 main_ui.json)能从 growth_trial.json 引用。但 `run_pipeline` 测试只加载单文件,集成测试需用 GUI/CLI。 ### 与 Python orchestration 的本质区别 | 状态机(推荐) | Python orchestration(次选) | |--------------|--------------------------| | 流程推进由 MaaFramework 调度 | 自己写 `for/if` 调度 | | 每个节点 `next` 显式声明后继 | Python 函数串行 `run_task` | | `[JumpBack]` 自动状态回退 | 手动实现回退逻辑 | | 跨页面异常有自然路径 | 需手动 try/except | 详见 [.claude/skills/pipeline-option/SKILL.md](../pipeline-option/SKILL.md) 的「不要做 #10」和 [.claude/skills/pipeline-guide/SKILL.md](../pipeline-guide/SKILL.md) 的「跨页面状态机」典型模式。