@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/runtime/actions.js
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { rmSync } from 'node:fs';
|
|
11
|
-
import { loadInterface, planController, resolveResourcePaths, } from '../interface/index.js';
|
|
11
|
+
import { computePipelineOverride, loadInterface, appendOverride, planController, resolveResourcePaths, } from '../interface/index.js';
|
|
12
12
|
import { spawnDaemon } from '../client/daemon.js';
|
|
13
|
-
|
|
14
|
-
|
|
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);
|
|
15
22
|
const client = spawnDaemon(options);
|
|
16
23
|
try {
|
|
17
24
|
return await fn(client);
|
|
@@ -49,6 +56,8 @@ export async function seedObservation(c, prefix) {
|
|
|
49
56
|
export const streamStart = (c, args = {}) => c.call('stream_start', args, 15000);
|
|
50
57
|
export const streamStop = (c) => c.call('stream_stop', {}, 10000);
|
|
51
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);
|
|
52
61
|
export const frameGet = (c, args) => c.call('frame_get', args, 30000);
|
|
53
62
|
/** 等状态谓词成立:stable=画面静下来 / change=画面动了。取代调用方硬睡时钟。 */
|
|
54
63
|
export const waitState = (c, args = {}) => c.call('wait', args, Math.min(120000, Number(args.timeout ?? 10000)) + 5000);
|
|
@@ -69,12 +78,25 @@ export function run(c, args, timeoutMs = 30000) {
|
|
|
69
78
|
return c.call('run', { ...args, timeoutMs: t === 0 ? 0 : budget }, budget + 15000);
|
|
70
79
|
}
|
|
71
80
|
export const input = (c, payload, timeoutMs = 20000) => c.call('input', payload, timeoutMs);
|
|
72
|
-
|
|
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);
|
|
73
86
|
/* ─────────────────────── 项目(interface.json)驱动 ─────────────────────── */
|
|
74
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* 控制器类型 → 需要扫描的设备类型;**无设备可枚举的类型返回 null**(调用方跳过扫描)。
|
|
89
|
+
* daemon 的 device_list 只枚举 adb/win32——PlayCover/MacOS/Linux 给 'win32' 会白白发起
|
|
90
|
+
* 一次必然失败的扫描(曾经就是这样,把无关窗口列表塞进计划错误里)。
|
|
91
|
+
*/
|
|
75
92
|
export function deviceKindFor(loaded, controllerName) {
|
|
76
93
|
const c = controllerName ? loaded.controllers.find((x) => x.name === controllerName) : loaded.controllers[0];
|
|
77
|
-
|
|
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;
|
|
78
100
|
}
|
|
79
101
|
/**
|
|
80
102
|
* 载入项目 → 扫设备 → 规划控制器 → 解析资源路径。
|
|
@@ -91,12 +113,15 @@ export async function planProject(c, dir, overrides = {}) {
|
|
|
91
113
|
const controllerName = overrides.controller ?? loaded.controllers[0]?.name ?? null;
|
|
92
114
|
const kind = deviceKindFor(loaded, controllerName);
|
|
93
115
|
let devices = [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
|
99
123
|
}
|
|
124
|
+
/* kind=null(playcover/macos/linux):没有可枚举设备,planController 走 --target 分支 */
|
|
100
125
|
const plan = planController(loaded, {
|
|
101
126
|
controller: controllerName ?? undefined,
|
|
102
127
|
win32: { hwnd: overrides.hwnd ?? null },
|
|
@@ -104,7 +129,97 @@ export async function planProject(c, dir, overrides = {}) {
|
|
|
104
129
|
adb: { address: overrides.address ?? null },
|
|
105
130
|
manualTarget: overrides.manualTarget ?? null,
|
|
106
131
|
}, devices);
|
|
107
|
-
const resource = resolveResourcePaths(loaded, controllerName ?? undefined, overrides.resource);
|
|
132
|
+
const resource = resolveResourcePaths(loaded, controllerName ?? undefined, overrides.resource, overrides.cwd);
|
|
108
133
|
return { loaded, plan, controllerName, resource };
|
|
109
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
|
+
}
|
|
110
225
|
//# sourceMappingURL=actions.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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCR 模型是否在资源链里:只回答"链上有没有框架会去读的那几个文件"。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:OCR 的 miss 与模板匹配不同——**模型没加载时框架不报错**,只回一个空结果
|
|
5
|
+
* (`detail.all: []`、`hit:false`、约 200ms),与"roi 里真的没有字"完全同形(实测:一个不带
|
|
6
|
+
* `model/` 的资源目录跑 `--type OCR` 就是这个签名)。不做这条判断,`describeMisses` 会把
|
|
7
|
+
* "没加载 OCR 模型"报成"roi 内无文字"——正是本提案要清掉的那类误诊。
|
|
8
|
+
*
|
|
9
|
+
* 依据(MaaFramework 5.11.2 pinned source;本机运行时 5.14.2,相差一个小版本,
|
|
10
|
+
* 相关实现见 `Resource/ResourceMgr.cpp::load_bundle` 与 `Resource/OCRResMgr.cpp`
|
|
11
|
+
* 的 `lazy_load` / `load_deter` / `load_recer`):
|
|
12
|
+
* - `load_bundle` 对每个 bundle 调一次 `lazy_load(<bundle>/model/ocr)`,把该目录登记进
|
|
13
|
+
* roots 列表(后加载的在后),目录不存在就不登记;
|
|
14
|
+
* - `load_deter(name)` 遍历 roots,取第一个存在 `<root>/<name>/det.onnx` 的 root;
|
|
15
|
+
* `load_recer(name)` 遍历 roots,取第一个 `<root>/<name>/rec.onnx` 与
|
|
16
|
+
* `<root>/<name>/keys.txt` **同时**存在的 root;
|
|
17
|
+
* - `load_ocrer(name)` 要求 deter 与 recer 都拿到,所以判据是**合并**的:det 与 rec 允许
|
|
18
|
+
* 来自不同 root(base 放 det、overlay 放 rec+keys 是齐的),但 rec 与 keys 必须同属一个
|
|
19
|
+
* root——它们是 load_recer 同一次调用里的两项,不会被拆开拼;
|
|
20
|
+
* - `<name>` 来自节点参数 `model`,缺省空串 = 直接放在 `model/ocr/` 下;
|
|
21
|
+
* - 缺任一项 → 拿不到 ocrer,`OCRer::predict` 只是记日志并回空结果,**不抛错**。
|
|
22
|
+
*
|
|
23
|
+
* 因此这里不能把"某一段目录三件套齐全"当判据:那会把"det 在 base、rec+keys 在 overlay"
|
|
24
|
+
* 误报成 missing,而它其实完全可用——误报会把人引去补模型,真正该看的 roi 反而被略过。
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
const DET = 'det.onnx';
|
|
29
|
+
/** recer 的两项必须同目录同时存在:对应 `load_recer` 同一次调用里的两个文件。 */
|
|
30
|
+
const RECER = ['rec.onnx', 'keys.txt'];
|
|
31
|
+
/**
|
|
32
|
+
* 资源链上是否能找到 OCR 模型。`modelName` 是节点/参数里声明的模型名(缺省空串)。
|
|
33
|
+
*
|
|
34
|
+
* 只查文件存在性、不解析 onnx:判断的是"框架会不会找到它",不是"模型能不能跑"。
|
|
35
|
+
* 按框架的合并规则判:det 与 rec 可以来自不同段,rec 与 keys 必须同段。
|
|
36
|
+
*/
|
|
37
|
+
export function ocrModelStatus(resourceDirs, modelName) {
|
|
38
|
+
const name = typeof modelName === 'string' ? modelName.trim() : '';
|
|
39
|
+
// 每段一个候选目录,顺序与框架扫 roots 的顺序一致(这里不需要顺序,只在同段内成组)。
|
|
40
|
+
const folders = resourceDirs.map((dir) => name === '' ? join(dir, 'model', 'ocr') : join(dir, 'model', 'ocr', name));
|
|
41
|
+
if (!folders.some((folder) => existsSync(join(folder, DET))))
|
|
42
|
+
return 'missing';
|
|
43
|
+
return folders.some((folder) => RECER.every((f) => existsSync(join(folder, f)))) ? 'present' : 'missing';
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=ocr-model.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cliPackage, maaNodeVersion } from '../pkg.js';
|
|
2
|
+
/**
|
|
3
|
+
* 本工具**实际兑现**的 PI 能力版本。
|
|
4
|
+
*
|
|
5
|
+
* 不写"最新":这个值给 agent 判特性用,谎报会让它以为没实现的行为存在。
|
|
6
|
+
* 当前兑现到 v2.7.0:option 四级链 / preset / import / `attach_resource_path` /
|
|
7
|
+
* option 适用性过滤 / `resource.hash` 完整性校验(v2.6,warn-only)/ pretask(v2.7)。
|
|
8
|
+
*/
|
|
9
|
+
const PI_INTERFACE_VERSION = 'v2.7.0';
|
|
10
|
+
/** 本 CLI 的人类输出固定中文、没有 UI 语言切换——按协议示例的 `zh_cn` 形态如实报。 */
|
|
11
|
+
const PI_CLIENT_LANGUAGE = 'zh_cn';
|
|
12
|
+
/** 生成要注入 agent 子进程的环境变量;返回空对象表示本次没有可注入的项。 */
|
|
13
|
+
export function buildAgentEnv(i) {
|
|
14
|
+
const env = {
|
|
15
|
+
PI_INTERFACE_VERSION,
|
|
16
|
+
PI_CLIENT_NAME: 'maafw-live',
|
|
17
|
+
PI_CLIENT_VERSION: cliPackage().version,
|
|
18
|
+
PI_CLIENT_LANGUAGE,
|
|
19
|
+
};
|
|
20
|
+
const maafw = maaNodeVersion();
|
|
21
|
+
if (maafw)
|
|
22
|
+
env.PI_CLIENT_MAAFW_VERSION = maafw;
|
|
23
|
+
/* 顶层 version 是**项目**版本(不是 interface_version=2) */
|
|
24
|
+
if (i.loaded.projectVersion)
|
|
25
|
+
env.PI_VERSION = i.loaded.projectVersion;
|
|
26
|
+
const c = i.controllerName ? i.loaded.controllers.find((x) => x.name === i.controllerName) : undefined;
|
|
27
|
+
if (c && c.raw !== undefined)
|
|
28
|
+
env.PI_CONTROLLER = JSON.stringify(c.raw);
|
|
29
|
+
const r = i.resourceName ? i.loaded.resources.find((x) => x.name === i.resourceName) : undefined;
|
|
30
|
+
if (r && r.raw !== undefined)
|
|
31
|
+
env.PI_RESOURCE = JSON.stringify(r.raw);
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=pi-env.js.map
|