@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/reco.js
CHANGED
|
@@ -3,13 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 走 daemon 的 reco_test —— 它把识别放进一次性子进程执行(beta 绑定在无效模板上会原生崩溃,
|
|
5
5
|
* 子进程炸掉只损失一次测试,不带走调用方)。支持阈值扫描:一次调用给出阈值-命中曲线。
|
|
6
|
-
* 项目模式下资源目录从 interface.json
|
|
6
|
+
* 项目模式下资源目录从 interface.json 解析(整条链,含控制器 attach 段),避免手写路径与项目声明不一致。
|
|
7
|
+
*
|
|
8
|
+
* 节点来源三选一:`--node`(argv 手写)/ `--node-file`(文件,JSONC)/ `--node-name`(资源里按原名跑)。
|
|
9
|
+
* 前两者测的是"你写的那份",后者测的是"项目里那份"——真实定义还会叠加 overlay 资源包。
|
|
7
10
|
*/
|
|
11
|
+
import { basename } from 'node:path';
|
|
8
12
|
import { EXIT, fail } from '../protocol.js';
|
|
9
13
|
import { recoTest, withDaemon } from '../runtime/actions.js';
|
|
10
14
|
import { defaultFramesDir, resolveFrame } from '../runtime/keyframes.js';
|
|
15
|
+
import { readNodeFile, readParamFile } from '../runtime/node-source.js';
|
|
16
|
+
import { ocrModelStatus } from '../runtime/ocr-model.js';
|
|
17
|
+
import { roiOutOfBounds } from '../runtime/reco-args.js';
|
|
18
|
+
import { describeMisses } from '../runtime/reco-verdict.js';
|
|
11
19
|
import { describeSession, ensureSession, offlineResource } from '../runtime/session.js';
|
|
12
|
-
import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
|
|
20
|
+
import { CONNECT_OPTIONS, daemonFail, argPath, sessionOptions } from './runtime.js';
|
|
13
21
|
function parseJson(raw, what) {
|
|
14
22
|
if (raw === undefined)
|
|
15
23
|
return {};
|
|
@@ -41,13 +49,46 @@ export function suggestThreshold(results) {
|
|
|
41
49
|
lowMatch,
|
|
42
50
|
};
|
|
43
51
|
}
|
|
52
|
+
function resourceInfo(chain) {
|
|
53
|
+
return {
|
|
54
|
+
name: chain.name,
|
|
55
|
+
paths: chain.paths,
|
|
56
|
+
labels: chain.paths.map((p) => basename(p)),
|
|
57
|
+
source: chain.source,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function resourceLine(chain) {
|
|
61
|
+
const labels = chain.paths.map((p) => basename(p));
|
|
62
|
+
return ' 资源链(layer=resource):' + (chain.name ?? '(未命名)') + ' ' + labels.join(' + ');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* OCR 模型名:框架按 `model/ocr/<name>/` 找模型,缺省空串 = 直接放在 `model/ocr/` 下。
|
|
66
|
+
* 优先级:资源 dump(V2 缺省也填满)> 手写节点的 V2 param / V1 顶层 model > `--type` 的 `--param`。
|
|
67
|
+
*/
|
|
68
|
+
export function ocrModelName(node, param, dump) {
|
|
69
|
+
const fromDump = dump?.recognition?.param?.model;
|
|
70
|
+
if (typeof fromDump === 'string')
|
|
71
|
+
return fromDump;
|
|
72
|
+
if (typeof param?.model === 'string')
|
|
73
|
+
return param.model;
|
|
74
|
+
const rec = node?.recognition;
|
|
75
|
+
if (rec && typeof rec === 'object' && !Array.isArray(rec)) {
|
|
76
|
+
const m = rec.param?.model;
|
|
77
|
+
if (typeof m === 'string')
|
|
78
|
+
return m;
|
|
79
|
+
}
|
|
80
|
+
if (typeof node?.model === 'string')
|
|
81
|
+
return node.model;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
44
84
|
export const recoCommand = {
|
|
45
85
|
name: 'reco',
|
|
46
|
-
summary: '识别单测(子进程隔离):--param/--sweep 阈值扫描,或 --node
|
|
47
|
-
usage:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
summary: '识别单测(子进程隔离):--param/--param-file/--sweep 阈值扫描,或 --node/--node-file 整节点透传、--node-name 按资源原名跑;--act 真机执行动作半',
|
|
87
|
+
usage: `maafw-live reco --type TemplateMatch --resource-dir <dir> --image <png|kf:ID> [--param <json>|--param-file <json|->] [--sweep <json>]
|
|
88
|
+
maafw-live reco --node <json> | --node-file <json|-> --resource-dir <dir> --image <png|kf:ID> (V1 扁平 / V2 嵌套节点均原样透传)
|
|
89
|
+
maafw-live reco --node <json> --resource-dir <dir> --from-kf <kf:库UUID:序号> (库内留存帧:离线、不需要设备)
|
|
90
|
+
maafw-live reco --project <dir> --node-name <节点名> --image <png> (按资源里的原名跑,整条资源链含 attach)
|
|
91
|
+
maafw-live reco --node-file <json> --act --project <dir> (识别拿框 → 用框 run_action 真机执行)`,
|
|
51
92
|
options: {
|
|
52
93
|
...CONNECT_OPTIONS,
|
|
53
94
|
type: { type: 'string' },
|
|
@@ -55,25 +96,42 @@ export const recoCommand = {
|
|
|
55
96
|
image: { type: 'string' },
|
|
56
97
|
seq: { type: 'string' },
|
|
57
98
|
param: { type: 'string' },
|
|
99
|
+
'param-file': { type: 'string' },
|
|
58
100
|
sweep: { type: 'string' },
|
|
59
101
|
node: { type: 'string' },
|
|
102
|
+
'node-file': { type: 'string' },
|
|
103
|
+
'node-name': { type: 'string' },
|
|
60
104
|
act: { type: 'boolean' },
|
|
61
105
|
'from-kf': { type: 'string' },
|
|
62
106
|
'frames-dir': { type: 'string' },
|
|
63
107
|
},
|
|
64
108
|
async run(ctx) {
|
|
65
|
-
const o = sessionOptions(ctx
|
|
66
|
-
const
|
|
67
|
-
const
|
|
109
|
+
const o = sessionOptions(ctx);
|
|
110
|
+
const str = (k) => typeof ctx.values[k] === 'string' && ctx.values[k] !== '' ? ctx.values[k] : undefined;
|
|
111
|
+
const type = str('type') ?? 'TemplateMatch';
|
|
112
|
+
const imageRaw = str('image');
|
|
68
113
|
/* `--image kf:…:0011` 是 SKILL / roadmap 里记过的写法:以 kf: 开头就按**库帧 ID**解释,
|
|
69
114
|
* 而不是文件路径(否则一路走到 ENOENT,实测踩过)。规范入口仍是 --from-kf。 */
|
|
70
115
|
const kfFromImage = imageRaw?.startsWith('kf:') === true ? imageRaw : undefined;
|
|
71
|
-
const image = kfFromImage ? undefined : imageRaw;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
116
|
+
const image = kfFromImage ? undefined : argPath(ctx, imageRaw);
|
|
117
|
+
const seqRaw = str('seq');
|
|
118
|
+
const seq = seqRaw !== undefined ? Number(seqRaw) : undefined;
|
|
119
|
+
const act = ctx.values.act === true;
|
|
120
|
+
/* ── 节点来源:三者互斥(测的是同一个位置,同时给只能有一个生效,那就不该"看起来都生效") ── */
|
|
121
|
+
const nodeJson = str('node');
|
|
122
|
+
const nodeFile = str('node-file') === '-' ? '-' : argPath(ctx, str('node-file'));
|
|
123
|
+
const nodeName = str('node-name');
|
|
124
|
+
const nodeFlags = [
|
|
125
|
+
nodeJson !== undefined ? '--node' : '',
|
|
126
|
+
nodeFile !== undefined ? '--node-file' : '',
|
|
127
|
+
nodeName !== undefined ? '--node-name' : '',
|
|
128
|
+
].filter((x) => x !== '');
|
|
129
|
+
if (nodeFlags.length > 1) {
|
|
130
|
+
return fail('BAD_ARGUMENTS', '节点来源互斥:' + nodeFlags.join(' / ') + ' 只能给一个', '--node 手写节点 JSON;--node-file 从文件读一个节点;--node-name 按资源里的原名跑', EXIT.USAGE);
|
|
131
|
+
}
|
|
74
132
|
let node;
|
|
75
|
-
if (
|
|
76
|
-
const parsed = parseJson(
|
|
133
|
+
if (nodeJson !== undefined) {
|
|
134
|
+
const parsed = parseJson(nodeJson, '--node');
|
|
77
135
|
if (parsed.error)
|
|
78
136
|
return fail('BAD_ARGUMENTS', parsed.error, undefined, EXIT.USAGE);
|
|
79
137
|
if (!parsed.value || typeof parsed.value !== 'object' || Array.isArray(parsed.value)) {
|
|
@@ -81,23 +139,48 @@ export const recoCommand = {
|
|
|
81
139
|
}
|
|
82
140
|
node = parsed.value;
|
|
83
141
|
}
|
|
84
|
-
|
|
142
|
+
else if (nodeFile !== undefined) {
|
|
143
|
+
const f = readNodeFile(nodeFile);
|
|
144
|
+
if (f.error)
|
|
145
|
+
return fail('BAD_ARGUMENTS', f.error, f.hint, EXIT.USAGE);
|
|
146
|
+
node = f.node;
|
|
147
|
+
}
|
|
148
|
+
const nodeSource = nodeJson !== undefined ? 'node' : (nodeFile !== undefined ? 'node-file' : (nodeName !== undefined ? 'node-name' : null));
|
|
149
|
+
/* ── 参数来源:--param / --param-file 互斥;它们与 --sweep 都只作用于 --type 路径 ── */
|
|
150
|
+
const paramSpec = str('param');
|
|
151
|
+
const paramFile = str('param-file') === '-' ? '-' : argPath(ctx, str('param-file'));
|
|
152
|
+
if (paramSpec !== undefined && paramFile !== undefined) {
|
|
153
|
+
return fail('BAD_ARGUMENTS', '--param 与 --param-file 互斥:一个给字面 JSON,一个给文件', undefined, EXIT.USAGE);
|
|
154
|
+
}
|
|
155
|
+
let param;
|
|
156
|
+
if (paramFile !== undefined) {
|
|
157
|
+
const f = readParamFile(paramFile);
|
|
158
|
+
if (f.error)
|
|
159
|
+
return fail('BAD_ARGUMENTS', f.error, undefined, EXIT.USAGE);
|
|
160
|
+
param = { value: f.param };
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
param = parseJson(paramSpec, '--param');
|
|
164
|
+
}
|
|
85
165
|
if (param.error)
|
|
86
166
|
return fail('BAD_ARGUMENTS', param.error, undefined, EXIT.USAGE);
|
|
87
|
-
const sweep = parseJson(
|
|
167
|
+
const sweep = parseJson(str('sweep'), '--sweep');
|
|
88
168
|
if (sweep.error)
|
|
89
169
|
return fail('BAD_ARGUMENTS', sweep.error, undefined, EXIT.USAGE);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
?
|
|
94
|
-
|
|
170
|
+
/* 节点自带的参数就是它的定义;再给 --param 是被静默忽略的(回执看不出用了哪套)。这里直接拦下:
|
|
171
|
+
* 想扫阈值,先用 --node-file 把它摘成 --type 形态,或者干脆别给节点来源。 */
|
|
172
|
+
if (nodeSource !== null && (paramSpec !== undefined || paramFile !== undefined || sweep.value !== undefined)) {
|
|
173
|
+
return fail('BAD_ARGUMENTS', '--param / --param-file / --sweep 只作用于 --type 路径:与 ' + (nodeName !== undefined ? '--node-name' : '--node/--node-file') + ' 同给时它们不会生效', '节点来源测的是节点自带参数(含资源覆盖后的值);要扫阈值就把节点摘成 --type 形态', EXIT.USAGE);
|
|
174
|
+
}
|
|
175
|
+
const paramSource = nodeSource ?? (paramFile !== undefined ? 'param-file' : (sweep.value !== undefined ? 'sweep' : (paramSpec !== undefined ? 'param' : 'type')));
|
|
176
|
+
/* ── 库帧来源(离线,镜像 crop / annotate 的 --from-kf):先走离线读侧解析,daemon 只接身份与路径 ── */
|
|
177
|
+
const fromKf = str('from-kf') ?? kfFromImage;
|
|
95
178
|
if (fromKf !== undefined && seq !== undefined) {
|
|
96
179
|
return fail('BAD_ARGUMENTS', '--from-kf 与 --seq 互斥:前者在库内留存帧上跑,后者在本会话缓冲帧上跑', '取库里有哪些帧:maafw-live kf list', EXIT.USAGE);
|
|
97
180
|
}
|
|
98
181
|
let kfSource;
|
|
99
182
|
if (fromKf !== undefined) {
|
|
100
|
-
const dir =
|
|
183
|
+
const dir = argPath(ctx, str('frames-dir')) ?? defaultFramesDir();
|
|
101
184
|
const rf = resolveFrame(dir, fromKf);
|
|
102
185
|
if (rf.status !== 'available' || !rf.path || !rf.record) {
|
|
103
186
|
const code = rf.status === 'corrupt' ? 'KF_CORRUPT' : (rf.status === 'unsupported' ? 'KF_UNSUPPORTED' : 'KF_MISSING');
|
|
@@ -109,35 +192,60 @@ export const recoCommand = {
|
|
|
109
192
|
captureSeq: rec.captureSeq, capturedAt: rec.capturedAt,
|
|
110
193
|
};
|
|
111
194
|
}
|
|
112
|
-
|
|
195
|
+
/* 给了静态图就是离线评估:识别输入与设备无关,没必要为它连一次设备会话
|
|
196
|
+
* (旧行为是 `--project --image` 会 ensureSession,而同一份回执又把它标成"离线评估"——
|
|
197
|
+
* 工具自身的口径分叉)。--seq 是本会话缓冲帧,仍要有会话。 */
|
|
198
|
+
const offline = kfSource !== undefined || image !== undefined;
|
|
113
199
|
if (act && offline) {
|
|
114
|
-
return fail('BAD_ARGUMENTS', '--act 要在真机上执行动作、图像取自当前画面:不能与 --from-kf 同用', '离线只做识别判据;要连真机就把 --
|
|
200
|
+
return fail('BAD_ARGUMENTS', '--act 要在真机上执行动作、图像取自当前画面:不能与 --image / --from-kf 同用', '离线只做识别判据;要连真机就把 --image 换成 --project 跑 --act', EXIT.USAGE);
|
|
201
|
+
}
|
|
202
|
+
if (act) {
|
|
203
|
+
if (nodeName !== undefined) {
|
|
204
|
+
return fail('BAD_ARGUMENTS', '--act 只接受 --node / --node-file:--node-name 按资源原名跑识别,动作半不桥接', '识别判据用 --node-name 验;动作半用 maafw-live run 验(它才会桥接 agent 子进程)', EXIT.USAGE);
|
|
205
|
+
}
|
|
206
|
+
if (!node) {
|
|
207
|
+
return fail('BAD_ARGUMENTS', '--act 需要同时给 --node / --node-file <json>(整节点,含 action)', undefined, EXIT.USAGE);
|
|
208
|
+
}
|
|
209
|
+
if (node.action === undefined) {
|
|
210
|
+
return fail('BAD_ARGUMENTS', '--act 要求节点带 action(V1 action 字符串 / V2 action 对象)', '只测识别就去掉 --act', EXIT.USAGE);
|
|
211
|
+
}
|
|
115
212
|
}
|
|
116
213
|
try {
|
|
117
|
-
return await withDaemon(async (client) => {
|
|
214
|
+
return await withDaemon(ctx, async (client) => {
|
|
118
215
|
const s = offline ? { connected: false } : await ensureSession(client, o);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
216
|
+
/* 资源链:--resource-dir 是显式单包;否则按项目声明解析**整条链**(resource.path[] 叠加
|
|
217
|
+
* 控制器 attach_resource_path)。必须走 resolveResourcePaths —— 只写 resource.path 会让
|
|
218
|
+
* attach 进来的一段凭空消失,测出来的节点就不是 run 里那个。 */
|
|
219
|
+
const dirFlag = argPath(ctx, str('resource-dir'));
|
|
220
|
+
let chain;
|
|
221
|
+
if (dirFlag !== undefined) {
|
|
222
|
+
chain = { paths: [dirFlag], name: '(--resource-dir)', source: 'resource-dir' };
|
|
223
|
+
}
|
|
224
|
+
else if (s.plan?.resource?.paths.length) {
|
|
225
|
+
chain = { paths: s.plan.resource.paths, name: s.plan.resource.selected, source: 'project' };
|
|
226
|
+
}
|
|
227
|
+
else if (o.project) {
|
|
122
228
|
/* 离线:项目只作资源来源(模板与 OCR 模型),不扫设备不连接 */
|
|
123
|
-
const off = offlineResource(o.project, o.controller, o.resource);
|
|
229
|
+
const off = offlineResource(o.project, o.controller, o.resource, ctx.cwd);
|
|
124
230
|
if (off.error) {
|
|
125
|
-
return fail('PLAN_FAILED', '
|
|
231
|
+
return fail('PLAN_FAILED', '解析项目资源失败:' + off.error, '也可以直接给 --resource-dir <dir>', EXIT.FINDINGS);
|
|
126
232
|
}
|
|
127
|
-
|
|
233
|
+
chain = { paths: off.paths, name: off.selected, source: 'project' };
|
|
128
234
|
}
|
|
129
|
-
|
|
130
|
-
|
|
235
|
+
else {
|
|
236
|
+
chain = { paths: [], name: null, source: 'project' };
|
|
131
237
|
}
|
|
238
|
+
if (!chain.paths.length) {
|
|
239
|
+
return fail('BAD_ARGUMENTS', '缺少资源目录:给 --resource-dir <dir>,或用 --project <dir> 让 interface.json 决定', nodeName !== undefined ? '--node-name 按资源里的原名跑,没有资源就没有节点可查' : undefined, EXIT.USAGE);
|
|
240
|
+
}
|
|
241
|
+
/* 单包沿用旧字段名 resourceDir(daemon 两种写法都收:单段还是整链);整链才送 resourceDirs */
|
|
242
|
+
const resources = chain.paths.length > 1 ? { resourceDirs: chain.paths } : { resourceDir: chain.paths[0] };
|
|
132
243
|
if (act) {
|
|
133
244
|
/* --act:识别拿框 → 真机 run_action。图像来自当前画面(daemon 抓帧),不吃 --image/--seq */
|
|
134
|
-
if (!node) {
|
|
135
|
-
return fail('BAD_ARGUMENTS', '--act 需要同时给 --node <json>(整节点,含 action)', undefined, EXIT.USAGE);
|
|
136
|
-
}
|
|
137
245
|
if (!o.project && !o.kind) {
|
|
138
246
|
return fail('BAD_ARGUMENTS', '--act 要真机执行动作:给 --project <dir> 或 --kind 连接设备', '多步操作用 maafw-live repl', EXIT.USAGE);
|
|
139
247
|
}
|
|
140
|
-
const r = await recoTest(client, {
|
|
248
|
+
const r = await recoTest(client, { ...resources, act: true, node });
|
|
141
249
|
if (r.ok === false) {
|
|
142
250
|
return fail('RECO_ACT', String(r.error ?? '动作单测失败'), r.stage === 'recognition' ? '识别未命中/报错——动作半未执行' : undefined, EXIT.FINDINGS);
|
|
143
251
|
}
|
|
@@ -146,35 +254,126 @@ export const recoCommand = {
|
|
|
146
254
|
return {
|
|
147
255
|
exitCode: EXIT.OK,
|
|
148
256
|
human: [
|
|
149
|
-
...describeSession(s),
|
|
257
|
+
...describeSession(s, ctx.flags.verbose),
|
|
258
|
+
resourceLine(chain),
|
|
150
259
|
'节点单测(识别→动作,真机执行):',
|
|
151
260
|
' 识别框 ' + JSON.stringify(reco?.box ?? null),
|
|
152
261
|
' 动作回执 ' + JSON.stringify(r.action ?? null),
|
|
153
262
|
' 边界帧 seq ' + String(retention?.before?.seq) + ' → ' + String(retention?.after?.seq) +
|
|
154
263
|
'(frame_get / kf promote 可复核动作效果)',
|
|
155
264
|
],
|
|
156
|
-
data: r,
|
|
265
|
+
data: { ...r, resource: resourceInfo(chain), layer: 'resource', paramSource },
|
|
157
266
|
};
|
|
158
267
|
}
|
|
159
268
|
if (!image && seq === undefined && !offline) {
|
|
160
269
|
return fail('BAD_ARGUMENTS', '缺少图像:一次性命令给 --image <png>;用缓冲帧(--seq)需要在 maafw-live repl 里先 stream start', '库里留存帧用 --from-kf <kf:…>(离线、不需要设备)', EXIT.USAGE);
|
|
161
270
|
}
|
|
162
271
|
const r = await recoTest(client, {
|
|
163
|
-
|
|
272
|
+
...resources,
|
|
273
|
+
type,
|
|
274
|
+
...(nodeName !== undefined ? { nodeName } : {}),
|
|
164
275
|
...(image ? { image } : {}),
|
|
165
276
|
...(seq !== undefined ? { seq } : {}),
|
|
166
277
|
...(kfSource ? { kfSource } : {}),
|
|
167
278
|
...(node ? { node } : {}),
|
|
168
|
-
...(
|
|
169
|
-
...(
|
|
279
|
+
...(nodeSource === null && param.value ? { param: param.value } : {}),
|
|
280
|
+
...(nodeSource === null && sweep.value ? { sweep: sweep.value } : {}),
|
|
170
281
|
});
|
|
171
282
|
const results = r.results ?? [];
|
|
283
|
+
const shownType = String(r.type ?? type);
|
|
284
|
+
/* ── 能力边界与"查无此节点"要报成明确结论,不能落进"看起来像 miss" ── */
|
|
285
|
+
const code = String(r.code ?? '');
|
|
286
|
+
if (r.ok === false && (code === 'CUSTOM_NODE' || code === 'NODE_MISSING')) {
|
|
287
|
+
const candidates = Array.isArray(r.candidates) ? r.candidates.map(String) : [];
|
|
288
|
+
const hint = code === 'CUSTOM_NODE'
|
|
289
|
+
? 'custom/agent 识别要桥接 agent 子进程:用 maafw-live run --project 跑;reco 只测资源合并后的普通节点'
|
|
290
|
+
: candidates.length
|
|
291
|
+
/* 拼错一个字时,正确名字就在资源里——不给近邻等于让人对着资源目录逐个比对 */
|
|
292
|
+
? '资源里最接近的节点名:' + candidates.join(' / ') + '(按资源合并后的原名跑;单包时 overlay 段的节点不在)'
|
|
293
|
+
: '确认资源名与链(--resource)是否正确;手写节点用 --node-file';
|
|
294
|
+
return {
|
|
295
|
+
exitCode: EXIT.FINDINGS,
|
|
296
|
+
human: [
|
|
297
|
+
...describeSession(s, ctx.flags.verbose), resourceLine(chain),
|
|
298
|
+
...(nodeName !== undefined ? [' 节点来源:--node-name ' + nodeName] : []),
|
|
299
|
+
...(candidates.length ? [' 资源里最接近的节点名:' + candidates.join(' / ')] : []),
|
|
300
|
+
],
|
|
301
|
+
error: {
|
|
302
|
+
code: code === 'CUSTOM_NODE' ? 'RECO_CUSTOM_NODE' : 'RECO_NODE_MISSING',
|
|
303
|
+
message: String(r.error),
|
|
304
|
+
hint,
|
|
305
|
+
},
|
|
306
|
+
data: { ...r, resource: resourceInfo(chain), layer: 'resource', paramSource },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const th = suggestThreshold(results);
|
|
310
|
+
/* roi 越界 = 参数用错空间(repl 侧同一判据,见 runtime/reco-args)。
|
|
311
|
+
* 只有 daemon 回执带 w/h 时才判得动:`--image`(0.3.0 起补 w/h)与 kfSource 都有,
|
|
312
|
+
* 缓冲帧的 meta.seq 分支本来就有;判不动就如实不判,不猜。 */
|
|
313
|
+
const meta = r.meta;
|
|
314
|
+
const oob = roiOutOfBounds(node || nodeName !== undefined ? undefined : param.value?.roi, meta?.w, meta?.h);
|
|
315
|
+
if (oob) {
|
|
316
|
+
return fail('ROI_OUT_OF_BOUNDS', oob, '把 roi 按所用图的尺寸给:--image 是控制器空间原图,缓冲帧是 L1 小图(差一个 scale)', EXIT.FINDINGS);
|
|
317
|
+
}
|
|
318
|
+
/* 离线评估(静态图 / 库帧)标记:防"拿旧截图出分冒充实测"(本仓 G21 同族事故)。
|
|
319
|
+
* 分数可用于调参与回归,不构成真机验收证据;验收判据只认真机 run 的 record.ok。
|
|
320
|
+
* `--seq` 是本会话帧流的缓冲帧,不在此列。 */
|
|
321
|
+
const offlineEvidence = image !== undefined || kfSource !== undefined;
|
|
322
|
+
const nodeDump = r.nodeDump;
|
|
323
|
+
/* OCR 的"空结果"与"模型没加载"同形:先看链上有没有模型,再谈 roi(判据见 runtime/ocr-model) */
|
|
324
|
+
const ocrModel = shownType === 'OCR'
|
|
325
|
+
? ocrModelStatus(chain.paths, ocrModelName(node, param.value, nodeDump))
|
|
326
|
+
: undefined;
|
|
327
|
+
const verdict = describeMisses(results, shownType, th ? { bestScore: th.bestScore, lowMatch: th.lowMatch } : null, ocrModel ? { ocrModel } : {});
|
|
328
|
+
const anyHit = results.some((x) => x.ok === true);
|
|
329
|
+
/* 只有会带 0..1 置信度的算法才有"无置信度"这回事:OCR / ColorMatch 本来就没有分数,
|
|
330
|
+
* 把它们标成"无置信度"正是这条判据要避免的误导。 */
|
|
331
|
+
const scoringType = shownType === 'TemplateMatch';
|
|
172
332
|
const human = [
|
|
173
|
-
...describeSession(s),
|
|
174
|
-
|
|
175
|
-
...
|
|
333
|
+
...describeSession(s, ctx.flags.verbose),
|
|
334
|
+
resourceLine(chain),
|
|
335
|
+
...(nodeName !== undefined
|
|
336
|
+
? [
|
|
337
|
+
' 节点来源:--node-name ' + nodeName + '(按资源里的原名跑;dump 只进回执,不参与识别)',
|
|
338
|
+
...(nodeDump
|
|
339
|
+
? [' 节点定义:recognition=' + String(nodeDump.recognition?.type ?? '?') +
|
|
340
|
+
' action=' + String(nodeDump.action?.type ?? '?')]
|
|
341
|
+
: []),
|
|
342
|
+
' layer=resource:这里只到资源合并结果;run --project 还会再叠 interface 的四级 pipeline_override' +
|
|
343
|
+
'(GUI 选项改过同一字段时两者对不上是预期行为)',
|
|
344
|
+
]
|
|
345
|
+
: []),
|
|
346
|
+
'识别 ' + shownType + ':' + results.length + ' 例' +
|
|
347
|
+
(node ? '(整节点透传)' : nodeName !== undefined ? '(资源原名)' : ''),
|
|
348
|
+
...(offlineEvidence
|
|
349
|
+
? [' ⚠ 离线评估(' + (kfSource ? '库帧 --from-kf' : '静态图 --image') +
|
|
350
|
+
'):分数可用于调参/回归,不构成真机验收证据——验收只认真机 run 的 record.ok']
|
|
351
|
+
: []),
|
|
352
|
+
...results.map((x, i) => {
|
|
353
|
+
const d = verdict.perResult[i];
|
|
354
|
+
/* 计数逐例如实给:`all` 是框架的原始候选,`filtered` 是 expected 过滤后的那层
|
|
355
|
+
* (模板匹配没有 filtered,只有 all) */
|
|
356
|
+
const counts = [];
|
|
357
|
+
if (d?.rawCount !== null && d?.rawCount !== undefined)
|
|
358
|
+
counts.push('all=' + d.rawCount);
|
|
359
|
+
if (d?.filteredCount !== null && d?.filteredCount !== undefined)
|
|
360
|
+
counts.push('filtered=' + d.filteredCount);
|
|
361
|
+
const mark = x.ok ? '[命中]' : (d?.failed ? '[报错]' : '[未中]');
|
|
362
|
+
return ' ' + mark + ' ' + JSON.stringify(x.param) + ' ' + x.ms + 'ms' +
|
|
363
|
+
(counts.length ? ' ' + counts.join(' ') : '') + (d?.missReason ? ' ← ' + d.missReason : '');
|
|
364
|
+
}),
|
|
176
365
|
];
|
|
177
|
-
|
|
366
|
+
/* 单例时逐例行已经写了原因,不再重复;多例(扫描)才给一条汇总 */
|
|
367
|
+
if (verdict.missReason && results.length > 1)
|
|
368
|
+
human.push(' 未命中原因:' + verdict.missReason);
|
|
369
|
+
else if (verdict.missReasons.length > 1)
|
|
370
|
+
human.push(' 未命中原因:' + verdict.missReasons.join(' / '));
|
|
371
|
+
/* 调用报错与未命中必须分开读:前者是"没跑成",先看原生消息;两者都算 FINDINGS(见下方退出码) */
|
|
372
|
+
const failedCount = verdict.perResult.filter((d) => d.failed).length;
|
|
373
|
+
if (failedCount > 0) {
|
|
374
|
+
human.push(' 识别调用报错 ' + failedCount + ' 例(上面标 [报错]):这不是未命中,先处理原生消息,' +
|
|
375
|
+
'别去调阈值/改 roi');
|
|
376
|
+
}
|
|
178
377
|
if (th && th.suggest !== null) {
|
|
179
378
|
human.push(' 实测最高置信度 ' + th.bestScore + ' → threshold 起点建议 ' + th.suggest +
|
|
180
379
|
'(实测 − 0.1 留余量,换场景会掉;ROI 收紧可再提)');
|
|
@@ -183,22 +382,52 @@ export const recoCommand = {
|
|
|
183
382
|
human.push(' 实测最高置信度 ' + th.bestScore + ':模板与该画面不匹配——threshold 建议不适用,' +
|
|
184
383
|
'先核对模板来源画面与当前画面(识别未命中不是调低阈值的理由)');
|
|
185
384
|
}
|
|
186
|
-
else if (results.length > 0) {
|
|
385
|
+
else if (results.length > 0 && !anyHit && scoringType) {
|
|
187
386
|
/* 第三形态:识别跑了,但一个置信度都没带回来(实测:模板文件读不到时就是这个签名——
|
|
188
387
|
* ~230ms、`[未中]`、detail.best 空、all 为空)。它和"分数低的未命中"必须分得开:
|
|
189
|
-
* 后者是画面不匹配(有分数),前者是**输入当时读不到**,处置方向完全不同。
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'
|
|
388
|
+
* 后者是画面不匹配(有分数),前者是**输入当时读不到**,处置方向完全不同。
|
|
389
|
+
* 判据必须先看有没有命中:OCR 命中时也没有数值分,旧判据会把命中标成"无置信度"。 */
|
|
390
|
+
human.push(' 识别未返回任何置信度(' + results.length + ' 例):这与"分数低的未命中"不同。'
|
|
391
|
+
+ '先看 **roi 是否越出所用图的尺寸**(越界会先被上面的校验拦下;没拦下说明尺寸判不动)——'
|
|
392
|
+
+ 'ctrl 坐标的 roi 打在 L1 小图上就是这个签名,实测踩过。再看模板/资源当时是否读得到'
|
|
393
|
+
+ '(资源包未加载、模板文件缺失或不可读、临时文件正被杀软扫描)。'
|
|
394
|
+
+ '先核对 --resource-dir 与模板是否存在,再重跑一次看是否复现;别当成"画面不匹配"去调阈值');
|
|
193
395
|
}
|
|
194
396
|
if (r.ok === false)
|
|
195
397
|
human.push('error: ' + String(r.error));
|
|
196
|
-
/* 机器侧也要能一眼认出第三形态(阈值建议缺席 ≠
|
|
197
|
-
const noConfidence = th === null && results.length > 0;
|
|
398
|
+
/* 机器侧也要能一眼认出第三形态(阈值建议缺席 ≠ 没跑):命中与否优先于"有没有分数" */
|
|
399
|
+
const noConfidence = scoringType && th === null && results.length > 0 && !anyHit;
|
|
400
|
+
const enriched = results.map((x, i) => {
|
|
401
|
+
const d = verdict.perResult[i];
|
|
402
|
+
return {
|
|
403
|
+
...x,
|
|
404
|
+
...(d?.missReason ? { missReason: d.missReason } : {}),
|
|
405
|
+
...(d?.rawCount !== null && d?.rawCount !== undefined ? { rawCount: d.rawCount } : {}),
|
|
406
|
+
...(d?.filteredCount !== null && d?.filteredCount !== undefined ? { filteredCount: d.filteredCount } : {}),
|
|
407
|
+
/* 机器侧要能区分"没跑成"与"未命中":miss 也会带 missReason,`failed` 只给调用报错 */
|
|
408
|
+
...(d?.failed ? { failed: true } : {}),
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
/* 退出码对齐文档契约(README 的码表、protocol.ts 的 FINDINGS 注释都写着"reco 未命中 → 3"):
|
|
412
|
+
* 未命中与调用报错都算"跑成了但发现了问题";全部命中才是 0。 */
|
|
413
|
+
const anyMiss = results.length > 0 && results.some((x) => x.ok !== true);
|
|
198
414
|
return {
|
|
199
|
-
exitCode: r.ok === false ? EXIT.FINDINGS : EXIT.OK,
|
|
415
|
+
exitCode: r.ok === false || anyMiss ? EXIT.FINDINGS : EXIT.OK,
|
|
200
416
|
human,
|
|
201
|
-
data: {
|
|
417
|
+
data: {
|
|
418
|
+
...r,
|
|
419
|
+
results: enriched,
|
|
420
|
+
resource: resourceInfo(chain),
|
|
421
|
+
layer: 'resource',
|
|
422
|
+
paramSource,
|
|
423
|
+
...(th ? { thresholdSuggestion: th } : {}),
|
|
424
|
+
...(noConfidence ? { noConfidence: true } : {}),
|
|
425
|
+
...(verdict.missReason ? { missReason: verdict.missReason } : {}),
|
|
426
|
+
...(verdict.missReasons.length > 1 ? { missReasons: verdict.missReasons } : {}),
|
|
427
|
+
...(verdict.rawCount !== null ? { rawCount: verdict.rawCount } : {}),
|
|
428
|
+
...(verdict.filteredCount !== null ? { filteredCount: verdict.filteredCount } : {}),
|
|
429
|
+
...(offlineEvidence ? { offlineEvidence: true } : {}),
|
|
430
|
+
},
|
|
202
431
|
};
|
|
203
432
|
});
|
|
204
433
|
}
|