@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/runtime.js
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
* (控制器规划、资源解析、时间线)都不许写在这里——REPL 与其它壳(编辑器插件、面板)调的是同一批动作函数。
|
|
6
6
|
*/
|
|
7
7
|
import { mkdirSync } from 'node:fs';
|
|
8
|
-
import {
|
|
8
|
+
import { watchLogs } from '../runtime/log-watch.js';
|
|
9
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
9
10
|
import { defaultRunDir } from '../client/daemon.js';
|
|
10
|
-
import { computePipelineOverride, mergeOverride } from '../interface/index.js';
|
|
11
11
|
import { EXIT, fail } from '../protocol.js';
|
|
12
|
+
import { parseOptionArgs } from '../option-args.js';
|
|
12
13
|
import * as act from '../runtime/actions.js';
|
|
13
14
|
import { withDaemon } from '../runtime/actions.js';
|
|
14
15
|
import { describeSession, ensureSession, SessionError } from '../runtime/session.js';
|
|
16
|
+
import { writeRunReceipt } from '../runtime/receipts.js';
|
|
17
|
+
import { pruneDir } from '../runtime/prune.js';
|
|
18
|
+
import { installRunSignalGuard } from '../runtime/signal-guard.js';
|
|
15
19
|
/** 连接类选项:凡是要设备的命令都接受同一组,保证"怎么连"只有一套说法。 */
|
|
16
20
|
export const CONNECT_OPTIONS = {
|
|
17
21
|
project: { type: 'string' },
|
|
@@ -24,15 +28,30 @@ export const CONNECT_OPTIONS = {
|
|
|
24
28
|
'short-side': { type: 'string' },
|
|
25
29
|
'long-side': { type: 'string' },
|
|
26
30
|
raw: { type: 'boolean' },
|
|
31
|
+
/* 跳过 interface.json 的 pretask(它会连 Controller 之前跑一次,非零即中止连接) */
|
|
32
|
+
'no-pretask': { type: 'boolean' },
|
|
27
33
|
};
|
|
28
|
-
|
|
34
|
+
/**
|
|
35
|
+
* 路径参数的解析基准:调用方给的目录就是 `ctx.cwd`(CLI 的 --cwd),不是 process.cwd()。
|
|
36
|
+
* 只处理**纯路径参数**(--project / --resource-dir / --out / --image / --frames-dir 等)——
|
|
37
|
+
* `--resource` 同时是"资源名",能不能按路径解析要留给 resolveResourcePaths(名字优先),
|
|
38
|
+
* 所以它在 SessionOptions.cwd 里透传,不在这里 resolve。
|
|
39
|
+
*/
|
|
40
|
+
export function argPath(ctx, v) {
|
|
41
|
+
if (v === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
return isAbsolute(v) ? v : resolve(ctx.cwd, v);
|
|
44
|
+
}
|
|
45
|
+
export function sessionOptions(ctx) {
|
|
46
|
+
const values = ctx.values;
|
|
29
47
|
const num = (k) => {
|
|
30
48
|
const v = values[k];
|
|
31
49
|
return typeof v === 'string' && v !== '' ? Number(v) : undefined;
|
|
32
50
|
};
|
|
33
51
|
const str = (k) => (typeof values[k] === 'string' ? values[k] : undefined);
|
|
34
52
|
return {
|
|
35
|
-
project: str('project'),
|
|
53
|
+
project: argPath(ctx, str('project')),
|
|
54
|
+
cwd: ctx.cwd,
|
|
36
55
|
controller: str('controller'),
|
|
37
56
|
resource: str('resource'),
|
|
38
57
|
hwnd: str('hwnd'),
|
|
@@ -42,22 +61,32 @@ export function sessionOptions(values) {
|
|
|
42
61
|
shortSide: num('short-side'),
|
|
43
62
|
longSide: num('long-side'),
|
|
44
63
|
rawSize: values.raw === true,
|
|
64
|
+
...(values['no-pretask'] === true ? { pretask: false } : {}),
|
|
45
65
|
};
|
|
46
66
|
}
|
|
47
|
-
/** daemon
|
|
67
|
+
/** daemon 抛出的错误翻译成结构化失败。退出码按错误的**性质**分:
|
|
68
|
+
* 前置缺失(脚本找不到 / 会话规划失败)退 4;运行时故障(调用超时、进程退出、发送失败)退 1——
|
|
69
|
+
* 同一个 code 在所有路径退同一个码,agent 才不会按"缺环境"去查"daemon 卡住"。 */
|
|
48
70
|
export function daemonFail(e, hint) {
|
|
49
71
|
if (e instanceof SessionError)
|
|
50
72
|
return fail(e.code, e.message, hint, EXIT.ENV);
|
|
51
73
|
const msg = e instanceof Error ? e.message : String(e);
|
|
52
|
-
|
|
74
|
+
/* 超时与进程退出是运行时故障,不是前置缺失:退 1,让"重试 / 换参数"排在"装环境"之前 */
|
|
75
|
+
if (/^daemon 调用超时|^daemon 已退出|^daemon 请求发送失败/.test(msg)) {
|
|
76
|
+
return fail('DAEMON_ERROR', msg, hint ?? '重试或看 maafw-live probe;连续出现先 kill 会话重连', EXIT.FAIL);
|
|
77
|
+
}
|
|
78
|
+
if (/^找不到 daemon 脚本/.test(msg)) {
|
|
79
|
+
return fail('DAEMON_ERROR', msg, hint ?? 'daemon 脚本缺失:检查 npm 安装是否完整', EXIT.ENV);
|
|
80
|
+
}
|
|
81
|
+
return fail('DAEMON_ERROR', msg, hint ?? '用 maafw-live probe 检查运行时;连接类错误先 maafw-live device 看有没有设备', EXIT.FAIL);
|
|
53
82
|
}
|
|
54
83
|
export const probeCommand = {
|
|
55
84
|
name: 'probe',
|
|
56
85
|
summary: '自检 maa-node 运行时:绑定版本、adb/win32 设备发现数',
|
|
57
86
|
usage: 'maafw-live probe',
|
|
58
|
-
async run() {
|
|
87
|
+
async run(ctx) {
|
|
59
88
|
try {
|
|
60
|
-
return await withDaemon(async (client) => {
|
|
89
|
+
return await withDaemon(ctx, async (client) => {
|
|
61
90
|
const info = await act.probe(client);
|
|
62
91
|
const devices = await act.deviceList(client, 'all').catch(() => []);
|
|
63
92
|
const human = [
|
|
@@ -87,7 +116,7 @@ export const deviceCommand = {
|
|
|
87
116
|
return fail('BAD_ARGUMENTS', 'kind 只能是 all / adb / win32', undefined, EXIT.USAGE);
|
|
88
117
|
}
|
|
89
118
|
try {
|
|
90
|
-
return await withDaemon(async (client) => {
|
|
119
|
+
return await withDaemon(ctx, async (client) => {
|
|
91
120
|
const list = await act.deviceList(client, kind);
|
|
92
121
|
const human = list.length
|
|
93
122
|
? list.map((d) => {
|
|
@@ -115,14 +144,14 @@ export const connectCommand = {
|
|
|
115
144
|
usage: 'maafw-live connect --project <dir> | --kind win32|adb|gamepad [--target ...]',
|
|
116
145
|
options: { ...CONNECT_OPTIONS },
|
|
117
146
|
async run(ctx) {
|
|
118
|
-
const o = sessionOptions(ctx
|
|
147
|
+
const o = sessionOptions(ctx);
|
|
119
148
|
if (!o.project && !o.kind) {
|
|
120
149
|
return fail('BAD_ARGUMENTS', '要么给 --project <dir>(按 interface.json 规划),要么给 --kind', '推荐 --project:控制器类型/窗口正则/输入方法/识别缩放全跟项目走', EXIT.USAGE);
|
|
121
150
|
}
|
|
122
151
|
try {
|
|
123
|
-
return await withDaemon(async (client) => {
|
|
152
|
+
return await withDaemon(ctx, async (client) => {
|
|
124
153
|
const s = await ensureSession(client, o);
|
|
125
|
-
const lines = describeSession(s);
|
|
154
|
+
const lines = describeSession(s, ctx.flags.verbose);
|
|
126
155
|
if (s.streamStopped) {
|
|
127
156
|
/* 重建控制器会让旧流失效:如实说,别让调用方以为是设备卡了(曾经是"流静默死掉") */
|
|
128
157
|
lines.push('', '帧流:已停止(本次连接重建了控制器);要看帧就 stream start 重开。');
|
|
@@ -146,16 +175,18 @@ export const connectCommand = {
|
|
|
146
175
|
};
|
|
147
176
|
export const disconnectCommand = {
|
|
148
177
|
name: 'disconnect',
|
|
149
|
-
summary: '断开设备并销毁 Tasker/Controller
|
|
150
|
-
usage: 'maafw-live disconnect --
|
|
178
|
+
summary: '断开设备并销毁 Tasker/Controller(会话内命令,等价于结束 REPL 会话)',
|
|
179
|
+
usage: 'maafw-live disconnect --session <name>(一次性形态:为断开先新连一次,没意义)',
|
|
151
180
|
options: { ...CONNECT_OPTIONS },
|
|
181
|
+
/* 为断开先新连一次是自相矛盾的操作——断开会话态才需要"断开"(P0-3 L4) */
|
|
182
|
+
requiresSession: true,
|
|
152
183
|
async run(ctx) {
|
|
153
|
-
const o = sessionOptions(ctx
|
|
184
|
+
const o = sessionOptions(ctx);
|
|
154
185
|
if (!o.project && !o.kind) {
|
|
155
186
|
return fail('BAD_ARGUMENTS', '需要 --project 或 --kind 才能建立会话再断开', undefined, EXIT.USAGE);
|
|
156
187
|
}
|
|
157
188
|
try {
|
|
158
|
-
return await withDaemon(async (client) => {
|
|
189
|
+
return await withDaemon(ctx, async (client) => {
|
|
159
190
|
await ensureSession(client, o);
|
|
160
191
|
const r = await act.disconnect(client);
|
|
161
192
|
return { exitCode: EXIT.OK, human: ['已断开:' + JSON.stringify(r)], data: r };
|
|
@@ -166,27 +197,32 @@ export const disconnectCommand = {
|
|
|
166
197
|
}
|
|
167
198
|
},
|
|
168
199
|
};
|
|
200
|
+
/** 缺省落点 png/ 的保留上限(与 runs/ 同量级的"不留着看"产物) */
|
|
201
|
+
const PNG_KEEP = 30;
|
|
169
202
|
export const screencapCommand = {
|
|
170
203
|
name: 'screencap',
|
|
171
204
|
summary: '截一帧图并落盘(需要连接:给 --project 或 --kind)',
|
|
172
205
|
usage: 'maafw-live screencap [--project <dir>|--kind ...] [--out <png>]',
|
|
173
206
|
options: { ...CONNECT_OPTIONS, out: { type: 'string' } },
|
|
174
207
|
async run(ctx) {
|
|
175
|
-
const o = sessionOptions(ctx
|
|
176
|
-
const out = typeof ctx.values.out === 'string'
|
|
177
|
-
|
|
178
|
-
: join(defaultRunDir(), 'png', 'shot_' + Date.now() + '.png');
|
|
208
|
+
const o = sessionOptions(ctx);
|
|
209
|
+
const out = argPath(ctx, typeof ctx.values.out === 'string' ? ctx.values.out : undefined)
|
|
210
|
+
?? join(defaultRunDir(), 'png', 'shot_' + Date.now() + '.png');
|
|
179
211
|
try {
|
|
180
212
|
mkdirSync(dirname(out), { recursive: true });
|
|
181
213
|
}
|
|
182
214
|
catch { /* 落盘失败交给 daemon 报错 */ }
|
|
215
|
+
/* 只修剪缺省落点 png/——显式 --out 是用户指定的位置,不动它 */
|
|
216
|
+
if (dirname(out) === join(defaultRunDir(), 'png')) {
|
|
217
|
+
pruneDir(dirname(out), { keep: PNG_KEEP, suffix: '.png' });
|
|
218
|
+
}
|
|
183
219
|
try {
|
|
184
|
-
return await withDaemon(async (client) => {
|
|
220
|
+
return await withDaemon(ctx, async (client) => {
|
|
185
221
|
const s = await ensureSession(client, o);
|
|
186
222
|
const r = await act.screencap(client, out);
|
|
187
223
|
return {
|
|
188
224
|
exitCode: r.ok === false ? EXIT.FAIL : EXIT.OK,
|
|
189
|
-
human: [...describeSession(s), '截图:' + String(r.path ?? out) + (r.bytes ? ' (' + r.bytes + ' bytes)' : '')],
|
|
225
|
+
human: [...describeSession(s, ctx.flags.verbose), '截图:' + String(r.path ?? out) + (r.bytes ? ' (' + r.bytes + ' bytes)' : '')],
|
|
190
226
|
data: r,
|
|
191
227
|
written: r.path ? [String(r.path)] : [out],
|
|
192
228
|
};
|
|
@@ -197,21 +233,97 @@ export const screencapCommand = {
|
|
|
197
233
|
}
|
|
198
234
|
},
|
|
199
235
|
};
|
|
236
|
+
export const eventsCommand = {
|
|
237
|
+
name: 'events',
|
|
238
|
+
summary: '查帧流事件(stable/change/run),每条自带画面锚点;--watch 可自起帧流现场采集',
|
|
239
|
+
usage: 'maafw-live events [--limit <n>] [--since <seq>] [--watch <ms>] [--fps <n>]\n' +
|
|
240
|
+
' --watch <ms>:本次调用内先起帧流、等 ms 毫秒、再查(**推荐**:daemon 随命令退出而销毁,\n' +
|
|
241
|
+
' 事件环不跨进程存活,所以"查上一次留下的记录"是拿不到的)\n' +
|
|
242
|
+
' 不带 --watch:只查当前 daemon 会话里已有的事件——一次性 CLI 里这通常为空,\n' +
|
|
243
|
+
' 该用法主要用于 REPL 之外的脚本在同进程内先跑 run 再查\n' +
|
|
244
|
+
' 每条事件 frame.retained=l0 表示画面还能取(frame get <seq> / kf promote),gone 表示已滚出',
|
|
245
|
+
options: { ...CONNECT_OPTIONS, limit: { type: 'string' }, since: { type: 'string' }, watch: { type: 'string' }, fps: { type: 'string' } },
|
|
246
|
+
async run(ctx) {
|
|
247
|
+
const o = sessionOptions(ctx);
|
|
248
|
+
const rawLimit = ctx.values.limit;
|
|
249
|
+
const limit = typeof rawLimit === 'string' ? Number(rawLimit) : 20;
|
|
250
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
251
|
+
return fail('BAD_ARGUMENTS', '--limit 必须是正整数', undefined, EXIT.USAGE);
|
|
252
|
+
}
|
|
253
|
+
const rawSince = ctx.values.since;
|
|
254
|
+
const since = typeof rawSince === 'string' ? Number(rawSince) : undefined;
|
|
255
|
+
if (rawSince !== undefined && !Number.isFinite(since)) {
|
|
256
|
+
return fail('BAD_ARGUMENTS', '--since 必须是数字(seq)', undefined, EXIT.USAGE);
|
|
257
|
+
}
|
|
258
|
+
const rawWatch = ctx.values.watch;
|
|
259
|
+
const watch = typeof rawWatch === 'string' ? Number(rawWatch) : undefined;
|
|
260
|
+
if (rawWatch !== undefined && (!Number.isFinite(watch) || watch <= 0)) {
|
|
261
|
+
return fail('BAD_ARGUMENTS', '--watch 必须是正毫秒数', undefined, EXIT.USAGE);
|
|
262
|
+
}
|
|
263
|
+
const rawFps = ctx.values.fps;
|
|
264
|
+
const fps = typeof rawFps === 'string' ? Number(rawFps) : undefined;
|
|
265
|
+
if (rawFps !== undefined && (!Number.isFinite(fps) || fps <= 0)) {
|
|
266
|
+
return fail('BAD_ARGUMENTS', '--fps 必须是正数', undefined, EXIT.USAGE);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return await withDaemon(ctx, async (client) => {
|
|
270
|
+
const s = await ensureSession(client, o);
|
|
271
|
+
/* --watch:daemon 随命令退出而销毁,事件环不跨进程存活——所以"自己起流、现场采集"
|
|
272
|
+
* 是一次性 CLI 拿到事件的唯一可靠方式。用 `wait` 的 change/stable 语义不合适
|
|
273
|
+
* (它等到第一个满足就返回,而我们要的是"跑满这段窗口"),直接起流后按毫秒等。 */
|
|
274
|
+
let watched = null;
|
|
275
|
+
if (watch !== undefined) {
|
|
276
|
+
await act.streamStart(client, fps !== undefined ? { fps } : {});
|
|
277
|
+
await new Promise((r) => setTimeout(r, watch));
|
|
278
|
+
watched = { ...(fps !== undefined ? { fps } : {}), ms: watch };
|
|
279
|
+
}
|
|
280
|
+
const r = await act.eventsList(client, { limit, ...(since !== undefined ? { since } : {}) });
|
|
281
|
+
const evs = r.events ?? [];
|
|
282
|
+
const human = [...describeSession(s, ctx.flags.verbose), '事件 ' + evs.length + ' 条' +
|
|
283
|
+
(r.matched !== r.total ? '(匹配 ' + r.matched + ' / 环内共 ' + r.total + ')' : '') +
|
|
284
|
+
(r.truncated ? ',已按 --limit 截取' : '') +
|
|
285
|
+
(watched ? '(本次起流 ' + watched.ms + 'ms' + (watched.fps ? ' @' + watched.fps + 'fps' : '') + ' 现场采集)' : '')];
|
|
286
|
+
if (!evs.length) {
|
|
287
|
+
human.push(watched
|
|
288
|
+
? ' (这 ' + watched.ms + 'ms 内画面没有变化:事件只在"动了"或"静下来"时产生,' +
|
|
289
|
+
'静止画面不产生事件。要抓变化就加长 --watch 或在等待期间对设备做操作)'
|
|
290
|
+
: ' (环内没有事件:daemon 随命令退出而销毁,事件环不跨进程存活——' +
|
|
291
|
+
'加 --watch <ms> 让它本次调用内起流采集,或改用 REPL 会话)');
|
|
292
|
+
}
|
|
293
|
+
for (const e of evs) {
|
|
294
|
+
const f = e.frame;
|
|
295
|
+
const anchor = !f
|
|
296
|
+
? ' frame=?(无锚点)'
|
|
297
|
+
: f.retained === 'l0'
|
|
298
|
+
? ' frame=l0:' + String(f.seq) + (f.source ? '(' + f.source + ')' : '') + ' → frame get ' + String(f.seq)
|
|
299
|
+
: ' frame=gone(seq ' + String(f.seq) + ' 已滚出 L0,取不到了)';
|
|
300
|
+
human.push(' ' + JSON.stringify(e) + anchor);
|
|
301
|
+
}
|
|
302
|
+
return { exitCode: EXIT.OK, human, data: r };
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
return daemonFail(e, '事件环只在 daemon 存活期间保留;一次性 CLI 请用 --watch <ms> 现场采集');
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
200
310
|
export const frameCommand = {
|
|
201
311
|
name: 'frame',
|
|
312
|
+
/* frame get 的环恒空(NO_FRAME);frame status 读的是流状态,空环也算有内容的回答 */
|
|
313
|
+
requiresSession: (ctx) => ctx.positionals[0] === 'get',
|
|
202
314
|
summary: '帧流状态与历史帧取用(环形缓冲只在 REPL 会话里才有内容)',
|
|
203
315
|
usage: 'maafw-live frame status | maafw-live frame get [seq] [--roi x,y,w,h] [--out <png>] [--src auto|full|ring]\n' +
|
|
204
316
|
' --src full 强制要控制器分辨率原图(L0 已淘汰则报错,绝不给缩小图冒充)',
|
|
205
317
|
options: { ...CONNECT_OPTIONS, roi: { type: 'string' }, out: { type: 'string' }, src: { type: 'string' } },
|
|
206
318
|
async run(ctx) {
|
|
207
319
|
const sub = ctx.positionals[0] ?? 'status';
|
|
208
|
-
const o = sessionOptions(ctx
|
|
320
|
+
const o = sessionOptions(ctx);
|
|
209
321
|
if (sub === 'status') {
|
|
210
322
|
try {
|
|
211
|
-
return await withDaemon(async (client) => {
|
|
323
|
+
return await withDaemon(ctx, async (client) => {
|
|
212
324
|
const s = await ensureSession(client, o);
|
|
213
325
|
const st = await act.streamStatus(client);
|
|
214
|
-
return { exitCode: EXIT.OK, human: [...describeSession(s), '帧流:' + JSON.stringify(st)], data: st };
|
|
326
|
+
return { exitCode: EXIT.OK, human: [...describeSession(s, ctx.flags.verbose), '帧流:' + JSON.stringify(st)], data: st };
|
|
215
327
|
});
|
|
216
328
|
}
|
|
217
329
|
catch (e) {
|
|
@@ -232,14 +344,14 @@ export const frameCommand = {
|
|
|
232
344
|
if (roi && (roi.length !== 4 || roi.some((n) => !Number.isFinite(n)))) {
|
|
233
345
|
return fail('BAD_ARGUMENTS', '--roi 需要 4 个数字:x,y,w,h(控制器分辨率坐标,与 pipeline 的 roi 同空间)', undefined, EXIT.USAGE);
|
|
234
346
|
}
|
|
235
|
-
const out = typeof ctx.values.out === 'string' ? ctx.values.out : undefined;
|
|
347
|
+
const out = argPath(ctx, typeof ctx.values.out === 'string' ? ctx.values.out : undefined);
|
|
236
348
|
const srcRaw = typeof ctx.values.src === 'string' ? ctx.values.src.toLowerCase() : 'auto';
|
|
237
349
|
if (!['auto', 'full', 'ring'].includes(srcRaw)) {
|
|
238
350
|
return fail('BAD_ARGUMENTS', '--src 只支持 auto | full | ring', undefined, EXIT.USAGE);
|
|
239
351
|
}
|
|
240
352
|
const src = srcRaw;
|
|
241
353
|
try {
|
|
242
|
-
return await withDaemon(async (client) => {
|
|
354
|
+
return await withDaemon(ctx, async (client) => {
|
|
243
355
|
await ensureSession(client, o);
|
|
244
356
|
const r = await act.frameGet(client, { seq, roi, out, src });
|
|
245
357
|
if (r.ok === false) {
|
|
@@ -269,6 +381,9 @@ export const frameCommand = {
|
|
|
269
381
|
};
|
|
270
382
|
export const waitCommand = {
|
|
271
383
|
name: 'wait',
|
|
384
|
+
/* 预热截图不进环 → 恒 NO_FRAME 退 3;就算有帧也没流,stable 秒成立/change 必超时——
|
|
385
|
+
* 两种结果都是假信号(P0-3 L4) */
|
|
386
|
+
requiresSession: true,
|
|
272
387
|
summary: '等状态谓词成立:stable=画面静下来 / change=画面动了(取代硬睡时钟)',
|
|
273
388
|
usage: 'maafw-live wait stable|change [--timeout ms] [--quiet ms] [--roi x,y,w,h] [--threshold n]\n' +
|
|
274
389
|
' 超时不算命令失败(ok=true, satisfied=false, 退出码 3)——"没等到"是观测结果',
|
|
@@ -283,7 +398,7 @@ export const waitCommand = {
|
|
|
283
398
|
return fail('BAD_ARGUMENTS', 'wait 只认 stable | change' + (modeRaw ? '(收到 ' + JSON.stringify(modeRaw) + ')' : ''), '用法:maafw-live wait stable|change [--timeout ms] [--quiet ms] [--roi x,y,w,h] [--threshold n]', EXIT.USAGE);
|
|
284
399
|
}
|
|
285
400
|
const mode = modeRaw;
|
|
286
|
-
const o = sessionOptions(ctx
|
|
401
|
+
const o = sessionOptions(ctx);
|
|
287
402
|
const timeout = ctx.values.timeout !== undefined ? Number(ctx.values.timeout) : undefined;
|
|
288
403
|
if (timeout !== undefined && !Number.isFinite(timeout)) {
|
|
289
404
|
return fail('BAD_ARGUMENTS', '--timeout 必须是毫秒数', undefined, EXIT.USAGE);
|
|
@@ -303,7 +418,7 @@ export const waitCommand = {
|
|
|
303
418
|
return fail('BAD_ARGUMENTS', '--roi 需要 4 个数字:x,y,w,h', undefined, EXIT.USAGE);
|
|
304
419
|
}
|
|
305
420
|
try {
|
|
306
|
-
return await withDaemon(async (client) => {
|
|
421
|
+
return await withDaemon(ctx, async (client) => {
|
|
307
422
|
await ensureSession(client, o);
|
|
308
423
|
const r = await act.waitState(client, {
|
|
309
424
|
mode,
|
|
@@ -335,9 +450,15 @@ export const waitCommand = {
|
|
|
335
450
|
export const runCommand = {
|
|
336
451
|
name: 'run',
|
|
337
452
|
summary: '运行 pipeline:项目模式(推荐)或 resource 目录模式;节点事件按帧序对齐。退出码按任务级 record.ok',
|
|
338
|
-
usage: 'maafw-live run --entry <task> --project <dir> | --resource-dir <dir> [--timeout <ms|0>] [--override <json>] [--preset <name>] [--resource <名|路径>]
|
|
453
|
+
usage: 'maafw-live run --entry <task> --project <dir> | --resource-dir <dir> [--timeout <ms|0>] [--override <json>] [--preset <name>] [--resource <名|路径>] [--option <键>=<值>|<键>.<字段>=<值>]…\n' +
|
|
454
|
+
' --option 按次给选项取值(可多次):<键>=<case 名>(select/switch;checkbox 用逗号分隔)、\n' +
|
|
455
|
+
' <键>.<字段>=<值>(input 的输入字段)。优先级 CLI > preset > default_case;\n' +
|
|
456
|
+
' input/hotkey 的 pipeline_override 模板靠它替换 {字段名}(hotkey 按控制器 type 映射成键码)\n' +
|
|
457
|
+
' 项目模式的 pretask 在连 Controller **之前**执行(非零退出即中止,--no-pretask 跳过)\n' +
|
|
339
458
|
' --timeout 0 = 不自动停止(停止权交调用方:事件流判断 + maafw-live stop / 客户端 run_stop);>0 时超时自动 post_stop\n' +
|
|
340
|
-
' 项目模式自动合成 pipeline_override(global→resource→controller→task,preset/default 取值),--override
|
|
459
|
+
' 项目模式自动合成 pipeline_override(global→resource→controller→task,preset/default 取值),--override 最后覆盖\n' +
|
|
460
|
+
' --watch <日志条目> = 跑的同时实时 tail(可多次给):文件 / glob("logs/*.txt")/ 目录(取每 tick 最新一个文件,日更轮转不丢)\n' +
|
|
461
|
+
' --watch-include/--watch-exclude <regex> 筛选推送行;--watch-interval <ms> 推送间隔(默认 2000)',
|
|
341
462
|
options: {
|
|
342
463
|
...CONNECT_OPTIONS,
|
|
343
464
|
entry: { type: 'string' },
|
|
@@ -345,17 +466,27 @@ export const runCommand = {
|
|
|
345
466
|
timeout: { type: 'string' },
|
|
346
467
|
override: { type: 'string' },
|
|
347
468
|
preset: { type: 'string' },
|
|
469
|
+
/* 按次取值(可多次给):<键>=<case 名>(select/switch/checkbox)、<键>.<字段>=<值>(input) */
|
|
470
|
+
option: { type: 'string', multiple: true },
|
|
471
|
+
/* 运行期日志跟随:跑的时候把项目 agent 写的日志实时打出来(可多次给)。
|
|
472
|
+
* 条目语义见 watchLogs.entries:文件 / glob / 目录(每 tick 取最新 mtime)。 */
|
|
473
|
+
watch: { type: 'string', multiple: true },
|
|
474
|
+
'watch-interval': { type: 'string' },
|
|
475
|
+
'watch-include': { type: 'string', multiple: true },
|
|
476
|
+
'watch-exclude': { type: 'string', multiple: true },
|
|
348
477
|
},
|
|
349
478
|
async run(ctx) {
|
|
350
|
-
const o = sessionOptions(ctx
|
|
479
|
+
const o = sessionOptions(ctx);
|
|
351
480
|
const entry = (typeof ctx.values.entry === 'string' ? ctx.values.entry : ctx.positionals[0]) ?? undefined;
|
|
352
|
-
const resourceDir = typeof ctx.values['resource-dir'] === 'string' ? ctx.values['resource-dir'] : undefined;
|
|
481
|
+
const resourceDir = argPath(ctx, typeof ctx.values['resource-dir'] === 'string' ? ctx.values['resource-dir'] : undefined);
|
|
353
482
|
if (!entry) {
|
|
354
483
|
return fail('BAD_ARGUMENTS', '缺少入口任务名:--entry <task>', '项目模式=interface.json 的 task 名;bundle 模式=pipeline 首节点名', EXIT.USAGE);
|
|
355
484
|
}
|
|
356
485
|
if (!o.project && !resourceDir) {
|
|
357
486
|
return fail('BAD_ARGUMENTS', '要么给 --project <dir>,要么给 --resource-dir <dir>', undefined, EXIT.USAGE);
|
|
358
487
|
}
|
|
488
|
+
/* --override 收对象(=一层)或数组(=逐层):框架按数组逐层继承,对象会被摊成
|
|
489
|
+
* [obj] 等价一层。字段缺失继承前值——与 GUI 客户端同一语义(@8061d5b 已核)。 */
|
|
359
490
|
let override = {};
|
|
360
491
|
if (typeof ctx.values.override === 'string') {
|
|
361
492
|
try {
|
|
@@ -369,73 +500,105 @@ export const runCommand = {
|
|
|
369
500
|
if (!Number.isFinite(timeoutMs)) {
|
|
370
501
|
return fail('BAD_ARGUMENTS', '--timeout 必须是毫秒数', undefined, EXIT.USAGE);
|
|
371
502
|
}
|
|
503
|
+
const presetName = typeof ctx.values.preset === 'string' ? ctx.values.preset : null;
|
|
504
|
+
const parsedOptions = parseOptionArgs(ctx.values.option);
|
|
505
|
+
if (parsedOptions.error) {
|
|
506
|
+
return fail('BAD_ARGUMENTS', parsedOptions.error, '例:--option taskOpt=b --option 章节数.章节号=4', EXIT.USAGE);
|
|
507
|
+
}
|
|
508
|
+
const { cases: optionCases, fields: optionFields } = parsedOptions;
|
|
509
|
+
/* run 是"启动工作"类命令:pretask 要跑(P0-2——观测/输入类命令不跑,run 保留)。
|
|
510
|
+
* --no-pretask 显式关。 */
|
|
511
|
+
if (o.pretask === undefined)
|
|
512
|
+
o.pretask = true;
|
|
513
|
+
/* pretask 在连接之前就跑,所以按次取值必须在 ensureSession 之前挂进 SessionOptions——
|
|
514
|
+
* 否则同一次 run 里 pretask 的末位 JSON 与 pipeline_override 用的不是同一份取值。 */
|
|
515
|
+
if (Object.keys(optionCases).length)
|
|
516
|
+
o.optionCases = optionCases;
|
|
517
|
+
if (Object.keys(optionFields).length)
|
|
518
|
+
o.optionFields = optionFields;
|
|
372
519
|
try {
|
|
373
|
-
return await withDaemon(async (client) => {
|
|
520
|
+
return await withDaemon(ctx, async (client) => {
|
|
374
521
|
const s = await ensureSession(client, o);
|
|
375
522
|
let args;
|
|
376
523
|
let resolvedEntry = entry;
|
|
377
|
-
let chain = null;
|
|
378
524
|
let extraWarns = [];
|
|
379
525
|
let appliedLines = [];
|
|
380
526
|
if (o.project) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (task?.controller?.length && ctrlName && !task.controller.includes(ctrlName)) {
|
|
391
|
-
return fail('TASK_NOT_APPLICABLE', '任务 ' + task.name + ' 不支持当前控制器 ' + ctrlName + '(声明:' + task.controller.join(' / ') + ')', '换 --controller ' + task.controller[0] + ',或选别的任务', EXIT.FINDINGS);
|
|
392
|
-
}
|
|
393
|
-
if (task?.resource?.length && resName && !task.resource.includes(resName)) {
|
|
394
|
-
extraWarns.push('任务 ' + task.name + ' 声明资源 ' + task.resource.join(' / ') + ',当前选中的是 ' + resName);
|
|
395
|
-
}
|
|
396
|
-
/* pipeline_override 四级合并链(preset > default_case 取值) + 用户 --override 最后覆盖 */
|
|
397
|
-
chain = computePipelineOverride({
|
|
398
|
-
loaded: s.plan.loaded,
|
|
399
|
-
controllerName: ctrlName,
|
|
400
|
-
resourceName: resName,
|
|
401
|
-
taskName: task?.name ?? null,
|
|
402
|
-
presetName: typeof ctx.values.preset === 'string' ? ctx.values.preset : null,
|
|
527
|
+
/* 入口解析 / task 适用性 / 四级 override / agent 桥接只有一份实现:
|
|
528
|
+
* REPL `run` 调的是同一个 prepareProjectRun,不再各长一套规则。 */
|
|
529
|
+
const prep = act.prepareProjectRun({
|
|
530
|
+
project: s.plan,
|
|
531
|
+
entry,
|
|
532
|
+
presetName,
|
|
533
|
+
override,
|
|
534
|
+
...(Object.keys(optionCases).length ? { optionCases } : {}),
|
|
535
|
+
...(Object.keys(optionFields).length ? { optionFields } : {}),
|
|
403
536
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const agents = s.plan.loaded.agents;
|
|
411
|
-
if (agents.length) {
|
|
412
|
-
extraWarns.push('项目声明了 ' + agents.length + ' 个 agent 子进程(' +
|
|
413
|
-
agents.map((a) => [a.exec, ...a.args].join(' ')).join(' ; ') +
|
|
414
|
-
'),run 时自动 spawn 并桥接——注意 maa-node 与 agent 侧 maa 库须同版本(协议握手要求)');
|
|
415
|
-
}
|
|
416
|
-
args = {
|
|
417
|
-
resourceDirs: paths,
|
|
418
|
-
entry: resolvedEntry,
|
|
419
|
-
override: { ...chain.override },
|
|
420
|
-
timeoutMs,
|
|
421
|
-
/* agent 桥接:daemon spawn 子进程(identifier 追加为末参)并 bind/connect 到本次 Resource */
|
|
422
|
-
agents: s.plan.loaded.agents.map((a) => ({ exec: a.exec, args: a.args })),
|
|
423
|
-
agentCwd: s.plan.loaded.dir ?? undefined,
|
|
424
|
-
};
|
|
537
|
+
if (!prep.ok)
|
|
538
|
+
return fail(prep.code, prep.error, prep.hint, EXIT.FINDINGS);
|
|
539
|
+
args = prep.args;
|
|
540
|
+
resolvedEntry = prep.entry;
|
|
541
|
+
extraWarns = prep.warns;
|
|
542
|
+
appliedLines = prep.applied;
|
|
425
543
|
}
|
|
426
544
|
else {
|
|
427
545
|
args = { resourceDir, entry, override, timeoutMs };
|
|
546
|
+
/* bundle 模式不载入 interface,preset 没有可应用的四级链——静默丢参数会让调用方
|
|
547
|
+
* 以为 P1 已生效;与 REPL 同一句警告。 */
|
|
548
|
+
if (presetName)
|
|
549
|
+
extraWarns.push('--resource-dir 是 bundle 模式:--preset ' + presetName + ' 不生效(不叠 interface 四级 override)');
|
|
550
|
+
if (Object.keys(optionCases).length || Object.keys(optionFields).length) {
|
|
551
|
+
extraWarns.push('--resource-dir 是 bundle 模式:--option 不生效(取值只作用于 interface 的 option 链与模板替换)');
|
|
552
|
+
}
|
|
428
553
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
554
|
+
/* 运行期日志跟随:run 会阻塞到任务结束,期间帧流/事件都不可用(框架占住控制器),
|
|
555
|
+
* 唯一还在更新的只有项目 agent 自己写的日志。给了 --watch 就在这里边跑边推。 */
|
|
556
|
+
const watchRaw = ctx.values.watch;
|
|
557
|
+
const watchEntries = (Array.isArray(watchRaw) ? watchRaw : watchRaw === undefined ? [] : [watchRaw])
|
|
558
|
+
.filter((x) => typeof x === 'string' && x.length > 0)
|
|
559
|
+
.map((x) => (isAbsolute(x) ? x : resolve(ctx.cwd, x)));
|
|
560
|
+
const watchInterval = Number(ctx.values['watch-interval'] ?? 2000);
|
|
561
|
+
/* include/exclude 正则:编译失败直接报 BAD_ARGUMENTS,不静默当无过滤跑 */
|
|
562
|
+
const watchInclude = (Array.isArray(ctx.values['watch-include']) ? ctx.values['watch-include'] : ctx.values['watch-include'] === undefined ? [] : [ctx.values['watch-include']])
|
|
563
|
+
.filter((x) => typeof x === 'string');
|
|
564
|
+
const watchExclude = (Array.isArray(ctx.values['watch-exclude']) ? ctx.values['watch-exclude'] : ctx.values['watch-exclude'] === undefined ? [] : [ctx.values['watch-exclude']])
|
|
565
|
+
.filter((x) => typeof x === 'string');
|
|
566
|
+
let includeRes;
|
|
567
|
+
let excludeRes;
|
|
568
|
+
try {
|
|
569
|
+
includeRes = watchInclude.length ? watchInclude.map((r) => new RegExp(r)) : undefined;
|
|
570
|
+
excludeRes = watchExclude.length ? watchExclude.map((r) => new RegExp(r)) : undefined;
|
|
432
571
|
}
|
|
433
|
-
|
|
572
|
+
catch (e) {
|
|
573
|
+
return fail('BAD_ARGUMENTS', '--watch-include/--watch-exclude 不是合法正则:' + String(e.message), undefined, EXIT.USAGE);
|
|
574
|
+
}
|
|
575
|
+
const watcher = watchEntries.length
|
|
576
|
+
? watchLogs({ entries: watchEntries, intervalMs: Number.isFinite(watchInterval) ? watchInterval : 2000, include: includeRes, exclude: excludeRes }, (lines) => {
|
|
577
|
+
/* 直写 stdout:--json 模式下也会混进人类可读行,所以下面的信封里另给 watch 统计,
|
|
578
|
+
* 并在 --json 时把人类行改走 stderr(stdout 只留信封)。 */
|
|
579
|
+
const sink = ctx.flags.json ? process.stderr : process.stdout;
|
|
580
|
+
for (const l of lines)
|
|
581
|
+
sink.write('[watch] ' + l + '\n');
|
|
582
|
+
})
|
|
583
|
+
: null;
|
|
584
|
+
/* 信号处理(审计 #6):Ctrl-C / kill 时不许让 daemon 里的任务裸跑——向 daemon 发
|
|
585
|
+
* run_stop(post_stop),run 回执照常落盘,退出码区分「我停的」与「它坏的」:
|
|
586
|
+
* SIGINT=130 / SIGTERM=143(128+signo),任务自身失败仍是 1。
|
|
587
|
+
* 第二次按信号直接 bail——第一次没杀掉说明 daemon 已不应答,再等没有现场可留。 */
|
|
588
|
+
/* 信号处理(审计 #6):Ctrl-C / kill 时不许让 daemon 里的任务裸跑——向 daemon 发
|
|
589
|
+
* run_stop,run 回执照常落盘,退出码区分「我停的」与「它坏的」(128+signo)。 */
|
|
590
|
+
/* 信号处理(审计 #6):Ctrl-C / kill 时不许让 daemon 里的任务裸跑——向 daemon 发
|
|
591
|
+
* run_stop,run 回执照常落盘,退出码区分「我停的」与「它坏的」(128+signo)。 */
|
|
592
|
+
const sigGuard = installRunSignalGuard(() => act.runStop(client).then(() => undefined));
|
|
593
|
+
/* finally 摘钩:act.run 抛错(daemon 死了)也不留监听残留 */
|
|
594
|
+
const r = await act.run(client, args, timeoutMs).finally(() => sigGuard.restore());
|
|
595
|
+
const interrupted = sigGuard.code;
|
|
596
|
+
const watchStats = watcher ? watcher.stop() : null;
|
|
434
597
|
if (r.ok === false || !r.record) {
|
|
435
598
|
/* 调用级失败:任务根本没跑起来(未连接 / 资源缺失 / 已有任务在跑)→ FINDINGS */
|
|
436
599
|
return {
|
|
437
600
|
exitCode: EXIT.FINDINGS,
|
|
438
|
-
human: [...describeSession(s), '调用失败:' + String(r.error ?? 'run 未返回记录')],
|
|
601
|
+
human: [...describeSession(s, ctx.flags.verbose), '调用失败:' + String(r.error ?? 'run 未返回记录')],
|
|
439
602
|
data: r,
|
|
440
603
|
warnings: [...extraWarns, ...(r.warns ?? [])],
|
|
441
604
|
};
|
|
@@ -445,7 +608,30 @@ export const runCommand = {
|
|
|
445
608
|
const rec = r.record;
|
|
446
609
|
const taskOk = rec.ok === true;
|
|
447
610
|
const nodes = rec.nodes ?? [];
|
|
611
|
+
/* 三态结论词:`status:0 + stopped:true` 单看 status 会被读成"成功完成"(本仓 G21 实测误读)。
|
|
612
|
+
* 判据用 record 自带的 timeoutMs,不靠调用方回忆当初给了什么:
|
|
613
|
+
* 跑满窗口被截断 → 非成功非失败(是"没跑完",不是"跑完了没成功");
|
|
614
|
+
* stopped 但没跑满 → 是任务自己结束、只是没成功。
|
|
615
|
+
* 退出码仍按任务级 record.ok,不给截断开绿灯。 */
|
|
616
|
+
const budget = typeof rec.timeoutMs === 'number' ? rec.timeoutMs : null;
|
|
617
|
+
const ranFull = rec.stopped === true && budget !== null && budget > 0
|
|
618
|
+
&& typeof rec.durationMs === 'number' && rec.durationMs >= budget;
|
|
619
|
+
const verdict = interrupted
|
|
620
|
+
? (interrupted === EXIT.SIGINT ? '被 Ctrl-C 中断(SIGINT)' : '被终止信号中断(SIGTERM)')
|
|
621
|
+
: taskOk
|
|
622
|
+
? '完成'
|
|
623
|
+
: ranFull
|
|
624
|
+
? '超时截断(非成功,也非失败:任务没跑完就被 post_stop 了)'
|
|
625
|
+
: rec.stopped === true
|
|
626
|
+
? '被停止(run_stop / 任务自身请求 post_stop,未成功)'
|
|
627
|
+
: '失败';
|
|
448
628
|
const agentLines = [];
|
|
629
|
+
if (rec.hashCheck?.expected !== undefined) {
|
|
630
|
+
agentLines.push(rec.hashCheck.match === true
|
|
631
|
+
? ' 资源完整性:' + String(rec.hashCheck.resourceName) + ' hash 校验通过'
|
|
632
|
+
: ' 资源完整性:' + String(rec.hashCheck.resourceName) + ' hash 不匹配(期望 ' +
|
|
633
|
+
String(rec.hashCheck.expected) + ' / 实际 ' + String(rec.hashCheck.actual || '?') + '),按 v2.6 语义不阻断');
|
|
634
|
+
}
|
|
449
635
|
if (rec.agent) {
|
|
450
636
|
agentLines.push(rec.agent.ok
|
|
451
637
|
? ' agent 已连接' + (rec.agent.reused ? '(复用)' : '') + ':custom action ' +
|
|
@@ -453,9 +639,10 @@ export const runCommand = {
|
|
|
453
639
|
: ' agent 连接失败:' + String(rec.agent.error ?? '?'));
|
|
454
640
|
}
|
|
455
641
|
const human = [
|
|
456
|
-
...describeSession(s),
|
|
457
|
-
'任务 ' + resolvedEntry + ':' +
|
|
458
|
-
'(status=' + String(rec.status) + ',' + String(rec.durationMs) + 'ms
|
|
642
|
+
...describeSession(s, ctx.flags.verbose),
|
|
643
|
+
'任务 ' + resolvedEntry + ':' + verdict +
|
|
644
|
+
'(status=' + String(rec.status) + ',' + String(rec.durationMs) + 'ms' +
|
|
645
|
+
(budget !== null ? ' / timeout=' + String(budget) + 'ms' : '') + ',帧序 ' +
|
|
459
646
|
String(rec.startSeq) + '..' + String(rec.endSeq) + ')',
|
|
460
647
|
...agentLines,
|
|
461
648
|
...appliedLines.map((l) => ' override: ' + l),
|
|
@@ -478,10 +665,25 @@ export const runCommand = {
|
|
|
478
665
|
if (rec.agent && rec.agent.ok === false) {
|
|
479
666
|
extraWarns.push('agent 连接失败:' + String(rec.agent.error ?? '?'));
|
|
480
667
|
}
|
|
668
|
+
if (watchStats && watchStats.files.length === 0) {
|
|
669
|
+
extraWarns.push('--watch 没有命中任何文件(' + watchEntries.join(' , ') + '):检查路径是否存在、日志是否在跑起来之后才创建');
|
|
670
|
+
}
|
|
671
|
+
if (watchStats) {
|
|
672
|
+
human.push(' 运行期日志跟随:' + String(watchStats.pushed) + ' 行(来自 ' +
|
|
673
|
+
String(watchStats.files.length) + ' 个文件)');
|
|
674
|
+
}
|
|
675
|
+
/* 回执全量落盘(与 repl 同一助手、同一目录约定):W1 runs 存储的种子。
|
|
676
|
+
* 失败只降级为不打路径行,不影响任务结论与退出码。 */
|
|
677
|
+
const receipt = writeRunReceipt(r, resolvedEntry);
|
|
678
|
+
if (receipt.path !== undefined) {
|
|
679
|
+
human.push(' 完整回执(含全部节点与 msgs):' + receipt.path);
|
|
680
|
+
}
|
|
481
681
|
return {
|
|
482
|
-
|
|
682
|
+
/* 中断优先于任务结论:退出码讲"我怎么走的",不讲"任务结果如何"(回执里有)。
|
|
683
|
+
* 130=SIGINT / 143=SIGTERM,都是 128+signo 惯例。 */
|
|
684
|
+
exitCode: interrupted || (taskOk ? EXIT.OK : EXIT.FAIL),
|
|
483
685
|
human,
|
|
484
|
-
data: r,
|
|
686
|
+
data: watchStats ? { ...r, watch: watchStats } : r,
|
|
485
687
|
warnings: [...extraWarns, ...(r.warns ?? [])],
|
|
486
688
|
};
|
|
487
689
|
});
|
|
@@ -495,9 +697,11 @@ export const stopCommand = {
|
|
|
495
697
|
name: 'stop',
|
|
496
698
|
summary: '停止运行中的任务(post_stop)',
|
|
497
699
|
usage: 'maafw-live stop',
|
|
498
|
-
|
|
700
|
+
/* 一次性形态没意义:停的是自己刚起的空 daemon,回 ok 误导(设计审查 P0-3 L4) */
|
|
701
|
+
requiresSession: true,
|
|
702
|
+
async run(ctx) {
|
|
499
703
|
try {
|
|
500
|
-
return await withDaemon(async (client) => {
|
|
704
|
+
return await withDaemon(ctx, async (client) => {
|
|
501
705
|
const r = await act.runStop(client);
|
|
502
706
|
return { exitCode: EXIT.OK, human: ['已请求停止:' + JSON.stringify(r)], data: r };
|
|
503
707
|
});
|
|
@@ -509,6 +713,6 @@ export const stopCommand = {
|
|
|
509
713
|
};
|
|
510
714
|
export const RUNTIME_COMMANDS = [
|
|
511
715
|
probeCommand, deviceCommand, connectCommand, disconnectCommand,
|
|
512
|
-
screencapCommand, frameCommand, waitCommand, runCommand, stopCommand,
|
|
716
|
+
screencapCommand, frameCommand, eventsCommand, waitCommand, runCommand, stopCommand,
|
|
513
717
|
];
|
|
514
718
|
//# sourceMappingURL=runtime.js.map
|