@windsland52/maafw-live 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +24 -9
  2. package/docs/daemon-protocol.md +15 -10
  3. package/docs/keyframe-retention-contract-v0.md +23 -34
  4. package/docs/session-unify-v0.md +115 -0
  5. package/lib/client/daemon.js +45 -3
  6. package/lib/commands/annotate.js +15 -8
  7. package/lib/commands/calibrate.js +3 -3
  8. package/lib/commands/color.js +3 -3
  9. package/lib/commands/crop.js +8 -8
  10. package/lib/commands/dispatch.js +121 -0
  11. package/lib/commands/env.js +200 -6
  12. package/lib/commands/index.js +7 -1
  13. package/lib/commands/input.js +71 -33
  14. package/lib/commands/kf.js +49 -13
  15. package/lib/commands/probe.js +70 -0
  16. package/lib/commands/reco.js +285 -56
  17. package/lib/commands/repl.js +1305 -502
  18. package/lib/commands/roi-check.js +273 -0
  19. package/lib/commands/roi-compare.js +388 -0
  20. package/lib/commands/runtime.js +295 -91
  21. package/lib/commands/timing.js +28 -10
  22. package/lib/daemon/framed.mjs +1014 -184
  23. package/lib/daemon/reco_child.mjs +150 -9
  24. package/lib/flags.js +2 -0
  25. package/lib/index.js +24 -20
  26. package/lib/interface/hotkey.js +142 -0
  27. package/lib/interface/index.js +1 -1
  28. package/lib/interface/load.js +107 -17
  29. package/lib/interface/override.js +218 -34
  30. package/lib/interface/plan.js +108 -17
  31. package/lib/option-args.js +29 -0
  32. package/lib/pkg.js +36 -0
  33. package/lib/protocol.js +4 -0
  34. package/lib/runtime/actions.js +127 -12
  35. package/lib/runtime/log-watch.js +160 -0
  36. package/lib/runtime/node-source.js +100 -0
  37. package/lib/runtime/ocr-model.js +45 -0
  38. package/lib/runtime/pi-env.js +34 -0
  39. package/lib/runtime/pretask.js +217 -0
  40. package/lib/runtime/prune.js +28 -0
  41. package/lib/runtime/receipts.js +57 -0
  42. package/lib/runtime/reco-args.js +38 -0
  43. package/lib/runtime/reco-verdict.js +82 -0
  44. package/lib/runtime/roi-compare.js +198 -0
  45. package/lib/runtime/session-forward.js +79 -0
  46. package/lib/runtime/session-ipc.js +471 -0
  47. package/lib/runtime/session.js +57 -8
  48. package/lib/runtime/signal-guard.js +35 -0
  49. package/lib/types/commands/dispatch.d.ts +22 -0
  50. package/lib/types/commands/env.d.ts +19 -0
  51. package/lib/types/commands/index.d.ts +1 -1
  52. package/lib/types/commands/probe.d.ts +15 -0
  53. package/lib/types/commands/reco.d.ts +11 -7
  54. package/lib/types/commands/repl.d.ts +8 -0
  55. package/lib/types/commands/roi-check.d.ts +3 -0
  56. package/lib/types/commands/roi-compare.d.ts +3 -0
  57. package/lib/types/commands/runtime.d.ts +20 -2
  58. package/lib/types/flags.d.ts +4 -0
  59. package/lib/types/interface/hotkey.d.ts +36 -0
  60. package/lib/types/interface/index.d.ts +1 -1
  61. package/lib/types/interface/load.d.ts +84 -7
  62. package/lib/types/interface/override.d.ts +58 -13
  63. package/lib/types/interface/plan.d.ts +23 -2
  64. package/lib/types/option-args.d.ts +19 -0
  65. package/lib/types/pkg.d.ts +8 -0
  66. package/lib/types/protocol.d.ts +16 -0
  67. package/lib/types/runtime/actions.d.ts +89 -7
  68. package/lib/types/runtime/log-watch.d.ts +28 -0
  69. package/lib/types/runtime/node-source.d.ts +25 -0
  70. package/lib/types/runtime/ocr-model.d.ts +8 -0
  71. package/lib/types/runtime/pi-env.d.ts +24 -0
  72. package/lib/types/runtime/pretask.d.ts +53 -0
  73. package/lib/types/runtime/prune.d.ts +4 -0
  74. package/lib/types/runtime/receipts.d.ts +7 -0
  75. package/lib/types/runtime/reco-args.d.ts +18 -0
  76. package/lib/types/runtime/reco-verdict.d.ts +50 -0
  77. package/lib/types/runtime/roi-compare.d.ts +117 -0
  78. package/lib/types/runtime/session-forward.d.ts +10 -0
  79. package/lib/types/runtime/session-ipc.d.ts +117 -0
  80. package/lib/types/runtime/session.d.ts +26 -2
  81. package/lib/types/runtime/signal-guard.d.ts +19 -0
  82. package/lib/usage.js +3 -3
  83. package/package.json +17 -4
  84. package/skills/maafw-live/SKILL.md +32 -2
  85. package/skills/maafw-live/references/pitfalls.md +97 -4
  86. package/skills/maafw-live/references/workflows.md +5 -0
  87. package/docs/keyframe-evidence-contract-v0.md +0 -61
  88. package/lib/client/daemon.js.map +0 -1
  89. package/lib/commands/annotate.js.map +0 -1
  90. package/lib/commands/calibrate.js.map +0 -1
  91. package/lib/commands/color.js.map +0 -1
  92. package/lib/commands/crop.js.map +0 -1
  93. package/lib/commands/env.js.map +0 -1
  94. package/lib/commands/index.js.map +0 -1
  95. package/lib/commands/input.js.map +0 -1
  96. package/lib/commands/kf.js.map +0 -1
  97. package/lib/commands/reco.js.map +0 -1
  98. package/lib/commands/repl.js.map +0 -1
  99. package/lib/commands/runtime.js.map +0 -1
  100. package/lib/commands/skill.js.map +0 -1
  101. package/lib/commands/timing.js.map +0 -1
  102. package/lib/commands/version.js.map +0 -1
  103. package/lib/exec.js.map +0 -1
  104. package/lib/flags.js.map +0 -1
  105. package/lib/image.js.map +0 -1
  106. package/lib/index.js.map +0 -1
  107. package/lib/interface/index.js.map +0 -1
  108. package/lib/interface/jsonc.js.map +0 -1
  109. package/lib/interface/load.js.map +0 -1
  110. package/lib/interface/override.js.map +0 -1
  111. package/lib/interface/plan.js.map +0 -1
  112. package/lib/pkg.js.map +0 -1
  113. package/lib/protocol.js.map +0 -1
  114. package/lib/runtime/actions.js.map +0 -1
  115. package/lib/runtime/keyframes.js.map +0 -1
  116. package/lib/runtime/session.js.map +0 -1
  117. package/lib/update-check.js.map +0 -1
  118. package/lib/usage.js.map +0 -1
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * reco_child —— 识别单测的一次性子进程(崩溃隔离)。
3
3
  * 用法:node reco_child.mjs --cfg <json> --out <result.json>
