@windsland52/maafw-live 0.0.0-stage → 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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -2
  3. package/bin/maafw-live.mjs +25 -0
  4. package/docs/daemon-protocol.md +121 -0
  5. package/docs/keyframe-retention-contract-v0.md +225 -0
  6. package/docs/session-unify-v0.md +115 -0
  7. package/lib/client/daemon.js +276 -0
  8. package/lib/commands/annotate.js +179 -0
  9. package/lib/commands/calibrate.js +61 -0
  10. package/lib/commands/color.js +73 -0
  11. package/lib/commands/crop.js +228 -0
  12. package/lib/commands/dispatch.js +121 -0
  13. package/lib/commands/env.js +409 -0
  14. package/lib/commands/index.js +70 -0
  15. package/lib/commands/input.js +257 -0
  16. package/lib/commands/kf.js +167 -0
  17. package/lib/commands/probe.js +70 -0
  18. package/lib/commands/reco.js +440 -0
  19. package/lib/commands/repl.js +1501 -0
  20. package/lib/commands/roi-check.js +273 -0
  21. package/lib/commands/roi-compare.js +388 -0
  22. package/lib/commands/runtime.js +718 -0
  23. package/lib/commands/skill.js +169 -0
  24. package/lib/commands/timing.js +247 -0
  25. package/lib/commands/version.js +66 -0
  26. package/lib/daemon/framed.mjs +3680 -0
  27. package/lib/daemon/reco_child.mjs +287 -0
  28. package/lib/exec.js +96 -0
  29. package/lib/flags.js +44 -0
  30. package/lib/image.js +90 -0
  31. package/lib/index.js +143 -0
  32. package/lib/interface/hotkey.js +142 -0
  33. package/lib/interface/index.js +12 -0
  34. package/lib/interface/jsonc.js +95 -0
  35. package/lib/interface/load.js +397 -0
  36. package/lib/interface/override.js +287 -0
  37. package/lib/interface/plan.js +279 -0
  38. package/lib/option-args.js +29 -0
  39. package/lib/pkg.js +54 -0
  40. package/lib/protocol.js +94 -0
  41. package/lib/runtime/actions.js +225 -0
  42. package/lib/runtime/keyframes.js +73 -0
  43. package/lib/runtime/log-watch.js +160 -0
  44. package/lib/runtime/node-source.js +100 -0
  45. package/lib/runtime/ocr-model.js +45 -0
  46. package/lib/runtime/pi-env.js +34 -0
  47. package/lib/runtime/pretask.js +217 -0
  48. package/lib/runtime/prune.js +28 -0
  49. package/lib/runtime/receipts.js +57 -0
  50. package/lib/runtime/reco-args.js +38 -0
  51. package/lib/runtime/reco-verdict.js +82 -0
  52. package/lib/runtime/roi-compare.js +198 -0
  53. package/lib/runtime/session-forward.js +79 -0
  54. package/lib/runtime/session-ipc.js +471 -0
  55. package/lib/runtime/session.js +135 -0
  56. package/lib/runtime/signal-guard.js +35 -0
  57. package/lib/types/client/daemon.d.ts +86 -0
  58. package/lib/types/commands/annotate.d.ts +3 -0
  59. package/lib/types/commands/calibrate.d.ts +7 -0
  60. package/lib/types/commands/color.d.ts +9 -0
  61. package/lib/types/commands/crop.d.ts +3 -0
  62. package/lib/types/commands/dispatch.d.ts +22 -0
  63. package/lib/types/commands/env.d.ts +21 -0
  64. package/lib/types/commands/index.d.ts +13 -0
  65. package/lib/types/commands/input.d.ts +17 -0
  66. package/lib/types/commands/kf.d.ts +10 -0
  67. package/lib/types/commands/probe.d.ts +15 -0
  68. package/lib/types/commands/reco.d.ts +27 -0
  69. package/lib/types/commands/repl.d.ts +10 -0
  70. package/lib/types/commands/roi-check.d.ts +3 -0
  71. package/lib/types/commands/roi-compare.d.ts +3 -0
  72. package/lib/types/commands/runtime.d.ts +66 -0
  73. package/lib/types/commands/skill.d.ts +17 -0
  74. package/lib/types/commands/timing.d.ts +64 -0
  75. package/lib/types/commands/version.d.ts +3 -0
  76. package/lib/types/exec.d.ts +31 -0
  77. package/lib/types/flags.d.ts +62 -0
  78. package/lib/types/image.d.ts +27 -0
  79. package/lib/types/index.d.ts +1 -0
  80. package/lib/types/interface/hotkey.d.ts +36 -0
  81. package/lib/types/interface/index.d.ts +11 -0
  82. package/lib/types/interface/jsonc.d.ts +9 -0
  83. package/lib/types/interface/load.d.ts +190 -0
  84. package/lib/types/interface/override.d.ts +78 -0
  85. package/lib/types/interface/plan.d.ts +68 -0
  86. package/lib/types/option-args.d.ts +19 -0
  87. package/lib/types/pkg.d.ts +14 -0
  88. package/lib/types/protocol.d.ts +124 -0
  89. package/lib/types/runtime/actions.d.ts +248 -0
  90. package/lib/types/runtime/keyframes.d.ts +43 -0
  91. package/lib/types/runtime/log-watch.d.ts +28 -0
  92. package/lib/types/runtime/node-source.d.ts +25 -0
  93. package/lib/types/runtime/ocr-model.d.ts +8 -0
  94. package/lib/types/runtime/pi-env.d.ts +24 -0
  95. package/lib/types/runtime/pretask.d.ts +53 -0
  96. package/lib/types/runtime/prune.d.ts +4 -0
  97. package/lib/types/runtime/receipts.d.ts +7 -0
  98. package/lib/types/runtime/reco-args.d.ts +18 -0
  99. package/lib/types/runtime/reco-verdict.d.ts +50 -0
  100. package/lib/types/runtime/roi-compare.d.ts +117 -0
  101. package/lib/types/runtime/session-forward.d.ts +10 -0
  102. package/lib/types/runtime/session-ipc.d.ts +117 -0
  103. package/lib/types/runtime/session.d.ts +77 -0
  104. package/lib/types/runtime/signal-guard.d.ts +19 -0
  105. package/lib/types/update-check.d.ts +20 -0
  106. package/lib/types/usage.d.ts +3 -0
  107. package/lib/update-check.js +97 -0
  108. package/lib/usage.js +50 -0
  109. package/package.json +94 -4
  110. package/skills/README.md +51 -0
  111. package/skills/maafw-live/SKILL.md +293 -0
  112. package/skills/maafw-live/references/examples.md +238 -0
  113. package/skills/maafw-live/references/pitfalls.md +211 -0
  114. package/skills/maafw-live/references/workflows.md +164 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * 输入注入命令:click / swipe / key / text。
