@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,25 +1,93 @@
1
+ import { hotkeyTableFor, resolveHotkeyTokens } from './hotkey.js';
1
2
  /**
2
- * 把一份 pipeline_override 并入累计结果:同节点字段替换(与协议"不深度合并"一致)。
3
+ * 把调用方的覆盖(`--override`)追加到分层数组尾部 = 最高优先级。
4
+ * 对象 = 一层;数组 = 逐层(框架本来就接受数组,逐层继承,别在这里预合并)。
5
+ * 四级链、`run --override`、`timing --override` 共用这一个追加口——三处曾经各写一份,
6
+ * 代价是同一个参数在不同命令里表现不同。
7
+ */
8
+ export function appendOverride(layers, ov) {
9
+ if (!ov)
10
+ return;
11
+ const list = Array.isArray(ov) ? ov : [ov];
12
+ for (const layer of list)
13
+ if (layer && typeof layer === 'object' && Object.keys(layer).length)
14
+ layers.push(layer);
15
+ }
16
+ const PLACEHOLDER = /\{([^{}]+)\}/g;
17
+ /**
18
+ * 按 `pipeline_type` 转型。枚举只有 `string` / `int` / `bool`(没有 float)。
19
+ *
20
+ * 协议只写了"会根据该类型进行类型转换",**没规定** int 遇到 `abc`、bool 接受哪些字面量 ——
21
+ * 这里的规则是本地约定,宁可转不了就整条模板放弃,也不猜一个值写进 pipeline。
22
+ */
23
+ function convert(raw, type) {
24
+ if (type === 'int')
25
+ return /^-?\d+$/.test(raw.trim()) ? Number(raw.trim()) : undefined;
26
+ if (type === 'bool') {
27
+ const s = raw.trim().toLowerCase();
28
+ if (s === 'true' || s === '1')
29
+ return true;
30
+ if (s === 'false' || s === '0')
31
+ return false;
32
+ return undefined;
33
+ }
34
+ return raw;
35
+ }
36
+ /**
37
+ * 解析一个 option 的当前取值(**唯一一份实现**):pipeline_override 合成与 pretask 的末位 JSON
38
+ * 都走这里,两边各写一套迟早会在"preset 的 input 值是对象还是字符串"上分叉。
3
39
  *
4
- * **唯一一份实现**:四级链、`run --override`、`timing --override` 都走这里。这三处曾经各写一份,
5
- * 代价是同一个参数在不同命令里表现不同。非对象值必须**原样替换、不能展开**:字符串会被摊成
6
- * `{0:'a',1:'b'}`,`null` / 数字会静默变成空对象——等于把用户写的节点悄悄丢掉。
40
+ * 优先级(协议未规定,本地约定):CLI > preset > 声明里的初始值(`default_case` / `inputs[].default`)。
7
41
  */
