first commit

This commit is contained in:
魏天炜
2026-11-12 15:29:12 +08:00
commit 5dabea54ab
93 changed files with 12170 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
// 把 runs/<run>/ 里的真实 trace 整理成回放页面需要的数据,并算出“证据”。
// 用法:node tools/build-viewer.js → 生成 ../explainers/real-run-replay.html
const fs = require('node:fs');
const path = require('node:path');
const DEMO = path.join(__dirname, '..');
const RUNS = path.join(DEMO, 'runs');
const OUT = path.join(DEMO, '..', 'explainers', 'real-run-replay.html');
const SECRET = 'LOCAL-ONLY-SECRET';
const readJsonl = (f) => (fs.existsSync(f) ? fs.readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l)) : []);
function build(runName) {
const dir = path.join(RUNS, runName);
const meta = JSON.parse(fs.readFileSync(path.join(dir, 'meta.json'), 'utf8'));
let recs = fs.readdirSync(path.join(dir, 'trace')).flatMap((f) => readJsonl(path.join(dir, 'trace', f)));
recs.sort((a, b) => a.t - b.t);
const t0 = recs[0].t;
// 路径里的本机用户目录对观众没有意义,换成相对路径
const strip = (s) => s.split(dir + '/').join('').split(dir).join('<run>');
recs = JSON.parse(strip(JSON.stringify(recs)));
recs.forEach((r) => { r.s = (r.t - t0) / 1000; });
const session = JSON.parse(strip(JSON.stringify(readJsonl(path.join(dir, 'session.jsonl')))));
session.forEach((e) => { e.s = (e.at - t0) / 1000; });
const end = recs[recs.length - 1].s;
// ---- 证据 ----
const workers = [];
for (const r of recs) {
if (r.kind === 'worker_spawn') workers.push({ pid: r.worker_pid, start: r.s, end: null, signal: null, reason: r.reason, models: 0, tools: 0 });
if (r.kind === 'worker_exit') { const w = workers.find((x) => x.pid === r.worker_pid); if (w) { w.end = r.s; w.signal = r.signal; } }
if (r.role === 'cloud-worker' && r.kind === 'model_request') { const w = workers.find((x) => x.pid === r.pid); if (w) w.models++; }
if (r.role === 'cloud-worker' && r.kind === 'tool_roundtrip') { const w = workers.find((x) => x.pid === r.pid); if (w) w.tools++; }
}
workers.forEach((w) => { if (w.end == null) w.end = end; });
const workerAlive = workers.reduce((a, w) => a + (w.end - w.start), 0);
const reqs = recs.filter((r) => r.kind === 'model_request');
const resps = recs.filter((r) => r.kind === 'model_response');
const kill = recs.find((r) => r.kind === 'inject_kill');
let killEv = null;
if (kill) {
const killedReq = reqs.filter((r) => r.pid === kill.worker_pid).pop();
const nextReq = reqs.find((r) => r.s > kill.s);
killEv = {
at: kill.s, pid: kill.worker_pid, after_seq: kill.after_seq,
lostRequest: !!killedReq && !resps.some((x) => x.pid === kill.worker_pid && x.s > killedReq.s),
nextPid: nextReq?.pid, nextAt: nextReq?.s,
identical: !!(killedReq && nextReq && JSON.stringify(killedReq.request) === JSON.stringify(nextReq.request)),
bytes: killedReq?.request_bytes,
};
}
const offline = recs.find((r) => r.kind === 'device_offline');
const onlineAgain = offline && recs.find((r) => r.kind === 'device_online' && r.s > offline.s);
let offEv = null;
if (offline && onlineAgain) {
const firstExec = recs.find((r) => r.kind === 'tool_exec' && r.s > onlineAgain.s);
const park = recs.find((r) => r.kind === 'worker_park');
offEv = {
from: offline.s, to: onlineAgain.s,
workersDuring: workers.filter((w) => w.start < onlineAgain.s && w.end > (park ? park.s + 0.1 : offline.s)).length,
parkedTool: park ? recs.find((r) => r.kind === 'dispatch_offline')?.tool : null,
parkAt: park?.s,
resumeMs: firstExec ? Math.round((firstExec.s - onlineAgain.s) * 1000) : null,
};
}
const gatewayText = reqs.map((r) => JSON.stringify(r.request)).join('\n');
const uploaded = [];
for (const r of recs.filter((x) => x.kind === 'tool_exec')) {
if (r.tool === 'read_file') uploaded.push(`文件 ${r.input.path}`);
if (r.tool === 'run_command') uploaded.push(`命令输出 \`${r.input.command}\``);
}
const jobs = recs.filter((r) => r.kind === 'job_start').map((j) => {
const x = recs.find((r) => r.kind === 'job_exit' && r.job_id === j.job_id);
return { id: j.job_id, start: j.s, end: x ? x.s : end, code: x?.exit_code, pid: j.child_pid };
});
const replies = recs.filter((r) => r.kind === 'session_append' && r.type === 'reply_to_user');
const answeredDuringJob = jobs.length && replies.some((r) => r.s > jobs[0].start + 0.5 && r.s < jobs[0].end);
const evidence = {
wall: end, workerCount: workers.length, workerAlive, workers,
gateway: { count: reqs.length, bytes: reqs.reduce((a, r) => a + r.request_bytes, 0), ms: resps.reduce((a, r) => a + r.ms, 0), secretInGateway: gatewayText.includes(SECRET) },
secretInSession: JSON.stringify(session).includes(SECRET),
uploaded,
kill: killEv, offline: offEv, jobs, answeredDuringJob,
toolExecs: recs.filter((r) => r.kind === 'tool_exec').length,
sessionEvents: session.length,
};
return { name: runName, meta, recs, session, evidence, finalDiff: fs.readFileSync(path.join(dir, 'final.diff'), 'utf8'), end };
}
const runs = fs.readdirSync(RUNS).filter((d) => fs.existsSync(path.join(RUNS, d, 'meta.json'))).sort().map(build);
const tpl = fs.readFileSync(path.join(__dirname, 'viewer-template.html'), 'utf8');
const data = JSON.stringify(runs).replace(/</g, '\\u003c');
fs.writeFileSync(OUT, tpl.replace('/*__DATA__*/null', data));
console.log(`写入 ${OUT},${runs.length} 次运行,${(fs.statSync(OUT).size / 1024).toFixed(0)} KB`);
for (const r of runs) console.log(r.name, JSON.stringify({ ...r.evidence, workers: r.evidence.workers.length }));