@windsland52/maafw-live 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -2
  3. package/bin/maafw-live.mjs +25 -0
  4. package/docs/daemon-protocol.md +121 -0
  5. package/docs/keyframe-retention-contract-v0.md +225 -0
  6. package/docs/session-unify-v0.md +115 -0
  7. package/lib/client/daemon.js +276 -0
  8. package/lib/commands/annotate.js +179 -0
  9. package/lib/commands/calibrate.js +61 -0
  10. package/lib/commands/color.js +73 -0
  11. package/lib/commands/crop.js +228 -0
  12. package/lib/commands/dispatch.js +121 -0
  13. package/lib/commands/env.js +409 -0
  14. package/lib/commands/index.js +70 -0
  15. package/lib/commands/input.js +257 -0
  16. package/lib/commands/kf.js +167 -0
  17. package/lib/commands/probe.js +70 -0
  18. package/lib/commands/reco.js +440 -0
  19. package/lib/commands/repl.js +1501 -0
  20. package/lib/commands/roi-check.js +273 -0
  21. package/lib/commands/roi-compare.js +388 -0
  22. package/lib/commands/runtime.js +718 -0
  23. package/lib/commands/skill.js +169 -0
  24. package/lib/commands/timing.js +247 -0
  25. package/lib/commands/version.js +66 -0
  26. package/lib/daemon/framed.mjs +3680 -0
  27. package/lib/daemon/reco_child.mjs +287 -0
  28. package/lib/exec.js +96 -0
  29. package/lib/flags.js +44 -0
  30. package/lib/image.js +90 -0
  31. package/lib/index.js +143 -0
  32. package/lib/interface/hotkey.js +142 -0
  33. package/lib/interface/index.js +12 -0
  34. package/lib/interface/jsonc.js +95 -0
  35. package/lib/interface/load.js +397 -0
  36. package/lib/interface/override.js +287 -0
  37. package/lib/interface/plan.js +279 -0
  38. package/lib/option-args.js +29 -0
  39. package/lib/pkg.js +54 -0
  40. package/lib/protocol.js +94 -0
  41. package/lib/runtime/actions.js +225 -0
  42. package/lib/runtime/keyframes.js +73 -0
  43. package/lib/runtime/log-watch.js +160 -0
  44. package/lib/runtime/node-source.js +100 -0
  45. package/lib/runtime/ocr-model.js +45 -0
  46. package/lib/runtime/pi-env.js +34 -0
  47. package/lib/runtime/pretask.js +217 -0
  48. package/lib/runtime/prune.js +28 -0
  49. package/lib/runtime/receipts.js +57 -0
  50. package/lib/runtime/reco-args.js +38 -0
  51. package/lib/runtime/reco-verdict.js +82 -0
  52. package/lib/runtime/roi-compare.js +198 -0
  53. package/lib/runtime/session-forward.js +79 -0
  54. package/lib/runtime/session-ipc.js +471 -0
  55. package/lib/runtime/session.js +135 -0
  56. package/lib/runtime/signal-guard.js +35 -0
  57. package/lib/types/client/daemon.d.ts +86 -0
  58. package/lib/types/commands/annotate.d.ts +3 -0
  59. package/lib/types/commands/calibrate.d.ts +7 -0
  60. package/lib/types/commands/color.d.ts +9 -0
  61. package/lib/types/commands/crop.d.ts +3 -0
  62. package/lib/types/commands/dispatch.d.ts +22 -0
  63. package/lib/types/commands/env.d.ts +21 -0
  64. package/lib/types/commands/index.d.ts +13 -0
  65. package/lib/types/commands/input.d.ts +17 -0
  66. package/lib/types/commands/kf.d.ts +10 -0
  67. package/lib/types/commands/probe.d.ts +15 -0
  68. package/lib/types/commands/reco.d.ts +27 -0
  69. package/lib/types/commands/repl.d.ts +10 -0
  70. package/lib/types/commands/roi-check.d.ts +3 -0
  71. package/lib/types/commands/roi-compare.d.ts +3 -0
  72. package/lib/types/commands/runtime.d.ts +66 -0
  73. package/lib/types/commands/skill.d.ts +17 -0
  74. package/lib/types/commands/timing.d.ts +64 -0
  75. package/lib/types/commands/version.d.ts +3 -0
  76. package/lib/types/exec.d.ts +31 -0
  77. package/lib/types/flags.d.ts +62 -0
  78. package/lib/types/image.d.ts +27 -0
  79. package/lib/types/index.d.ts +1 -0
  80. package/lib/types/interface/hotkey.d.ts +36 -0
  81. package/lib/types/interface/index.d.ts +11 -0
  82. package/lib/types/interface/jsonc.d.ts +9 -0
  83. package/lib/types/interface/load.d.ts +190 -0
  84. package/lib/types/interface/override.d.ts +78 -0
  85. package/lib/types/interface/plan.d.ts +68 -0
  86. package/lib/types/option-args.d.ts +19 -0
  87. package/lib/types/pkg.d.ts +14 -0
  88. package/lib/types/protocol.d.ts +124 -0
  89. package/lib/types/runtime/actions.d.ts +248 -0
  90. package/lib/types/runtime/keyframes.d.ts +43 -0
  91. package/lib/types/runtime/log-watch.d.ts +28 -0
  92. package/lib/types/runtime/node-source.d.ts +25 -0
  93. package/lib/types/runtime/ocr-model.d.ts +8 -0
  94. package/lib/types/runtime/pi-env.d.ts +24 -0
  95. package/lib/types/runtime/pretask.d.ts +53 -0
  96. package/lib/types/runtime/prune.d.ts +4 -0
  97. package/lib/types/runtime/receipts.d.ts +7 -0
  98. package/lib/types/runtime/reco-args.d.ts +18 -0
  99. package/lib/types/runtime/reco-verdict.d.ts +50 -0
  100. package/lib/types/runtime/roi-compare.d.ts +117 -0
  101. package/lib/types/runtime/session-forward.d.ts +10 -0
  102. package/lib/types/runtime/session-ipc.d.ts +117 -0
  103. package/lib/types/runtime/session.d.ts +77 -0
  104. package/lib/types/runtime/signal-guard.d.ts +19 -0
  105. package/lib/types/update-check.d.ts +20 -0
  106. package/lib/types/usage.d.ts +3 -0
  107. package/lib/update-check.js +97 -0
  108. package/lib/usage.js +50 -0
  109. package/package.json +94 -4
  110. package/skills/README.md +51 -0
  111. package/skills/maafw-live/SKILL.md +293 -0
  112. package/skills/maafw-live/references/examples.md +238 -0
  113. package/skills/maafw-live/references/pitfalls.md +211 -0
  114. package/skills/maafw-live/references/workflows.md +164 -0
