Back to skills
SKILL.md
Pipeline Option
ASecurityAdd runtime UI options (select/checkbox/switch/input) to MaaFramework option surfaces such as `assets/interface.json` or `assets/resource/tasks/**/*.json`. Use when adding a user-facing toggle, selector, checkbox, or input; wiring options to `pipeline_override`; aligning option paths with Python `context.get_node_data()` or CustomAction params; or reviewing option behavior across Pipeline JSON and Python.
- 66 stars
- 0 votes
- 0 copies
- 2 views
- Added September 12, 2026
Works with
Security analysis
100/100Pro scans all 5 files and shows the line behind each finding
npx -y skills add KhazixW2/MaaGumballs --skill pipeline-option --agent claude-codeAre you the author of Pipeline Option?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/khazixw2-pipeline-option)---
name: pipeline-option
description: "Add runtime UI options (select/checkbox/switch/input) to MaaFramework option surfaces such as `assets/interface.json` or `assets/resource/tasks/**/*.json`. Use when adding a user-facing toggle, selector, checkbox, or input; wiring options to `pipeline_override`; aligning option paths with Python `context.get_node_data()` or CustomAction params; or reviewing option behavior across Pipeline JSON and Python."
---
# Pipeline Option 工作流
## TL;DR:先识别 option surface
新增一个 UI 选项需要先识别本项目使用的 option surface。MaaGumballs 主要是 `assets/interface.json`,M9A 同时使用 `assets/interface.json` 与 `assets/resource/tasks/**/*.json`。不要假设所有 Maa 项目只有一种入口。
常见联动点如下,按项目实际协议取用,缺关键点会导致 UI 看不到选项或运行时读不到值:
| # | 位置 | 内容 |
|---|------|------|
| 1 | option 定义处 | `assets/interface.json` 的 `option` 字典,或 `assets/resource/tasks/**/*.json` 里的 task option 定义 |
| 2 | task 注册处 | `interface.json` task 的 `option: []`,或 tasks JSON 的任务/预设引用 |
| 3 | `assets/resource/base/pipeline/*.json` | **预定义**目标节点(pipeline_override 不会创建节点) |
| 4 | Python 代码(仅 Python 需要读取或执行 Custom 时必需) | `context.get_node_data()`、`argv.custom_action_param`、`argv.custom_recognition_param` 与 option 路径保持一致 |
> ⚠️ **pipeline_override 只做属性合并,不会凭空创建节点。** 少了第 3 步,`context.get_node_data()` 会返回 `None`,运行时静默失败。
完整协议参考(嵌套 option、global_option、controller/resource 限制、占位符注入):[references/protocol.md](references/protocol.md)
## 历史校正
- **不要把 pure override 当成唯一最佳解**:只改已有节点字段时 pure override 最小;但涉及运行时事件库、计数、动态目标、识别后处理、失败策略、跨节点状态时,CustomAction/CustomRecognition 更合适。
- **不要把 Flag 节点当成唯一配置入口**:M9A 的 v5 object-form 里,参数经常通过 `action.param.custom_action_param`、`custom_action_param_code`、`recognition.param.custom_recognition_param` 进入 Custom;这和 `context.get_node_data("Flag")` 是不同通道。
- **字段路径必须闭环**:UI 写哪条路径,Python 就读哪条路径;pure override 则 Python 不读,直接观察运行时行为。
- **`enabled`/`enable` 不是审美选择**:MaaFramework 原生启停用 `enabled`;历史项目若已有 `enable` helper,可沿用并兼容,否则优先 `enabled`。
---
## 4 种 type 速查
| type | 选择 | override 字段 | 节点预定义形态 |
|------|------|---------------|---------------|
| `select` | 单选互斥 | `expected` | `recognition: "OCR"` + `expected: [...]` |
| `switch` | 二元 Yes/No | `enabled`(或项目已有的 `enable`) | `{"enabled": bool}` / `{"enable": bool}` |
| `input` | 自由文本 | `{name}` 占位符可注入目标字段 | 按最终读取方预定义 `expected` 或 `action.param.custom_action_param` |
| `checkbox` | 多选 | `enabled` | `{"enabled": false}` |
## 选哪个模式?
| 你的需求 | 推荐模式 |
|---------|---------|
| 启用/禁用一个 Python 业务函数 | **A**(switch + Flag 节点 + Python 读 flag) |
| 从多个互斥选项里选一个值 | **B**(select + OCR 节点) |
| 同时启用多个独立的功能模块 | **C**(checkbox + 多个 Flag 节点) |
| 用户输入自定义文本 | **D**(input + 占位符注入) |
| 切换行为(点哪个按钮 / 走哪条 next 链)但不想改 Python | **E**(pure override 现有节点字段) |
> **经验法则**:行为只等于“覆盖已有节点字段”时优先 pure override;一旦需要运行时数据、计数、动态识别、失败策略或跨节点状态,改用 Flag + Python / CustomAction / CustomRecognition。目标是让改动面和逻辑复杂度匹配。
### 先确认代码读取路径
加选项前先 `rg "get_node_data|_node_enabled|Flag_" agent assets`,确认这次配置到底由谁读取。
常见对应关系:
| UI override 写什么 | Python 应该读什么 | 备注 |
|-------------------|-------------------|------|
| `{ "Flag_X": { "enabled": true } }` | `node.get("enabled")` | MaaFramework 标准启停字段 |
| `{ "Flag_X": { "enable": true } }` | `node.get("enable")` 或兼容 helper | 仅用于已有项目约定/历史字段 |
| `{ "SomeOCR": { "expected": ["A"] } }` | `node["recognition"]["param"]["expected"][0]` | 节点必须预定义为 OCR |
| `{ "SomeNode": { "next": ["A"] } }` | 不读,直接由 pipeline 行为生效 | pure override 模式 |
**不要字段错位**:例如 UI 写 `AutoSky_CloneConfig.enable`,Python 却调用读取 `expected` 的函数;或 UI 写 `expected`,Python 只看 `enabled`。这种错误不会报 JSON 语法错,但运行时会表现为"选项没生效"。
### 启停节点必须短路调用
如果同一个节点既保存 `enable`/`enabled` 开关,又是可执行的 Recognition/Action 节点,先读取开关,只有开启时才调用它。关闭节点后继续检查同级候选分支,不能调用已关闭节点,也不能因为开关值直接 `continue` 整个事件扫描。
```python
if _node_enabled(context, "OptionalNode"):
result = context.run_recognition("OptionalNode", image)
if result and result.hit:
context.run_task("OptionalNode")
continue
# OptionalNode 关闭或未命中,继续识别 CombatNode、ExploreNode 等同级分支。
```
---
## 模式 A:开关(switch + Flag 节点)— 最常用
**适用**:开启/关闭某个功能。
### interface.json
```jsonc
"开启5月城堡相亲": {
"type": "switch",
"description": "是否开启5月自动相亲",
"default_case": "Yes",
"cases": [
{
"name": "Yes",
"pipeline_override": { "Flag_EnableMarryTask": { "enabled": true } }
},
{
"name": "No",
"pipeline_override": { "Flag_EnableMarryTask": { "enabled": false } }
}
]
}
```
### 配套 pipeline 节点(必须预定义!)
```jsonc
"Flag_EnableMarryTask": { "enabled": true }
```
> 若项目历史节点使用 `enable` 而非 `enabled`,必须同时保证 Python 侧有兼容读取,例如 `node.get("enable", node.get("enabled", True))`。否则优先使用协议字段 `enabled`。
### 注册到 task
```jsonc
"task": [{
"name": "推年计划",
"entry": "Auto_YearlyTask",
"option": ["开启5月城堡相亲", /* 其他选项 */]
}]
```
### Python 读取(建议放在业务函数入口)
```python
def handle_marry_festival(context: Context) -> bool:
"""处理春林节相亲(5月)"""
EnableMarryTask = context.get_node_data("Flag_EnableMarryTask").get("enabled")
if not EnableMarryTask:
logger.info("自动相亲已关闭,跳过")
return True
# ... 正常逻辑
```
---
## 模式 B:单选(select + OCR 节点)
**适用**:选择城市、关卡、模式等互斥选项。
### interface.json
```jsonc
"选择刷取任务国家": {
"type": "select",
"description": "选择要刷取任务的目标城市",
"default_case": "雄月城",
"cases": [
{ "name": "王座堡", "pipeline_override": { "EnterCity": { "expected": ["王座堡"] } } },
{ "name": "雄月城", "pipeline_override": { "EnterCity": { "expected": ["雄月城"] } } }
]
}
```
### 配套 OCR 节点
```jsonc
"EnterCity": {
"recognition": "OCR", // ⚠️ 必须是 OCR,否则 expected 不生效
"expected": ["王座堡", "圣盾堡", "雄月城", "翠庭"],
"roi": [58, 320, 600, 682],
"action": "Click"
}
```
### Python 读取
```python
data = context.get_node_data("EnterCity")
city = data.get("recognition", {}).get("param", {}).get("expected", ["王座堡"])[0]
```
---
## 模式 C:多选(checkbox + 多个 Flag 节点)
**适用**:多条件检测(好苗子条件)、可叠加的功能模块。
### interface.json
```jsonc
"开启好娃提醒": {
"type": "checkbox",
"default_case": ["科内塔之怒"],
"cases": [
{ "name": "科内塔之怒", "pipeline_override": { "检测_科内塔之怒": { "enabled": true } } },
{ "name": "太阳+科内塔之怒", "pipeline_override": { "检测_太阳+科内塔之怒": { "enabled": true } } }
]
}
```
### 配套节点(每个 case 一个,默认全 false)
```jsonc
"检测_科内塔之怒": { "expected": ["koneita"], "enabled": false },
"检测_太阳+科内塔之怒": { "expected": ["sun_and_koneita"], "enabled": false }
```
### Python 读取(遍历收集)
```python
def _get_enabled_checks(context) -> list:
enabled = []
for key in ["检测_科内塔之怒", "检测_太阳+科内塔之怒"]:
node = context.get_node_data(key)
if node and node.get("enabled", False):
expected = node.get("recognition", {}).get("param", {}).get("expected", [])
if expected:
enabled.append(expected[0])
return enabled
```
---
## 模式 D:自由输入(input + 占位符注入)
**适用**:用户输入自定义关卡号、自定义黑名单任务等。`input` 只是 UI surface;占位符可以注入 `expected` 或 `action.param.custom_action_param`,最终路径由消费方决定。
### interface.json
```jsonc
"自定义任务黑名单": {
"type": "input",
"inputs": [
{
"name": "任务名称",
"pipeline_type": "string",
"default": "",
"verify": "^[^,,]*$",
"pattern_msg": "不能包含逗号"
}
],
"pipeline_override": {
"CustomTaskBlacklist": {
"expected": ["{任务名称}"] // {名称} 占位符被实际输入替换
}
}
}
```
### Python 读取
```python
data = context.get_node_data("CustomTaskBlacklist")
value = data.get("recognition", {}).get("param", {}).get("expected", [""])[0]
```
### ⚠️ 常见错误:混淆 `get_node_data()` 和 CustomAction 参数通道
`custom_action_param` 可以用,但要读对地方:
- 如果参数是给当前 CustomAction 执行时使用,写入 `action.param.custom_action_param`,在 Python 里读 `argv.custom_action_param`。
- 如果参数是给普通业务代码提前读取,写到一个预定义节点的 `recognition.param.expected`、`enabled`、`enable` 或其他明确字段,再用 `context.get_node_data("Node")` 读取对应路径。
- 不要把用户输入塞到 `pipeline_override.custom_action_param` 后,再用 `get_node_data("X").get("custom_action_param")` 读顶层字段;这两个不是同一条通道。
**错误信号**:UI 显示已选择,但 Python 读到默认值或 `None`。排查时打印完整 `context.get_node_data("X")`,确认字段实际落点。
---
## 模式 E:行为覆盖(pure override 现有节点字段)— 最简
**适用**:行为切换映射到现有 pipeline 节点的**单个字段**(`next` 数组 / `action` 类型 / `recognition` 算法 / 任何可覆盖字段),且**不需要 Python 判断**。
**核心思路**:用户切换 UI 选项 → 改变 pipeline 节点的字段值 → 框架自身根据新值执行。**Python 代码完全不动。**
### 典型场景:开关决定点哪个按钮
```jsonc
"开启自动接受佣兵": {
"type": "switch",
"default_case": "No",
"cases": [
{
"name": "Yes",
"description": "直接点确认",
"pipeline_override": {
"Event_MercenaryJoin": {
"next": ["Event_MercenaryJoinConfirm"]
}
}
},
{
"name": "No",
"description": "直接点取消",
"pipeline_override": {
"Event_MercenaryJoin": {
"next": ["Event_MercenaryJoinCancel"]
}
}
}
]
}
```
`Event_MercenaryJoin` 节点本身在 [event_utils.json](../../assets/resource/base/pipeline/event_utils.json) 里有完整定义(`recognition` / `expected` / `roi` / `timeout` 都在),`pipeline_override` 只覆盖 `next` 字段,其他字段保持原值。
### `next` 数组的单元素 vs 多元素语义
| 写法 | 语义 | 何时用 |
|------|------|-------|
| `["A"]` | **强约束**:只走 A | 行为已确定,单路径足够(**模式 E 的典型形态**) |
| `["A", "B"]` | **回退链**:优先 A,A 失败走 B | 兜底机制("优先点确认,找不到才点取消") |
| `["A", "B", "[JumpBack]C"]` | 失败后跳回 C 节点重试 | 复杂回退 |
### 可被 override 的字段
| 字段 | override 效果 | 典型用途 |
|------|--------------|---------|
| `next` | 改变后续节点列表 | 切换行为路径(模式 E 主力) |
| `action` | 改变点击/滑动/输入动作 | 切换操作类型 |
| `recognition` | 改变识别算法 | 切换识别方式(OCR ↔ Template) |
| `expected` | 改变识别期望值 | 配合 select 选值 |
| `roi` | 改变识别区域 | 适配不同界面尺寸 |
| `timeout` | 改变超时时间 | 适配不同网络/性能 |
> **关键认识**:上面这些字段都是普通 JSON 值,pipeline_override 一视同仁做深合并。**模式 A 用的 `enabled` 字段只是最常见的入口,不是唯一可 override 的字段。**
### 模式 A vs 模式 E 对比
| 场景 | 模式 A(Flag + Python) | 模式 E(pure override) |
|------|------------------------|----------------------|
| 行为由 Python `if` 控制 | ✅ 必须 | ❌ 绕远路 |
| 行为由 pipeline 字段决定 | ❌ 多此一举 | ✅ 最简 |
| 需要运行时根据 flag 走不同代码分支 | ✅ 唯一选择 | ❌ 不行 |
| 改动 Python 代码 | ✅ 需要 | ❌ 不需要 |
| 需要新加 Flag 节点 | ✅ 需要 | ❌ 不需要 |
### 实战决策流程
```
要加新选项
│
├─ 行为切换对应到一个 pipeline 节点的某个字段?
│ └─ ✅ 用模式 E(pure override)
│ 示例:佣兵加入时点"确认"还是"取消"
│
└─ ❌ 行为在 Python 业务逻辑里
└─ 用模式 A(Flag 节点 + Python 读 flag)
示例:跳过整个 handle_sailing_festival 函数
```
---
## 补充:能用状态机就别写 Python orchestration
**MaaFramework 的 `next` + `[JumpBack]` 是为跨页面状态推进设计的原语**。如果一个流程的步骤可以**列举**为有限个页面状态(入口 → A → B → C → 战斗),优先用 JSON 状态机;不要写 Python 把 `context.run_task` 串起来。
详见 [.claude/skills/pipeline-guide/SKILL.md](../pipeline-guide/SKILL.md) 的「跨页面状态机」典型模式。
### 状态机 vs Python orchestration 对比
| 场景 | 状态机(推荐) | Python orchestration(次选) |
|------|--------------|--------------------------|
| 有限页面状态推进(如活动流程) | ✅ 链 `next` + `[JumpBack]` | ❌ 自己写 `for/while` 调度 |
| 按 flag 跳过整段函数 | ❌ 不适合 | ✅ 读 flag + 早返回 |
| 复杂的运行时分支逻辑 | ❌ 难表达 | ✅ Python 灵活 |
### 跨文件节点引用的测试陷阱
MaaFramework 全局加载时,所有 `assets/resource/base/pipeline/*.json` 会合并到同一命名空间,所以 `[JumpBack]OtherFileNode` 能解析。但 `run_pipeline` 测试工具**只加载单文件**,跨文件引用会报"加载 Pipeline 失败"。
**应对**:
- 集成测试必须用 MaaFramework GUI / CLI 触发,不能依赖 `run_pipeline`
- 单元测试每个节点用 `run_pipeline` 是 OK 的(无跨文件依赖)
- 若某个流程有跨文件引用,本地调试时考虑用 `MaaCli` 跑全 bundle
---
## 补充:状态机驱动的「流程型选项」
如果一个 UI 选项代表的是**进入某个跨页面流程**(如"开启成长试炼"→ 大地图 → 难度选择 → 队伍 → 战斗),把选项的 `pipeline_override` 用于:
1. 切换"是否启用流程"的 Flag 节点
2. 注入该流程入口节点所需参数(如难度 `expected`)
而**不要**用 Python orchestration 串联流程中的每个节点。完整流程示例:
```jsonc
// 选项定义
"开启3月成长试炼": {
"type": "switch",
"default_case": "No",
"cases": [
{
"name": "Yes",
"pipeline_override": {
"Flag_GrowthTrialMode": { "enabled": true },
"GrowthTrial_Difficulty_Select": { "expected": ["噩梦"] }
}
},
{
"name": "No",
"pipeline_override": {
"Flag_GrowthTrialMode": { "enabled": false }
}
}
]
}
```
```jsonc
// 入口节点(路由)
"GrowthTrial_Start": {
"next": [
"GrowthTrial_TeamReady", // 已在队伍配置页
"[JumpBack]GrowthTrial_Difficulty_Select", // 在难度选择页
"[JumpBack]GrowthTrial_Enter" // 在大地图
]
}
```
战斗入口自动接力:
```jsonc
"GrowthTrial_EnterBattle": {
"action": "Click",
"next": [
"GrowthTrial_FightStart", // 战斗开始
"[JumpBack]GrowthTrial_TravelSelect_Boat", // 弹出旅行框
"[JumpBack]GrowthTrial_TravelSelect_Walk" // fallback
]
}
```
---
## 命名与默认值
### 命名约定
| 角色 | 风格 | 示例 |
|------|------|------|
| option 名(用户可见) | 中文动词起头 | `开启5月城堡相亲`、`选择刷取任务国家` |
| 节点名(pipeline) | 英文 | `Flag_EnableMarryTask`、`EnterCity`、`检测_科内塔之怒` |
| switch case 名 | **严格 `Yes` / `No`** | 不要用 `true/false` 或 `是/否`(Client 解析跨平台不一致) |
### 默认值策略
> **保持现有行为是底线。** 老用户不该因新选项而行为改变。
| 场景 | 推荐 default |
|------|-------------|
| 新开关让功能默认关闭 | `No`(明确告知用户"关了") |
| 新开关让功能默认开启 | `Yes`(保留旧行为) |
| 旧代码无条件开启 | `Yes`(兼容) |
| 旧代码无条件关闭 | `No`(兼容) |
---
## 读取位置
| 决策类型 | 放哪读 | 理由 |
|---------|-------|------|
| 是否执行某段流程 | 业务函数入口 `handle_xxx` | 与现有同名函数风格一致,子函数自治 |
| 用哪个值做主逻辑 | 任务入口 `run` 或 `YearlyTaskProcessor` | 一次读取、多次复用 |
> **反例**:不要把"是否开启 X"的判断堆在通用 `dispatch` 函数(如 `handle_festival_by_month`)里。每加一个开关 dispatch 就多一个 `if-elif`,越来越臃肿。
---
## ✅ 推荐做法
1. **先复用现有模式**:参考同项目里现成的同类选项(开关 → `开启5月城堡相亲`;选择 → `选择刷取任务国家`)
2. **3 处同步改完再跑**:不要中途停下来"先编译试试"
3. **JSON 改完跑资源加载检查**:本仓库与 M9A 的真实路径是 `python tools/ci/check_resource.py assets/resource/base`;如果目标项目路径不同,先用 `Get-ChildItem tools -Recurse -Filter check_resource.py` 发现真实脚本。pipeline 加载错误(如重复 key)会立刻报
4. **默认值遵循现状**:选项是"开"还是"关"取决于旧代码行为,不是你的偏好
5. **在 task 的 `doc` 数组里加一行说明**:用户能看懂每个选项的作用
6. **让 option 通道匹配实际复杂度**:行为只动已有 pipeline 字段时用 pure override;Python 需要做真实判断、计数、动态识别或安全策略时,用 Flag + Python / CustomAction / CustomRecognition,不要为了少写 Python 把复杂逻辑硬塞进 JSON。
---
## ❌ 不要做
### 1. 不要只通过 pipeline_override 定义节点
```jsonc
// ❌ 错:节点没在 pipeline JSON 中预定义 → 不会被加载 → get_node_data() 返回 None
// ✅ 对:在 pipeline JSON 里预定义
"Flag_EnableSailingFestivalPurchase": { "enabled": true }
```
**验证方法**:加完后跑 `python tools/ci/check_resource.py assets/resource/base`(或目标项目真实资源检查命令),并在 Python 里加个 `None` 兜底日志。
### 2. 不要忘了注册到 task 的 option 数组
```jsonc
// ❌ 错:option 定义了但 task 不引用 → UI 上看不到
"option": []
// ✅ 对:同步注册
"option": ["开启3月启航节购买"]
```
### 3. 不要把判断塞到 dispatch 函数
```python
# ❌ 错:dispatch 越来越臃肿
def handle_festival_by_month(month):
if month == 3 and not context.get_node_data("Flag_X").get("enabled"):
return True
if month == 3:
return handle_sailing_festival(context)
# ... 每加一个开关都得多一个 if
# ✅ 对:业务函数自治
def handle_sailing_festival(context):
if not context.get_node_data("Flag_X").get("enabled"):
return True
# ... 正常逻辑
```
### 4. 不要混淆字段路径
| 用途 | 字段路径 | 备注 |
|------|---------|------|
| `select` | `data["recognition"]["param"]["expected"][0]` | 节点必须 `recognition: "OCR"` |
| `input` 注入 CustomAction | `data["action"]["param"]["custom_action_param"][key]` 或执行时的 `argv.custom_action_param` | 取决于参数消费位置 |
| `input` 注入普通节点字段 | 读取被占位符覆盖的真实路径,如 `recognition.param.expected` | input 不限定注入目标 |
| `switch` / `checkbox` | `data["enabled"]` | 最简单 |
| 历史 `enable` 开关 | `data.get("enable", data.get("enabled", default))` | 仅在项目已有该字段时使用 |
| 模式 E 不读 | (不读,直接看 override 后节点的运行时行为) | pure override 模式,Python 拿不到也不需要 flag |
### 5. 不要用非 `Yes`/`No` 的 switch case 名
```jsonc
// ❌ 错:Client 解析可能不一致
{ "name": "true" } / { "name": "是" } / { "name": "ON" }
// ✅ 对:跨 Client 一致
{ "name": "Yes" } / { "name": "No" }
```
### 6. 不要让 input 注入路径和读取路径错位
`input` 的 `{name}` 占位符既可以注入 OCR `expected`,也可以注入 `action.param.custom_action_param`。选择哪条路径取决于最终消费方:普通 OCR/配置读取用实际节点字段,CustomAction 执行参数用 `argv.custom_action_param`。不要写入一条路径却从另一条路径读取。
### 7. 不要用中文做 pipeline 节点名
```jsonc
// ❌ 错:中文节点名 + 英文字段访问
"开启5月": { "enabled": true }
// ✅ 对:英文 Flag_ 命名
"Flag_EnableMarryTask": { "enabled": true }
```
中文做 option 名(用户可见),英文做 pipeline 节点名(代码访问)。混了会让代码和配置都对不上。
### 8. 不要在多文件 pipeline 里重复定义同名节点
`parse_and_override_once` 合并所有 pipeline JSON 时**严格拒绝**重复顶层 key。检查方法:
```bash
grep -rn "^\s*\"YourNodeName\":" assets/resource/base/pipeline/
```
两个文件都定义同一个顶层节点会直接让整个 `check_resource.py` 失败,且 Python `json.load()` 检测不出来(Python 会静默覆盖),必须用 C++ 解析器或 C++ 模拟检测。
### 9. 不要为了"配置统一"硬塞 Flag 节点
```jsonc
// ❌ 错:行为切换只动 pipeline 字段,但你硬加了 Flag 节点 + Python 分支
"Flag_AcceptMercenary": { "enabled": true }, // ← 不必要
def handle_mercenary_join(context):
if not context.get_node_data("Flag_AcceptMercenary").get("enabled"):
return True
context.run_task("Event_MercenaryJoin") # 实际行为由 Event_MercenaryJoin.next 决定
// ✅ 对:直接 override `next`,零 Python 改动
"开启自动接受佣兵": {
"type": "switch",
"pipeline_override": {
"Event_MercenaryJoin": { "next": ["Event_MercenaryJoinConfirm"] }
}
}
```
**判断口诀**:如果你的 Python 分支里**只做了一件事**(调用 `run_task` 让 pipeline 接手),那这个分支完全可以由 `pipeline_override` 替代。Flag 节点 + Python 分支只在你需要在 Python 侧做**真正的条件逻辑**(不只是转发)时才必要。
### 10. 不要用 Python orchestration 替代状态机
如果一个跨页面流程可以**列举**为有限个页面状态(A → B → C → D),优先用 MaaFramework 的 `next` + `[JumpBack]` 串起来。**不要**写 Python `for` 循环 + `context.run_task()` 调度。
```jsonc
// ✅ 对:纯 JSON 状态机(推荐)
"GrowthTrial_Start": {
"next": [
"GrowthTrial_TeamReady", // 已在队伍配置页
"[JumpBack]GrowthTrial_Difficulty_Select", // 在难度选择页
"[JumpBack]GrowthTrial_Enter" // 在大地图
]
}
"GrowthTrial_Enter": {
"next": [
"GrowthTrial_Enter_Click", // 找到图标
"[JumpBack]BigMap_Activity_Resident", // 切"常驻"tab
"[JumpBack]BigMap_Activity" // 打开活动页
]
}
"GrowthTrial_EnterBattle": {
"action": "Click",
"next": [
"GrowthTrial_FightStart", // 战斗开始
"[JumpBack]GrowthTrial_TravelSelect_Boat", // 弹出旅行框
"[JumpBack]GrowthTrial_TravelSelect_Walk" // fallback
]
}
```
```python
# ❌ 错:自己重新发明状态机
def enter_growth_trial(context):
found = False
for attempt in range(5):
if context.run_recognition("BigMap_GrowthTrial_OCR", ...).hit:
found = True
break
context.run_task("Map_SwipeUp_OnBigMap")
if not found:
return False
context.run_task("GrowthTrial_Enter")
# ... 又是 for/if 链
return True
```
**自检问题**:
- 我的 Python 代码里是否在**调 `run_task` 把控制权交给 pipeline**?是 → 考虑改用 `next` 链
- 我的"流程推进"是否依赖**显式的状态变量**(如 `found`)?是 → 改用 `[JumpBack]` 让框架自动回退
- 我的"流程"是否**可以画成状态机图**?是 → 用 JSON `next` 链
**注意**:MaaFramework 全局加载时跨文件节点引用会解析(`main_ui.json` 里的 `BigMap_Activity*` 能在 `growth_trial.json` 引用),但**`run_pipeline` 测试工具只加载单文件**——集成测试必须用 MaaFramework GUI/CLI 触发。
### 11. 不要让 UI override 字段和 Python 读取字段不一致
```jsonc
// ❌ 错:UI 写 enable
"开启克隆体": {
"type": "switch",
"cases": [
{ "name": "Yes", "pipeline_override": { "AutoSky_CloneConfig": { "enable": true } } }
]
}
```
```python
# ❌ 错:代码却读 expected,永远读不到用户开关
self._clone_enabled = _read_expected_value(context, "AutoSky_CloneConfig")
```
```python
# ✅ 对:读同一个字段,或提供 enable/enabled 兼容
self._clone_enabled = _node_enabled(context, "AutoSky_CloneConfig")
```
自检口诀:**UI 写哪条路径,Python 就读哪条路径;pure override 则 Python 不读。**
---
## 验证流程
改完一次完整流程,**按顺序**做这 4 步:
1. **JSON 语法检查**
```bash
python -c "import json; json.load(open('assets/interface.json', encoding='utf-8'))"
python -c "import json; json.load(open('assets/resource/base/pipeline/auto_task.json', encoding='utf-8'))"
```
2. **资源加载检查**
```bash
python tools/ci/check_resource.py assets/resource/base
```
期望输出 `All directories checked.`
3. **Pipeline 节点测试**(可选)
```python
data = context.get_node_data("Flag_EnableSailingFestivalPurchase")
assert data is not None, "节点未预定义"
assert "enabled" in data
```
4. **端到端验证**:用 Pipeline Testing Skill 跑一次实际流程
---
## 完整协议
更多 type 字段、嵌套 option、global_option、controller/resource 限制、`{占位符}` 注入机制等高级特性见 [references/protocol.md](references/protocol.md)。
Files in this skill
- README.md
- SKILL.md
- evals/evals.json
- maahub_meta.json
- references/protocol.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.