4
- * cfg = { resourceDir, type, image, cases?, node?, templateImage? }
4
+ * cfg = { resourceDirs? | resourceDir, type, image, cases?, node?, nodeName?, templateImage? }
5
5
  * cases: 父进程(framed.mjs)已折叠好的参数用例列表,逐例执行
6
6
  * node : 整节点 JSON 原样透传(V1 扁平 / V2 嵌套均支持,不转换)
7
+ * nodeName: 按资源里的原名跑(不 dump 注入——dump 是缺省填满的 V2,与手写稀疏 V1 不是同一份)
7
8
  * param/sweep: 直接调用(调试)时的单例路径,与 cases 二选一
8
9
  *
9
10
  * 为什么要造一个"图像假控制器":裸 post_recognition 不产出 RecoId,于是 recognition_detail 拿不到
@@ -78,6 +79,78 @@ function strip(detail) {
78
79
  return copy
79
80
  }
80
81
 
82
+ function writeOut(payload) {
83
+ fs.writeFileSync(out, JSON.stringify(payload), "utf8")
84
+ }
85
+
86
+ /** dump 里的识别类型:V2 是 `recognition.type`,个别版本/形态可能还是 V1 的字符串。 */
87
+ function recoTypeOf(dump) {
88
+ const rec = dump && dump.recognition
89
+ if (typeof rec === "string") return rec
90
+ if (rec && typeof rec === "object") return String(rec.type ?? "")
91
+ return ""
92
+ }
93
+
94
+ function editDistance(a, b) {
95
+ if (a === b) return 0
96
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i)
97
+ for (let i = 1; i <= a.length; i++) {
98
+ const cur = [i]
99
+ for (let j = 1; j <= b.length; j++) {
100
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))
101
+ }
102
+ prev = cur
103
+ }
104
+ return prev[b.length]
105
+ }
106
+
107
+ /**
108
+ * 近邻节点名:命令拼错会给建议,节点名拼错同样要给——否则 `--node-name` 写错一个字只会得到
109
+ * "资源里没有这个节点",而正确名字就在资源里。距离阈值随名字长度放宽(长名的单个字符笔误
110
+ * 距离仍是 1;短名放宽到 2 就够,再宽会把无关节点列出来)。
111
+ */
112
+ function nearestNames(names, target, limit) {
113
+ const want = String(target)
114
+ const cap = Math.max(2, Math.min(4, Math.floor(want.length / 3)))
115
+ const scored = []
116
+ for (const n of Array.isArray(names) ? names : []) {
117
+ if (typeof n !== "string" || !n || n === want) continue
118
+ const d = editDistance(n.toLowerCase(), want.toLowerCase())
119
+ if (d <= cap) scored.push({ n, d })
120
+ }
121
+ return scored.sort((x, y) => x.d - y.d || x.n.localeCompare(y.n)).slice(0, limit).map((x) => x.n)
122
+ }
123
+
124
+ /**
125
+ * dump 里的 Custom 识别:顶层 `type: "Custom"` / V1 的 `custom_recognition`,**以及 And / Or 里嵌的
126
+ * 子识别**。只看顶层会让"And 里挂了一个 custom 子识别"落回 miss 或"识别调用报错",看起来像没命中,
127
+ * 而它其实和顶层 custom 是同一条能力边界(reco_test 不桥接 agent 子进程)。
128
+ * 返回命中位置(便于在报错里说清是哪一层),深度与条数都封顶,避免异常 dump 撑爆回执。
129
+ */
130
+ function customHits(dump) {
131
+ const hits = []
132
+ /* key 要一起带下来:字符串 `"Custom"` 只有落在**识别类型位**上才算(V1 的 `recognition` / V2 的
133
+ * `type`)。整棵子树里任何字符串都认会把 `expected: "Custom"`(OCR 要认的文字)和名字恰好叫
134
+ * Custom 的引用节点误判成 Custom 子识别。 */
135
+ const walk = (v, path, key, depth) => {
136
+ if (hits.length >= 8 || depth > 6 || v === null || v === undefined) return
137
+ if (typeof v === "string") {
138
+ if (v === "Custom" && (key === "recognition" || key === "type")) hits.push(path)
139
+ return
140
+ }
141
+ if (typeof v !== "object") return
142
+ if (Array.isArray(v)) {
143
+ v.forEach((x, i) => walk(x, path + "[" + i + "]", null, depth + 1))
144
+ return
145
+ }
146
+ if (typeof v.type === "string" && v.type === "Custom") { hits.push(path + ".type"); return }
147
+ if (v.custom_recognition !== undefined) { hits.push(path + ".custom_recognition"); return }
148
+ for (const [k, x] of Object.entries(v)) walk(x, path === "" ? k : path + "." + k, k, depth + 1)
149
+ }
150
+ walk(dump && dump.recognition, "recognition", "recognition", 0)
151
+ return hits
152
+ }
153
+
81
154
  async function main() {
82
155
  const maa = require("@maaxyz/maa-node")
83
156
  const image = toArrayBuffer(fs.readFileSync(cfg.image))
@@ -91,8 +164,31 @@ async function main() {
91
164
  await ctrl.post_connection().wait()
92
165
  if (!ctrl.connected) throw new Error("假控制器连接失败")
93
166
 
167
+ /* 资源链:interface.json 的 resource.path[](+ 控制器 attach_resource_path)按声明顺序叠加,
168
+ * 后一段覆盖前一段——与 cmdRun 的 buildResource 同一语义。只加载单包时,overlay 包里的节点改写、
169
+ * 只存在于 overlay 的模板、被 And/Or 按名引用的子节点全部缺失,测出来的就不是项目里那个节点。 */
170
+ const dirs = Array.isArray(cfg.resourceDirs) && cfg.resourceDirs.length
171
+ ? cfg.resourceDirs.map(String)
172
+ : [String(cfg.resourceDir)]
94
173
  const res = new maa.Resource()
95
- await res.post_bundle(cfg.resourceDir).wait()
174
+ /* 逐段点名失败:整链一起报只说"加载失败",看不出是哪一层读不进(原先是父进程
175
+ * probeResourceChain 先加载一遍做先验——探针挪进这里后只加载一次,失败定位不变)。 */
176
+ for (let i = 0; i < dirs.length; i++) {
177
+ const where = '第 ' + (i + 1) + '/' + dirs.length + ' 段(' + dirs[i] + ')'
178
+ let job = null
179
+ try {
180
+ job = res.post_bundle(dirs[i])
181
+ await job.wait()
182
+ } catch (e) {
183
+ writeOut({ ok: false, code: 'RESOURCE_LOAD', error: '资源链加载失败:' + where + ':' + String(e && e.message || e) })
184
+ return
185
+ }
186
+ const st = Number(job && job.status)
187
+ if (Number.isFinite(st) && st !== 3000) {
188
+ writeOut({ ok: false, code: 'RESOURCE_LOAD', error: '资源链加载失败:' + where + '返回 status=' + st + '(成功应为 3000)' })
189
+ return
190
+ }
191
+ }
96
192
  /* 面板裁出来的模板走 override_image:不必把它塞进任何资源目录,也不写用户项目。 */
97
193
  if (cfg.templateImage) res.override_image("@reco_template", toArrayBuffer(fs.readFileSync(cfg.templateImage)))
98
194
 
@@ -101,25 +197,66 @@ async function main() {
101
197
  tasker.resource = res
102
198
 
103
199
  const nodeMode = cfg.node !== undefined
200
+ const nodeNameMode = typeof cfg.nodeName === "string" && cfg.nodeName !== ""
104
201
  const node = nodeMode ? nodeEntry(cfg.node, cfg.templateImage) : null
105
- const cases = nodeMode ? [node] : casesOf(cfg)
202
+ /* --node-name:dump 只进回执给人看,**不注入识别**。dump 是 DumpTask(V2 嵌套 + 缺省值全部
203
+ * 填满),与手写稀疏 V1 JSON 不是同一份;dump 再注入会让"测的到底是不是真节点"多一层不确定性。 */
204
+ let nodeDump = null
205
+ if (nodeNameMode) {
206
+ nodeDump = res.get_node_data_parsed(cfg.nodeName)
207
+ if (!nodeDump) {
208
+ const candidates = nearestNames(res.node_list, cfg.nodeName, 3)
209
+ writeOut({
210
+ ok: false, code: "NODE_MISSING", nodeName: cfg.nodeName, resourceDirs: dirs,
211
+ ...(candidates.length ? { candidates } : {}),
212
+ error: "资源里没有名为 " + cfg.nodeName + " 的节点(只查资源合并结果,不含 interface 的 pipeline_override)",
213
+ })
214
+ return
215
+ }
216
+ /* custom / agent 节点:reco_test 不桥接 agent 子进程(run 才桥接),明确报能力边界而不是让它看起来像 miss。
217
+ * And / Or 里嵌的子识别同样算——它们会在 run_recognition 里被真的走到。 */
218
+ const customs = customHits(nodeDump)
219
+ if (customs.length > 0) {
220
+ const onlyTop = customs.length === 1
221
+ && (customs[0] === "recognition.type" || customs[0] === "recognition")
222
+ writeOut({
223
+ ok: false, code: "CUSTOM_NODE", nodeName: cfg.nodeName, nodeDump, resourceDirs: dirs, customPaths: customs,
224
+ error: onlyTop
225
+ ? "节点 " + cfg.nodeName + " 的识别是 Custom:reco_test 不桥接 custom/agent 识别(run --project 才桥接 agent 子进程)"
226
+ : "节点 " + cfg.nodeName + " 的识别含 Custom 子识别(" + customs.length + " 处:"
227
+ + customs.slice(0, 3).join(" / ") + (customs.length > 3 ? " …" : "")
228
+ + "):reco_test 不桥接 custom/agent 识别(run --project 才桥接 agent 子进程)",
229
+ })
230
+ return
231
+ }
232
+ }
233
+ const cases = nodeMode || nodeNameMode ? [node] : casesOf(cfg)
106
234
  const results = []
107
235
  let slot = null
108
236
  let started = Date.now()
109
237
 
110
238
  res.register_custom_action("@reco/run", async self => {
111
- const entry = nodeMode ? node : { recognition: cfg.type, ...(slot || {}) }
112
- if (!nodeMode && cfg.templateImage && entry.template === undefined) entry.template = "@reco_template"
239
+ /* 入口三形态:--node-name 用资源原名(无 override);--node 原样透传;--type 用折叠好的一例参数。
240
+ * param 只用于回执展示"测的是哪个/组参数"。 */
241
+ let param = null
113
242
  let detail = null
114
243
  try {
115
- detail = await self.context.run_recognition("@reco/node", image, { "@reco/node": entry })
244
+ if (nodeNameMode) {
245
+ param = { nodeName: cfg.nodeName }
246
+ detail = await self.context.run_recognition(cfg.nodeName, image)
247
+ } else {
248
+ const entry = nodeMode ? node : { recognition: cfg.type, ...(slot || {}) }
249
+ if (!nodeMode && cfg.templateImage && entry.template === undefined) entry.template = "@reco_template"
250
+ param = entry
251
+ detail = await self.context.run_recognition("@reco/node", image, { "@reco/node": entry })
252
+ }
116
253
  } catch (e) {
117
- results.push({ param: entry, ms: Date.now() - started, ok: false, error: String(e && e.message || e), detail: null })
254
+ results.push({ param, ms: Date.now() - started, ok: false, error: String(e && e.message || e), detail: null })
118
255
  return true
119
256
  }
120
257
  /* ok 只认 hit===true:miss 时 run_recognition 仍返回 detail 对象(hit:false、box 全零),
121
258
  * 旧判 !!detail 恒真——识别未中会标成命中,制造假观测证据。 */
122
- results.push({ param: entry, ms: Date.now() - started, ok: !!detail && detail.hit === true, detail: strip(detail) })
259
+ results.push({ param, ms: Date.now() - started, ok: !!detail && detail.hit === true, detail: strip(detail) })
123
260
  return true
124
261
  })
125
262
 
@@ -135,7 +272,11 @@ async function main() {
135
272
  try { ctrl.destroy() } catch (e) { /* ignore */ }
136
273
  }
137
274
 
138
- fs.writeFileSync(out, JSON.stringify({ ok: true, type: cfg.type, results }), "utf8")
275
+ writeOut({
276
+ ok: true,
277
+ ...(nodeNameMode ? { type: recoTypeOf(nodeDump), nodeName: cfg.nodeName, nodeDump } : { type: cfg.type }),
278
+ results,
279
+ })
139
280
  }
140
281
 
141
282
  main()
package/lib/flags.js CHANGED
@@ -9,6 +9,8 @@ export const GLOBAL_OPTIONS = {
9
9
  verbose: { type: 'boolean' },
10
10
  help: { type: 'boolean', short: 'h' },
11
11
  version: { type: 'boolean', short: 'V' },
12
+ /** 把命令转发到命名会话的常驻 daemon(`repl --session <name>` 起的那个) */
13
+ session: { type: 'string' },
12
14
  };
13
15
  const DEFAULT_LIMIT = 200;
14
16
  /**
package/lib/index.js CHANGED
@@ -15,9 +15,11 @@ import { resolve } from 'node:path';
15
15
  import { pathToFileURL } from 'node:url';
16
16
  import { parseArgs } from 'node:util';
17
17
  import { findCommand, suggestCommand } from './commands/index.js';
18
+ import { dispatchArgv } from './commands/dispatch.js';
18
19
  import { GLOBAL_OPTIONS, readGlobalFlags } from './flags.js';
19
20
  import { cliPackage } from './pkg.js';
20
21
  import { EXIT, emit, fail } from './protocol.js';
22
+ import { forwardToSession, stripSessionFlag } from './runtime/session-forward.js';
21
23
  import { commandUsage, topLevelUsage } from './usage.js';
22
24
  function print(lines, toStderr = false) {
23
25
  const text = lines.join('\n') + '\n';
@@ -72,28 +74,28 @@ export async function main(argv) {
72
74
  print(topLevelUsage(), true);
73
75
  return EXIT.USAGE;
74
76
  }
75
- // 解析选项:全局 + 命令私有
76
- const options = { ...GLOBAL_OPTIONS, ...(cmd.options ?? {}) };
77
- let parsed;
77
+ /* cwd 与 --json/--color 先粗解析(全局选项集即可,命令私有选项不在这层消费)。
78
+ * --session 转发发生在 dispatchArgv **之前**:会话侧跑的是同一份命令实现,
79
+ * 本地不该为"转发给会话的命令"先新起一个 daemon 再丢(requiresSession 闸门
80
+ * 在会话侧才有意义——ctx.client 已注入时不拦)。 */
81
+ let rough;
78
82
  try {
79
- parsed = parseArgs({
83
+ rough = parseArgs({
80
84
  args: argv.slice(1),
81
- options,
85
+ options: GLOBAL_OPTIONS,
82
86
  allowPositionals: true,
83
- strict: true,
87
+ strict: false,
84
88
  });
85
89
  }
86
- catch (e) {
87
- const result = fail('BAD_ARGUMENTS', message(e), `用法:${cmd.usage}`, EXIT.USAGE);
88
- emit(cmd.name, result, { json: argv.includes('--json'), color: false });
89
- return EXIT.USAGE;
90
+ catch {
91
+ /* 全局层解析不过就让 dispatchArgv 走完整错误路径(BAD_ARGUMENTS 信封) */
92
+ rough = { values: {}, positionals: argv.slice(1) };
90
93
  }
91
- // cwd 先解析,因为 --json 之外的所有输出都要用到它
92
94
  let cwd = process.cwd();
93
- if (typeof parsed.values.cwd === 'string' && parsed.values.cwd !== '') {
94
- cwd = resolve(parsed.values.cwd);
95
+ if (typeof rough.values.cwd === 'string' && rough.values.cwd !== '') {
96
+ cwd = resolve(rough.values.cwd);
95
97
  }
96
- const flags = readGlobalFlags(parsed.values, cwd);
98
+ const flags = readGlobalFlags(rough.values, cwd);
97
99
  if (flags.help) {
98
100
  print(commandUsage(cmd));
99
101
  return EXIT.OK;
@@ -103,13 +105,15 @@ export async function main(argv) {
103
105
  emit(cmd.name, result, { json: flags.json, color: flags.color });
104
106
  return EXIT.ENV;
105
107
  }
106
- let result;
107
- try {
108
- result = await cmd.run({ positionals: parsed.positionals, values: parsed.values, flags, cwd });
109
- }
110
- catch (e) {
111
- result = fail('UNEXPECTED', message(e), '这是未预期异常,请把 --json 输出一并反馈', EXIT.FAIL);
108
+ /* `--session <name>`:把整条 argv(剥掉 --session 本体)送进常驻会话执行。
109
+ * `repl` 自己的 --session 是"起/查/收会话"的参数,不是转发旗标,不能拦。 */
110
+ const sessionName = typeof rough.values.session === 'string' ? rough.values.session : undefined;
111
+ if (sessionName && cmd.name !== 'repl') {
112
+ const result = await forwardToSession(sessionName, stripSessionFlag(argv));
113
+ emit(cmd.name, result, { json: flags.json, color: flags.color });
114
+ return result.exitCode;
112
115
  }
116
+ const { result } = await dispatchArgv(argv, { cwd });
113
117
  emit(cmd.name, result, { json: flags.json, color: flags.color });
114
118
  return result.exitCode;
115
119
  }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * hotkey 占位符 → 当前控制器的虚拟按键码(PI v2.8.0+)。
3
+ *
4
+ * 协议只给规则、不给表:`{名称}` / `{名称}.primary` / `{名称}.modifier1` / `{名称}.modifier2`
5
+ * 要替换成"**当前控制器 type** 支持的虚拟按键码",且"映射规则应与 ClickKey、KeyDown 等动作所用
6
+ * 键码表一致"(MaaFW 5.14.2 `3.3-ProjectInterfaceV2协议`,pinned commit 8061d5bc)。
7
+ *
8
+ * 这里四张表的数值是各 OS **公开的键码常量**,不是谁发明的东西:
9
+ * - `Win32` → Windows Virtual-Key Codes(`VK_*`,见 winuser.h)
10
+ * - `Adb` → Android `KeyEvent`(`KEYCODE_*`)
11
+ * - `MacOS` → Carbon `Events.h`(`kVK_*`,ANSI 键盘物理位置)
12
+ * - `WlRoots`/`Linux` → Linux `input-event-codes.h`(`KEY_*`/evdev)
13
+ *
14
+ * 取值与 MXU 的实现交叉核对过(MXU 是 AGPL-3.0,本仓 MIT:**只核数值,不搬代码**)。
15
+ * 键名按 MXU 的持久化约定规范化(大写、`+` 连接、末段为主键),因为它就是协议里"人类可读
16
+ * 快捷键字符串"的实际来源之一。
17
+ */
18
+ const WIN32 = {
19
+ BACKSPACE: 0x08, TAB: 0x09, ENTER: 0x0d, SHIFT: 0x10, CTRL: 0x11, ALT: 0x12,
20
+ META: 0x5b, COMMAND: 0x5b, CMD: 0x5b, PAUSE: 0x13, CAPSLOCK: 0x14, ESC: 0x1b, SPACE: 0x20,
21
+ PAGEUP: 0x21, PAGEDOWN: 0x22, END: 0x23, HOME: 0x24,
22
+ LEFT: 0x25, UP: 0x26, RIGHT: 0x27, DOWN: 0x28, INSERT: 0x2d, DELETE: 0x2e,
23
+ '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34,
24
+ '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39,
25
+ A: 0x41, B: 0x42, C: 0x43, D: 0x44, E: 0x45, F: 0x46, G: 0x47, H: 0x48, I: 0x49,
26
+ J: 0x4a, K: 0x4b, L: 0x4c, M: 0x4d, N: 0x4e, O: 0x4f, P: 0x50, Q: 0x51, R: 0x52,
27
+ S: 0x53, T: 0x54, U: 0x55, V: 0x56, W: 0x57, X: 0x58, Y: 0x59, Z: 0x5a,
28
+ F1: 0x70, F2: 0x71, F3: 0x72, F4: 0x73, F5: 0x74, F6: 0x75,
29
+ F7: 0x76, F8: 0x77, F9: 0x78, F10: 0x79, F11: 0x7a, F12: 0x7b,
30
+ };
31
+ const MACOS = {
32
+ BACKSPACE: 0x33, TAB: 0x30, ENTER: 0x24, SHIFT: 0x38, CTRL: 0x3b, ALT: 0x3a,
33
+ META: 0x37, COMMAND: 0x37, CMD: 0x37, CAPSLOCK: 0x39, ESC: 0x35, SPACE: 0x31,
34
+ PAGEUP: 0x74, PAGEDOWN: 0x79, END: 0x77, HOME: 0x73,
35
+ LEFT: 0x7b, UP: 0x7e, RIGHT: 0x7c, DOWN: 0x7d, DELETE: 0x75,
36
+ '0': 0x1d, '1': 0x12, '2': 0x13, '3': 0x14, '4': 0x15,
37
+ '5': 0x17, '6': 0x16, '7': 0x1a, '8': 0x1c, '9': 0x19,
38
+ A: 0x00, B: 0x0b, C: 0x08, D: 0x02, E: 0x0e, F: 0x03, G: 0x05, H: 0x04, I: 0x22,
39
+ J: 0x26, K: 0x28, L: 0x25, M: 0x2e, N: 0x2d, O: 0x1f, P: 0x23, Q: 0x0c, R: 0x0f,
40
+ S: 0x01, T: 0x11, U: 0x20, V: 0x09, W: 0x0d, X: 0x07, Y: 0x10, Z: 0x06,
41
+ F1: 0x7a, F2: 0x78, F3: 0x63, F4: 0x76, F5: 0x60, F6: 0x61,
42
+ F7: 0x62, F8: 0x64, F9: 0x65, F10: 0x6d, F11: 0x67, F12: 0x6f,
43
+ };
44
+ const ADB = {
45
+ BACKSPACE: 67, TAB: 61, ENTER: 66, SHIFT: 59, CTRL: 113, ALT: 57,
46
+ META: 117, COMMAND: 117, CMD: 117, SPACE: 62, ESC: 111, DELETE: 112,
47
+ HOME: 3, END: 123, PAGEUP: 92, PAGEDOWN: 93,
48
+ LEFT: 21, RIGHT: 22, UP: 19, DOWN: 20,
49
+ '0': 7, '1': 8, '2': 9, '3': 10, '4': 11, '5': 12, '6': 13, '7': 14, '8': 15, '9': 16,
50
+ A: 29, B: 30, C: 31, D: 32, E: 33, F: 34, G: 35, H: 36, I: 37, J: 38, K: 39, L: 40,
51
+ M: 41, N: 42, O: 43, P: 44, Q: 45, R: 46, S: 47, T: 48, U: 49, V: 50, W: 51, X: 52,
52
+ Y: 53, Z: 54,
53
+ F1: 131, F2: 132, F3: 133, F4: 134, F5: 135, F6: 136, F7: 137, F8: 138, F9: 139,
54
+ F10: 140, F11: 141, F12: 142,
55
+ };
56
+ const WLROOTS = {
57
+ BACKSPACE: 14, TAB: 15, ENTER: 28, SHIFT: 42, CTRL: 29, ALT: 56,
58
+ META: 125, COMMAND: 125, CMD: 125, SPACE: 57, ESC: 1, DELETE: 111,
59
+ HOME: 102, END: 107, PAGEUP: 104, PAGEDOWN: 109,
60
+ LEFT: 105, RIGHT: 106, UP: 103, DOWN: 108,
61
+ '0': 11, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6, '6': 7, '7': 8, '8': 9, '9': 10,
62
+ A: 30, B: 48, C: 46, D: 32, E: 18, F: 33, G: 34, H: 35, I: 23, J: 36, K: 37, L: 38,
63
+ M: 50, N: 49, O: 24, P: 25, Q: 16, R: 19, S: 31, T: 20, U: 22, V: 47, W: 17, X: 45,
64
+ Y: 21, Z: 44,
65
+ F1: 59, F2: 60, F3: 61, F4: 62, F5: 63, F6: 64, F7: 65, F8: 66, F9: 67, F10: 68,
66
+ F11: 87, F12: 88,
67
+ };
68
+ /** 同义键名归一:用户手写的组合键五花八门,认得出就认,认不出会在下面报"未知按键"。 */
69
+ const ALIASES = {
70
+ CONTROL: 'CTRL', ESCAPE: 'ESC', RETURN: 'ENTER', DEL: 'DELETE', INS: 'INSERT',
71
+ PGUP: 'PAGEUP', PGDN: 'PAGEDOWN', WIN: 'META', WINDOWS: 'META', SUPER: 'META',
72
+ SPACEBAR: 'SPACE', CAPS: 'CAPSLOCK', OPTION: 'ALT',
73
+ };
74
+ /** 按控制器 **type** 取表(不是 name)。没有表的类型返回 null——调用方要告警,不能退回 Win32 猜。 */
75
+ export function hotkeyTableFor(controllerType, useWin32VkCode = false) {
76
+ switch (controllerType) {
77
+ case 'Win32':
78
+ return WIN32;
79
+ case 'Adb':
80
+ return ADB;
81
+ case 'MacOS':
82
+ return MACOS;
83
+ case 'WlRoots':
84
+ return WLROOTS;
85
+ /* Linux 控制器可声明 `linux.use_win32_vk_code`:true 时服务端按 Win32 VK 解释,false 用 evdev */
86
+ case 'Linux':
87
+ return useWin32VkCode ? WIN32 : WLROOTS;
88
+ default:
89
+ return null;
90
+ }
91
+ }
92
+ function normalizeKey(name) {
93
+ const up = name.trim().toUpperCase();
94
+ return ALIASES[up] ?? up;
95
+ }
96
+ /** "Ctrl+Shift+A" → { primary: "A", modifiers: ["Ctrl","Shift"] };末段为主键。 */
97
+ export function splitHotkeyCombo(value) {
98
+ const parts = value.split('+').map((p) => p.trim()).filter(Boolean);
99
+ if (!parts.length)
100
+ return { primary: '', modifiers: [] };
101
+ return { primary: parts[parts.length - 1], modifiers: parts.slice(0, -1) };
102
+ }
103
+ /**
104
+ * 把一个 hotkey 字段解析成四种占位符的取值。
105
+ *
106
+ * 缺修饰键不是错(单键 "E" 本来就没有修饰键),补 0 = 无按键;**主键查不到是错**——
107
+ * 写个 0 进去等于"按了空键",任务会静默点不动。
108
+ */
109
+ export function resolveHotkeyTokens(fieldName, raw, table) {
110
+ const { primary, modifiers } = splitHotkeyCombo(raw);
111
+ if (!primary)
112
+ return { tokens: {}, error: '取值为空' };
113
+ const code = table[normalizeKey(primary)];
114
+ if (code === undefined) {
115
+ return { tokens: {}, error: '主键 ' + primary + ' 不在该控制器的键码表里(' + raw + ')' };
116
+ }
117
+ const mod = (name) => {
118
+ if (!name)
119
+ return 0;
120
+ const c = table[normalizeKey(name)];
121
+ /* 修饰键查不到也按"无"处理会让组合键变成单键——那是静默错,直接报错 */
122
+ if (c === undefined)
123
+ throw new Error('修饰键 ' + name + ' 不在该控制器的键码表里(' + raw + ')');
124
+ return c;
125
+ };
126
+ try {
127
+ const m1 = mod(modifiers[0]);
128
+ const m2 = mod(modifiers[1]);
129
+ return {
130
+ tokens: {
131
+ [fieldName]: code,
132
+ [fieldName + '.primary']: code,
133
+ [fieldName + '.modifier1']: m1,
134
+ [fieldName + '.modifier2']: m2,
135
+ },
136
+ };
137
+ }
138
+ catch (e) {
139
+ return { tokens: {}, error: String(e.message || e) };
140
+ }
141
+ }
142
+ //# sourceMappingURL=hotkey.js.map
@@ -8,5 +8,5 @@
8
8
  export { parseJsonc } from './jsonc.js';
9
9
  export { loadInterface, findInterfaceFile, } from './load.js';
10
10
  export { planController, resolveResourcePaths, } from './plan.js';
11
- export { computePipelineOverride, mergeOverride } from './override.js';
11
+ export { computePipelineOverride, appendOverride } from './override.js';
12
12
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * 载入项目 interface.json(ProjectInterface v2)。
3
3
  *
4
- * 只做本工具需要的窄解析:控制器声明(连接规划用)、资源声明(路径解析用)、任务入口
5
- * (run --project 用)。其余字段(task 参数、option、i18n 等)不属于 CLI 的职责,原样忽略。
4
+ * 解析面就是本工具真正消费的那部分:控制器(连接规划)、资源(路径解析)、任务入口
5
+ * (run --project)、option / preset / global_option(pipeline_override 四级链)、
6
+ * agent 与 pretask(子进程相关)、import(v2.2.0 合并规则)。
7
+ *
8
+ * 明确**不**解析、不消费的:task 级 pipeline_override(本工具只合成 option 链,任务级写死的
9
+ * 覆盖要由 Client 应用)、i18n(languages 与 `$` 键——当前不输出人类标签)、group / setting /
10
+ * telemetry、resource.hash(v2.6.0 的一致性校验,未实现)。
6
11
  * 全过程不抛异常:问题进 problems[],由调用方决定怎么呈现。
7
12
  */
8
13
  import { existsSync, readFileSync } from 'node:fs';
@@ -41,6 +46,7 @@ export function loadInterface(dir) {
41
46
  file,
42
47
  dir: file ? dirname(file) : null,
43
48
  version: null,
49
+ projectVersion: null,
44
50
  name: null,
45
51
  controllers: [],
46
52
  resources: [],
@@ -97,10 +103,20 @@ export function loadInterface(dir) {
97
103
  }
98
104
  const w = obj(c.win32);
99
105
  const g = obj(c.gamepad);
106
+ const mo = obj(c.macos);
107
+ const linuxDecl = obj(c.linux);
100
108
  controllers.push({
101
109
  name,
102
110
  type,
111
+ raw: c,
103
112
  display: Object.keys(display).length ? display : undefined,
113
+ macos: mo
114
+ ? {
115
+ titleRegex: str(mo.title_regex),
116
+ screencap: str(mo.screencap),
117
+ input: str(mo.input),
118
+ }
119
+ : undefined,
104
120
  win32: w
105
121
  ? {
106
122
  classRegex: str(w.class_regex),
@@ -120,6 +136,17 @@ export function loadInterface(dir) {
120
136
  : undefined,
121
137
  attachResourcePath: strList(c.attach_resource_path),
122
138
  option: strList(c.option),
139
+ ...(linuxDecl
140
+ ? {
141
+ linux: {
142
+ ...(linuxDecl.use_win32_vk_code === true ? { useWin32VkCode: true } : {}),
143
+ ...(str(linuxDecl.screencap) ? { screencap: str(linuxDecl.screencap) } : {}),
144
+ ...(str(linuxDecl.input) ? { input: str(linuxDecl.input) } : {}),
145
+ ...(str(linuxDecl.pipewire_source) ? { pipewireSource: str(linuxDecl.pipewire_source) } : {}),
146
+ },
147
+ }
148
+ : {}),
149
+ ...(c.permission_required === true ? { permissionRequired: true } : {}),
123
150
  });
124
151
  }
125
152
  if (!controllers.length)
@@ -133,7 +160,10 @@ export function loadInterface(dir) {
133
160
  problems.push({ message: 'resource 项缺少 name 或 path,已跳过' });
134
161
  continue;
135
162
  }
136
- resources.push({ name, paths, controllers: strList(r.controller), option: strList(r.option) });
163
+ resources.push({
164
+ name, paths, controllers: strList(r.controller), option: strList(r.option),
165
+ ...(r.hash !== undefined ? { hash: str(r.hash) } : {}), raw: r,
166
+ });
137
167
  }
138
168
  const tasks = [];
139
169
  const options = {};
@@ -147,7 +177,7 @@ export function loadInterface(dir) {
147
177
  const exec = a ? str(a.child_exec) : undefined;
148
178
  if (!a || !exec)
149
179
  continue;
150
- agents.push({ exec, args: strList(a.child_args) });
180
+ agents.push({ exec, args: strList(a.child_args), ...(str(a.identifier) !== undefined ? { identifier: str(a.identifier) } : {}) });
151
181
  }
152
182
  };
153
183
  const readTasks = (list, src) => {
@@ -158,12 +188,14 @@ export function loadInterface(dir) {
158
188
  problems.push({ message: 'task 项缺少 name,已跳过(' + src + ')' });
159
189
  continue;
160
190
  }
191
+ const po = obj(t.pipeline_override);
161
192
  tasks.push({
162
193
  name,
163
194
  entry: str(t.entry),
164
195
  controller: strList(t.controller),
165
196
  resource: strList(t.resource),
166
197
  option: strList(t.option),
198
+ ...(po ? { pipelineOverride: po } : {}),
167
199
  });
168
200
  }
169
201
  };
@@ -176,7 +208,10 @@ export function loadInterface(dir) {
176
208
  }
177
209
  for (const [key, val] of Object.entries(d)) {
178
210
  const v = obj(val);
179
- if (!v || !str(v.type)) {
211
+ /* type 缺省按 select(上游 PI 文档与 Types.h:212 的默认);仍记 problem,
212
+ * 因为它大概率是漏写而非有意缺省——警告可见,行为对齐上游。 */
213
+ const type = v ? (str(v.type) ?? 'select') : undefined;
214
+ if (!v || !type) {
180
215
  problems.push({ message: 'option ' + key + ' 缺少 type,已跳过(' + src + ')' });
181
216
  continue;
182
217
  }
@@ -193,13 +228,55 @@ export function loadInterface(dir) {
193
228
  option: strList(c2.option),
194
229
  });
195
230
  }
231
+ const tpl = obj(v.pipeline_override);
232
+ const inputs = [];
233
+ for (const it of Array.isArray(v.inputs) ? v.inputs : []) {
234
+ const f = obj(it);
235
+ const fname = f ? str(f.name) : undefined;
236
+ if (!f || !fname)
237
+ continue;
238
+ const pwd = f.password === true;
239
+ inputs.push({
240
+ name: fname,
241
+ ...(str(f.default) !== undefined && !pwd ? { default: str(f.default) } : {}),
242
+ ...(str(f.pipeline_type) !== undefined ? { pipelineType: str(f.pipeline_type) } : {}),
243
+ ...(str(f.verify) !== undefined ? { verify: str(f.verify) } : {}),
244
+ ...(pwd ? { password: true } : {}),
245
+ });
246
+ if (pwd && str(f.default) !== undefined) {
247
+ problems.push({ message: 'option ' + key + ' 的输入字段 ' + fname + ':password=true 时规范禁止同时设 default,default 已忽略' });
248
+ }
249
+ }
250
+ const hotkeys = [];
251
+ for (const it of Array.isArray(v.hotkeys) ? v.hotkeys : []) {
252
+ const f = obj(it);
253
+ const fname = f ? str(f.name) : undefined;
254
+ if (!f || !fname)
255
+ continue;
256
+ hotkeys.push({ name: fname, ...(str(f.default) !== undefined ? { default: str(f.default) } : {}) });
257
+ }
258
+ const minC = num(v.min_count);
259
+ const maxC = num(v.max_count);
260
+ if (type === 'checkbox') {
261
+ if (minC !== undefined && maxC !== undefined && minC > maxC) {
262
+ problems.push({ message: 'option ' + key + ':min_count=' + minC + ' > max_count=' + maxC + ',约束自相矛盾' });
263
+ }
264
+ if (minC !== undefined && cases.length && minC > cases.length) {
265
+ problems.push({ message: 'option ' + key + ':min_count=' + minC + ' 超过声明的 case 数 ' + cases.length + ',永远选不够' });
266
+ }
267
+ }
196
268
  options[key] = {
197
269
  name: key,
198
- type: String(v.type),
270
+ type,
199
271
  cases,
272
+ ...(tpl ? { pipelineOverride: tpl } : {}),
273
+ ...(inputs.length ? { inputs } : {}),
274
+ ...(hotkeys.length ? { hotkeys } : {}),
200
275
  ...(v.default_case !== undefined ? { defaultCase: v.default_case } : {}),
201
276
  controller: strList(v.controller),
202
277
  resource: strList(v.resource),
278
+ ...(type === 'checkbox' && minC !== undefined ? { minCount: minC } : {}),
279
+ ...(type === 'checkbox' && maxC !== undefined ? { maxCount: maxC } : {}),
203
280
  };
204
281
  }
205
282
  };
@@ -228,12 +305,28 @@ export function loadInterface(dir) {
228
305
  for (const g of strList(o.global_option))
229
306
  if (!globalOption.includes(g))
230
307
  globalOption.push(g);
231
- const mainPretasks = Array.isArray(o.pretask) ? o.pretask : (o.pretask !== undefined ? [o.pretask] : []);
232
- for (const pt of mainPretasks) {
233
- const p2 = obj(pt);
234
- if (p2)
235
- pretask.push({ name: str(p2.name), exec: str(p2.exec) });
236
- }
308
+ /* pretask:`exec` 必填;`args` / `option` / 过滤列表缺省为空。单条也可写成对象而不是数组。 */
309
+ const readPretasks = (list, src) => {
310
+ for (const pt of Array.isArray(list) ? list : (list !== undefined ? [list] : [])) {
311
+ const p2 = obj(pt);
312
+ const exec = p2 ? str(p2.exec) : undefined;
313
+ if (!p2)
314
+ continue;
315
+ if (!exec) {
316
+ problems.push({ message: 'pretask 缺少 exec,已跳过(' + src + ')' });
317
+ continue;
318
+ }
319
+ pretask.push({
320
+ ...(str(p2.name) !== undefined ? { name: str(p2.name) } : {}),
321
+ exec,
322
+ args: strList(p2.args),
323
+ option: strList(p2.option),
324
+ ...(strList(p2.controller).length ? { controller: strList(p2.controller) } : {}),
325
+ ...(strList(p2.resource).length ? { resource: strList(p2.resource) } : {}),
326
+ });
327
+ }
328
+ };
329
+ readPretasks(o.pretask, '主文件');
237
330
  /* import 合并(v2.2.0):只合 task/option/preset/global_option/pretask(及 group/setting,本工具不消费)。
238
331
  * controller/resource 不可导入——子文件声明了也忽略并记 problems。循环导入用 visited 集合挡住。
239
332
  *
@@ -283,15 +376,12 @@ export function loadInterface(dir) {
283
376
  for (const g of strList(child.global_option))
284
377
  if (!globalOption.includes(g))
285
378
  globalOption.push(g);
286
- for (const pt of Array.isArray(child.pretask) ? child.pretask : []) {
287
- const p2 = obj(pt);
288
- if (p2)
289
- pretask.push({ name: str(p2.name), exec: str(p2.exec) });
290
- }
379
+ readPretasks(child.pretask, rel);
291
380
  }
292
381
  return {
293
382
  ...base,
294
383
  version: version ?? null,
384
+ projectVersion: str(o.version) ?? null,
295
385
  name: str(o.name) ?? null,
296
386
  controllers,
297
387
  resources,