@@ -0,0 +1,94 @@
1
+ /** 信封结构版本。字段增删时递增,消费方可据此拒绝不认识的形状。 */
2
+ export const SCHEMA_VERSION = 1;
3
+ /**
4
+ * 退出码语义。
5
+ *
6
+ * 关键区分:`FAIL` 是「命令没跑成」,`FINDINGS` 是「跑成了,但发现了问题」。
7
+ * 技能与 CI 必须能分开这两种情况——reco 识别未命中不是执行失败,
8
+ * 但也不该返回 0 让调用方以为一切正常。
9
+ */
10
+ export const EXIT = {
11
+ /** 命令正常完成,未发现问题 */
12
+ OK: 0,
13
+ /** 命令自身失败(未预期异常、IO 错误) */
14
+ FAIL: 1,
15
+ /** 参数 / 用法错误 */
16
+ USAGE: 2,
17
+ /** 命令正常跑完,但发现了问题(reco 未命中、run 调用级失败、观测类命令的警告等) */
18
+ FINDINGS: 3,
19
+ /** 前置环境缺失(缺 maa-node / 缺 checkout / 缺设备) */
20
+ ENV: 4,
21
+ /** 被 SIGINT(Ctrl-C)中断:128+2 */
22
+ SIGINT: 130,
23
+ /** 被 SIGTERM 中断:128+15 */
24
+ SIGTERM: 143,
25
+ };
26
+ export function toEnvelope(command, r) {
27
+ return {
28
+ schemaVersion: SCHEMA_VERSION,
29
+ command,
30
+ ok: r.exitCode === EXIT.OK || r.exitCode === EXIT.FINDINGS,
31
+ exitCode: r.exitCode,
32
+ root: r.root ?? null,
33
+ written: r.written ?? [],
34
+ removed: r.removed ?? [],
35
+ skipped: r.skipped ?? [],
36
+ pending: r.pending ?? [],
37
+ suggestedCommands: r.suggestedCommands ?? [],
38
+ warnings: r.warnings ?? [],
39
+ data: r.data ?? null,
40
+ error: r.error ?? null,
41
+ };
42
+ }
43
+ const ANSI = {
44
+ reset: '\u001b[0m',
45
+ dim: '\u001b[2m',
46
+ red: '\u001b[31m',
47
+ yellow: '\u001b[33m',
48
+ green: '\u001b[32m',
49
+ cyan: '\u001b[36m',
50
+ };
51
+ function paint(text, code, on) {
52
+ return on ? code + text + ANSI.reset : text;
53
+ }
54
+ /**
55
+ * 输出命令结果。
56
+ *
57
+ * --json 时只输出信封(stdout 保持纯 JSON,可被管道解析);
58
+ * 否则输出命令自己的 human 渲染 + 警告 + 建议命令。
59
+ * 错误与警告一律走 stderr,避免污染 stdout 的 JSON。
60
+ */
61
+ export function emit(command, r, o) {
62
+ if (o.json) {
63
+ process.stdout.write(JSON.stringify(toEnvelope(command, r), null, 2) + '\n');
64
+ return;
65
+ }
66
+ for (const line of r.human ?? [])
67
+ process.stdout.write(line + '\n');
68
+ for (const w of r.warnings ?? []) {
69
+ process.stderr.write(paint('warning: ', ANSI.yellow, o.color) + w + '\n');
70
+ }
71
+ if (r.error) {
72
+ process.stderr.write(paint('error: ', ANSI.red, o.color) + r.error.message +
73
+ paint(' [' + r.error.code + ']', ANSI.dim, o.color) + '\n');
74
+ if (r.error.hint) {
75
+ process.stderr.write(paint(' hint: ' + r.error.hint, ANSI.dim, o.color) + '\n');
76
+ }
77
+ }
78
+ if (r.pending?.length) {
79
+ process.stdout.write('\n待办:\n');
80
+ for (const p of r.pending)
81
+ process.stdout.write(' - ' + p + '\n');
82
+ }
83
+ if (r.suggestedCommands?.length) {
84
+ process.stdout.write('\n建议下一步:\n');
85
+ for (const c of r.suggestedCommands) {
86
+ process.stdout.write(' ' + paint(c, ANSI.cyan, o.color) + '\n');
87
+ }
88
+ }
89
+ }
90
+ /** 失败结果的快捷构造 */
91
+ export function fail(code, message, hint, exitCode = EXIT.FAIL) {
92
+ return { exitCode, error: { code, message, ...(hint ? { hint } : {}) } };
93
+ }
94
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,225 @@
1
+ /**
2
+ * 运行时动作层:把 daemon 的 JSON 行命令包成有类型、有超时、有默认值的函数。
3
+ *
4
+ * 命令(`src/commands/*`)与 `maafw-live repl` 都只调这一层——同一条链路上不允许出现第二份
5
+ * 参数拼装逻辑,否则 CLI 与 REPL 会各长出一套规则。项目(interface.json)驱动的连接与运行
6
+ * 也在这里收口,避免"手动连 vs 项目连"两条路径对资源与坐标系的解释分叉。
7
+ */
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { rmSync } from 'node:fs';
11
+ import { computePipelineOverride, loadInterface, appendOverride, planController, resolveResourcePaths, } from '../interface/index.js';
12
+ import { spawnDaemon } from '../client/daemon.js';
13
+ import { buildAgentEnv } from './pi-env.js';
14
+ /**
15
+ * 给命令一个 daemon 客户端:ctx.client 有就借用(REPL / 命名会话,**不关**——
16
+ * 会话的生命周期不归命令管);没有才 spawn 一次性的,跑完即关。
17
+ * 统一入口的意义:命令只需写一遍,`--session` 转发与 REPL 复用走的是同一份实现。
18
+ */
19
+ export async function withDaemon(ctx, fn, options = {}) {
20
+ if (ctx.client)
21
+ return await fn(ctx.client);
22
+ const client = spawnDaemon(options);
23
+ try {
24
+ return await fn(client);
25
+ }
26
+ finally {
27
+ client.close();
28
+ }
29
+ }
30
+ /* ─────────────────────────── 直通命令 ─────────────────────────── */
31
+ export const probe = (c) => c.call('probe', {}, 30000);
32
+ export const deviceList = (c, kind = 'all') => c.call('device_list', { kind }, 40000);
33
+ export const connect = (c, args) => c.call('connect', args, 40000);
34
+ export const disconnect = (c) => c.call('disconnect', {}, 20000);
35
+ export const screencap = (c, out) => c.call('screencap', out ? { out } : {}, 30000);
36
+ /**
37
+ * 一次性命令的"先产生一次观测":截一帧进 daemon 的 L0,然后**把临时文件删掉**。
38
+ *
39
+ * 为什么需要这一帧:L0 是进程态,一次性命令(`crop` / `annotate` / `color`)每次 spawn 自己的
40
+ * daemon,不先截一帧就没有原图可用。为什么必须删:daemon 收帧时就把像素读进缓冲了,
41
+ * 落盘那份只为喂它一次——不删就是每跑一次在系统临时目录留一张 720p 截图(实测攒到 42 张 / 54MB)。
42
+ * 失败不抛:没有观测时命令自己会报"先 screencap / stream"这类可行动的错。
43
+ */
44
+ export async function seedObservation(c, prefix) {
45
+ const file = join(tmpdir(), 'maafw_' + prefix + '_' + Date.now() + '.png');
46
+ try {
47
+ await screencap(c, file).catch(() => null);
48
+ }
49
+ finally {
50
+ try {
51
+ rmSync(file, { force: true });
52
+ }
53
+ catch { /* ignore */ }
54
+ }
55
+ }
56
+ export const streamStart = (c, args = {}) => c.call('stream_start', args, 15000);
57
+ export const streamStop = (c) => c.call('stream_stop', {}, 10000);
58
+ export const streamStatus = (c) => c.call('stream_status', {}, 10000);
59
+ /** 事件环查询:一次性 CLI 用。事件环活在 daemon 里,跨进程只能现查(REPL 另有本地副本)。 */
60
+ export const eventsList = (c, args = {}) => c.call('events_list', args, 10000);
61
+ export const frameGet = (c, args) => c.call('frame_get', args, 30000);
62
+ /** 等状态谓词成立:stable=画面静下来 / change=画面动了。取代调用方硬睡时钟。 */
63
+ export const waitState = (c, args = {}) => c.call('wait', args, Math.min(120000, Number(args.timeout ?? 10000)) + 5000);
64
+ /** 两帧在若干 ROI 上的差分:回答"这一步改了哪几格"。 */
65
+ export const frameDiff = (c, args) => c.call('frame_diff', args, 30000);
66
+ export const colorProbe = (c, args) => c.call('color_probe', args, 15000);
67
+ export const tplCrop = (c, args) => c.call('tpl_crop', args, 240000);
68
+ export const annotate = (c, args) => c.call('annotate', args, 120000);
69
+ export const calibrate = (c, args = {}) => c.call('calibrate', args, 120000);
70
+ export const runStop = (c) => c.call('run_stop', {}, 10000);
71
+ export const l0Status = (c) => c.call('l0_status', {}, 10000);
72
+ export const kfPromote = (c, args) => c.call('kf_promote', args, 30000);
73
+ export function run(c, args, timeoutMs = 30000) {
74
+ /* timeoutMs=0 = 不自动停(停止权交调用方):客户端调用超时放到 24h 档。
75
+ * 不用 Infinity——Node 的 setTimeout 上限 2^31-1,超限会立即触发,反而误杀。 */
76
+ const t = Number(timeoutMs);
77
+ const budget = t === 0 ? 86400000 : Math.min(86400000, Math.max(500, t || 30000));
78
+ return c.call('run', { ...args, timeoutMs: t === 0 ? 0 : budget }, budget + 15000);
79
+ }
80
+ export const input = (c, payload, timeoutMs = 20000) => c.call('input', payload, timeoutMs);
81
+ /** 截图能力探测(A 路前提):两次 post_screencap 间隔比 hash/亮度。timeout 给 interval+10s。 */
82
+ export const screencapProbe = (c, intervalMs = 3000, occluded) => c.call('screencap_probe', { intervalMs, ...(occluded !== undefined ? { occluded } : {}) }, intervalMs + 15000);
83
+ /** 识别单测。`timeoutMs` 供**多候选**调用方放宽(候选逐例串行,耗时随候选数线性增长:
84
+ * daemon 侧的子进程预算也跟着用例数放宽,见 framed.mjs 的 recoChildBudget)。 */
85
+ export const recoTest = (c, args, timeoutMs = 90000) => c.call('reco_test', args, timeoutMs);
86
+ /* ─────────────────────── 项目(interface.json)驱动 ─────────────────────── */
87
+ /**
88
+ * 控制器类型 → 需要扫描的设备类型;**无设备可枚举的类型返回 null**(调用方跳过扫描)。
89
+ * daemon 的 device_list 只枚举 adb/win32——PlayCover/MacOS/Linux 给 'win32' 会白白发起
90
+ * 一次必然失败的扫描(曾经就是这样,把无关窗口列表塞进计划错误里)。
91
+ */
92
+ export function deviceKindFor(loaded, controllerName) {
93
+ const c = controllerName ? loaded.controllers.find((x) => x.name === controllerName) : loaded.controllers[0];
94
+ const t = String(c?.type);
95
+ if (t === 'Adb')
96
+ return 'adb';
97
+ if (t === 'Win32' || t === 'Gamepad')
98
+ return 'win32';
99
+ return null;
100
+ }
101
+ /**
102
+ * 载入项目 → 扫设备 → 规划控制器 → 解析资源路径。
103
+ *
104
+ * 为什么不猜设备:interface.json 的 controller[] 已经声明了 Adb/Win32、窗口类名与标题正则、
105
+ * 截图与输入方法、以及识别缩放(display_short_side)——猜错不只是连错窗口,还会把 roi 与
106
+ * 模板的坐标系一起猜错。interface.json 的读法只有 src/interface/ 一份,别处不许再解析一遍。
107
+ */
108
+ export async function planProject(c, dir, overrides = {}) {
109
+ const loaded = loadInterface(dir);
110
+ if (!loaded.file) {
111
+ return { loaded, plan: null, controllerName: null, resource: null, error: loaded.problems[0]?.message ?? '未找到 interface.json' };
112
+ }
113
+ const controllerName = overrides.controller ?? loaded.controllers[0]?.name ?? null;
114
+ const kind = deviceKindFor(loaded, controllerName);
115
+ let devices = [];
116
+ if (kind) {
117
+ try {
118
+ devices = await deviceList(c, kind);
119
+ }
120
+ catch (e) {
121
+ return { loaded, plan: null, controllerName, resource: null, error: '扫描设备失败:' + String(e.message || e) };
122
+ }
123
+ }
124
+ /* kind=null(playcover/macos/linux):没有可枚举设备,planController 走 --target 分支 */
125
+ const plan = planController(loaded, {
126
+ controller: controllerName ?? undefined,
127
+ win32: { hwnd: overrides.hwnd ?? null },
128
+ gamepad: { hwnd: overrides.gamepadHwnd ?? null },
129
+ adb: { address: overrides.address ?? null },
130
+ manualTarget: overrides.manualTarget ?? null,
131
+ }, devices);
132
+ const resource = resolveResourcePaths(loaded, controllerName ?? undefined, overrides.resource, overrides.cwd);
133
+ return { loaded, plan, controllerName, resource };
134
+ }
135
+ /**
136
+ * 项目模式 run 的唯一装配点:一次性 CLI 与 REPL `run` 都走这里。
137
+ *
138
+ * 为什么必须共享:REPL 曾经只吃 `--override`、不叠 interface 的四级链,同一项目同一入口
139
+ * 在 REPL 里 option / preset / default_case 全部不生效——agent 主路径与一次性命令跑出两套
140
+ * 行为,而且不报错。入口解析、task 适用性、pretask/agent 警示也一并收口,避免第二份规则。
141
+ */
142
+ export function prepareProjectRun(input) {
143
+ const { project, entry } = input;
144
+ const { loaded } = project;
145
+ const warns = [];
146
+ const paths = project.resource?.paths ?? [];
147
+ if (!paths.length) {
148
+ return {
149
+ ok: false,
150
+ code: 'RESOURCE_NOT_FOUND',
151
+ error: '项目里没有解析出资源路径:检查 interface.json 的 resource[] 声明',
152
+ warns,
153
+ };
154
+ }
155
+ const task = loaded.tasks.find((t) => t.name === entry || t.entry === entry);
156
+ const resolvedEntry = task?.entry ?? entry;
157
+ const ctrlName = project.controllerName ?? null;
158
+ const resName = project.resource?.selected ?? null;
159
+ /* task 适用性(task.controller / task.resource 声明) */
160
+ if (task?.controller?.length && ctrlName && !task.controller.includes(ctrlName)) {
161
+ return {
162
+ ok: false,
163
+ code: 'TASK_NOT_APPLICABLE',
164
+ error: '任务 ' + task.name + ' 不支持当前控制器 ' + ctrlName + '(声明:' + task.controller.join(' / ') + ')',
165
+ hint: '换控制器(CLI 用 --controller <名>,REPL 重新 connect project)或选别的任务',
166
+ warns,
167
+ };
168
+ }
169
+ if (task?.resource?.length && resName && !task.resource.includes(resName)) {
170
+ warns.push('任务 ' + task.name + ' 声明资源 ' + task.resource.join(' / ') + ',当前选中的是 ' + resName);
171
+ }
172
+ /* 多个资源条目都适用时 resource[] 只能选一个:plan 侧记下"选了哪个、还有谁适用",
173
+ * 在这里升成 warning——human 的 note 行机器消费面(--json)读不到。 */
174
+ for (const n of project.resource?.notes ?? [])
175
+ warns.push(n);
176
+ /* pipeline_override 四级合并链(preset > default_case 取值) + 用户 --override 最后覆盖 */
177
+ const chain = computePipelineOverride({
178
+ loaded,
179
+ controllerName: ctrlName,
180
+ resourceName: resName,
181
+ taskName: task?.name ?? null,
182
+ presetName: input.presetName ?? null,
183
+ ...(input.optionCases ? { optionCases: input.optionCases } : {}),
184
+ ...(input.optionFields ? { optionFields: input.optionFields } : {}),
185
+ });
186
+ warns.push(...chain.warns);
187
+ const agents = loaded.agents;
188
+ /* agent 子进程要的环境:PI_* 按 interface.json 的**当前选择**生成(控制器 / 资源 / 项目版本)。
189
+ * 不注入时 agent 不会报错,只会落到自带默认值——这是最安静的一种"跑的不是同一个配置"。 */
190
+ const agentEnv = buildAgentEnv({ loaded, controllerName: ctrlName, resourceName: resName });
191
+ if (agents.length) {
192
+ const injected = Object.keys(agentEnv);
193
+ warns.push('项目声明了 ' + agents.length + ' 个 agent 子进程(' +
194
+ agents.map((a) => [a.exec, ...a.args].join(' ')).join(' ; ') +
195
+ '),run 时自动 spawn 并桥接' +
196
+ (injected.length ? '(注入 ' + injected.join(' / ') + ')' : '') +
197
+ '——注意 maa-node 与 agent 侧 maa 库须同版本(协议握手要求)');
198
+ }
199
+ /* 分层 override:四级链已按层序排好(task 基底 + 各 option 层),用户 --override
200
+ * 追加到尾部=最高优先级。别在这里摊平——框架的层间字段继承(recognition/param 同
201
+ * type 按键继承)只有数组形态能保住层边界(上游 @8061d5b 已核)。 */
202
+ const override = [...chain.override];
203
+ if (input.override)
204
+ appendOverride(override, input.override);
205
+ return {
206
+ ok: true,
207
+ args: {
208
+ resourceDirs: paths,
209
+ /* v2.6 资源完整性:只在加载完 path[] 后读 hash(attach 不能算进去),
210
+ * 两段在 resolveResourcePaths 已经分好 */
211
+ ...(project.resource?.hashCheck ? { resourceHashCheck: project.resource.hashCheck } : {}),
212
+ entry: resolvedEntry,
213
+ override,
214
+ /* agent 桥接:daemon spawn 子进程(identifier 追加为末参)并 bind/connect 到本次 Resource */
215
+ agents: agents.map((a) => ({ exec: a.exec, args: a.args })),
216
+ agentCwd: loaded.dir ?? undefined,
217
+ agentEnv,
218
+ },
219
+ entry: resolvedEntry,
220
+ taskName: task?.name ?? null,
221
+ applied: chain.applied,
222
+ warns,
223
+ };
224
+ }
225
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 关键帧库的离线读侧:list / resolve 不依赖 daemon 存活(契约 §4.1)。
3
+ *
4
+ * manifest 格式与 daemon/framed.mjs 的写入侧一一对应:
5
+ * { schema:1, libraryId, next, frames:[{ id, file, sha256, session, capturedAt,
6
+ * captureSeq, ctrlW, ctrlH, smallW, smallH, scale, source, note }] }
7
+ * 写侧(升格)在 daemon;本模块只读,两个消费方(CLI kf、REPL kf)共用,不出现第二份解析。
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ export const KF_SCHEMA = 1;
14
+ export function defaultFramesDir() {
15
+ return join(homedir(), '.maafw-live', 'frames');
16
+ }
17
+ /** 读 manifest。坏文件/未知 schema 明确报错,不默认为当前版。 */
18
+ export function loadManifest(dir) {
19
+ const file = join(dir, 'manifest.json');
20
+ if (!existsSync(file))
21
+ return {};
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(readFileSync(file, 'utf8'));
25
+ }
26
+ catch (e) {
27
+ return { error: 'manifest.json 损坏(非合法 JSON):' + String(e.message) };
28
+ }
29
+ const m = raw;
30
+ if (!m || typeof m !== 'object' || !Array.isArray(m.frames)) {
31
+ return { error: 'manifest.json 结构不符(缺 frames 数组)' };
32
+ }
33
+ if (m.schema !== KF_SCHEMA) {
34
+ return { error: 'manifest schema=' + String(m.schema) + ' 不被当前版本支持(认识:' + KF_SCHEMA + ')' };
35
+ }
36
+ return { manifest: m };
37
+ }
38
+ /** 按完整 ID 帧解析:available 时给出经过完整性校验的路径;missing/corrupt/unsupported 各自可诊断。 */
39
+ export function resolveFrame(dir, id) {
40
+ /* 非字符串 id(缺字段、拼错的调用方)也走"不可复核"这条路,而不是抛 TypeError——
41
+ * 解析函数的契约是**如实报状态**,不该给调用方一个异常。 */
42
+ if (typeof id !== 'string' || !id.startsWith('kf:')) {
43
+ return { status: 'missing', reason: '要完整 ID:kf:<库UUID>:<序号>(裸序号只可作展示简称,不能跨库引用)' };
44
+ }
45
+ const { manifest, error } = loadManifest(dir);
46
+ if (error)
47
+ return { status: 'unsupported', reason: error };
48
+ if (!manifest)
49
+ return { status: 'missing', reason: '库内无 manifest(本机不可复核,不代表历史从未观测)' };
50
+ const rec = manifest.frames.find((f) => f.id === id);
51
+ if (!rec)
52
+ return { status: 'missing', reason: 'manifest 无此 ID 记录' };
53
+ const abs = join(dir, ...rec.file.split('/'));
54
+ if (!existsSync(abs))
55
+ return { status: 'missing', record: rec, reason: 'L0 文件丢失' };
56
+ let sha = '';
57
+ try {
58
+ sha = createHash('sha256').update(readFileSync(abs)).digest('hex');
59
+ }
60
+ catch (e) {
61
+ return { status: 'corrupt', record: rec, reason: '读取失败:' + String(e.message) };
62
+ }
63
+ if (sha !== rec.sha256)
64
+ return { status: 'corrupt', record: rec, reason: 'sha256 不匹配(文件损坏或被改)' };
65
+ return { status: 'available', record: rec, path: abs };
66
+ }
67
+ /** list 用:一行人类可读摘要(与 --json 的 data 各自完整,不共用裁剪)。 */
68
+ export function describeRecord(r) {
69
+ const dt = r.capturedAt ? r.capturedAt.replace('T', ' ').replace(/\.\d+Z$/, 'Z') : '?';
70
+ return r.id + ' seq=' + r.captureSeq + ' ' + r.ctrlW + 'x' + r.ctrlH + ' ' + dt + ' [' + r.source + ']' +
71
+ (r.note ? ' ' + r.note : '');
72
+ }
73
+ //# sourceMappingURL=keyframes.js.map
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 运行期日志跟随:run 这类长任务在执行中不产出任何可观测信号(daemon 的帧流被框架
3
+ * 截图链路占住,events 环为空),唯一还在更新的只有**项目 agent 自己写的日志**。
4
+ * 所以「跑的时候能不能看见正在发生什么」这件事,实现上就是「跟着 tail 那几份日志」。
5
+ *
6
+ * 本模块只做跟随与筛选,不解释日志内容 —— 判读规则属于项目(每个项目的日志格式不同)。
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ /* 条目 → 当下实际要跟随的文件集合(每 tick 重算,覆盖日更轮转与 glob) */
11
+ function resolveEntryPaths(entry) {
12
+ if (/[*?\[\]]/.test(entry)) {
13
+ try {
14
+ return fs.globSync(entry);
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ }
20
+ try {
21
+ if (fs.statSync(entry).isDirectory()) {
22
+ const hits = fs.readdirSync(entry)
23
+ .map((f) => { try {
24
+ const p = path.join(entry, f);
25
+ return { p, m: fs.statSync(p).mtimeMs };
26
+ }
27
+ catch {
28
+ return null;
29
+ } })
30
+ .filter((x) => x !== null);
31
+ hits.sort((a, b) => b.m - a.m);
32
+ return hits.slice(0, 1).map((x) => x.p);
33
+ }
34
+ }
35
+ catch { /* 不存在也是合法状态(等它出现) */ }
36
+ return [entry];
37
+ }
38
+ export function watchLogs(opts, onFlush) {
39
+ const intervalMs = opts.intervalMs ?? 1500;
40
+ const maxLines = opts.maxLinesPerFlush ?? 40;
41
+ const sources = opts.entries ?? opts.paths ?? [];
42
+ /* FileState 挂在「条目→文件」的映射上:同一条目解析出新文件时重置 offset 重读
43
+ * (日更轮转 = 条目解析到另一个文件名),解析回原文件时保持原 offset。 */
44
+ const byEntry = new Map();
45
+ const resolveStates = () => {
46
+ const out = [];
47
+ for (const entry of sources) {
48
+ const prev = byEntry.get(entry) ?? [];
49
+ const prevByPath = new Map(prev.map((s) => [s.path, s]));
50
+ const states = resolveEntryPaths(entry).map((p) => {
51
+ const seen = prevByPath.get(p);
52
+ if (seen)
53
+ return seen;
54
+ /* 新解析出来的文件保持"从尾部开始"的首次跟随语义(等价于 run 启动时只关心之后的事)。
55
+ * 从 0 起读会把该文件的整份历史一次倒出来(目录条目首跑就踩这个)。 */
56
+ let size = 0;
57
+ try {
58
+ size = fs.statSync(p).size;
59
+ }
60
+ catch { /* 还没出现:0 起读等它长出来 */ }
61
+ return { path: p, offset: size, lastSize: size };
62
+ });
63
+ byEntry.set(entry, states);
64
+ out.push(...states);
65
+ }
66
+ return out;
67
+ };
68
+ /* resolveStates 已为每个条目 seed 好"新文件=尾部起读",不需要再单独扫一遍。
69
+ * 首次跟随语义(只关心从现在起的事)由 FileState 的 offset=statSync.size 体现。 */
70
+ resolveStates();
71
+ const stats = { lines: 0, pushed: 0, files: [] };
72
+ let buffer = [];
73
+ let stopped = false;
74
+ const readNew = () => {
75
+ /* 每 tick 重解析条目:文件在启动后新建、日更轮转(跨午夜换文件名)、目录里出现
76
+ * 更新的文件都能跟上——这是 4.1 那次 `--watch log_20261110.txt` 跨午夜跟丢的修法。
77
+ * 新解析出的文件 seed 到尾部起读(resolveStates),同一条目换文件不整份倒历史。 */
78
+ for (const st of resolveStates()) {
79
+ let s;
80
+ try {
81
+ s = fs.statSync(st.path);
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ if (!stats.files.includes(st.path))
87
+ stats.files.push(st.path);
88
+ /* 轮转或截断:回到 0 重读 */
89
+ if (s.size < st.offset) {
90
+ st.offset = 0;
91
+ }
92
+ if (s.size === st.offset) {
93
+ st.lastSize = s.size;
94
+ continue;
95
+ }
96
+ const len = s.size - st.offset;
97
+ const buf = Buffer.alloc(len);
98
+ let fd;
99
+ try {
100
+ fd = fs.openSync(st.path, "r");
101
+ fs.readSync(fd, buf, 0, len, st.offset);
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ finally {
107
+ if (fd !== undefined) {
108
+ try {
109
+ fs.closeSync(fd);
110
+ }
111
+ catch {
112
+ /* ignore */
113
+ }
114
+ }
115
+ }
116
+ st.offset = s.size;
117
+ st.lastSize = s.size;
118
+ const text = buf.toString("utf8");
119
+ for (const line of text.split(/\r?\n/)) {
120
+ if (!line.trim())
121
+ continue;
122
+ stats.lines += 1;
123
+ if (opts.include?.length && !opts.include.some((re) => re.test(line)))
124
+ continue;
125
+ if (opts.exclude?.length && opts.exclude.some((re) => re.test(line)))
126
+ continue;
127
+ buffer.push(line);
128
+ }
129
+ }
130
+ };
131
+ const timer = setInterval(() => {
132
+ if (stopped)
133
+ return;
134
+ readNew();
135
+ if (!buffer.length)
136
+ return;
137
+ const took = buffer.slice(0, maxLines);
138
+ const rest = buffer.length - took.length;
139
+ buffer = [];
140
+ stats.pushed += took.length;
141
+ onFlush(rest > 0 ? [...took, "… 还有 " + String(rest) + " 行未显示"] : took);
142
+ }, intervalMs);
143
+ timer.unref?.();
144
+ return {
145
+ stop() {
146
+ stopped = true;
147
+ clearInterval(timer);
148
+ /* 收尾时把剩余行补推一次,避免最后几行丢在缓冲里 */
149
+ readNew();
150
+ if (buffer.length) {
151
+ const rest = buffer;
152
+ buffer = [];
153
+ stats.pushed += rest.length;
154
+ onFlush(rest.slice(0, maxLines));
155
+ }
156
+ return stats;
157
+ },
158
+ };
159
+ }
160
+ //# sourceMappingURL=log-watch.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * reco 的文件参数:`--node-file` / `--param-file`(`-` = stdin,两个 flag 对称支持)。
3
+ *
4
+ * 为什么要有文件通道:整节点 JSON 只能走 argv,而 Windows 上 bash → `cmd.exe`(npm shim) → node
5
+ * 会把 `^` 当转义符吃掉——`expected: "^解锁"` 到进程时已变成 `"解锁"`,三种正则给出相同结果,
6
+ * 一度被误判成"工具不应用 expected 过滤"。文件绕开的是**参数被 shell 改写**,不是 JSON 解析。
7
+ *
8
+ * 为什么按 JSONC 解析:pipeline 常带 `//` 注释,strict `JSON.parse` 会误伤真实文件;解析器复用
9
+ * interface 那一份(`parseJsonc`),不在这里再写一遍脱敏逻辑。
10
+ *
11
+ * 为什么不用 `@a.json`:PowerShell 里 `@` 是 splat 前缀,会再踩一次"被 shell 破坏的参数",
12
+ * 与"不在工具里猜测/修复被破坏参数"的纪律冲突。
13
+ */
14
+ import { readFileSync } from 'node:fs';
15
+ import { parseJsonc } from '../interface/jsonc.js';
16
+ function message(e) {
17
+ return e instanceof Error ? e.message : String(e);
18
+ }
19
+ /** 读 `--xxx-file <path>`:`-` 读 stdin(一次性命令才有意义,REPL 里 stdin 是命令流) */
20
+ export function readJsoncArg(spec, flag) {
21
+ let text;
22
+ if (spec === '-') {
23
+ try {
24
+ text = readFileSync(0, 'utf8');
25
+ }
26
+ catch (e) {
27
+ return { error: flag + ' - 从 stdin 读取失败:' + message(e) };
28
+ }
29
+ }
30
+ else {
31
+ try {
32
+ text = readFileSync(spec, 'utf8');
33
+ }
34
+ catch (e) {
35
+ return { error: flag + ' 读不到文件:' + spec + '(' + message(e) + ')' };
36
+ }
37
+ }
38
+ try {
39
+ return { value: parseJsonc(text) };
40
+ }
41
+ catch (e) {
42
+ return { error: flag + ' 不是合法 JSON/JSONC:' + message(e) };
43
+ }
44
+ }
45
+ /**
46
+ * 节点表识别:文件里是 `{ "<节点名>": {…}, … }` 而不是一个节点。
47
+ *
48
+ * 判据:**单节点必有 `recognition`**(V1 字符串或 V2 对象),且节点表的每个值都是对象、
49
+ * 至少有一个带 `recognition`。要求"至少一个带 recognition"是为了把 `{"action":{…}}` 这种
50
+ * **缺 recognition 的单节点**留给"缺字段"的报错——它看起来也像"所有值都是对象"。
51
+ * 这条只服务于报错文案(提示去用 `--node-name`),不构成"pipeline 文件"能力,也不改变执行路径。
52
+ */
53
+ export function isNodeTable(value) {
54
+ if (value.recognition !== undefined)
55
+ return false;
56
+ const keys = Object.keys(value);
57
+ if (keys.length === 0)
58
+ return false;
59
+ const allObjects = keys.every((k) => {
60
+ const v = value[k];
61
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
62
+ });
63
+ if (!allObjects)
64
+ return false;
65
+ return keys.some((k) => value[k].recognition !== undefined);
66
+ }
67
+ /** `--node-file` → 单个节点 */
68
+ export function readNodeFile(spec) {
69
+ const j = readJsoncArg(spec, '--node-file');
70
+ if (j.error)
71
+ return { error: j.error };
72
+ const v = j.value;
73
+ if (!v || typeof v !== 'object' || Array.isArray(v)) {
74
+ return { error: '--node-file 里必须是单个节点对象(pipeline 节点)' };
75
+ }
76
+ const node = v;
77
+ if (isNodeTable(node)) {
78
+ return {
79
+ error: '--node-file 拿到的是节点表(多节点),不是单个节点',
80
+ hint: '这是 pipeline 文件的形状:按原名跑用 --node-name <节点名>(配 --project 或 --resource-dir),' +
81
+ '只测其中一节就从文件里摘出那个节点',
82
+ };
83
+ }
84
+ if (node.recognition === undefined) {
85
+ return { error: '--node-file 的节点缺少 recognition 字段(V1 字符串或 V2 {type,param})' };
86
+ }
87
+ return { node };
88
+ }
89
+ /** `--param-file` → recognition.param 那一层参数对象 */
90
+ export function readParamFile(spec) {
91
+ const j = readJsoncArg(spec, '--param-file');
92
+ if (j.error)
93
+ return { error: j.error };
94
+ const v = j.value;
95
+ if (!v || typeof v !== 'object' || Array.isArray(v)) {
96
+ return { error: '--param-file 里必须是参数对象(recognition.param 那一层,形如 {"template":"a.png"})' };
97
+ }
98
+ return { param: v };
99
+ }
100
+ //# sourceMappingURL=node-source.js.map