Skip to content

Commit 651c300

Browse files
authored
Merge pull request #6 from KhazixW2/main
feat: 新增4个skills, pipeline-guide, pipeline-generate, pipeline-testing, pipeline-option
2 parents 230f7ac + 64e4ab8 commit 651c300

16 files changed

Lines changed: 2514 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# pipeline-generate
2+
3+
本目录包含 MaaFramework pipeline OCR 节点生成工具。
4+
5+
## 包含内容
6+
7+
- `generate_node.py`: 生成单个 OCR 节点并合并到目标 pipeline 文件
8+
- `generate_sweep.py`: 批量生成不同 expand 值的 sweep pipeline,辅助选择最佳 ROI
9+
- `SKILL.md`: skill 说明文档
10+
- `maahub_meta.json`: 网站元信息
11+
12+
## 使用方法
13+
14+
### 生成单个节点
15+
16+
```bash
17+
python .claude/skills/pipeline-generate/generate_node.py "目标文字" NodeName path/to/pipeline.json --expand 20 --overwrite
18+
```
19+
20+
### 扫描 expand 值
21+
22+
```bash
23+
python .claude/skills/pipeline-generate/generate_sweep.py "目标文字" "x,y,w,h" 0,5,10,15,20,25,30
24+
```
25+
26+
## 说明
27+
28+
- `generate_node.py` 将根据 OCR 识别结果生成节点并将其写入 pipeline。
29+
- `generate_sweep.py` 生成一个 sweep pipeline,用于通过 run_pipeline 验证不同 expand 值的效果。
30+
- 目标 pipeline 支持相对路径和绝对路径。
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
name: "pipeline-generate"
3+
description: "自动生成 Pipeline OCR 文本节点。直接调 ocr() 拿目标文字 box,扩大 ROI 后合并到目标 pipeline 文件。提供单节点生成 (generate_node.py) + ROI 扫描找最佳 expand (generate_sweep.py) 两个脚本。"
4+
---
5+
6+
# pipeline-generate
7+
8+
## 概念
9+
10+
Pipeline 由 Node 组成。本 skill 针对**OCR 文本识别节点**,按 Pipeline 协议生成节点 JSON 并合并到目标 pipeline 文件。
11+
12+
**核心流程**:连接设备 → `ocr()` 拿 box → 扩大 ROI → 合并节点
13+
14+
**自带脚本**(与本 SKILL.md 同目录):
15+
16+
| 脚本 | 用途 |
17+
|------|------|
18+
| `generate_node.py` | 单节点生成(默认 `expand=20`|
19+
| `generate_sweep.py` | 多 expand 变体扫描,找最佳 ROI |
20+
21+
## MCP 工具绑定
22+
23+
依赖 `maa-mcp` MCP 服务。
24+
25+
| 工具 | 说明 |
26+
|------|------|
27+
| `find_adb_device_list` / `connect_adb_device` | 连接设备 |
28+
| `ocr` | **截图 + OCR 一步完成**(内部已调 screencap,外部不要再调) |
29+
| `load_pipeline` / `save_pipeline` | 读/写 pipeline JSON |
30+
| `check_and_download_ocr` | 首次需下载 OCR 模型 |
31+
| `run_pipeline` | 测试 pipeline 节点 |
32+
33+
## 输入参数
34+
35+
| 参数 | 必填 | 默认 | 说明 |
36+
|------|------|------|------|
37+
| `target_text` ||| 要识别的目标中文文字 |
38+
| `node_name` ||| 节点名(PascalCase) |
39+
| `pipeline_file` ||| 目标 pipeline 路径(相对 `assets/resource/base/pipeline/xxx.json` 或绝对路径) |
40+
| `action_type` || `Click` | Click / DoNothing / LongPress / Swipe / ClickKey / InputText |
41+
| `expand_offset` || `20` | ROI 扩边像素(**推荐先用 sweep 找最佳**|
42+
| `post_delay` || `500` | |
43+
| `timeout` || `2000` | |
44+
| `overwrite` || `False` | 节点名冲突时是否覆盖 |
45+
46+
## 3 步工作流(伪代码)
47+
48+
```python
49+
# === Step 1: 连接设备 ===
50+
from maa_mcp.adb import find_adb_device_list, connect_adb_device
51+
controller_id = connect_adb_device(find_adb_device_list()[0])
52+
53+
# === Step 2: OCR 拿 box + 算 ROI ===
54+
from maa_mcp.vision import ocr
55+
from maa_mcp.download import check_and_download_ocr
56+
57+
ocr_results = ocr(controller_id)
58+
if isinstance(ocr_results, str) and "OCR 模型文件不存在" in ocr_results:
59+
check_and_download_ocr()
60+
ocr_results = ocr(controller_id)
61+
62+
matched = [r for r in ocr_results if target_text in (r.text if hasattr(r, "text") else r["text"])]
63+
best = max(matched, key=lambda r: r.score if hasattr(r, "score") else r["score"])
64+
box = best.box if hasattr(best, "box") else best["box"]
65+
66+
# 扩大 ROI(720p 硬编码 + 4 边裁剪)
67+
SCREEN_W, SCREEN_H = 720, 1280
68+
x, y, w, h = box
69+
E = expand_offset
70+
roi = [
71+
max(0, x - E),
72+
max(0, y - E),
73+
min(SCREEN_W - max(0, x - E), w + 2 * E),
74+
min(SCREEN_H - max(0, y - E), h + 2 * E),
75+
]
76+
77+
# === Step 3: 合并到目标 pipeline ===
78+
from maa_mcp.pipeline_tools import load_pipeline, save_pipeline
79+
from pathlib import Path
80+
81+
PROJECT_ROOT = Path(__file__).resolve().parents[3]
82+
pipeline_path = Path(pipeline_file)
83+
if not pipeline_path.is_absolute():
84+
pipeline_path = PROJECT_ROOT / "assets" / "resource" / "base" / "pipeline" / pipeline_file
85+
86+
existing = load_pipeline(str(pipeline_path)) or {}
87+
if node_name in existing and not overwrite:
88+
raise RuntimeError(f"节点 '{node_name}' 已存在")
89+
existing[node_name] = {
90+
"recognition": "OCR",
91+
"expected": [target_text],
92+
"roi": roi,
93+
"action": action_type,
94+
"post_delay": post_delay,
95+
"timeout": timeout,
96+
}
97+
save_pipeline(
98+
pipeline_json=json.dumps(existing, ensure_ascii=False, indent=4),
99+
output_path=str(pipeline_path),
100+
overwrite=True,
101+
)
102+
```
103+
104+
## ROI 扩大示意
105+
106+
```
107+
原始 box: ┌────┐
108+
│ 文字│
109+
└────┘
110+
扩大后 roi: ┌──────────┐
111+
│ ┌────┐ │
112+
│ │文字│ │
113+
│ └────┘ │
114+
└──────────┘
115+
```
116+
117+
## 使用流程
118+
119+
> 脚本位于 `.claude/skills/pipeline-generate/`,所有命令从**项目根目录** `f:\workspace\MAAGC` 运行。
120+
121+
### 步骤 1: Sweep 找最佳 expand
122+
123+
```bash
124+
# 生成多个 expand 变体的测试 pipeline
125+
python .claude/skills/pipeline-generate/generate_sweep.py "角色" "46,1248,50,30" 0,5,10,15,20,25,30
126+
```
127+
128+
然后用 `run_pipeline` 逐个测试每个 `Sweep_<text>_eN` 节点,**`BackButton_500ms` 返回大地图**(详见 [pipeline-testing](../pipeline-testing/SKILL.md))。记录成功的 expand 值(score ≥ 0.99 为佳)。
129+
130+
### 步骤 2: 正式生成节点
131+
132+
```bash
133+
python .claude/skills/pipeline-generate/generate_node.py "角色" UI_RoleListPage main_ui.json --expand 20 --overwrite
134+
```
135+
136+
## 关键经验
137+
138+
1. **`ocr()` 自动截图**:内部已调 `controller.post_screencap()`**不要**再手动 screencap。证据:[vision.py:58](../MaaMCP/maa_mcp/vision.py#L58)
139+
2. **ROI 不是越大越好**:默认 `expand=75` 会失败(OCR 把"角色"拆成"电"+"色")。多数节点 sweet spot 是 `expand=20-30`
140+
3. **特殊节点需要小 ROI**:"城堡" expand≥20 全失败,**只接受 0-15**(上方有图标 M/3.9m/1077/👍 干扰)。
141+
4. **`expected` 必须是中文**`["角色"]` 正确,`["Role"]` 永远找不到。
142+
5. **OCR 非确定性**:同一 ROI 不同次结果可能不同,`timeout: 2000` 期间会重试。
143+
6. **OCR 失败不要换 TemplateMatch**:先用 sweep 找 sweet spot,多数情况能解决。
144+
7. **可滚动 UI 用大 ROI + 父级 orchestrator****重要**):
145+
- **不要**在 Click 节点的 `next` 里放 `[JumpBack]CastleSwipeDown/Up` —— 找不到文字时会**死循环滑动**
146+
- 正确模式参考 marry.json 里的 `CastleHall` 节点:父级 orchestrator 节点的 `next` 列表里放 `[JumpBack]XXXEntry` + `[JumpBack]XXXSwipeDown` + `[JumpBack]XXXSwipeUp`
147+
- 滚动容错 ROI 范围参考 `CastleHallEntry`: `[60, 391, 609, 795]`
148+
8. **`run_pipeline` 必须有手动超时意识**:超过 ~10 秒不返回要主动停止,可能 ROI/expected 配错或 OCR 引擎卡住。
149+
9. **改完 pipeline 文件后调 `load_pipeline(path)` 即可****不需要重启 server**`run_pipeline` 每次都按 `pipeline_path` 从磁盘读最新内容,reload 后立即生效。
150+
10. **可滚动 UI 用统一大 ROI**:当多个目标在同一个可滚动列表(如城堡建筑列表)时,**所有节点共用同一 ROI** `[x, top_y, w, full_h]`,覆盖整个滚动区域。避免每个节点各自 ROI 滚动后失效。前提:每个节点的 `expected` 文字是唯一的(OCR 按 expected 匹配不会冲突)。
151+
11. **ROI 上边界 ≤ 元素最小 y**:目标元素在 y=424 时,ROI y 起点必须 ≤ 424,否则切掉顶部导致 OCR 失败。例:原 ROI `[100, 450, ...]` 把"城堡管理"切掉 26px → 改为 `[100, 400, ...]` 通过。
152+
12. **卡住时截图查看**:节点超时、OCR 找不到、行为异常时,调 `screencap` 看当前屏幕实际状态。可能界面已不在预期页、可能位置已被遮挡。
153+
154+
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) 的「跨页面状态机」。
155+
156+
14. **跨文件节点引用在 `run_pipeline` 测试中会失败**:MaaFramework 全局加载时所有 `assets/resource/base/pipeline/*.json` 合并到同一命名空间,`[JumpBack]OtherFileNode` 能解析。但 `run_pipeline` **只加载单文件**,跨文件引用会报"加载 Pipeline 失败"。**应对**
157+
- 单元测试每个节点用 `run_pipeline`(无跨文件依赖的子流程)是 OK 的
158+
- 含跨文件引用的状态机流程,集成测试必须用 MaaFramework GUI/CLI 触发
159+
- 调试时可考虑 `MaaCli` 命令行运行全 bundle
160+
161+
### 已验证最优 expand(5 节点实测)
162+
163+
| 节点 | expand | score | 备注 |
164+
|------|--------|-------|------|
165+
| `UI_RoleListPage` | **20** | 0.9997 | 中部偏左 |
166+
| `UI_RoleFormationPage` | **20** | 0.998 | 角色右边 |
167+
| `UI_CastlePage` | **3** | 0.997 | ⚠️ 仅 0-15 |
168+
| `UI_TeamPage` | **20** | 0.997 | 城堡右边 |
169+
| `ClickGoToArchipelago` | **20** | 0.991 | 中间大地图按钮 |
170+
171+
### 已验证:可滚动 UI 统一 ROI(10 城堡建筑)
172+
173+
| 节点 | 统一 ROI | score | 备注 |
174+
|------|----------|-------|------|
175+
| `CastleManage` | `[100, 400, 520, 880]` | 0.999 | 顶部 |
176+
| `Market` | 同上 | 0.999 | 顶部 |
177+
| `Blacksmith` | 同上 | 0.998 | 顶部 |
178+
| `AlchemyWorkshop` | 同上 | 0.999 | 顶部 |
179+
| `TrainingCenter` | 同上 | 1.000 | 顶部 |
180+
| `CastleMainHall` | 同上 | 0.876 | 顶部只露 25px |
181+
| `Shrine` | 同上 | 0.999 | 中段 |
182+
| `Family` | 同上 | 0.999 | 中段 |
183+
| `Museum` | 同上 | 0.999 | 底部 |
184+
| `Manor` | 同上 | 0.999 | 底部 |
185+
186+
**关键设计**
187+
- 所有节点 ROI 完全相同(`[100, 400, 520, 880]`,覆盖 y=400-1280)
188+
- 不靠 expand 微调,靠 `expected` 文字差异让 OCR 区分
189+
- 不放 `next` 链(避免死循环)
190+
191+
---
192+
193+
## 跨页面状态机流程(用 `next` + `[JumpBack]`
194+
195+
当生成的活动流程需要**跨多个页面跳转**(如:大地图 → 活动入口 → 难度选择 → 队伍 → 战斗),用 MaaFramework 的 `next` + `[JumpBack]` 机制串接各页面节点,**不要写 Python orchestration**
196+
197+
### 模式:状态机入口节点
198+
199+
```jsonc
200+
{
201+
"MyActivity_Start": {
202+
"next": [
203+
"MyActivity_TeamReady", // 已在队伍配置页 → 点击"进入战斗"
204+
"[JumpBack]MyActivity_Difficulty_Select", // 在难度选择页 → 选难度
205+
"[JumpBack]MyActivity_Enter" // 在大地图 → 找入口
206+
],
207+
"timeout": 10000
208+
},
209+
210+
"MyActivity_Enter": {
211+
"next": [
212+
"MyActivity_Enter_Click", // 找到图标 → 点击
213+
"[JumpBack]BigMap_Activity_Resident", // 切"常驻"tab
214+
"[JumpBack]BigMap_Activity" // 打开活动页
215+
],
216+
"timeout": 10000
217+
},
218+
219+
"MyActivity_EnterBattle": {
220+
"recognition": "OCR",
221+
"expected": ["进入战斗"],
222+
"action": "Click",
223+
"next": [
224+
"MyActivity_FightStart", // 战斗开始
225+
"[JumpBack]MyActivity_TravelSelect_Boat", // 乘船
226+
"[JumpBack]MyActivity_TravelSelect_Walk" // 步行 fallback
227+
]
228+
},
229+
230+
"MyActivity_TravelSelect_Boat": {
231+
"recognition": "OCR",
232+
"expected": ["确定"],
233+
"roi": [490, 740, 100, 80], // 窄 ROI 限定乘船行
234+
"action": "Click"
235+
},
236+
237+
"MyActivity_TravelSelect_Walk": {
238+
"recognition": "OCR",
239+
"expected": ["确定"],
240+
"roi": [490, 590, 100, 80], // 窄 ROI 限定步行行
241+
"action": "Click"
242+
}
243+
}
244+
```
245+
246+
### 关键设计要点
247+
248+
1. **`[JumpBack]` 是状态回退的关键**:命中后执行完节点链,自动返回父节点的 `next` 继续。
249+
2. **窄 ROI 区分同名字段**:用 y 范围 [490, 740, 100, 80] vs [490, 590, 100, 80] 区分两个"确定"按钮行(y 范围不重叠)。
250+
3. **`target_offset` 偏移点击**:识别难度文字后用 `target_offset: [270, 0, 0, 0]` 把点击位置右移到"确定"按钮上。
251+
4. **跨文件节点引用**:MaaFramework 全局加载会合并所有 `pipeline/*.json`,所以 `[JumpBack]BigMap_Activity`(在 main_ui.json)能从 growth_trial.json 引用。但 `run_pipeline` 测试只加载单文件,集成测试需用 GUI/CLI。
252+
253+
### 与 Python orchestration 的本质区别
254+
255+
| 状态机(推荐) | Python orchestration(次选) |
256+
|--------------|--------------------------|
257+
| 流程推进由 MaaFramework 调度 | 自己写 `for/if` 调度 |
258+
| 每个节点 `next` 显式声明后继 | Python 函数串行 `run_task` |
259+
| `[JumpBack]` 自动状态回退 | 手动实现回退逻辑 |
260+
| 跨页面异常有自然路径 | 需手动 try/except |
261+
262+
详见 [.claude/skills/pipeline-option/SKILL.md](../pipeline-option/SKILL.md) 的「不要做 #10」和 [.claude/skills/pipeline-guide/SKILL.md](../pipeline-guide/SKILL.md) 的「跨页面状态机」典型模式。

0 commit comments

Comments
 (0)