@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
@@ -8,7 +8,11 @@ import { EXIT, fail } from '../protocol.js';
8
8
  import { input, withDaemon } from '../runtime/actions.js';
9
9
  import { describeSession, ensureSession } from '../runtime/session.js';
10
10
  import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
11
- function needSession(o) {
11
+ function needSession(o, ctx) {
12
+ /* 会话侧(--session 转发 / REPL)ctx.client 已注入、设备已连,连接参数再强要 argv
13
+ * 会拒掉 `click x y --session <名>` 这种本来合法的用法。 */
14
+ if (ctx?.client)
15
+ return null;
12
16
  if (!o.project && !o.kind) {
13
17
  return fail('BAD_ARGUMENTS', '要设备才能注入输入:给 --project <dir>(推荐)或 --kind win32|adb|gamepad', 'CLI 不做连接持久化;多步操作用 maafw-live repl 连接一次后连续注入', EXIT.USAGE);
14
18
  }
@@ -22,26 +26,49 @@ function num(values, key) {
22
26
  }
23
27
  return undefined;
24
28
  }
25
- async function inject(o, payload, human, timeoutMs = 20000) {
29
+ async function inject(ctx, o, payload, human, timeoutMs = 20000, expectGuard = false) {
26
30
  try {
27
- return await withDaemon(async (client) => {
31
+ return await withDaemon(ctx, async (client) => {
28
32
  const s = await ensureSession(client, o);
29
33
  const r = await input(client, payload, timeoutMs);
30
- return { exitCode: EXIT.OK, human: [...describeSession(s), ...human(r)], data: r };
34
+ /* daemon 回执恒 ok:true 包 handler 返回值——"业务拒绝"({ok:false,code,...})落在 data,
35
+ * 不抛错。必须查 r.ok===false:帧身份守卫拒发就在这里,不能再走"已点击"成功路径。 */
36
+ if (r.ok === false) {
37
+ const msg = String(r.error ?? 'daemon 拒绝');
38
+ if (expectGuard && /帧身份守卫|EXPECT_FAILED/.test(msg + String(r.code ?? ''))) {
39
+ return fail('EXPECT_FAILED', msg, '重看当前画面再点', EXIT.FINDINGS);
40
+ }
41
+ return fail(String(r.code ?? 'INPUT_REJECTED'), msg, undefined, EXIT.FINDINGS);
42
+ }
43
+ /* 动作回执帧(daemon 已落盘到 runDir/action-frames/):一次性 CLI 下 seq 没有消费者
44
+ * (frame_get 不可达),路径才是能复核"动作前后"的东西。 */
45
+ const ret = (r.retention ?? {});
46
+ const files = [ret.before?.file, ret.after?.file].filter((f) => typeof f === 'string');
47
+ const evidence = files.length ? ['证据帧:' + files.join(' 、')] : [];
48
+ return { exitCode: EXIT.OK, human: [...describeSession(s, ctx.flags.verbose), ...human(r), ...evidence], data: r };
31
49
  });
32
50
  }
33
51
  catch (e) {
52
+ /* 帧身份守卫拒发:daemon 回 ok:false 经 client.call 变 Error(只带 error 文本)。
53
+ * 用文本前缀识别——守卫错误都以"帧身份守卫"开头;落到 FINDINGS(画面已变是"发现",不是崩)。 */
54
+ if (expectGuard && /帧身份守卫/.test(String(e?.message ?? e))) {
55
+ return fail('EXPECT_FAILED', String(e?.message ?? e), '重看当前画面再点', EXIT.FINDINGS);
56
+ }
34
57
  return daemonFail(e);
35
58
  }
36
59
  }
37
60
  export const clickCommand = {
38
61
  name: 'click',
39
62
  summary: '点击一个坐标(控制器分辨率空间,默认短边 720)',
40
- usage: 'maafw-live click <x> <y> [--project <dir>|--kind ...] [--contact n] [--pressure n]',
41
- options: { ...CONNECT_OPTIONS, contact: { type: 'string' }, pressure: { type: 'string' } },
63
+ usage: 'maafw-live click <x> <y> [--project <dir>|--kind ...] [--contact n] [--pressure n]\n' +
64
+ ' [--expect-seq N | --expect-frame <hash>]——帧身份守卫:画面已变则拒发输入(FINDINGS 退出)',
65
+ options: {
66
+ ...CONNECT_OPTIONS, contact: { type: 'string' }, pressure: { type: 'string' },
67
+ 'expect-seq': { type: 'string' }, 'expect-frame': { type: 'string' },
68
+ },
42
69
  async run(ctx) {
43
- const o = sessionOptions(ctx.values);
44
- const miss = needSession(o);
70
+ const o = sessionOptions(ctx);
71
+ const miss = needSession(o, ctx);
45
72
  if (miss)
46
73
  return miss;
47
74
  const x = Number(ctx.positionals[0]);
@@ -49,7 +76,18 @@ export const clickCommand = {
49
76
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
50
77
  return fail('BAD_ARGUMENTS', '用法:maafw-live click <x> <y>', undefined, EXIT.USAGE);
51
78
  }
52
- return inject(o, { kind: 'click', x, y, contact: num(ctx.values, 'contact') ?? 0, pressure: num(ctx.values, 'pressure') ?? 1 }, (r) => ['已点击 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
79
+ const expectSeq = num(ctx.values, 'expect-seq');
80
+ const expectFrame = typeof ctx.values['expect-frame'] === 'string' && ctx.values['expect-frame']
81
+ ? String(ctx.values['expect-frame']) : undefined;
82
+ /* 帧身份守卫在一次性 CLI 下必然失败:daemon 随命令死,seq 从 1 起——守卫核对的"当前帧"
83
+ * 只能是这一帧本身。要跨命令的帧身份得用常驻会话(maafw-live repl --session <名>,
84
+ * 会话侧注入 ctx.client,与 withDaemon 的新建路径区分开)。 */
85
+ if ((expectSeq !== undefined || expectFrame !== undefined) && ctx.client === undefined) {
86
+ return fail('EXPECT_FAILED', '帧身份守卫(--expect-seq / --expect-frame)需要常驻会话——一次性命令的 daemon 每条都重启,seq 从 1 起,守卫永远核不到你存的那帧。要跨命令核帧用:maafw-live repl --session <名> --cmd "click x y --expect-seq N"', undefined, EXIT.FINDINGS);
87
+ }
88
+ return inject(ctx, o, { kind: 'click', x, y, contact: num(ctx.values, 'contact') ?? 0, pressure: num(ctx.values, 'pressure') ?? 1,
89
+ ...(expectSeq !== undefined ? { expectSeq } : {}),
90
+ ...(expectFrame ? { expectFrame } : {}) }, (r) => ['已点击 (' + x + ', ' + y + '),' + String(r.ms) + 'ms'], 20000, true);
53
91
  },
54
92
  };
55
93
  export const swipeCommand = {
@@ -62,8 +100,8 @@ export const swipeCommand = {
62
100
  pressure: { type: 'string' }, via: { type: 'string' },
63
101
  },
64
102
  async run(ctx) {
65
- const o = sessionOptions(ctx.values);
66
- const miss = needSession(o);
103
+ const o = sessionOptions(ctx);
104
+ const miss = needSession(o, ctx);
67
105
  if (miss)
68
106
  return miss;
69
107
  const [x1, y1, x2, y2] = ctx.positionals.slice(0, 4).map((v) => Number(v));
@@ -78,7 +116,7 @@ export const swipeCommand = {
78
116
  return fail('BAD_ARGUMENTS', '--via 每段要 2 个数字(x,y),段间用 ; 分隔,例:"100,200;150,250"', undefined, EXIT.USAGE);
79
117
  }
80
118
  }
81
- return inject(o, {
119
+ return inject(ctx, o, {
82
120
  kind: 'swipe', x1, y1, x2, y2,
83
121
  duration: num(ctx.values, 'duration') ?? 300,
84
122
  contact: num(ctx.values, 'contact') ?? 0,
@@ -96,14 +134,14 @@ export const keyCommand = {
96
134
  usage: 'maafw-live key <code> [--project <dir>|--kind ...]',
97
135
  options: { ...CONNECT_OPTIONS },
98
136
  async run(ctx) {
99
- const o = sessionOptions(ctx.values);
100
- const miss = needSession(o);
137
+ const o = sessionOptions(ctx);
138
+ const miss = needSession(o, ctx);
101
139
  if (miss)
102
140
  return miss;
103
141
  const code = Number(ctx.positionals[0]);
104
142
  if (!Number.isFinite(code))
105
143
  return fail('BAD_ARGUMENTS', '用法:maafw-live key <code>', undefined, EXIT.USAGE);
106
- return inject(o, { kind: 'key', code }, (r) => ['已按键 ' + code + ',' + String(r.ms) + 'ms']);
144
+ return inject(ctx, o, { kind: 'key', code }, (r) => ['已按键 ' + code + ',' + String(r.ms) + 'ms']);
107
145
  },
108
146
  };
109
147
  export const keysCommand = {
@@ -112,8 +150,8 @@ export const keysCommand = {
112
150
  usage: 'maafw-live keys <code[,code+...]> [--hold ms] [--project <dir>|--kind ...]',
113
151
  options: { ...CONNECT_OPTIONS, hold: { type: 'string' } },
114
152
  async run(ctx) {
115
- const o = sessionOptions(ctx.values);
116
- const miss = needSession(o);
153
+ const o = sessionOptions(ctx);
154
+ const miss = needSession(o, ctx);
117
155
  if (miss)
118
156
  return miss;
119
157
  const raw = String(ctx.positionals[0] ?? '');
@@ -121,7 +159,7 @@ export const keysCommand = {
121
159
  if (!codes.length) {
122
160
  return fail('BAD_ARGUMENTS', '用法:maafw-live keys <code[,code+...]>(至少一个正整数键码)', undefined, EXIT.USAGE);
123
161
  }
124
- return inject(o, { kind: 'keys', codes, hold: num(ctx.values, 'hold') ?? 60 }, (r) => ['已组合键 ' + codes.join('+') + ',' + String(r.ms) + 'ms']);
162
+ return inject(ctx, o, { kind: 'keys', codes, hold: num(ctx.values, 'hold') ?? 60 }, (r) => ['已组合键 ' + codes.join('+') + ',' + String(r.ms) + 'ms']);
125
163
  },
126
164
  };
127
165
  export const pressCommand = {
@@ -130,8 +168,8 @@ export const pressCommand = {
130
168
  usage: 'maafw-live press <x> <y> [--duration ms] [--project <dir>|--kind ...]',
131
169
  options: { ...CONNECT_OPTIONS, duration: { type: 'string' } },
132
170
  async run(ctx) {
133
- const o = sessionOptions(ctx.values);
134
- const miss = needSession(o);
171
+ const o = sessionOptions(ctx);
172
+ const miss = needSession(o, ctx);
135
173
  if (miss)
136
174
  return miss;
137
175
  const x = Number(ctx.positionals[0]);
@@ -139,7 +177,7 @@ export const pressCommand = {
139
177
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
140
178
  return fail('BAD_ARGUMENTS', '用法:maafw-live press <x> <y>', undefined, EXIT.USAGE);
141
179
  }
142
- return inject(o, { kind: 'press', x, y, duration: num(ctx.values, 'duration') ?? 800 }, (r) => ['已长按 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
180
+ return inject(ctx, o, { kind: 'press', x, y, duration: num(ctx.values, 'duration') ?? 800 }, (r) => ['已长按 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
143
181
  },
144
182
  };
145
183
  export const dbclickCommand = {
@@ -148,8 +186,8 @@ export const dbclickCommand = {
148
186
  usage: 'maafw-live dbclick <x> <y> [--gap ms] [--project <dir>|--kind ...]',
149
187
  options: { ...CONNECT_OPTIONS, gap: { type: 'string' } },
150
188
  async run(ctx) {
151
- const o = sessionOptions(ctx.values);
152
- const miss = needSession(o);
189
+ const o = sessionOptions(ctx);
190
+ const miss = needSession(o, ctx);
153
191
  if (miss)
154
192
  return miss;
155
193
  const x = Number(ctx.positionals[0]);
@@ -157,7 +195,7 @@ export const dbclickCommand = {
157
195
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
158
196
  return fail('BAD_ARGUMENTS', '用法:maafw-live dbclick <x> <y>', undefined, EXIT.USAGE);
159
197
  }
160
- return inject(o, { kind: 'dbclick', x, y, gap: num(ctx.values, 'gap') ?? 60 }, (r) => ['已双击 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
198
+ return inject(ctx, o, { kind: 'dbclick', x, y, gap: num(ctx.values, 'gap') ?? 60 }, (r) => ['已双击 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
161
199
  },
162
200
  };
163
201
  export const scrollCommand = {
@@ -166,8 +204,8 @@ export const scrollCommand = {
166
204
  usage: 'maafw-live scroll <dx> <dy> [--project <dir>|--kind ...]',
167
205
  options: { ...CONNECT_OPTIONS },
168
206
  async run(ctx) {
169
- const o = sessionOptions(ctx.values);
170
- const miss = needSession(o);
207
+ const o = sessionOptions(ctx);
208
+ const miss = needSession(o, ctx);
171
209
  if (miss)
172
210
  return miss;
173
211
  const dx = Number(ctx.positionals[0]);
@@ -175,7 +213,7 @@ export const scrollCommand = {
175
213
  if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
176
214
  return fail('BAD_ARGUMENTS', '用法:maafw-live scroll <dx> <dy>(如 0 -120 向上一格)', undefined, EXIT.USAGE);
177
215
  }
178
- return inject(o, { kind: 'scroll', dx, dy }, (r) => ['已滚动 (' + dx + ', ' + dy + '),' + String(r.ms) + 'ms']);
216
+ return inject(ctx, o, { kind: 'scroll', dx, dy }, (r) => ['已滚动 (' + dx + ', ' + dy + '),' + String(r.ms) + 'ms']);
179
217
  },
180
218
  };
181
219
  export const moveCommand = {
@@ -184,8 +222,8 @@ export const moveCommand = {
184
222
  usage: 'maafw-live move <dx> <dy> [--project <dir>|--kind ...]',
185
223
  options: { ...CONNECT_OPTIONS },
186
224
  async run(ctx) {
187
- const o = sessionOptions(ctx.values);
188
- const miss = needSession(o);
225
+ const o = sessionOptions(ctx);
226
+ const miss = needSession(o, ctx);
189
227
  if (miss)
190
228
  return miss;
191
229
  const dx = Number(ctx.positionals[0]);
@@ -193,7 +231,7 @@ export const moveCommand = {
193
231
  if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
194
232
  return fail('BAD_ARGUMENTS', '用法:maafw-live move <dx> <dy>', undefined, EXIT.USAGE);
195
233
  }
196
- return inject(o, { kind: 'move', dx, dy }, (r) => ['已相对移动 (' + dx + ', ' + dy + '),' + String(r.ms) + 'ms']);
234
+ return inject(ctx, o, { kind: 'move', dx, dy }, (r) => ['已相对移动 (' + dx + ', ' + dy + '),' + String(r.ms) + 'ms']);
197
235
  },
198
236
  };
199
237
  export const textCommand = {
@@ -202,14 +240,14 @@ export const textCommand = {
202
240
  usage: 'maafw-live text <string> [--project <dir>|--kind ...]',
203
241
  options: { ...CONNECT_OPTIONS },
204
242
  async run(ctx) {
205
- const o = sessionOptions(ctx.values);
206
- const miss = needSession(o);
243
+ const o = sessionOptions(ctx);
244
+ const miss = needSession(o, ctx);
207
245
  if (miss)
208
246
  return miss;
209
247
  const text = ctx.positionals.join(' ');
210
248
  if (!text)
211
249
  return fail('BAD_ARGUMENTS', '用法:maafw-live text <string>', undefined, EXIT.USAGE);
212
- return inject(o, { kind: 'text', text }, (r) => ['已输入 ' + text.length + ' 字符,' + String(r.ms) + 'ms']);
250
+ return inject(ctx, o, { kind: 'text', text }, (r) => ['已输入 ' + text.length + ' 字符,' + String(r.ms) + 'ms']);
213
251
  },
214
252
  };
215
253
  export const INPUT_COMMANDS = [
@@ -15,10 +15,13 @@ export const kfCommand = {
15
15
  name: 'kf',
16
16
  summary: '关键帧:status 看 L0 缓存,promote 升格原图进本地库,list/resolve 离线解析',
17
17
  usage: 'maafw-live kf status [--project <dir>|--kind ...]\n' +
18
- ' maafw-live kf promote <seq>|latest [--note <s>] [--project <dir>|--kind ...]\n' +
18
+ ' maafw-live kf promote <seq|seq seq|a..b|latest> [--note <s>] [--project <dir>|--kind ...]\n' +
19
19
  ' maafw-live kf list [--dir <frames>] (离线,不需要设备)\n' +
20
20
  ' maafw-live kf resolve <kf:...> [--dir <frames>] (离线)',
21
21
  options: { ...CONNECT_OPTIONS, note: { type: 'string' }, dir: { type: 'string' }, latest: { type: 'boolean' } },
22
+ /* promote 要的是"捕获帧的那个 daemon 会话":一次性进程 seq=0 恒失败
23
+ *(P0-3 L4)。list/resolve/status 离线或边界语义可用,不拦。 */
24
+ requiresSession: (ctx) => ctx.positionals[0] === 'promote',
22
25
  async run(ctx) {
23
26
  const sub = ctx.positionals[0] ?? 'status';
24
27
  if (sub === 'list' || sub === 'resolve') {
@@ -58,7 +61,7 @@ export const kfCommand = {
58
61
  }
59
62
  if (sub === 'status') {
60
63
  try {
61
- return await withDaemon(async (client) => {
64
+ return await withDaemon(ctx, async (client) => {
62
65
  const st = await l0Status(client);
63
66
  const roll = st.roll;
64
67
  const anchor = st.anchor;
@@ -82,27 +85,60 @@ export const kfCommand = {
82
85
  }
83
86
  }
84
87
  if (sub === 'promote') {
85
- const o = sessionOptions(ctx.values);
86
- const seqRaw = ctx.positionals[1];
87
- let args;
88
- if (seqRaw !== undefined && /^\d+$/.test(seqRaw))
89
- args = { seq: Number(seqRaw) };
90
- else if (seqRaw === 'latest' || ctx.values.latest === true)
88
+ const o = sessionOptions(ctx);
89
+ /* 批量:kf promote 3909 3910 | kf promote 10..15 | kf promote 10,11,12。
90
+ * 逐条升格、成功/淘汰分开报告;淘汰的附 frame_get <seq> 降级命令(L1 参考帧)。 */
91
+ const seqToks = ctx.positionals.slice(1).flatMap((t) => t.split(',')).filter((t) => t !== '');
92
+ let args = {};
93
+ const range = seqToks.length === 1 && /^(\d+)\.\.(\d+)$/.exec(seqToks[0]);
94
+ if (range) {
95
+ const a = Number(range[1]), b = Number(range[2]);
96
+ if (a > b || b - a > 200) {
97
+ return fail('BAD_ARGUMENTS', '范围 ' + seqToks[0] + ' 非法(a..b 且跨度 ≤200)', undefined, EXIT.USAGE);
98
+ }
99
+ args.seqs = Array.from({ length: b - a + 1 }, (_, i) => a + i);
100
+ }
101
+ else if (seqToks.length > 1 || (seqToks.length === 1 && /^\d+$/.test(seqToks[0]))) {
102
+ const nums = seqToks.map((t) => Number(t));
103
+ if (nums.some((n) => !Number.isInteger(n) || n <= 0)) {
104
+ return fail('BAD_ARGUMENTS', 'seq 必须是正整数:' + seqToks.join(' '), undefined, EXIT.USAGE);
105
+ }
106
+ if (nums.length === 1)
107
+ args = { seq: nums[0] };
108
+ else
109
+ args = { seqs: nums };
110
+ }
111
+ else if (seqToks[0] === 'latest' || ctx.values.latest === true) {
91
112
  args = { latest: true };
92
- else
93
- args = {};
113
+ }
94
114
  if (typeof ctx.values.note === 'string')
95
115
  args.note = ctx.values.note;
96
- if (!args.seq && !args.latest) {
97
- return fail('BAD_ARGUMENTS', '给序号(kf promote 42)或 kf promote latest;身份在接受请求时固定,已淘汰则失败', '升格要在捕获它的 daemon 会话里做(repl:stream/输入/run 后立刻升格);一次性命令的 L0 通常只有边界帧', EXIT.USAGE);
116
+ if (!args.seq && !args.latest && !(args.seqs && args.seqs.length)) {
117
+ return fail('BAD_ARGUMENTS', '给序号(kf promote 42)、多个序号(kf promote 42 43)、范围(kf promote 10..15)、或 kf promote latest;身份在接受请求时固定,已淘汰则失败', '升格要在捕获它的 daemon 会话里做(repl:stream/输入/run 后立刻升格);一次性命令的 L0 通常只有边界帧', EXIT.USAGE);
98
118
  }
99
119
  try {
100
- return await withDaemon(async (client) => {
120
+ return await withDaemon(ctx, async (client) => {
101
121
  if (o.project || o.kind) {
102
122
  /* 设备会话存在才值得连:L0 缓存是进程态,连一次至少能把输入/截图边界帧留下 */
103
123
  await ensureSession(client, o);
104
124
  }
105
125
  const r = await kfPromote(client, args);
126
+ /* 批量:逐条列结果;淘汰的 seq 附可复制的 frame_get 降级命令(L1 参考帧) */
127
+ if (Array.isArray(r.results)) {
128
+ const results = r.results;
129
+ const lines = [];
130
+ for (const it of results) {
131
+ if (it.ok)
132
+ lines.push(' seq=' + it.seq + ' → ' + it.id + (it.idempotent ? '(幂等)' : ''));
133
+ else
134
+ lines.push(' seq=' + it.seq + ' ✗ ' + it.error);
135
+ }
136
+ const failed = Number(r.failed ?? 0);
137
+ if (failed > 0) {
138
+ return { exitCode: EXIT.FINDINGS, human: ['批量升格:' + r.promoted + ' 成功 / ' + failed + ' 失败', ...lines], data: r };
139
+ }
140
+ return { exitCode: EXIT.OK, human: ['批量升格 ' + r.promoted + ' 例全部成功', ...lines], data: r };
141
+ }
106
142
  if (r.ok === false) {
107
143
  return fail('KF_PROMOTE', String(r.error ?? '升格失败'), 'L0 淘汰是诚实失败:换 frame_get 导 L1 参考帧,或重采', EXIT.FINDINGS);
108
144
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `maafw-live screencap-probe` —— 截图能力探测:判定"后台可截"(A 路前提)。
3
+ *
4
+ * 方法(契约 demo-trace §3):用另一个窗口把目标**完全盖住**,两次 `post_screencap`
5
+ * 间隔比 blockHash 与亮度。探针不知道"此刻是否真的遮挡"——所以 `--occluded` 让用户
6
+ * 显式声明;不给时判定结果如实标 `occluded:null`,不把"未声明遮挡"混成"前台结论"。
7
+ *
8
+ * 判据:
9
+ * changed==true + occluded → 后台可截(A 路前提成立)
10
+ * changed==true + !occluded → 像素在变——要判"后台可截"得在遮挡下重测
11
+ * changed==false + 正常亮度 → 静止(目标停止渲染?改应用设置)
12
+ * 黑图 / post_screencap 报错 → 截图方法不兼容,换 `win32.screencap` 方法
13
+ */
14
+ import { EXIT, fail } from '../protocol.js';
15
+ import { screencapProbe, withDaemon } from '../runtime/actions.js';
16
+ import { describeSession, ensureSession } from '../runtime/session.js';
17
+ import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
18
+ const needSession = (o, ctx) => ctx?.client || o.project || o.kind ? null : fail('NO_SESSION', '用法:maafw-live screencap-probe --project <dir> 或 --kind ...', undefined, EXIT.USAGE);
19
+ export const screencapProbeCommand = {
20
+ name: 'screencap-probe',
21
+ summary: '截图能力探测:判定"后台可截"(A 路前提)——两次截图比像素与亮度',
22
+ usage: 'maafw-live screencap-probe [--project <dir>|--kind ...] [--interval ms] [--occluded]\n' +
23
+ ' 先 connect/起会话;要用另一个窗口把目标完全盖住后,带 --occluded 再跑一次',
24
+ options: {
25
+ ...CONNECT_OPTIONS,
26
+ interval: { type: 'string' },
27
+ occluded: { type: 'boolean' },
28
+ },
29
+ async run(ctx) {
30
+ const o = sessionOptions(ctx);
31
+ const miss = needSession(o, ctx);
32
+ if (miss)
33
+ return miss;
34
+ const interval = ctx.values.interval !== undefined ? Number(ctx.values.interval) : 3000;
35
+ const occluded = ctx.values.occluded === true;
36
+ try {
37
+ return await withDaemon(ctx, async (client) => {
38
+ const s = await ensureSession(client, o);
39
+ const r = await screencapProbe(client, interval, occluded);
40
+ const first = (r.first ?? {});
41
+ const second = (r.second ?? {});
42
+ const lines = [
43
+ ...describeSession(s, ctx.flags.verbose),
44
+ '截图能力探测:两次 post_screencap 间隔 ' + String(r.intervalMs) + 'ms'
45
+ + (r.occluded === true ? '(已声明窗口被完全遮挡)' : '(未声明遮挡——判据只在遮挡态成立)'),
46
+ ];
47
+ if (first.ok)
48
+ lines.push(' 第 1 帧:' + first.w + 'x' + first.h + ' hash=' + String(first.hash).slice(0, 24) + '… mean=' + first.mean);
49
+ else if (first.error !== undefined || 'ok' in first)
50
+ lines.push(' 第 1 帧:失败——' + String(first.error));
51
+ if (second.ok)
52
+ lines.push(' 第 2 帧:' + second.w + 'x' + second.h + ' hash=' + String(second.hash).slice(0, 24) + '… mean=' + second.mean);
53
+ else if (second.error !== undefined || 'ok' in second)
54
+ lines.push(' 第 2 帧:失败——' + String(second.error));
55
+ if (r.changed !== null && r.changed !== undefined)
56
+ lines.push(' 像素:' + (r.changed ? '在变' : '未变'));
57
+ lines.push(' 判定:' + String(r.verdict) + '——' + String(r.note ?? ''));
58
+ const verdict = String(r.verdict);
59
+ const exitCode = verdict === 'changing' && occluded ? EXIT.OK
60
+ : verdict === 'error' ? EXIT.FINDINGS
61
+ : EXIT.FINDINGS; // black/static 都是要处置的发现;changing 但没声明遮挡 = 未定论
62
+ return { exitCode, human: lines, data: r };
63
+ });
64
+ }
65
+ catch (e) {
66
+ return daemonFail(e);
67
+ }
68
+ },
69
+ };
70
+ //# sourceMappingURL=probe.js.map