3
+ *
4
+ * 全部经 maafw 控制器本体注入(不直接调 adb),坐标是控制器分辨率空间——默认短边 720,
5
+ * 与 pipeline 里 roi / 模板图同一坐标系。要设备就必须给 --project(推荐)或 --kind。
6
+ */
7
+ import { EXIT, fail } from '../protocol.js';
8
+ import { input, withDaemon } from '../runtime/actions.js';
9
+ import { describeSession, ensureSession } from '../runtime/session.js';
10
+ import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
11
+ function needSession(o, ctx) {
12
+ /* 会话侧(--session 转发 / REPL)ctx.client 已注入、设备已连,连接参数再强要 argv
13
+ * 会拒掉 `click x y --session <名>` 这种本来合法的用法。 */
14
+ if (ctx?.client)
15
+ return null;
16
+ if (!o.project && !o.kind) {
17
+ return fail('BAD_ARGUMENTS', '要设备才能注入输入:给 --project <dir>(推荐)或 --kind win32|adb|gamepad', 'CLI 不做连接持久化;多步操作用 maafw-live repl 连接一次后连续注入', EXIT.USAGE);
18
+ }
19
+ return null;
20
+ }
21
+ function num(values, key) {
22
+ const v = values[key];
23
+ if (typeof v === 'string' && v !== '') {
24
+ const n = Number(v);
25
+ return Number.isFinite(n) ? n : undefined;
26
+ }
27
+ return undefined;
28
+ }
29
+ async function inject(ctx, o, payload, human, timeoutMs = 20000, expectGuard = false) {
30
+ try {
31
+ return await withDaemon(ctx, async (client) => {
32
+ const s = await ensureSession(client, o);
33
+ const r = await input(client, payload, timeoutMs);
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 };
49
+ });
50
+ }
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
+ }
57
+ return daemonFail(e);
58
+ }
59
+ }
60
+ export const clickCommand = {
61
+ name: 'click',
62
+ summary: '点击一个坐标(控制器分辨率空间,默认短边 720)',
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
+ },
69
+ async run(ctx) {
70
+ const o = sessionOptions(ctx);
71
+ const miss = needSession(o, ctx);
72
+ if (miss)
73
+ return miss;
74
+ const x = Number(ctx.positionals[0]);
75
+ const y = Number(ctx.positionals[1]);
76
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
77
+ return fail('BAD_ARGUMENTS', '用法:maafw-live click <x> <y>', undefined, EXIT.USAGE);
78
+ }
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);
91
+ },
92
+ };
93
+ export const swipeCommand = {
94
+ name: 'swipe',
95
+ summary: '滑动(起点到终点,控制器分辨率空间);--via 给中途路径点 = 一条笔画画折线',
96
+ usage: 'maafw-live swipe <x1> <y1> <x2> <y2> [--duration ms] [--via "x,y;x,y"] [--project <dir>|--kind ...]\n' +
97
+ ' 带 --via 时走 touch_down→逐点 touch_move→touch_up(中途不抬手)——连线/谱曲/拖拽排序要的就是这个',
98
+ options: {
99
+ ...CONNECT_OPTIONS, duration: { type: 'string' }, contact: { type: 'string' },
100
+ pressure: { type: 'string' }, via: { type: 'string' },
101
+ },
102
+ async run(ctx) {
103
+ const o = sessionOptions(ctx);
104
+ const miss = needSession(o, ctx);
105
+ if (miss)
106
+ return miss;
107
+ const [x1, y1, x2, y2] = ctx.positionals.slice(0, 4).map((v) => Number(v));
108
+ if (![x1, y1, x2, y2].every((n) => Number.isFinite(n))) {
109
+ return fail('BAD_ARGUMENTS', '用法:maafw-live swipe <x1> <y1> <x2> <y2>', undefined, EXIT.USAGE);
110
+ }
111
+ const viaRaw = typeof ctx.values.via === 'string' ? ctx.values.via : undefined;
112
+ let via;
113
+ if (viaRaw !== undefined) {
114
+ via = viaRaw.split(';').map((s) => s.split(',').map((x) => Number(String(x).trim())));
115
+ if (!via.length || via.some((p) => p.length !== 2 || p.some((n) => !Number.isFinite(n)))) {
116
+ return fail('BAD_ARGUMENTS', '--via 每段要 2 个数字(x,y),段间用 ; 分隔,例:"100,200;150,250"', undefined, EXIT.USAGE);
117
+ }
118
+ }
119
+ return inject(ctx, o, {
120
+ kind: 'swipe', x1, y1, x2, y2,
121
+ duration: num(ctx.values, 'duration') ?? 300,
122
+ contact: num(ctx.values, 'contact') ?? 0,
123
+ pressure: num(ctx.values, 'pressure') ?? 1,
124
+ ...(via ? { via } : {}),
125
+ }, (r) => [via
126
+ ? '已折线滑动 (' + x1 + ',' + y1 + ') → ' + via.map((p) => '(' + p.join(',') + ')').join(' → ')
127
+ + ' → (' + x2 + ',' + y2 + '),' + String(r.ms) + 'ms(一条笔画,' + String(via.length) + ' 个路径点)'
128
+ : '已滑动 (' + x1 + ',' + y1 + ') → (' + x2 + ',' + y2 + '),' + String(r.ms) + 'ms']);
129
+ },
130
+ };
131
+ export const keyCommand = {
132
+ name: 'key',
133
+ summary: '按键(Android KeyEvent 码:3=Home、4=返回、187=最近任务)',
134
+ usage: 'maafw-live key <code> [--project <dir>|--kind ...]',
135
+ options: { ...CONNECT_OPTIONS },
136
+ async run(ctx) {
137
+ const o = sessionOptions(ctx);
138
+ const miss = needSession(o, ctx);
139
+ if (miss)
140
+ return miss;
141
+ const code = Number(ctx.positionals[0]);
142
+ if (!Number.isFinite(code))
143
+ return fail('BAD_ARGUMENTS', '用法:maafw-live key <code>', undefined, EXIT.USAGE);
144
+ return inject(ctx, o, { kind: 'key', code }, (r) => ['已按键 ' + code + ',' + String(r.ms) + 'ms']);
145
+ },
146
+ };
147
+ export const keysCommand = {
148
+ name: 'keys',
149
+ summary: '组合键(key_down 全按下 → key_up 反序抬起;如 Ctrl+C = 17,67)',
150
+ usage: 'maafw-live keys <code[,code+...]> [--hold ms] [--project <dir>|--kind ...]',
151
+ options: { ...CONNECT_OPTIONS, hold: { type: 'string' } },
152
+ async run(ctx) {
153
+ const o = sessionOptions(ctx);
154
+ const miss = needSession(o, ctx);
155
+ if (miss)
156
+ return miss;
157
+ const raw = String(ctx.positionals[0] ?? '');
158
+ const codes = raw.split(/[+,]/).map((s) => Number(s.trim())).filter((n) => Number.isFinite(n) && n > 0);
159
+ if (!codes.length) {
160
+ return fail('BAD_ARGUMENTS', '用法:maafw-live keys <code[,code+...]>(至少一个正整数键码)', undefined, EXIT.USAGE);
161
+ }
162
+ return inject(ctx, o, { kind: 'keys', codes, hold: num(ctx.values, 'hold') ?? 60 }, (r) => ['已组合键 ' + codes.join('+') + ',' + String(r.ms) + 'ms']);
163
+ },
164
+ };
165
+ export const pressCommand = {
166
+ name: 'press',
167
+ summary: '长按(touch_down → 保持 duration → touch_up)',
168
+ usage: 'maafw-live press <x> <y> [--duration ms] [--project <dir>|--kind ...]',
169
+ options: { ...CONNECT_OPTIONS, duration: { type: 'string' } },
170
+ async run(ctx) {
171
+ const o = sessionOptions(ctx);
172
+ const miss = needSession(o, ctx);
173
+ if (miss)
174
+ return miss;
175
+ const x = Number(ctx.positionals[0]);
176
+ const y = Number(ctx.positionals[1]);
177
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
178
+ return fail('BAD_ARGUMENTS', '用法:maafw-live press <x> <y>', undefined, EXIT.USAGE);
179
+ }
180
+ return inject(ctx, o, { kind: 'press', x, y, duration: num(ctx.values, 'duration') ?? 800 }, (r) => ['已长按 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
181
+ },
182
+ };
183
+ export const dbclickCommand = {
184
+ name: 'dbclick',
185
+ summary: '双击(两次 click,间隔 --gap,默认 60ms)',
186
+ usage: 'maafw-live dbclick <x> <y> [--gap ms] [--project <dir>|--kind ...]',
187
+ options: { ...CONNECT_OPTIONS, gap: { type: 'string' } },
188
+ async run(ctx) {
189
+ const o = sessionOptions(ctx);
190
+ const miss = needSession(o, ctx);
191
+ if (miss)
192
+ return miss;
193
+ const x = Number(ctx.positionals[0]);
194
+ const y = Number(ctx.positionals[1]);
195
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
196
+ return fail('BAD_ARGUMENTS', '用法:maafw-live dbclick <x> <y>', undefined, EXIT.USAGE);
197
+ }
198
+ return inject(ctx, o, { kind: 'dbclick', x, y, gap: num(ctx.values, 'gap') ?? 60 }, (r) => ['已双击 (' + x + ', ' + y + '),' + String(r.ms) + 'ms']);
199
+ },
200
+ };
201
+ export const scrollCommand = {
202
+ name: 'scroll',
203
+ summary: '滚轮(dx/dy 格数,建议 120 的倍数 = WHEEL_DELTA;Win32/MacOS)',
204
+ usage: 'maafw-live scroll <dx> <dy> [--project <dir>|--kind ...]',
205
+ options: { ...CONNECT_OPTIONS },
206
+ async run(ctx) {
207
+ const o = sessionOptions(ctx);
208
+ const miss = needSession(o, ctx);
209
+ if (miss)
210
+ return miss;
211
+ const dx = Number(ctx.positionals[0]);
212
+ const dy = Number(ctx.positionals[1]);
213
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
214
+ return fail('BAD_ARGUMENTS', '用法:maafw-live scroll <dx> <dy>(如 0 -120 向上一格)', undefined, EXIT.USAGE);
215
+ }
216
+ return inject(ctx, o, { kind: 'scroll', dx, dy }, (r) => ['已滚动 (' + dx + ', ' + dy + '),' + String(r.ms) + 'ms']);
217
+ },
218
+ };
219
+ export const moveCommand = {
220
+ name: 'move',
221
+ summary: '鼠标相对移动(Win32/MacOS;FPS 锁鼠标场景配合 mouse_lock_follow)',
222
+ usage: 'maafw-live move <dx> <dy> [--project <dir>|--kind ...]',
223
+ options: { ...CONNECT_OPTIONS },
224
+ async run(ctx) {
225
+ const o = sessionOptions(ctx);
226
+ const miss = needSession(o, ctx);
227
+ if (miss)
228
+ return miss;
229
+ const dx = Number(ctx.positionals[0]);
230
+ const dy = Number(ctx.positionals[1]);
231
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
232
+ return fail('BAD_ARGUMENTS', '用法:maafw-live move <dx> <dy>', undefined, EXIT.USAGE);
233
+ }
234
+ return inject(ctx, o, { kind: 'move', dx, dy }, (r) => ['已相对移动 (' + dx + ', ' + dy + '),' + String(r.ms) + 'ms']);
235
+ },
236
+ };
237
+ export const textCommand = {
238
+ name: 'text',
239
+ summary: '输入文本(经控制器注入)',
240
+ usage: 'maafw-live text <string> [--project <dir>|--kind ...]',
241
+ options: { ...CONNECT_OPTIONS },
242
+ async run(ctx) {
243
+ const o = sessionOptions(ctx);
244
+ const miss = needSession(o, ctx);
245
+ if (miss)
246
+ return miss;
247
+ const text = ctx.positionals.join(' ');
248
+ if (!text)
249
+ return fail('BAD_ARGUMENTS', '用法:maafw-live text <string>', undefined, EXIT.USAGE);
250
+ return inject(ctx, o, { kind: 'text', text }, (r) => ['已输入 ' + text.length + ' 字符,' + String(r.ms) + 'ms']);
251
+ },
252
+ };
253
+ export const INPUT_COMMANDS = [
254
+ clickCommand, swipeCommand, keyCommand, keysCommand,
255
+ pressCommand, dbclickCommand, scrollCommand, moveCommand, textCommand,
256
+ ];
257
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 关键帧命令:kf status / kf promote / kf list / kf resolve。
3
+ *
4
+ * 升格(promote)是 daemon 内存操作——从 L0 缓存取原图落盘并登记,必须在捕获它的那个
5
+ * daemon 会话里做( REPL 里 stream/输入/run 之后立刻升格,"看到即升格,不攒到最后")。
6
+ * list / resolve 是纯离线文件操作,不依赖 daemon 存活(契约 §4.1)。
7
+ */
8
+ import { EXIT, fail } from '../protocol.js';
9
+ import { defaultFramesDir, describeRecord, loadManifest, resolveFrame } from '../runtime/keyframes.js';
10
+ import { kfPromote, l0Status, withDaemon } from '../runtime/actions.js';
11
+ import { ensureSession } from '../runtime/session.js';
12
+ import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
13
+ const DIR_HINT = '库根默认 ~/.maafw-live/frames(与 daemon 的 runDir 一致)';
14
+ export const kfCommand = {
15
+ name: 'kf',
16
+ summary: '关键帧:status 看 L0 缓存,promote 升格原图进本地库,list/resolve 离线解析',
17
+ usage: 'maafw-live kf status [--project <dir>|--kind ...]\n' +
18
+ ' maafw-live kf promote <seq|seq seq|a..b|latest> [--note <s>] [--project <dir>|--kind ...]\n' +
19
+ ' maafw-live kf list [--dir <frames>] (离线,不需要设备)\n' +
20
+ ' maafw-live kf resolve <kf:...> [--dir <frames>] (离线)',
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',
25
+ async run(ctx) {
26
+ const sub = ctx.positionals[0] ?? 'status';
27
+ if (sub === 'list' || sub === 'resolve') {
28
+ const dir = typeof ctx.values.dir === 'string' ? ctx.values.dir : defaultFramesDir();
29
+ if (sub === 'list') {
30
+ const { manifest, error } = loadManifest(dir);
31
+ if (error)
32
+ return fail('KF_MANIFEST', error, DIR_HINT, EXIT.FINDINGS);
33
+ if (!manifest) {
34
+ return {
35
+ exitCode: EXIT.OK,
36
+ human: ['关键帧库 ' + dir + ':尚无留存(manifest 未创建;升格后会自动生成)'],
37
+ data: { dir, count: 0, frames: [] },
38
+ };
39
+ }
40
+ return {
41
+ exitCode: EXIT.OK,
42
+ human: [
43
+ '关键帧库 ' + dir + ':' + manifest.frames.length + ' 条(libraryId ' + manifest.libraryId + ')',
44
+ ...manifest.frames.map(describeRecord),
45
+ ],
46
+ data: { dir, count: manifest.frames.length, frames: manifest.frames },
47
+ };
48
+ }
49
+ const id = ctx.positionals[1];
50
+ if (!id)
51
+ return fail('BAD_ARGUMENTS', '用法:maafw-live kf resolve <kf:库UUID:序号>', undefined, EXIT.USAGE);
52
+ const r = resolveFrame(dir, id);
53
+ const human = ['解析 ' + id + ':' + r.status + (r.reason ? '(' + r.reason + ')' : '')];
54
+ if (r.record)
55
+ human.push(' ' + describeRecord(r.record));
56
+ if (r.path)
57
+ human.push(' path: ' + r.path);
58
+ if (r.record)
59
+ human.push(' sha256: ' + r.record.sha256);
60
+ return { exitCode: EXIT.OK, human, data: r };
61
+ }
62
+ if (sub === 'status') {
63
+ try {
64
+ return await withDaemon(ctx, async (client) => {
65
+ const st = await l0Status(client);
66
+ const roll = st.roll;
67
+ const anchor = st.anchor;
68
+ return {
69
+ exitCode: EXIT.OK,
70
+ human: [
71
+ 'L0 原图缓存:滚动区 ' + String(roll?.count) + '/' + String(roll?.cap) +
72
+ '(' + String(roll?.bytes) + ' bytes),锚区 ' + String(anchor?.count) + '/' + String(anchor?.cap) +
73
+ '(' + String(anchor?.bytes) + ' bytes),seq=' + String(st.seq),
74
+ '关键帧库:' + String(st.framesDir),
75
+ '锚区条目(seq, source):',
76
+ ...(anchor?.entries ?? [])
77
+ .map((e) => ' seq=' + e.seq + ' ' + String(e.source)),
78
+ ],
79
+ data: st,
80
+ };
81
+ });
82
+ }
83
+ catch (e) {
84
+ return daemonFail(e);
85
+ }
86
+ }
87
+ if (sub === 'promote') {
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) {
112
+ args = { latest: true };
113
+ }
114
+ if (typeof ctx.values.note === 'string')
115
+ args.note = ctx.values.note;
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);
118
+ }
119
+ try {
120
+ return await withDaemon(ctx, async (client) => {
121
+ if (o.project || o.kind) {
122
+ /* 设备会话存在才值得连:L0 缓存是进程态,连一次至少能把输入/截图边界帧留下 */
123
+ await ensureSession(client, o);
124
+ }
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
+ }
142
+ if (r.ok === false) {
143
+ return fail('KF_PROMOTE', String(r.error ?? '升格失败'), 'L0 淘汰是诚实失败:换 frame_get 导 L1 参考帧,或重采', EXIT.FINDINGS);
144
+ }
145
+ const rec = r.record;
146
+ return {
147
+ exitCode: EXIT.OK,
148
+ human: [
149
+ '已升格:' + String(r.id) + (r.idempotent ? '(幂等重试,返回原对象)' : ''),
150
+ ...(rec ? [' 捕获 seq=' + rec.captureSeq + ',控制器尺寸 ' + rec.ctrlW + 'x' + rec.ctrlH + ',source=' + rec.source] : []),
151
+ ' path: ' + String(r.path),
152
+ ' sha256: ' + String(r.sha256),
153
+ ],
154
+ data: r,
155
+ written: [String(r.path)],
156
+ };
157
+ });
158
+ }
159
+ catch (e) {
160
+ return daemonFail(e);
161
+ }
162
+ }
163
+ return fail('BAD_ARGUMENTS', '用法:kf status | kf promote [seq|latest] | kf list | kf resolve <id>', undefined, EXIT.USAGE);
164
+ },
165
+ };
166
+ export const KF_COMMANDS = [kfCommand];
167
+ //# sourceMappingURL=kf.js.map
@@ -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