8
- export function mergeOverride(acc, po) {
9
- for (const [node, fields] of Object.entries(po)) {
10
- if (fields && typeof fields === 'object' && !Array.isArray(fields)) {
11
- acc[node] = { ...(acc[node] ?? {}), ...fields };
42
+ export function resolveOptionValue(o, ctx) {
43
+ if (o.type === 'input' || o.type === 'hotkey') {
44
+ const fields = {};
45
+ let via = 'declared';
46
+ for (const f of [...(o.inputs ?? []), ...(o.hotkeys ?? [])]) {
47
+ if (f.default !== undefined)
48
+ fields[f.name] = f.default;
49
+ }
50
+ const pv = ctx.presetTask?.option?.[o.name];
51
+ if (pv && typeof pv === 'object' && !Array.isArray(pv)) {
52
+ for (const [k, v] of Object.entries(pv))
53
+ fields[k] = String(v);
54
+ if (Object.keys(pv).length)
55
+ via = 'preset';
12
56
  }
13
- else {
14
- acc[node] = fields;
57
+ const cli = ctx.optionFields?.[o.name];
58
+ if (cli) {
59
+ for (const [k, v] of Object.entries(cli))
60
+ fields[k] = v;
61
+ if (Object.keys(cli).length)
62
+ via = 'cli';
15
63
  }
64
+ return { kind: 'fields', fields, via };
16
65
  }
66
+ const cli = ctx.optionCases?.[o.name];
67
+ if (cli !== undefined) {
68
+ return { kind: 'cases', cases: Array.isArray(cli) ? cli.map(String) : [String(cli)], via: 'cli' };
69
+ }
70
+ const pv = ctx.presetTask?.option?.[o.name];
71
+ /* preset 里 input 的值是对象——这里只处理 case 型,对象值留给出上面的分支 */
72
+ if (pv !== undefined && !(pv && typeof pv === 'object' && !Array.isArray(pv))) {
73
+ return { kind: 'cases', cases: Array.isArray(pv) ? pv.map(String) : [String(pv)], via: 'preset' };
74
+ }
75
+ if (o.defaultCase !== undefined) {
76
+ return {
77
+ kind: 'cases',
78
+ cases: Array.isArray(o.defaultCase) ? o.defaultCase.map(String) : [String(o.defaultCase)],
79
+ via: 'default',
80
+ };
81
+ }
82
+ return null;
17
83
  }
18
84
  export function computePipelineOverride(input) {
19
85
  const { loaded, controllerName, resourceName, taskName, presetName } = input;
20
86
  const warns = [];
21
87
  const applied = [];
22
- const acc = {};
88
+ /** 进过链的 option 键:用来抓"--option 给了值但没有任何级别引用它"这种静默失联 */
89
+ const reached = new Set();
90
+ const layers = [];
23
91
  const preset = presetName ? loaded.presets.find((p) => p.name === presetName) : undefined;
24
92
  if (presetName && !preset)
25
93
  warns.push('preset ' + presetName + ' 不存在(可用:' + loaded.presets.map((p) => p.name).join(' / ') + '或无)');
@@ -31,21 +99,47 @@ export function computePipelineOverride(input) {
31
99
  return false;
32
100
  return true;
33
101
  };
34
- /** 解析一个 option 的当前取值:preset > default_case;返回 case 名(单个或数组)。 */
35
- const valueOf = (name) => {
36
- const o = loaded.options[name];
37
- if (!o) {
38
- warns.push('option ' + name + ' 未在 interface 中定义,已跳过');
39
- return null;
40
- }
41
- const pv = presetTask?.option?.[name];
42
- if (pv !== undefined) {
43
- return { cases: Array.isArray(pv) ? pv.map(String) : [String(pv)], via: 'preset' };
44
- }
45
- if (o.defaultCase !== undefined) {
46
- return { cases: Array.isArray(o.defaultCase) ? o.defaultCase.map(String) : [String(o.defaultCase)], via: 'default' };
47
- }
48
- return null;
102
+ const valueCtx = {
103
+ ...(presetTask ? { presetTask } : {}),
104
+ ...(input.optionCases ? { optionCases: input.optionCases } : {}),
105
+ ...(input.optionFields ? { optionFields: input.optionFields } : {}),
106
+ };
107
+ /**
108
+ * 把模板里的 `{字段名}` 换成取值:整串是占位时用转型后的值(int → 数字),
109
+ * 出现在片段里时用它的文本形态(如 `MainChapter_{章节号}`)。
110
+ * 有任何一个占位取不到值就整条模板放弃——写一半的 override 比不写更难查。
111
+ */
112
+ const substitute = (tpl, tokens) => {
113
+ const missing = [];
114
+ const one = (name) => {
115
+ if (!(name in tokens)) {
116
+ if (!missing.includes(name))
117
+ missing.push(name);
118
+ return undefined;
119
+ }
120
+ return tokens[name];
121
+ };
122
+ const walk = (n) => {
123
+ if (typeof n === 'string') {
124
+ const whole = /^\{([^{}]+)\}$/.exec(n);
125
+ if (whole)
126
+ return one(whole[1]);
127
+ return n.replace(PLACEHOLDER, (_m, name) => {
128
+ const v = one(name);
129
+ return v === undefined ? '' : String(v);
130
+ });
131
+ }
132
+ if (Array.isArray(n))
133
+ return n.map(walk);
134
+ if (n && typeof n === 'object') {
135
+ const out = {};
136
+ for (const [k, v] of Object.entries(n))
137
+ out[k] = walk(v);
138
+ return out;
139
+ }
140
+ return n;
141
+ };
142
+ return { value: walk(tpl), missing };
49
143
  };
50
144
  const mergeOption = (name, level, chain = []) => {
51
145
  /* 嵌套 option 是"case 里再列 option",A → B → A 这类自引用会让递归停不下来:
@@ -55,17 +149,95 @@ export function computePipelineOverride(input) {
55
149
  return;
56
150
  }
57
151
  const o = loaded.options[name];
58
- if (o && !applicable(o))
152
+ if (!o) {
153
+ warns.push('option ' + name + ' 未在 interface 中定义,已跳过');
59
154
  return;
60
- const v = valueOf(name);
61
- if (!o)
155
+ }
156
+ reached.add(name);
157
+ if (!applicable(o)) {
158
+ if (input.optionCases?.[name] !== undefined || input.optionFields?.[name] !== undefined) {
159
+ warns.push('option ' + name + ' 不适用于当前控制器/资源(适用性过滤),--option 给的取值未生效');
160
+ }
161
+ return;
162
+ }
163
+ if (o.type === 'input' || o.type === 'hotkey') {
164
+ const tpl = o.pipelineOverride;
165
+ if (!tpl || !Object.keys(tpl).length) {
166
+ /* 没有模板 = 没有东西可丢。但如果有人专门给它传了 --option,那是一次静默失联,要说。 */
167
+ if (input.optionFields?.[o.name] !== undefined) {
168
+ warns.push('option ' + o.name + ' 声明为 ' + o.type + ' 但没有 pipeline_override 模板,--option 给的字段不参与覆盖');
169
+ }
170
+ return;
171
+ }
172
+ const resolved = resolveOptionValue(o, valueCtx);
173
+ const fields = resolved && resolved.kind === 'fields' ? resolved.fields : {};
174
+ const tokens = {};
175
+ if (o.type === 'hotkey') {
176
+ /* 占位符产出的是**当前控制器 type 的虚拟按键码整数**(Win32 VK / Android KeyEvent /
177
+ * Carbon kVK / evdev)。没有表的控制器类型直接跳过并说明——按 Win32 猜等于写错键。 */
178
+ const decl = controllerName ? loaded.controllers.find((x) => x.name === controllerName) : undefined;
179
+ const table = hotkeyTableFor(decl?.type ?? null, decl?.linux?.useWin32VkCode === true);
180
+ if (!table) {
181
+ warns.push('option ' + name + '(' + level + ')是 hotkey:当前控制器' +
182
+ (decl ? '类型 ' + decl.type + ' 没有键码映射表' : '未确定(没选中控制器)') +
183
+ ',这一层覆盖已跳过');
184
+ return;
185
+ }
186
+ for (const f of o.hotkeys ?? []) {
187
+ const raw = fields[f.name];
188
+ if (raw === undefined || raw === '')
189
+ continue; // 缺值交给下面的 missing 统一报
190
+ const hk = resolveHotkeyTokens(f.name, raw, table);
191
+ if (hk.error) {
192
+ warns.push('option ' + name + '(' + level + ')的 hotkey 字段 ' + f.name + ' 解析失败:' + hk.error + '——整条覆盖已跳过');
193
+ return;
194
+ }
195
+ Object.assign(tokens, hk.tokens);
196
+ }
197
+ }
198
+ else {
199
+ const bad = [];
200
+ for (const [k, raw] of Object.entries(fields)) {
201
+ const t = convert(raw, o.inputs?.find((f) => f.name === k)?.pipelineType);
202
+ if (t === undefined)
203
+ bad.push(k + '=' + raw);
204
+ else
205
+ tokens[k] = t;
206
+ }
207
+ if (bad.length) {
208
+ warns.push('option ' + name + '(' + level + ')的输入值按 pipeline_type 转不了型:' + bad.join(' / ') + '——整条覆盖已跳过');
209
+ return;
210
+ }
211
+ }
212
+ const r = substitute(tpl, tokens);
213
+ if (r.missing.length) {
214
+ warns.push('option ' + name + '(' + level + ')的 pipeline_override 模板里这些占位没有取值:' +
215
+ r.missing.join(' / ') + '——整条覆盖已跳过(用 --option ' + name + '.<字段>=<值> 给值)');
216
+ return;
217
+ }
218
+ layers.push(r.value);
219
+ /* password 字段(v2.10.0)回显掩码——值该进 tokens/pipeline_override,不进输出与日志。 */
220
+ const pwdNames = new Set((o.inputs ?? []).filter((f) => f.password).map((f) => f.name));
221
+ const shown = Object.entries(fields).map(([k, v]) => k + '=' + (pwdNames.has(k) ? '•••' : v)).join(',');
222
+ applied.push(name + '=(' + o.type + (shown ? ':' + shown : '') + ',' + level + ')');
62
223
  return;
63
- if (!v) {
64
- if (o.type === 'input' || o.type === 'hotkey')
65
- return; // 输入类不产生 pipeline_override,不算异常
224
+ }
225
+ const v = resolveOptionValue(o, valueCtx);
226
+ if (!v || v.kind !== 'cases') {
66
227
  warns.push('option ' + name + '(' + level + ')无 preset/default_case 取值,跳过其 pipeline_override');
67
228
  return;
68
229
  }
230
+ /* checkbox 的 min_count / max_count(v2.10.1):取值数量违反约束只警告不阻断——
231
+ * 声明的问题修在 interface.json,跑坏比选对来得早。 */
232
+ if (o.type === 'checkbox') {
233
+ const n = v.cases.length;
234
+ if (o.minCount !== undefined && n < o.minCount) {
235
+ warns.push('option ' + name + '(checkbox):选中 ' + n + ' 项 < min_count=' + o.minCount);
236
+ }
237
+ if (o.maxCount !== undefined && n > o.maxCount) {
238
+ warns.push('option ' + name + '(checkbox):选中 ' + n + ' 项 > max_count=' + o.maxCount);
239
+ }
240
+ }
69
241
  for (const cname of v.cases) {
70
242
  const c = o.cases.find((x) => x.name === cname);
71
243
  if (!c) {
@@ -73,7 +245,7 @@ export function computePipelineOverride(input) {
73
245
  continue;
74
246
  }
75
247
  if (c.pipelineOverride && Object.keys(c.pipelineOverride).length) {
76
- mergeOverride(acc, c.pipelineOverride);
248
+ layers.push(c.pipelineOverride);
77
249
  applied.push(name + '=' + cname + '(' + level + ',via ' + v.via + ')');
78
250
  }
79
251
  /* 嵌套子 option:父 case 生效后按声明顺序合并(更具体,晚于父级) */
@@ -81,6 +253,12 @@ export function computePipelineOverride(input) {
81
253
  mergeOption(child, level + ' > ' + name, [...chain, name]);
82
254
  }
83
255
  };
256
+ /* task 级覆盖是基底:协议只规定 option 四级顺序,没说它插在哪一级——这里按"先应用"处理。 */
257
+ const taskDecl = taskName ? loaded.tasks.find((x) => x.name === taskName) : undefined;
258
+ if (taskDecl?.pipelineOverride && Object.keys(taskDecl.pipelineOverride).length) {
259
+ layers.push(taskDecl.pipelineOverride);
260
+ applied.push('task.pipeline_override(task 级基底)');
261
+ }
84
262
  for (const name of loaded.globalOption)
85
263
  mergeOption(name, 'global_option');
86
264
  if (resourceName) {
@@ -98,6 +276,12 @@ export function computePipelineOverride(input) {
98
276
  for (const name of t?.option ?? [])
99
277
  mergeOption(name, 'task.option');
100
278
  }
101
- return { override: acc, warns, applied };
279
+ /* --option 的键必须有级别引用过它,否则用户以为改了、其实没进链 */
280
+ for (const key of [...Object.keys(input.optionCases ?? {}), ...Object.keys(input.optionFields ?? {})]) {
281
+ if (!reached.has(key)) {
282
+ warns.push('--option 里的 ' + key + ' 没有被任何级别引用(global/resource/controller/task 都没列它),取值未生效');
283
+ }
284
+ }
285
+ return { override: layers, warns, applied };
102
286
  }
103
287
  //# sourceMappingURL=override.js.map
@@ -4,8 +4,24 @@
4
4
  *
5
5
  * 为什么不猜设备:猜错不只是连错窗口,还会把 roi 与模板的坐标系一起猜错。
6
6
  */
7
+ import { existsSync, statSync } from 'node:fs';
7
8
  import { resolve } from 'node:path';
8
- const KIND_BY_TYPE = { Adb: 'adb', Win32: 'win32', Gamepad: 'gamepad', PlayCover: 'playcover', Linux: 'linux' };
9
+ function isDirectory(p) {
10
+ try {
11
+ return existsSync(p) && statSync(p).isDirectory();
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ const KIND_BY_TYPE = { Adb: 'adb', Win32: 'win32', Gamepad: 'gamepad', PlayCover: 'playcover', MacOS: 'macos', Linux: 'linux' };
18
+ /** 没有可枚举设备发现、且没有声明式入口的控制器:必须显式给 --target,工具不猜会话形态。
19
+ * PlayCover 有缺省(localhost:1717 / maa.playcover)、Linux 可由 interface.json linux{} 组装,
20
+ * 都不在此列。 */
21
+ const TARGET_REQUIRED = {
22
+ macos: 'MacOS 需要 --target <窗口 id 或标题子串>,或在 interface.json 的 macos.title_regex 声明匹配规则',
23
+ linux: 'Linux 需要 --target <config JSON>,或在 interface.json linux{} 里声明 screencap/input 等字段(Wlr 方法还需 wlr_socket_path,属用户配置、声明给不了)',
24
+ };
9
25
  function safeRegExp(pattern) {
10
26
  try {
11
27
  return new RegExp(pattern);
@@ -41,10 +57,13 @@ export function planController(loaded, overrides, devices) {
41
57
  controllerName: c.name,
42
58
  connect: {},
43
59
  candidates: [],
44
- error: '控制器 ' + c.name + ' 的类型 ' + c.type + ' 暂不受支持(本 CLI 支持 Adb / Win32 / Gamepad / PlayCover / Linux;MacOS 走手动 --kind macos)',
60
+ error: '控制器 ' + c.name + ' 的类型 ' + c.type + ' 暂不受支持(本 CLI 支持 Adb / Win32 / Gamepad / PlayCover / MacOS / Linux)',
45
61
  };
46
62
  }
47
63
  const notes = ['控制器 ' + c.name + '(来自 interface.json)'];
64
+ if (c.permissionRequired === true) {
65
+ notes.push('permission_required=true:该控制器需要管理员/root 权限运行,连接失败先查权限');
66
+ }
48
67
  const connect = { kind };
49
68
  if (c.display?.shortSide !== undefined)
50
69
  connect.shortSide = c.display.shortSide;
@@ -53,16 +72,56 @@ export function planController(loaded, overrides, devices) {
53
72
  if (c.display?.raw)
54
73
  connect.rawSize = true;
55
74
  if (c.display?.expand)
56
- notes.push('display_expand 暂不生效(daemon 未接入),已忽略');
57
- if (kind === 'playcover' || kind === 'linux') {
58
- /* 两类控制器没有可枚举的设备发现:必须显式给 target(playcover=<address>/<uuid>,
59
- linux=<config JSON>),工具不猜会话形态。 */
75
+ connect.expand = c.display.expand;
76
+ /* PlayCover:全缺省可构造(maa-support 同款缺省 localhost:1717 / maa.playcover)。
77
+ 声明或显式给了就带,没给交给 daemon 兜底。 */
78
+ if (kind === 'playcover') {
79
+ const t = overrides.manualTarget ?? null;
80
+ if (t)
81
+ connect.target = t;
82
+ else
83
+ notes.push('PlayCover 未给 target:按缺省 localhost:1717 / maa.playcover 构造');
84
+ return { ok: true, controllerName: c.name, connect, notes };
85
+ }
86
+ /* Linux:interface.json linux{} 交给 daemon 组装 config JSON(maa-support/MXU 同款规则)。
87
+ 用户 config JSON(--target)优先,其次声明式组装;Wlr 必填 wlr_socket_path,属用户配置,
88
+ interface 给不了——给不了时 daemon 侧如实报错。 */
89
+ if (kind === 'linux') {
90
+ const t = overrides.manualTarget ?? null;
91
+ if (c.linux)
92
+ connect.linuxDecl = c.linux;
93
+ if (t)
94
+ connect.target = t;
95
+ else if (!c.linux?.screencap && !c.linux?.input) {
96
+ return fail(c, 'TARGET_REQUIRED', TARGET_REQUIRED.linux, [], connect, notes);
97
+ }
98
+ else
99
+ notes.push('Linux 按 interface.json linux{} 组装 config(缺省 Wlr/Wlr)');
100
+ return { ok: true, controllerName: c.name, connect, notes };
101
+ }
102
+ const targetHint = TARGET_REQUIRED[kind];
103
+ if (targetHint) {
104
+ /* PlayCover / Linux 没有可枚举的设备发现,必须显式给 target,工具不猜会话形态。
105
+ MacOS 窗口能枚举但不在本工具的设备扫描通道里:按 PI 的 macos.title_regex 交给
106
+ daemon 侧 MacOSController.find() 匹配(不在这条通道枚举),没给 regex 仍要 target。 */
60
107
  const t = overrides.manualTarget ?? null;
61
- if (!t) {
62
- return fail(c, 'TARGET_REQUIRED', kind === 'playcover'
63
- ? 'PlayCover 需要 --target <address>/<uuid>(PlayCover 设置里可见)'
64
- : 'Linux 需要 --target <config JSON>(含 screencap_method / input_method 等必填字段)', [], connect, notes);
108
+ if (kind === 'macos') {
109
+ if (t)
110
+ connect.target = t;
111
+ else if (c.macos?.titleRegex)
112
+ connect.macosTitleRegex = c.macos.titleRegex;
113
+ else
114
+ return fail(c, 'TARGET_REQUIRED', targetHint, [], connect, notes);
115
+ if (c.macos?.screencap)
116
+ connect.screencap = c.macos.screencap;
117
+ if (c.macos?.input)
118
+ connect.input = c.macos.input;
119
+ if (c.macos?.titleRegex && !t)
120
+ notes.push('窗口:按 title_regex 匹配——' + c.macos.titleRegex);
121
+ return { ok: true, controllerName: c.name, connect, notes };
65
122
  }
123
+ if (!t)
124
+ return fail(c, 'TARGET_REQUIRED', targetHint, [], connect, notes);
66
125
  connect.target = t;
67
126
  return { ok: true, controllerName: c.name, connect, notes };
68
127
  }
@@ -150,39 +209,71 @@ export function planController(loaded, overrides, devices) {
150
209
  * 所选控制器的条目,条目自身多路径按声明顺序叠加(如"B 服"= base + bilibili);
151
210
  * attach_resource_path 在选中控制器后追加。override(--resource)先按资源名匹配,
152
211
  * 匹配不上再按文件系统路径解释(原有行为)。
212
+ *
213
+ * 有多个条目都适用时,选中的那个与备选一起进 notes——"取第一个"是语义,但静默取第一个
214
+ * 会让用错资源包的人查不出原因。
153
215
  */
154
- export function resolveResourcePaths(loaded, controllerName, override) {
216
+ export function resolveResourcePaths(loaded, controllerName, override,
217
+ /** `--resource` 当目录路径时的解析基准(CLI 的 ctx.cwd;缺省 = process.cwd()) */
218
+ cwd) {
155
219
  const dir = loaded.dir;
156
220
  /**
157
221
  * 追加控制器声明的 attach_resource_path(v2.2.0:"在 resource.path 加载完成后,额外加载")。
158
222
  * 它是**控制器的属性**,所以 `--resource` 走哪条分支都要追加——曾经两条提前 return 把它丢了。
159
223
  */
160
- const withAttach = (paths, names, selected) => {
224
+ const withAttach = (paths, names, selected, notes = [], expected) => {
161
225
  const out = [...paths];
226
+ const attachDirs = [];
162
227
  if (dir && controllerName) {
163
228
  const c = loaded.controllers.find((x) => x.name === controllerName);
164
229
  for (const p of c?.attachResourcePath ?? [])
165
- out.push(resolve(dir, p));
230
+ attachDirs.push(resolve(dir, p));
166
231
  }
167
- return { paths: [...new Set(out)], names, selected };
232
+ out.push(...attachDirs);
233
+ const hashCheck = expected && selected
234
+ ? { resourceName: selected, expected, pathDirs: [...paths], attachDirs }
235
+ : undefined;
236
+ return { paths: [...new Set(out)], names, selected, ...(notes.length ? { notes } : {}), ...(hashCheck ? { hashCheck } : {}) };
168
237
  };
169
238
  if (override) {
170
239
  const byName = loaded.resources.find((r) => r.name === override);
171
240
  if (byName && dir) {
172
- return withAttach(byName.paths.map((p) => resolve(dir, p)), [byName.name], byName.name);
241
+ return withAttach(byName.paths.map((p) => resolve(dir, p)), [byName.name], byName.name, [], byName.hash);
173
242
  }
174
- return withAttach([resolve(override)], ['(--resource)'], null);
243
+ /* 名字没匹配上时允许当目录路径(老行为),但必须真的存在——打错一个字的资源名
244
+ * 会被静默接受成路径,selected=null 让所有资源级过滤(option 适用性、pretask、
245
+ * hash 校验)跳过,比拒绝启动更难查。基准是调用方的 cwd(CLI 的 --cwd),
246
+ * 不是 interface.json 所在目录。 */
247
+ if (!isDirectory(resolve(cwd ?? '', override))) {
248
+ return {
249
+ paths: [], names: [], selected: null,
250
+ error: '--resource ' + JSON.stringify(override) + ' 不是资源名也不是存在的目录' +
251
+ (loaded.resources.length ? '(可用:' + loaded.resources.map((r) => r.name).join(' / ') + ')' : ''),
252
+ };
253
+ }
254
+ return withAttach([resolve(cwd ?? '', override)], ['(--resource)'], null);
175
255
  }
176
256
  if (!dir)
177
257
  return { paths: [], names: [], selected: null };
178
258
  const applicable = loaded.resources.filter((r) => !controllerName || !r.controllers.length || r.controllers.includes(controllerName));
179
259
  const paths = [];
180
260
  let selected = null;
261
+ const notes = [];
181
262
  if (applicable.length) {
182
263
  selected = applicable[0].name;
183
264
  for (const p of applicable[0].paths)
184
265
  paths.push(resolve(dir, p));
266
+ /* resource[] 是互斥单选:多个条目都适用时没有"都对"的答案,按声明序取第一个就是 Client 语义。
267
+ * 但用户/agent 看不到"其实还有别的可选"——用错资源包(中/英资产、多语言模型)会静默跑错,
268
+ * 所以把这次的选择和备选摆到台面上。 */
269
+ /* 候选 >5 时折叠全名列表(9 服全摆出来是噪音不是信息;--json 里 names 全量在)。
270
+ * 互斥单选的选择语义的说明保留——"已选哪份"正是 agent 要的。 */
271
+ if (applicable.length > 1) {
272
+ const names = applicable.map((r) => r.name);
273
+ const list = names.length <= 5 ? names.join(' / ') : names.slice(0, 3).join(' / ') + ' …共 ' + names.length + ' 个';
274
+ notes.push('当前控制器适用 ' + applicable.length + ' 个资源(' + list + '),已选 ' + selected + '——要换用 --resource <名>');
275
+ }
185
276
  }
186
- return withAttach(paths, selected ? [selected] : [], selected);
277
+ return withAttach(paths, selected ? [selected] : [], selected, notes, applicable[0]?.hash);
187
278
  }
188
279
  //# sourceMappingURL=plan.js.map
@@ -0,0 +1,29 @@
1
+ export function parseOptionArgs(list) {
2
+ const out = { cases: {}, fields: {} };
3
+ const items = Array.isArray(list) ? list : list === undefined ? [] : [list];
4
+ for (const item of items) {
5
+ const s = typeof item === 'string' ? item : '';
6
+ const eq = s.indexOf('=');
7
+ const lhs = eq > 0 ? s.slice(0, eq).trim() : '';
8
+ if (!lhs) {
9
+ out.error = '--option 要写成 <键>=<值> 或 <键>.<字段>=<值>:' + JSON.stringify(item);
10
+ return out;
11
+ }
12
+ const rhs = s.slice(eq + 1);
13
+ const dot = lhs.indexOf('.');
14
+ if (dot > 0) {
15
+ const key = lhs.slice(0, dot);
16
+ const field = lhs.slice(dot + 1);
17
+ if (!field) {
18
+ out.error = '--option 的字段名不能为空:' + JSON.stringify(item);
19
+ return out;
20
+ }
21
+ out.fields[key] = { ...(out.fields[key] ?? {}), [field]: rhs };
22
+ }
23
+ else {
24
+ out.cases[lhs] = rhs.includes(',') ? rhs.split(',').map((x) => x.trim()).filter(Boolean) : rhs;
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ //# sourceMappingURL=option-args.js.map
package/lib/pkg.js CHANGED
@@ -1,5 +1,7 @@
1
1
  /** 读取 CLI 自身包信息。src 与 lib 都在包根下一层,故 `../package.json` 两种运行方式都成立。 */
2
2
  import { readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
3
5
  import { fileURLToPath } from 'node:url';
4
6
  let cached = null;
5
7
  export function cliPackage() {
@@ -15,4 +17,38 @@ export function cliPackage() {
15
17
  }
16
18
  return cached;
17
19
  }
20
+ let maaNodeCached = null;
21
+ /**
22
+ * 运行中**实际加载**的 maa-node 版本(PI_CLIENT_MAAFW_VERSION)。
23
+ *
24
+ * 为什么不用 package.json 的声明值:那里写的是范围(^5.14.2),而 agent 侧拿这个变量是要判协议
25
+ * 兼容的——报范围等于没报。maa-node 不导出 `version`,也不保证导出 package.json,
26
+ * 所以从入口向上找包根。拿不到返回空串(调用方据此不注入,而不是编一个)。
27
+ */
28
+ export function maaNodeVersion() {
29
+ if (maaNodeCached !== null)
30
+ return maaNodeCached;
31
+ maaNodeCached = '';
32
+ try {
33
+ const req = createRequire(import.meta.url);
34
+ let dir = dirname(req.resolve('@maaxyz/maa-node'));
35
+ for (let i = 0; i < 6; i++) {
36
+ const p = join(dir, 'package.json');
37
+ try {
38
+ const j = JSON.parse(readFileSync(p, 'utf8'));
39
+ if (j.name === '@maaxyz/maa-node' && j.version) {
40
+ maaNodeCached = j.version;
41
+ break;
42
+ }
43
+ }
44
+ catch { /* 继续向上找 */ }
45
+ const up = dirname(dir);
46
+ if (up === dir)
47
+ break;
48
+ dir = up;
49
+ }
50
+ }
51
+ catch { /* 解析不到就留空 */ }
52
+ return maaNodeCached;
53
+ }
18
54
  //# sourceMappingURL=pkg.js.map
package/lib/protocol.js CHANGED
@@ -18,6 +18,10 @@ export const EXIT = {
18
18
  FINDINGS: 3,
19
19
  /** 前置环境缺失(缺 maa-node / 缺 checkout / 缺设备) */
20
20
  ENV: 4,
21
+ /** 被 SIGINT(Ctrl-C)中断:128+2 */
22
+ SIGINT: 130,
23
+ /** 被 SIGTERM 中断:128+15 */
24
+ SIGTERM: 143,
21
25
  };
22
26
  export function toEnvelope(command, r) {
23
27
  return {