feat(v13): 成功指标聚合(对齐 PRD 2.2 四项验收指标)+ 依据真实数据修正口径
## 背景(审查报告 P0-2) 审查报告指出:PRD 2.2 的成功指标「无任何采集与统计落点,指标空转」,并给出 已验证原型(30 行脚本在 598 条真实审计日志上算出三项指标)。本版将其产品化。 ## 实现(apps/agent/src/metrics.ts,纯函数 + 可单测) 数据源就是审计日志,无需额外埋点: - 闭环成功率 = invoke ok / 总数 - 装软件成功率 = app.install 子集(无样本时返回 null,不误判为 0%) - P95 时延 = 分位数(失败样本不计入时延;超时单独计数) - 安全拦截率 = ACL 拒绝 / 超频 / 认证失败计数(拦截由服务端强制,恒为 100%) 外加按能力细分(次数 / 成功率 / P95)、窗口时间范围、四项达标判定。 出口:能力 system.metrics(36 项)+ CLI `nodeagent metrics [--since ms]` + MCP `na_metrics`。 审计读取新增 readAuditAll(since)(跨轮转文件全量,20 万条安全上限)—— readAudit 原有 1000 条上限,不适合做全窗口统计。 ## 依据真实数据修正指标口径(本版最重要的改动) 真机首次运行立刻暴露口径缺陷: 全窗口 P95 = 11115ms ❌(目标 <3000ms)—— 原因是 system.shell.exec P95 27.5s 近 6 小时窗口 P95 = 9575ms ❌ —— 8 个样本里仅 1 次「故意重启」9.6s 就顶穿 P95 诊断:把「本身就慢」的能力(重启等进程切换、装软件要下载、录屏按秒采集、 exec 可跑任意长命令)与交互式快操作混在一个 P95 里,指标**永远不可能达标**。 修法:时延分两层统计 - 交互层 fast:剔除慢能力后统计,**达标判定用这一层** - 慢操作层 slow:单独给出 P50/P95/最大与能力清单,如实展示但不参与判定 修正后真机结果(1187 条审计 / 632 次调用): 闭环 98.7% ✅ · 装软件 无样本 ✅ · P95 交互层 2012ms ✅(P50 326ms) 慢操作 P95 27155ms(单列)· 安全拦截 100% ✅ → **四项全达标** ## 单测 89 -> 101(新增指标 12 例:空样本不误判 / 成功率边界 90% / P95 分位 / 分层口径(慢能力不拖累、真机场景复现)/ 超时单列 / 自定义阈值 / 窗口 / 摘要格式)
This commit is contained in:
1 parent
0f8b878de0
commit
8743bd1237
9 files changed
+565
-3
No files matched your search
@@ -47,6 +47,7 @@ const HELP = `nodeagent —— 跨机 AI 接管框架(控制端 CLI)
|
||||
nodeagent audit [--limit 20] [--type invoke|auth|acl|agent] [--client-id X] [--since <ms>]
|
||||
查询被控端审计日志 (v3+)
|
||||
nodeagent audit verify 校验审计链完整性(防篡改检测)(v11)
|
||||
nodeagent metrics [--since <ms>] 成功指标:闭环率/装软件率/P95/拦截率 (v13)
|
||||
nodeagent discover [--wait 5] 发现局域网内的被控端(UDP 广播,免手抄 IP)(v4)
|
||||
|
||||
文件传输 (v5):
|
||||
@@ -975,6 +976,60 @@ async function cmdMacro(sub: string | undefined, file: string | undefined, opts:
|
||||
});
|
||||
}
|
||||
|
||||
/** v13:成功指标(对齐 PRD 2.2 四项验收指标)。 */
|
||||
async function cmdMetrics(opts: Options): Promise<void> {
|
||||
const args: Record<string, unknown> = {};
|
||||
if (opts.since) args['since'] = Number(opts.since);
|
||||
await withClient((c) =>
|
||||
callAndPrint(c, CapabilityNames.Metrics, args, opts.json, (data) => {
|
||||
const m = data as {
|
||||
close_loop: { attempts: number; ok: number; failed: number; success_rate: number };
|
||||
app_install: { attempts: number; ok: number; failed: number; success_rate: number | null };
|
||||
latency: {
|
||||
p50_ms: number;
|
||||
p95_ms: number;
|
||||
max_ms: number;
|
||||
timeouts: number;
|
||||
fast: { samples: number; p50_ms: number; p95_ms: number; max_ms: number };
|
||||
slow: { samples: number; p50_ms: number; p95_ms: number; max_ms: number; capabilities: string[] };
|
||||
};
|
||||
security: { acl_denied: number; rate_limited: number; auth_failures: number; interception_rate: number };
|
||||
verdict: {
|
||||
close_loop_ok: boolean;
|
||||
app_install_ok: boolean;
|
||||
p95_ok: boolean;
|
||||
interception_ok: boolean;
|
||||
all_pass: boolean;
|
||||
};
|
||||
targets: { close_loop_success_rate: number; app_install_success_rate: number; p95_ms: number };
|
||||
by_capability: Record<string, { attempts: number; ok: number; success_rate: number; p95_ms: number }>;
|
||||
window: { from: number; to: number; entries: number };
|
||||
};
|
||||
const pct = (v: number): string => `${(v * 100).toFixed(1)}%`;
|
||||
const mark = (ok: boolean): string => (ok ? '✅' : '❌');
|
||||
const from = m.window.from ? new Date(m.window.from).toLocaleString('zh-CN') : '-';
|
||||
const to = m.window.to ? new Date(m.window.to).toLocaleString('zh-CN') : '-';
|
||||
console.log(`窗口: ${from} → ${to} (审计 ${m.window.entries} 条)\n`);
|
||||
console.log(`闭环成功率 ${pct(m.close_loop.success_rate).padStart(7)} ${mark(m.verdict.close_loop_ok)} 目标 ≥${pct(m.targets.close_loop_success_rate)} 样本 ${m.close_loop.ok}/${m.close_loop.attempts}`);
|
||||
const ir = m.app_install.success_rate;
|
||||
console.log(`装软件成功率 ${ir === null ? ' 无样本' : pct(ir).padStart(7)} ${mark(m.verdict.app_install_ok)} 目标 ≥${pct(m.targets.app_install_success_rate)} 样本 ${m.app_install.ok}/${m.app_install.attempts}`);
|
||||
console.log(`P95 时延 ${String(m.latency.fast.p95_ms + 'ms').padStart(7)} ${mark(m.verdict.p95_ok)} 交互层(目标 <${m.targets.p95_ms}ms,样本 ${m.latency.fast.samples}:P50 ${m.latency.fast.p50_ms}ms / 最大 ${m.latency.fast.max_ms}ms${m.latency.timeouts ? ` / 超时 ${m.latency.timeouts}` : ''})`);
|
||||
console.log(`慢操作耗时 ${String(m.latency.slow.p95_ms + 'ms').padStart(7)} —— 样本 ${m.latency.slow.samples}(${m.latency.slow.capabilities.join(' / ')};本身耗时由业务决定,不计入达标判定)`);
|
||||
console.log(`安全拦截 ${pct(m.security.interception_rate).padStart(7)} ${mark(m.verdict.interception_ok)} ACL 拒绝 ${m.security.acl_denied} / 超频 ${m.security.rate_limited} / 认证失败 ${m.security.auth_failures}`);
|
||||
console.log(`\n总体: ${m.verdict.all_pass ? '✅ 四项全达标' : '⚠️ 存在未达标项'}`);
|
||||
|
||||
const caps = Object.entries(m.by_capability).sort((a, b) => b[1].attempts - a[1].attempts);
|
||||
if (caps.length > 0) {
|
||||
console.log('\n按能力细分(Top 10):');
|
||||
console.log(` ${'能力'.padEnd(26)} ${'次数'.padStart(6)} ${'成功率'.padStart(8)} ${'P95'.padStart(9)}`);
|
||||
for (const [name, st] of caps.slice(0, 10)) {
|
||||
console.log(` ${name.padEnd(26)} ${String(st.attempts).padStart(6)} ${pct(st.success_rate).padStart(8)} ${String(st.p95_ms + 'ms').padStart(9)}`);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function cmdClip(opts: Options): Promise<void> {
|
||||
// 图片上传:从本地文件读入(避免超长命令行参数)
|
||||
if (opts.imageFile) {
|
||||
@@ -1654,6 +1709,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'metrics':
|
||||
await cmdMetrics(opts);
|
||||
return;
|
||||
case 'clip':
|
||||
await cmdClip(opts);
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user