| 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
历史校正
- 不要把 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 整个事件扫描。
if _node_enabled(context, "OptionalNode"):
result = context.run_recognition("OptionalNode", image)
if result and result.hit:
context.run_task("OptionalNode")
continue
模式 A:开关(switch + Flag 节点)— 最常用
适用:开启/关闭某个功能。
interface.json
"开启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 节点(必须预定义!)
"Flag_EnableMarryTask": { "enabled": true }
若项目历史节点使用 enable 而非 enabled,必须同时保证 Python 侧有兼容读取,例如 node.get("enable", node.get("enabled", True))。否则优先使用协议字段 enabled。
注册到 task
"task": [{
"name": "推年计划",
"entry": "Auto_YearlyTask",
"option": ["开启5月城堡相亲", ]
}]
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
"选择刷取任务国家": {
"type": "select",
"description": "选择要刷取任务的目标城市",
"default_case": "雄月城",
"cases": [
{ "name": "王座堡", "pipeline_override": { "EnterCity": { "expected": ["王座堡"] } } },
{ "name": "雄月城", "pipeline_override": { "EnterCity": { "expected": ["雄月城"] } } }
]
}
配套 OCR 节点
"EnterCity": {
"recognition": "OCR",
"expected": ["王座堡", "圣盾堡", "雄月城", "翠庭"],
"roi": [58, 320, 600, 682],
"action": "Click"
}
Python 读取
data = context.get_node_data("EnterCity")
city = data.get("recognition", {}).get("param", {}).get("expected", ["王座堡"])[0]
模式 C:多选(checkbox + 多个 Flag 节点)
适用:多条件检测(好苗子条件)、可叠加的功能模块。
interface.json
"开启好娃提醒": {
"type": "checkbox",
"default_case": ["科内塔之怒"],
"cases": [
{ "name": "科内塔之怒", "pipeline_override": { "检测_科内塔之怒": { "enabled": true } } },
{ "name": "太阳+科内塔之怒", "pipeline_override": { "检测_太阳+科内塔之怒": { "enabled": true } } }
]
}
配套节点(每个 case 一个,默认全 false)
"检测_科内塔之怒": { "expected": ["koneita"], "enabled": false },
"检测_太阳+科内塔之怒": { "expected": ["sun_and_koneita"], "enabled": false }
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
"自定义任务黑名单": {
"type": "input",
"inputs": [
{
"name": "任务名称",
"pipeline_type": "string",
"default": "",
"verify": "^[^,,]*$",
"pattern_msg": "不能包含逗号"
}
],
"pipeline_override": {
"CustomTaskBlacklist": {
"expected": ["{任务名称}"]
}
}
}
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 代码完全不动。
典型场景:开关决定点哪个按钮
"开启自动接受佣兵": {
"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 里有完整定义(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 的「跨页面状态机」典型模式。
状态机 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 用于:
- 切换"是否启用流程"的 Flag 节点
- 注入该流程入口节点所需参数(如难度
expected)
而不要用 Python orchestration 串联流程中的每个节点。完整流程示例:
"开启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 }
}
}
]
}
"GrowthTrial_Start": {
"next": [
"GrowthTrial_TeamReady",
"[JumpBack]GrowthTrial_Difficulty_Select",
"[JumpBack]GrowthTrial_Enter"
]
}
战斗入口自动接力:
"GrowthTrial_EnterBattle": {
"action": "Click",
"next": [
"GrowthTrial_FightStart",
"[JumpBack]GrowthTrial_TravelSelect_Boat",
"[JumpBack]GrowthTrial_TravelSelect_Walk"
]
}
命名与默认值
命名约定
| 角色 | 风格 | 示例 |
|---|
| 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,越来越臃肿。
✅ 推荐做法
- 先复用现有模式:参考同项目里现成的同类选项(开关 →
开启5月城堡相亲;选择 → 选择刷取任务国家)
- 3 处同步改完再跑:不要中途停下来"先编译试试"
- JSON 改完跑资源加载检查:本仓库与 M9A 的真实路径是
python tools/ci/check_resource.py assets/resource/base;如果目标项目路径不同,先用 Get-ChildItem tools -Recurse -Filter check_resource.py 发现真实脚本。pipeline 加载错误(如重复 key)会立刻报
- 默认值遵循现状:选项是"开"还是"关"取决于旧代码行为,不是你的偏好
- 在 task 的
doc 数组里加一行说明:用户能看懂每个选项的作用
- 让 option 通道匹配实际复杂度:行为只动已有 pipeline 字段时用 pure override;Python 需要做真实判断、计数、动态识别或安全策略时,用 Flag + Python / CustomAction / CustomRecognition,不要为了少写 Python 把复杂逻辑硬塞进 JSON。
❌ 不要做
1. 不要只通过 pipeline_override 定义节点
"Flag_EnableSailingFestivalPurchase": { "enabled": true }
验证方法:加完后跑 python tools/ci/check_resource.py assets/resource/base(或目标项目真实资源检查命令),并在 Python 里加个 None 兜底日志。
2. 不要忘了注册到 task 的 option 数组
"option": []
"option": ["开启3月启航节购买"]
3. 不要把判断塞到 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)
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 名
{ "name": "true" } / { "name": "是" } / { "name": "ON" }
{ "name": "Yes" } / { "name": "No" }
6. 不要让 input 注入路径和读取路径错位
input 的 {name} 占位符既可以注入 OCR expected,也可以注入 action.param.custom_action_param。选择哪条路径取决于最终消费方:普通 OCR/配置读取用实际节点字段,CustomAction 执行参数用 argv.custom_action_param。不要写入一条路径却从另一条路径读取。
7. 不要用中文做 pipeline 节点名
"开启5月": { "enabled": true }
"Flag_EnableMarryTask": { "enabled": true }
中文做 option 名(用户可见),英文做 pipeline 节点名(代码访问)。混了会让代码和配置都对不上。
8. 不要在多文件 pipeline 里重复定义同名节点
parse_and_override_once 合并所有 pipeline JSON 时严格拒绝重复顶层 key。检查方法:
grep -rn "^\s*\"YourNodeName\":" assets/resource/base/pipeline/
两个文件都定义同一个顶层节点会直接让整个 check_resource.py 失败,且 Python json.load() 检测不出来(Python 会静默覆盖),必须用 C++ 解析器或 C++ 模拟检测。
9. 不要为了"配置统一"硬塞 Flag 节点
"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 决定
"开启自动接受佣兵": {
"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() 调度。
"GrowthTrial_Start": {
"next": [
"GrowthTrial_TeamReady",
"[JumpBack]GrowthTrial_Difficulty_Select",
"[JumpBack]GrowthTrial_Enter"
]
}
"GrowthTrial_Enter": {
"next": [
"GrowthTrial_Enter_Click",
"[JumpBack]BigMap_Activity_Resident",
"[JumpBack]BigMap_Activity"
]
}
"GrowthTrial_EnterBattle": {
"action": "Click",
"next": [
"GrowthTrial_FightStart",
"[JumpBack]GrowthTrial_TravelSelect_Boat",
"[JumpBack]GrowthTrial_TravelSelect_Walk"
]
}
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")
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 读取字段不一致
"开启克隆体": {
"type": "switch",
"cases": [
{ "name": "Yes", "pipeline_override": { "AutoSky_CloneConfig": { "enable": true } } }
]
}
self._clone_enabled = _read_expected_value(context, "AutoSky_CloneConfig")
self._clone_enabled = _node_enabled(context, "AutoSky_CloneConfig")
自检口诀:UI 写哪条路径,Python 就读哪条路径;pure override 则 Python 不读。
验证流程
改完一次完整流程,按顺序做这 4 步:
-
JSON 语法检查
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'))"
-
资源加载检查
python tools/ci/check_resource.py assets/resource/base
期望输出 All directories checked.
-
Pipeline 节点测试(可选)
data = context.get_node_data("Flag_EnableSailingFestivalPurchase")
assert data is not None, "节点未预定义"
assert "enabled" in data
-
端到端验证:用 Pipeline Testing Skill 跑一次实际流程
完整协议
更多 type 字段、嵌套 option、global_option、controller/resource 限制、{占位符} 注入机制等高级特性见 references/protocol.md。