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.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add KhazixW2/MaaGumballs --skill pipeline-option --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Pipeline Option?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/khazixw2-pipeline-option)More formats (shields.io, HTML) on the badges page.
---
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)。
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!