@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.
- package/README.md +24 -9
- package/docs/daemon-protocol.md +15 -10
- package/docs/keyframe-retention-contract-v0.md +23 -34
- package/docs/session-unify-v0.md +115 -0
- package/lib/client/daemon.js +45 -3
- package/lib/commands/annotate.js +15 -8
- package/lib/commands/calibrate.js +3 -3
- package/lib/commands/color.js +3 -3
- package/lib/commands/crop.js +8 -8
- package/lib/commands/dispatch.js +121 -0
- package/lib/commands/env.js +200 -6
- package/lib/commands/index.js +7 -1
- package/lib/commands/input.js +71 -33
- package/lib/commands/kf.js +49 -13
- package/lib/commands/probe.js +70 -0
- package/lib/commands/reco.js +285 -56
- package/lib/commands/repl.js +1305 -502
- package/lib/commands/roi-check.js +273 -0
- package/lib/commands/roi-compare.js +388 -0
- package/lib/commands/runtime.js +295 -91
- package/lib/commands/timing.js +28 -10
- package/lib/daemon/framed.mjs +1014 -184
- package/lib/daemon/reco_child.mjs +150 -9
- package/lib/flags.js +2 -0
- package/lib/index.js +24 -20
- package/lib/interface/hotkey.js +142 -0
- package/lib/interface/index.js +1 -1
- package/lib/interface/load.js +107 -17
- package/lib/interface/override.js +218 -34
- package/lib/interface/plan.js +108 -17
- package/lib/option-args.js +29 -0
- package/lib/pkg.js +36 -0
- package/lib/protocol.js +4 -0
- package/lib/runtime/actions.js +127 -12
- package/lib/runtime/log-watch.js +160 -0
- package/lib/runtime/node-source.js +100 -0
- package/lib/runtime/ocr-model.js +45 -0
- package/lib/runtime/pi-env.js +34 -0
- package/lib/runtime/pretask.js +217 -0
- package/lib/runtime/prune.js +28 -0
- package/lib/runtime/receipts.js +57 -0
- package/lib/runtime/reco-args.js +38 -0
- package/lib/runtime/reco-verdict.js +82 -0
- package/lib/runtime/roi-compare.js +198 -0
- package/lib/runtime/session-forward.js +79 -0
- package/lib/runtime/session-ipc.js +471 -0
- package/lib/runtime/session.js +57 -8
- package/lib/runtime/signal-guard.js +35 -0
- package/lib/types/commands/dispatch.d.ts +22 -0
- package/lib/types/commands/env.d.ts +19 -0
- package/lib/types/commands/index.d.ts +1 -1
- package/lib/types/commands/probe.d.ts +15 -0
- package/lib/types/commands/reco.d.ts +11 -7
- package/lib/types/commands/repl.d.ts +8 -0
- package/lib/types/commands/roi-check.d.ts +3 -0
- package/lib/types/commands/roi-compare.d.ts +3 -0
- package/lib/types/commands/runtime.d.ts +20 -2
- package/lib/types/flags.d.ts +4 -0
- package/lib/types/interface/hotkey.d.ts +36 -0
- package/lib/types/interface/index.d.ts +1 -1
- package/lib/types/interface/load.d.ts +84 -7
- package/lib/types/interface/override.d.ts +58 -13
- package/lib/types/interface/plan.d.ts +23 -2
- package/lib/types/option-args.d.ts +19 -0
- package/lib/types/pkg.d.ts +8 -0
- package/lib/types/protocol.d.ts +16 -0
- package/lib/types/runtime/actions.d.ts +89 -7
- package/lib/types/runtime/log-watch.d.ts +28 -0
- package/lib/types/runtime/node-source.d.ts +25 -0
- package/lib/types/runtime/ocr-model.d.ts +8 -0
- package/lib/types/runtime/pi-env.d.ts +24 -0
- package/lib/types/runtime/pretask.d.ts +53 -0
- package/lib/types/runtime/prune.d.ts +4 -0
- package/lib/types/runtime/receipts.d.ts +7 -0
- package/lib/types/runtime/reco-args.d.ts +18 -0
- package/lib/types/runtime/reco-verdict.d.ts +50 -0
- package/lib/types/runtime/roi-compare.d.ts +117 -0
- package/lib/types/runtime/session-forward.d.ts +10 -0
- package/lib/types/runtime/session-ipc.d.ts +117 -0
- package/lib/types/runtime/session.d.ts +26 -2
- package/lib/types/runtime/signal-guard.d.ts +19 -0
- package/lib/usage.js +3 -3
- package/package.json +17 -4
- package/skills/maafw-live/SKILL.md +32 -2
- package/skills/maafw-live/references/pitfalls.md +97 -4
- package/skills/maafw-live/references/workflows.md +5 -0
- package/docs/keyframe-evidence-contract-v0.md +0 -61
- package/lib/client/daemon.js.map +0 -1
- package/lib/commands/annotate.js.map +0 -1
- package/lib/commands/calibrate.js.map +0 -1
- package/lib/commands/color.js.map +0 -1
- package/lib/commands/crop.js.map +0 -1
- package/lib/commands/env.js.map +0 -1
- package/lib/commands/index.js.map +0 -1
- package/lib/commands/input.js.map +0 -1
- package/lib/commands/kf.js.map +0 -1
- package/lib/commands/reco.js.map +0 -1
- package/lib/commands/repl.js.map +0 -1
- package/lib/commands/runtime.js.map +0 -1
- package/lib/commands/skill.js.map +0 -1
- package/lib/commands/timing.js.map +0 -1
- package/lib/commands/version.js.map +0 -1
- package/lib/exec.js.map +0 -1
- package/lib/flags.js.map +0 -1
- package/lib/image.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/interface/index.js.map +0 -1
- package/lib/interface/jsonc.js.map +0 -1
- package/lib/interface/load.js.map +0 -1
- package/lib/interface/override.js.map +0 -1
- package/lib/interface/plan.js.map +0 -1
- package/lib/pkg.js.map +0 -1
- package/lib/protocol.js.map +0 -1
- package/lib/runtime/actions.js.map +0 -1
- package/lib/runtime/keyframes.js.map +0 -1
- package/lib/runtime/session.js.map +0 -1
- package/lib/update-check.js.map +0 -1
- package/lib/usage.js.map +0 -1
package/lib/commands/input.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = [
|
package/lib/commands/kf.js
CHANGED
|
@@ -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
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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
|