@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.
- package/LICENSE +21 -0
- package/README.md +293 -2
- package/bin/maafw-live.mjs +25 -0
- package/docs/daemon-protocol.md +121 -0
- package/docs/keyframe-retention-contract-v0.md +225 -0
- package/docs/session-unify-v0.md +115 -0
- package/lib/client/daemon.js +276 -0
- package/lib/commands/annotate.js +179 -0
- package/lib/commands/calibrate.js +61 -0
- package/lib/commands/color.js +73 -0
- package/lib/commands/crop.js +228 -0
- package/lib/commands/dispatch.js +121 -0
- package/lib/commands/env.js +409 -0
- package/lib/commands/index.js +70 -0
- package/lib/commands/input.js +257 -0
- package/lib/commands/kf.js +167 -0
- package/lib/commands/probe.js +70 -0
- package/lib/commands/reco.js +440 -0
- package/lib/commands/repl.js +1501 -0
- package/lib/commands/roi-check.js +273 -0
- package/lib/commands/roi-compare.js +388 -0
- package/lib/commands/runtime.js +718 -0
- package/lib/commands/skill.js +169 -0
- package/lib/commands/timing.js +247 -0
- package/lib/commands/version.js +66 -0
- package/lib/daemon/framed.mjs +3680 -0
- package/lib/daemon/reco_child.mjs +287 -0
- package/lib/exec.js +96 -0
- package/lib/flags.js +44 -0
- package/lib/image.js +90 -0
- package/lib/index.js +143 -0
- package/lib/interface/hotkey.js +142 -0
- package/lib/interface/index.js +12 -0
- package/lib/interface/jsonc.js +95 -0
- package/lib/interface/load.js +397 -0
- package/lib/interface/override.js +287 -0
- package/lib/interface/plan.js +279 -0
- package/lib/option-args.js +29 -0
- package/lib/pkg.js +54 -0
- package/lib/protocol.js +94 -0
- package/lib/runtime/actions.js +225 -0
- package/lib/runtime/keyframes.js +73 -0
- 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 +135 -0
- package/lib/runtime/signal-guard.js +35 -0
- package/lib/types/client/daemon.d.ts +86 -0
- package/lib/types/commands/annotate.d.ts +3 -0
- package/lib/types/commands/calibrate.d.ts +7 -0
- package/lib/types/commands/color.d.ts +9 -0
- package/lib/types/commands/crop.d.ts +3 -0
- package/lib/types/commands/dispatch.d.ts +22 -0
- package/lib/types/commands/env.d.ts +21 -0
- package/lib/types/commands/index.d.ts +13 -0
- package/lib/types/commands/input.d.ts +17 -0
- package/lib/types/commands/kf.d.ts +10 -0
- package/lib/types/commands/probe.d.ts +15 -0
- package/lib/types/commands/reco.d.ts +27 -0
- package/lib/types/commands/repl.d.ts +10 -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 +66 -0
- package/lib/types/commands/skill.d.ts +17 -0
- package/lib/types/commands/timing.d.ts +64 -0
- package/lib/types/commands/version.d.ts +3 -0
- package/lib/types/exec.d.ts +31 -0
- package/lib/types/flags.d.ts +62 -0
- package/lib/types/image.d.ts +27 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/interface/hotkey.d.ts +36 -0
- package/lib/types/interface/index.d.ts +11 -0
- package/lib/types/interface/jsonc.d.ts +9 -0
- package/lib/types/interface/load.d.ts +190 -0
- package/lib/types/interface/override.d.ts +78 -0
- package/lib/types/interface/plan.d.ts +68 -0
- package/lib/types/option-args.d.ts +19 -0
- package/lib/types/pkg.d.ts +14 -0
- package/lib/types/protocol.d.ts +124 -0
- package/lib/types/runtime/actions.d.ts +248 -0
- package/lib/types/runtime/keyframes.d.ts +43 -0
- 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 +77 -0
- package/lib/types/runtime/signal-guard.d.ts +19 -0
- package/lib/types/update-check.d.ts +20 -0
- package/lib/types/usage.d.ts +3 -0
- package/lib/update-check.js +97 -0
- package/lib/usage.js +50 -0
- package/package.json +94 -4
- package/skills/README.md +51 -0
- package/skills/maafw-live/SKILL.md +293 -0
- package/skills/maafw-live/references/examples.md +238 -0
- package/skills/maafw-live/references/pitfalls.md +211 -0
- package/skills/maafw-live/references/workflows.md +164 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `maafw-live skill` —— 随包发布的 agent skill 与"装在 agent 里的那一份"如何对齐。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不写版本号:skill 讲的是本工具的命令与判据,跟包走才不会各说各话;但写进文件的版本号
|
|
5
|
+
* 每次发版都要改,而且从仓库装的副本仍然对不上。所以对齐用**比字节**:逐文件 same / different /
|
|
6
|
+
* missing / extra(extra 是目标目录多出来的文件,例如 skills CLI 放进来的 agent 元数据,不算漂移)。
|
|
7
|
+
*
|
|
8
|
+
* 与本仓 docs/ 的分工:docs/ 是字段与协议的唯一出处,skill 只写动线与判据——两者都随包发布,
|
|
9
|
+
* 区别在消费者(人读 docs,agent 装 skill),因此这里只做"是不是同一份字节",不解释内容。
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { EXIT, fail } from '../protocol.js';
|
|
16
|
+
const SKILL_NAME = 'maafw-live';
|
|
17
|
+
/** 包内 skill 目录:lib/commands/ → <包根>/skills/maafw-live(本地 checkout 同构) */
|
|
18
|
+
export function skillPayloadDir() {
|
|
19
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills', SKILL_NAME);
|
|
20
|
+
}
|
|
21
|
+
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
22
|
+
/** 目录内全部文件的相对路径('/' 分隔、排序稳定),供比对与安装共用。 */
|
|
23
|
+
function listFiles(dir) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const walk = (cur) => {
|
|
26
|
+
for (const e of readdirSync(cur, { withFileTypes: true })) {
|
|
27
|
+
const abs = join(cur, e.name);
|
|
28
|
+
if (e.isDirectory())
|
|
29
|
+
walk(abs);
|
|
30
|
+
else if (e.isFile())
|
|
31
|
+
out.push(relative(dir, abs).split(sep).join('/'));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(dir);
|
|
35
|
+
return out.sort();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 逐文件比对(纯函数,单测直接喂两个目录)。只比字节:任何不同都是 different,
|
|
39
|
+
* 但在**仅行尾不同**(CRLF ↔ LF)时补一条 note——那不是内容分叉,是工具改写。
|
|
40
|
+
*/
|
|
41
|
+
export function compareSkillTrees(payload, target) {
|
|
42
|
+
const files = listFiles(payload);
|
|
43
|
+
const rows = [];
|
|
44
|
+
for (const rel of files) {
|
|
45
|
+
const pb = readFileSync(join(payload, ...rel.split('/')));
|
|
46
|
+
const ta = join(target, ...rel.split('/'));
|
|
47
|
+
const base = { path: rel, bytes: pb.length, sha256: sha256(pb) };
|
|
48
|
+
if (!existsSync(ta)) {
|
|
49
|
+
rows.push({ ...base, status: 'missing' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const tb = readFileSync(ta);
|
|
53
|
+
if (pb.equals(tb)) {
|
|
54
|
+
rows.push({ ...base, status: 'same' });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const eolOnly = pb.toString('utf8').replace(/\r\n/g, '\n') === tb.toString('utf8').replace(/\r\n/g, '\n');
|
|
58
|
+
rows.push({ ...base, status: 'different', ...(eolOnly ? { note: '仅行尾不同(CRLF/LF)' } : {}) });
|
|
59
|
+
}
|
|
60
|
+
for (const rel of listFiles(target)) {
|
|
61
|
+
if (!files.includes(rel))
|
|
62
|
+
rows.push({ path: rel, status: 'extra' });
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
const driftOf = (rows) => rows.some((r) => r.status !== 'same' && r.status !== 'extra');
|
|
67
|
+
function readPayload(dir) {
|
|
68
|
+
return listFiles(dir).map((rel) => {
|
|
69
|
+
const buf = readFileSync(join(dir, ...rel.split('/')));
|
|
70
|
+
return { path: rel, bytes: buf.length, sha256: sha256(buf), text: buf.toString('utf8') };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export const skillCommand = {
|
|
74
|
+
name: 'skill',
|
|
75
|
+
summary: '随包 skill:看包内副本与文件指纹、比对已装副本(same/different/missing/extra)、字节精确安装',
|
|
76
|
+
usage: 'maafw-live skill 包内 skill 位置与文件指纹\n' +
|
|
77
|
+
' maafw-live skill --check <dir> 比对 <dir>/maafw-live 与包内副本(有漂移退出 3)\n' +
|
|
78
|
+
' maafw-live skill --install <dir> 把包内副本逐字节写到 <dir>/maafw-live\n' +
|
|
79
|
+
' maafw-live skill --print [--format json] 输出包内副本内容(harness 自取)',
|
|
80
|
+
options: {
|
|
81
|
+
check: { type: 'string' },
|
|
82
|
+
install: { type: 'string' },
|
|
83
|
+
print: { type: 'boolean' },
|
|
84
|
+
format: { type: 'string' },
|
|
85
|
+
},
|
|
86
|
+
async run(ctx) {
|
|
87
|
+
const payload = skillPayloadDir();
|
|
88
|
+
if (!existsSync(payload)) {
|
|
89
|
+
return fail('SKILL_NOT_FOUND', '包内未找到 skill 目录:' + payload, '本版本应随包发布 skills/maafw-live;从仓库运行时确认 skills/ 没有被删', EXIT.ENV);
|
|
90
|
+
}
|
|
91
|
+
const files = listFiles(payload);
|
|
92
|
+
const checkDir = typeof ctx.values.check === 'string' ? ctx.values.check : undefined;
|
|
93
|
+
if (checkDir !== undefined) {
|
|
94
|
+
const target = join(checkDir, SKILL_NAME);
|
|
95
|
+
if (!existsSync(target)) {
|
|
96
|
+
return fail('SKILL_DIR_NOT_FOUND', '目标目录下没有 ' + SKILL_NAME + ' skill:' + target, '给的是**安装根目录**(里面应当有 ' + SKILL_NAME + '/SKILL.md),例如 --global 装的 ~/.claude/skills', EXIT.ENV);
|
|
97
|
+
}
|
|
98
|
+
const rows = compareSkillTrees(payload, target);
|
|
99
|
+
const drifted = driftOf(rows);
|
|
100
|
+
const extra = rows.filter((r) => r.status === 'extra');
|
|
101
|
+
const mark = (s) => s === 'same' ? 'same ' : (s === 'different' ? 'different' : (s === 'missing' ? 'missing ' : 'extra '));
|
|
102
|
+
const human = [
|
|
103
|
+
'包内副本 ' + payload,
|
|
104
|
+
'已装副本 ' + target,
|
|
105
|
+
...rows.map((r) => ' ' + mark(r.status) + ' ' + r.path + (r.note ? '(' + r.note + ')' : '')),
|
|
106
|
+
drifted
|
|
107
|
+
? '结论:有漂移——用 --install <dir> 覆盖,或用 skills CLI 重新安装'
|
|
108
|
+
: '结论:逐字节一致' + (extra.length ? '(' + extra.length + ' 个多余文件不计漂移)' : ''),
|
|
109
|
+
];
|
|
110
|
+
return {
|
|
111
|
+
exitCode: drifted ? EXIT.FINDINGS : EXIT.OK,
|
|
112
|
+
human,
|
|
113
|
+
data: { payload, target, drift: drifted, files: rows },
|
|
114
|
+
...(drifted ? { warnings: ['skill 与包内副本不一致:已装副本可能对应旧版本 CLI'] } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const installDir = typeof ctx.values.install === 'string' ? ctx.values.install : undefined;
|
|
118
|
+
if (installDir !== undefined) {
|
|
119
|
+
const target = join(installDir, SKILL_NAME);
|
|
120
|
+
const written = [];
|
|
121
|
+
for (const rel of files) {
|
|
122
|
+
const dst = join(target, ...rel.split('/'));
|
|
123
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
124
|
+
writeFileSync(dst, readFileSync(join(payload, ...rel.split('/'))));
|
|
125
|
+
written.push(dst);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
exitCode: EXIT.OK,
|
|
129
|
+
human: [
|
|
130
|
+
'已写出 ' + files.length + ' 个文件 → ' + target,
|
|
131
|
+
...written.map((w) => ' ' + w),
|
|
132
|
+
'注:本命令只做逐字节复制;要 agent 自动更新请用 skills CLI(npx skills add …)',
|
|
133
|
+
],
|
|
134
|
+
data: { payload, target, files: written },
|
|
135
|
+
written,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (ctx.values.print === true) {
|
|
139
|
+
const rows = readPayload(payload);
|
|
140
|
+
const asJson = ctx.values.format === 'json';
|
|
141
|
+
if (asJson) {
|
|
142
|
+
return {
|
|
143
|
+
exitCode: EXIT.OK,
|
|
144
|
+
data: { payload, files: rows },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const human = [];
|
|
148
|
+
for (const f of rows) {
|
|
149
|
+
human.push('===== ' + f.path + ' =====');
|
|
150
|
+
human.push(...f.text.replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n'));
|
|
151
|
+
human.push('');
|
|
152
|
+
}
|
|
153
|
+
return { exitCode: EXIT.OK, human, data: { payload, files: rows.map(({ text, ...rest }) => rest) } };
|
|
154
|
+
}
|
|
155
|
+
const rows = readPayload(payload);
|
|
156
|
+
const total = rows.reduce((a, f) => a + f.bytes, 0);
|
|
157
|
+
return {
|
|
158
|
+
exitCode: EXIT.OK,
|
|
159
|
+
human: [
|
|
160
|
+
'包内 skill:' + payload,
|
|
161
|
+
'文件 ' + rows.length + ' 个,共 ' + total + ' 字节(skill 不写自己的版本号——对齐用比字节)',
|
|
162
|
+
...rows.map((f) => ' ' + f.sha256.slice(0, 12) + ' ' + String(f.bytes).padStart(6) + ' ' + f.path),
|
|
163
|
+
'下一步:maafw-live skill --check <安装根目录> 比对已装副本;--install <目录> 逐字节写出',
|
|
164
|
+
],
|
|
165
|
+
data: { payload, files: rows.map(({ text, ...rest }) => rest) },
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=skill.js.map
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timing:从帧流与节点事件反推 pipeline 的 delay / timeout 建议值。
|
|
3
|
+
*
|
|
4
|
+
* 做法:自动开流 → 跑任务(--runs n 次采样)→ 对齐两类时间线——
|
|
5
|
+
* - 节点时长(record.nodes)→ 节点级 timeout 建议(P95 × 1.5 余量)
|
|
6
|
+
* - 节点完成后的画面稳定时间(完成事件到下一个 stable 事件的时距)→ post-delay /
|
|
7
|
+
* wait_freezes 建议;下一节点先于 stable 开始则标"未稳定"(连续动画/识别窗口过紧)。
|
|
8
|
+
* 分布口径:建议值只取成功 run(失败 run 的时长是提前终止的,对 timeout 建议是误导);
|
|
9
|
+
* 单次采样退化为 P50=P95=该值,与历史单次行为逐字段一致。
|
|
10
|
+
*/
|
|
11
|
+
import { EXIT, fail } from '../protocol.js';
|
|
12
|
+
import { computePipelineOverride, appendOverride } from '../interface/index.js';
|
|
13
|
+
import { buildAgentEnv } from '../runtime/pi-env.js';
|
|
14
|
+
import { parseOptionArgs } from '../option-args.js';
|
|
15
|
+
import * as act from '../runtime/actions.js';
|
|
16
|
+
import { withDaemon } from '../runtime/actions.js';
|
|
17
|
+
import { describeSession, ensureSession } from '../runtime/session.js';
|
|
18
|
+
import { CONNECT_OPTIONS, daemonFail, argPath, sessionOptions } from './runtime.js';
|
|
19
|
+
/** 最近邻秩百分位:sorted 升序;n=1 时任意 p 返回该值,n 小时偏保守端(P95 取最大) */
|
|
20
|
+
export function percentile(values, p) {
|
|
21
|
+
if (!values.length)
|
|
22
|
+
return 0;
|
|
23
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
24
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
25
|
+
return sorted[idx];
|
|
26
|
+
}
|
|
27
|
+
/** 跨 run 聚合(纯函数,单测直断言):节点统计与建议值都只算成功样本 */
|
|
28
|
+
export function aggregateTiming(samples) {
|
|
29
|
+
const ok = samples.filter((s) => s.ok);
|
|
30
|
+
const failed = samples.length - ok.length;
|
|
31
|
+
const durations = ok.map((s) => s.durationMs);
|
|
32
|
+
const maxNodeP95 = ok.length ? percentile(ok.map((s) => Math.max(0, ...s.nodes.map((n) => n.ms))), 95) : 0;
|
|
33
|
+
const settleSamples = ok.map((s) => {
|
|
34
|
+
const ss = s.nodes.filter((n) => n.settle !== null).map((n) => n.settle);
|
|
35
|
+
return ss.length ? Math.max(...ss) : null;
|
|
36
|
+
});
|
|
37
|
+
const settles = settleSamples.filter((v) => v !== null);
|
|
38
|
+
const names = [...new Set(ok.flatMap((s) => s.nodes.map((n) => n.name)))];
|
|
39
|
+
const nodeStats = names.map((name) => {
|
|
40
|
+
/* 全部出现都算:同名节点 run 内重复执行时,慢的往往是第二次(首跑等动画) */
|
|
41
|
+
const per = ok.flatMap((s) => s.nodes.filter((n) => n.name === name));
|
|
42
|
+
const ms = per.map((n) => n.ms);
|
|
43
|
+
const st = per.filter((n) => n.settle !== null).map((n) => n.settle);
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
status: per.at(-1)?.status ?? 'unknown',
|
|
47
|
+
runs: ok.filter((s) => s.nodes.some((n) => n.name === name)).length,
|
|
48
|
+
occurrences: per.length,
|
|
49
|
+
msP50: percentile(ms, 50),
|
|
50
|
+
msP95: percentile(ms, 95),
|
|
51
|
+
settleP50: st.length ? percentile(st, 50) : null,
|
|
52
|
+
settleP95: st.length ? percentile(st, 95) : null,
|
|
53
|
+
settleSamples: st.length,
|
|
54
|
+
nextBeforeStable: per.some((n) => n.nextBeforeStable),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
successful: ok.length, failed,
|
|
59
|
+
durationP50: percentile(durations, 50),
|
|
60
|
+
durationP95: percentile(durations, 95),
|
|
61
|
+
maxNodeP95,
|
|
62
|
+
maxSettleP95: settles.length ? percentile(settles, 95) : null,
|
|
63
|
+
nodeStats,
|
|
64
|
+
suggest: {
|
|
65
|
+
nodeTimeoutMs: Math.ceil((maxNodeP95 * 1.5) / 100) * 100,
|
|
66
|
+
taskTimeoutMs: Math.ceil((percentile(durations, 95) * 2) / 1000) * 1000,
|
|
67
|
+
postDelayMs: settles.length ? Math.ceil((percentile(settles, 95) * 1.2) / 50) * 50 : null,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export const timingCommand = {
|
|
72
|
+
name: 'timing',
|
|
73
|
+
summary: '跑任务并反推 timing 建议:节点时长 → timeout,动作后稳定时间 → wait_freezes 兜底参考;--runs n 取 P50/P95 分布',
|
|
74
|
+
usage: 'maafw-live timing --entry <task> --project <dir> [--timeout <ms>] [--runs <n>] [--preset <name>] [--override <json>]\n' +
|
|
75
|
+
' 自动开流(帧流与节点事件对齐是分析前提);单次采样是下界,--runs 3-5 取分布再定稿',
|
|
76
|
+
options: {
|
|
77
|
+
...CONNECT_OPTIONS,
|
|
78
|
+
entry: { type: 'string' },
|
|
79
|
+
'resource-dir': { type: 'string' },
|
|
80
|
+
timeout: { type: 'string' },
|
|
81
|
+
runs: { type: 'string' },
|
|
82
|
+
override: { type: 'string' },
|
|
83
|
+
preset: { type: 'string' },
|
|
84
|
+
/* 与 run 同一取值通道:timing 测的就是 run 那条路,option 进不去会让
|
|
85
|
+
* "时长分布"测的是一个根本不该跑的配置(P1-#4)。 */
|
|
86
|
+
option: { type: 'string', multiple: true },
|
|
87
|
+
},
|
|
88
|
+
async run(ctx) {
|
|
89
|
+
const o = sessionOptions(ctx);
|
|
90
|
+
const entry = (typeof ctx.values.entry === 'string' ? ctx.values.entry : ctx.positionals[0]) ?? undefined;
|
|
91
|
+
const resourceDir = argPath(ctx, typeof ctx.values['resource-dir'] === 'string' ? ctx.values['resource-dir'] : undefined);
|
|
92
|
+
if (!entry)
|
|
93
|
+
return fail('BAD_ARGUMENTS', '缺少入口任务名:--entry <task>', undefined, EXIT.USAGE);
|
|
94
|
+
if (!o.project && !resourceDir) {
|
|
95
|
+
return fail('BAD_ARGUMENTS', '要么给 --project <dir>,要么给 --resource-dir <dir>', undefined, EXIT.USAGE);
|
|
96
|
+
}
|
|
97
|
+
const timeoutMs = typeof ctx.values.timeout === 'string' ? Number(ctx.values.timeout) : 120000;
|
|
98
|
+
if (!Number.isFinite(timeoutMs))
|
|
99
|
+
return fail('BAD_ARGUMENTS', '--timeout 必须是毫秒数', undefined, EXIT.USAGE);
|
|
100
|
+
const runs = typeof ctx.values.runs === 'string' ? Number(ctx.values.runs) : 1;
|
|
101
|
+
if (!Number.isInteger(runs) || runs < 1 || runs > 10) {
|
|
102
|
+
return fail('BAD_ARGUMENTS', '--runs 需为 1-10 的整数(真机动作采样,别当压测跑)', undefined, EXIT.USAGE);
|
|
103
|
+
}
|
|
104
|
+
let override = {};
|
|
105
|
+
if (typeof ctx.values.override === 'string') {
|
|
106
|
+
try {
|
|
107
|
+
override = JSON.parse(ctx.values.override);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return fail('BAD_ARGUMENTS', '--override 不是合法 JSON', undefined, EXIT.USAGE);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/* --option 与 run 同一取值通道(CLI > preset > default_case 由 computePipelineOverride 判) */
|
|
114
|
+
const parsedOptions = parseOptionArgs(ctx.values.option);
|
|
115
|
+
if (parsedOptions.error) {
|
|
116
|
+
return fail('BAD_ARGUMENTS', parsedOptions.error, '例:--option taskOpt=b --option 章节数.章节号=4', EXIT.USAGE);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return await withDaemon(ctx, async (client) => {
|
|
120
|
+
const s = await ensureSession(client, o);
|
|
121
|
+
let args;
|
|
122
|
+
if (o.project) {
|
|
123
|
+
const paths = s.plan?.resource?.paths ?? [];
|
|
124
|
+
if (!paths.length) {
|
|
125
|
+
return fail('RESOURCE_NOT_FOUND', '项目里没有解析出资源路径:检查 interface.json 的 resource[] 声明', undefined, EXIT.FINDINGS);
|
|
126
|
+
}
|
|
127
|
+
const task = s.plan?.loaded.tasks.find((t) => t.name === entry || t.entry === entry);
|
|
128
|
+
const chain = computePipelineOverride({
|
|
129
|
+
loaded: s.plan.loaded,
|
|
130
|
+
controllerName: s.plan?.controllerName ?? null,
|
|
131
|
+
resourceName: s.plan?.resource?.selected ?? null,
|
|
132
|
+
taskName: task?.name ?? null,
|
|
133
|
+
presetName: typeof ctx.values.preset === 'string' ? ctx.values.preset : null,
|
|
134
|
+
...(Object.keys(parsedOptions.cases).length ? { optionCases: parsedOptions.cases } : {}),
|
|
135
|
+
...(Object.keys(parsedOptions.fields).length ? { optionFields: parsedOptions.fields } : {}),
|
|
136
|
+
});
|
|
137
|
+
/* 用户 --override 追加到链尾 = 最高优先级:**与 run 共用同一份追加实现**。
|
|
138
|
+
* 分层数组交给框架逐层继承;不在这里预合并(层间 type 边界会被摊平毁掉)。 */
|
|
139
|
+
appendOverride(chain.override, override);
|
|
140
|
+
args = {
|
|
141
|
+
resourceDirs: paths, entry: task?.entry ?? entry,
|
|
142
|
+
override: chain.override, timeoutMs,
|
|
143
|
+
agents: s.plan.loaded.agents.map((a) => ({ exec: a.exec, args: a.args })),
|
|
144
|
+
agentCwd: s.plan.loaded.dir ?? undefined,
|
|
145
|
+
/* agent 子进程的环境变量声明按"当前 controller/resource 选择"生成——
|
|
146
|
+
* 此前 timing 不带 env,agent 拿到的配置与 run 跑的不是同一套(P1-#4)。 */
|
|
147
|
+
agentEnv: buildAgentEnv({
|
|
148
|
+
loaded: s.plan.loaded,
|
|
149
|
+
controllerName: s.plan?.controllerName ?? null,
|
|
150
|
+
resourceName: s.plan?.resource?.selected ?? null,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
args = { resourceDir, entry, override, timeoutMs };
|
|
156
|
+
}
|
|
157
|
+
/* 帧流是分析前提:开流 → 跑 n 次 → 停流;每 run 切自己的事件区间对齐。
|
|
158
|
+
* 末节点可能把下一 run 开头的 stable 算作自己的稳定点——语义上仍是
|
|
159
|
+
* "该节点之后画面何时静默",可接受。 */
|
|
160
|
+
await act.streamStart(client, { fps: 8 }).catch(() => null);
|
|
161
|
+
const samples = [];
|
|
162
|
+
try {
|
|
163
|
+
for (let i = 1; i <= runs; i++) {
|
|
164
|
+
const eventsBefore = client.events.events.length;
|
|
165
|
+
const r = await act.run(client, args, timeoutMs);
|
|
166
|
+
const evs = client.events.events.slice(eventsBefore);
|
|
167
|
+
if (r.ok === false || !r.record) {
|
|
168
|
+
return fail('TIMING_RUN', '任务没跑起来:' + String(r.error ?? '?'), undefined, EXIT.FINDINGS);
|
|
169
|
+
}
|
|
170
|
+
const rec = r.record;
|
|
171
|
+
/* 时间线对齐:run 类事件带节点名与消息;stable 是画面静默点 */
|
|
172
|
+
const runEvs = evs.filter((e) => e.type === 'run' && e.node);
|
|
173
|
+
const stables = evs.filter((e) => e.type === 'stable').map((e) => Number(e.t));
|
|
174
|
+
const nodes = rec.nodes.map((n) => {
|
|
175
|
+
const done = runEvs.filter((e) => e.node === n.name && /Succeeded$/.test(e.msg)).at(-1)
|
|
176
|
+
?? runEvs.filter((e) => e.node === n.name && /Failed$/.test(e.msg)).at(-1);
|
|
177
|
+
if (!done)
|
|
178
|
+
return { name: n.name, ms: n.ms, status: n.status, settle: null, nextBeforeStable: false };
|
|
179
|
+
const nextNode = runEvs.find((e) => e.t > done.t && e.node !== n.name);
|
|
180
|
+
const stable = stables.find((t) => t > done.t);
|
|
181
|
+
const settle = stable !== undefined ? stable - done.t : null;
|
|
182
|
+
return {
|
|
183
|
+
name: n.name, ms: n.ms, status: n.status,
|
|
184
|
+
settle,
|
|
185
|
+
nextBeforeStable: nextNode !== undefined && (stable === undefined || nextNode.t < stable),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
samples.push({ index: i, ok: rec.ok, durationMs: rec.durationMs, nodes, events: evs.length });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await act.streamStop(client).catch(() => null);
|
|
193
|
+
}
|
|
194
|
+
const agg = aggregateTiming(samples);
|
|
195
|
+
if (!agg.successful) {
|
|
196
|
+
return fail('TIMING_RUN', runs + ' 次采样全部未成功——没有可用于建议的成功样本', undefined, EXIT.FINDINGS);
|
|
197
|
+
}
|
|
198
|
+
const unsettled = agg.nodeStats.filter((n) => n.nextBeforeStable);
|
|
199
|
+
const last = samples.at(-1);
|
|
200
|
+
const human = [
|
|
201
|
+
...describeSession(s, ctx.flags.verbose),
|
|
202
|
+
'timing 采样 x' + runs + '(' + agg.successful + ' 成功' + (agg.failed ? ' / ' + agg.failed + ' 失败' : '') + '):',
|
|
203
|
+
...samples.map((smp) => ' #' + smp.index + ' ' + (smp.ok ? '成功' : '失败(不计入建议)') + ' ' + smp.durationMs + 'ms ' + smp.nodes.length + ' 节点'),
|
|
204
|
+
'节点时长分布(成功样本,P50 / P95):',
|
|
205
|
+
...agg.nodeStats.map((n) => ' [' + n.status + '] ' + n.name + ' ' + n.msP50 + ' / ' + n.msP95 + 'ms' +
|
|
206
|
+
(n.settleP50 !== null ? ' 稳定 +' + n.settleP50 + ' / +' + n.settleP95 + 'ms' : '') +
|
|
207
|
+
(n.occurrences > n.runs ? ' (重复执行 ' + n.occurrences + ' 次 / ' + n.runs + ' run——重试或 Next 环)' : '') +
|
|
208
|
+
(n.runs < agg.successful ? ' (' + n.runs + '/' + agg.successful + ' run 出现——分支或跳过)' : '') +
|
|
209
|
+
(n.nextBeforeStable ? ' (下一节点先于画面稳定开始——识别窗口紧或连续动画)' : '')),
|
|
210
|
+
'建议(P95 × 余量,n=' + agg.successful + ' 成功采样;界面切换优先用 next 等待节点表达,delay 尽量保持缺省):',
|
|
211
|
+
' 节点 timeout ≥ ' + agg.suggest.nodeTimeoutMs + 'ms(各 run 最长节点 P95 ' + agg.maxNodeP95 + 'ms × 1.5)',
|
|
212
|
+
' 任务 timeout ≥ ' + agg.suggest.taskTimeoutMs + 'ms(任务时长 P95 ' + agg.durationP95 + 'ms × 2)',
|
|
213
|
+
agg.suggest.postDelayMs !== null
|
|
214
|
+
? ' 动作后稳定参考 ' + agg.suggest.postDelayMs + 'ms(各 run 最长稳定 P95 ' + agg.maxSettleP95 + 'ms × 1.2)——优先由 next 等待节点的 timeout 覆盖该时长;确需兜底用 post_wait_freezes,post_delay 是最后手段'
|
|
215
|
+
: ' 动作后稳定时间:无 stable 事件可参考(画面持续变化或流太短)',
|
|
216
|
+
];
|
|
217
|
+
if (agg.failed)
|
|
218
|
+
human.push(' 注意:' + agg.failed + ' 个失败 run 未计入建议;失败原因与时长见 data.runs');
|
|
219
|
+
if (unsettled.length) {
|
|
220
|
+
human.push(' 注意:' + unsettled.map((n) => n.name).join(' , ') + ' 未等画面稳定就进入下一节点');
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
exitCode: EXIT.OK,
|
|
224
|
+
human,
|
|
225
|
+
data: {
|
|
226
|
+
runs: samples, distribution: agg, suggest: agg.suggest,
|
|
227
|
+
/* 兼容字段:单次消费方读 record / nodes 的形状不变 */
|
|
228
|
+
record: { ok: last.ok, durationMs: last.durationMs, nodes: last.nodes },
|
|
229
|
+
nodes: last.nodes,
|
|
230
|
+
sampledEvents: samples.reduce((a, smp) => a + smp.events, 0),
|
|
231
|
+
},
|
|
232
|
+
warnings: [
|
|
233
|
+
...(agg.failed ? [agg.failed + ' 个失败 run 未计入建议'] : []),
|
|
234
|
+
...samples.filter((smp) => !smp.ok).map((smp) => 'run #' + smp.index + ' 未成功——该样本已排除'),
|
|
235
|
+
...(unsettled.length ? [unsettled.length + ' 个节点未等画面稳定即进入下一节点:' + unsettled.map((n) => n.name).join(' , ')] : []),
|
|
236
|
+
...(runs === 1 ? ['单次采样=下界:加 --runs 3-5 取分布再定稿'] : []),
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
return daemonFail(e);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
export const TIMING_COMMANDS = [timingCommand];
|
|
247
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** `maafw-live version` —— CLI 与运行环境版本。技能用它做版本对齐;`--check` 对 registry 查有没有新版。 */
|
|
2
|
+
import { EXIT, fail } from '../protocol.js';
|
|
3
|
+
import { cliPackage } from '../pkg.js';
|
|
4
|
+
import { compareVersions, fetchLatest, readState, shouldCheck, updateCheckDisabled, writeState, } from '../update-check.js';
|
|
5
|
+
export const versionCommand = {
|
|
6
|
+
name: 'version',
|
|
7
|
+
summary: '打印 maafw-live CLI 及依赖的版本;--check 顺带对 registry 查有没有新版',
|
|
8
|
+
usage: 'maafw-live version [--check]',
|
|
9
|
+
options: { check: { type: 'boolean' } },
|
|
10
|
+
async run(ctx) {
|
|
11
|
+
const pkg = cliPackage();
|
|
12
|
+
const human = [
|
|
13
|
+
`${pkg.name} ${pkg.version}`,
|
|
14
|
+
// 装的是带 scope 的包名,敲的是裸名——同一个词的两种形态,这里显式给出敲的那个
|
|
15
|
+
` command maafw-live`,
|
|
16
|
+
` node ${process.version}`,
|
|
17
|
+
` platform ${process.platform} ${process.arch}`,
|
|
18
|
+
];
|
|
19
|
+
const data = {
|
|
20
|
+
name: pkg.name,
|
|
21
|
+
version: pkg.version,
|
|
22
|
+
node: process.version,
|
|
23
|
+
platform: process.platform,
|
|
24
|
+
arch: process.arch,
|
|
25
|
+
};
|
|
26
|
+
const explicit = ctx.values.check === true;
|
|
27
|
+
/* 显式请求:不受 TTY/限频约束,查不到要如实说。自动路径:只在交互终端、限频、失败静默。 */
|
|
28
|
+
const auto = !explicit && !updateCheckDisabled() && process.stdout.isTTY === true;
|
|
29
|
+
if (explicit || auto) {
|
|
30
|
+
let latest = auto ? readState()?.latest ?? null : null;
|
|
31
|
+
let fresh = false;
|
|
32
|
+
const cached = readState();
|
|
33
|
+
if (explicit || shouldCheck(cached, Date.now())) {
|
|
34
|
+
const fetched = await fetchLatest(pkg.name);
|
|
35
|
+
if (fetched) {
|
|
36
|
+
latest = fetched;
|
|
37
|
+
writeState({ checkedAt: Date.now(), latest: fetched });
|
|
38
|
+
fresh = true;
|
|
39
|
+
}
|
|
40
|
+
else if (explicit) {
|
|
41
|
+
return fail('UPDATE_CHECK_FAILED', '查询 registry 失败(离线 / 代理 / registry 抖动)', '加 --check 才需要联网;不带它 version 完全离线可用', EXIT.ENV);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (cached) {
|
|
45
|
+
latest = cached.latest;
|
|
46
|
+
}
|
|
47
|
+
if (latest) {
|
|
48
|
+
const cmp = compareVersions(latest, pkg.version);
|
|
49
|
+
data.latest = latest;
|
|
50
|
+
data.updateAvailable = cmp > 0;
|
|
51
|
+
data.source = fresh ? 'registry' : 'cache';
|
|
52
|
+
if (cmp > 0) {
|
|
53
|
+
human.push(` 新版 ${latest}(当前 ${pkg.version})`, ` 升级 npm install --global ${pkg.name}@latest`);
|
|
54
|
+
/* 自动提示不改退出码:版本旧不是"这次调用发现的问题" */
|
|
55
|
+
return { exitCode: explicit ? EXIT.FINDINGS : EXIT.OK, human, data };
|
|
56
|
+
}
|
|
57
|
+
human.push(` 最新 ${latest}(已是最新)`);
|
|
58
|
+
}
|
|
59
|
+
else if (explicit) {
|
|
60
|
+
human.push(' 最新 查询不到(registry 不可达)');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { exitCode: EXIT.OK, human, data };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=version.js.map
|