build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
72 lines
3.3 KiB
JavaScript
72 lines
3.3 KiB
JavaScript
/**
|
||
* 会话每轮指标:TTFT(turn/start → 首个流式 chunk)、轮时长、工具耗时
|
||
* 只输出时间与类型,不输出任何正文。
|
||
* 用法:node analyze-turn.mjs <session.jsonl.zstd>
|
||
*/
|
||
import { readFileSync } from 'node:fs'
|
||
import { zstdDecompressSync } from 'node:zlib'
|
||
|
||
const raw = readFileSync(process.argv[2])
|
||
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
||
const offs = []
|
||
for (let i = 0; i + 4 <= raw.length; i++) if (raw.compare(MAGIC, 0, 4, i, i + 4) === 0) offs.push(i)
|
||
let text = ''
|
||
const frames = offs.length > 0 ? offs : [0]
|
||
for (let f = 0; f < frames.length; f++) {
|
||
const s = frames[f], e = f + 1 < frames.length ? frames[f + 1] : raw.length
|
||
try { text += zstdDecompressSync(raw.subarray(s, e)).toString('utf8') } catch {}
|
||
}
|
||
if (text === '') { try { text = zstdDecompressSync(raw).toString('utf8') } catch (e) { console.log('fail', e.message); process.exit(1) } }
|
||
|
||
const pick = (o, keys, d = 0) => {
|
||
if (o === null || typeof o !== 'object' || d > 3) return undefined
|
||
for (const k of keys) if (typeof o[k] !== 'undefined') return o[k]
|
||
for (const v of Object.values(o)) { const h = pick(v, keys, d + 1); if (h !== undefined) return h }
|
||
return undefined
|
||
}
|
||
const ev = []
|
||
for (const line of text.split('\n')) {
|
||
if (line.trim() === '') continue
|
||
let o; try { o = JSON.parse(line) } catch { continue }
|
||
const kind = pick(o, ['kind', 'type', 'event'])
|
||
const ts = pick(o, ['ts', 'time', 'timestamp', 'at', 'createdAt'])
|
||
const t = typeof ts === 'number' ? (ts < 1e12 ? ts * 1000 : ts) : typeof ts === 'string' ? Date.parse(ts) : NaN
|
||
if (typeof kind === 'string') ev.push({ k: kind, t: Number.isNaN(t) ? undefined : t })
|
||
}
|
||
|
||
const fmt = (ms) => new Date(ms).toISOString().slice(11, 19) + 'Z'
|
||
let turns = 0
|
||
for (let i = 0; i < ev.length; i++) {
|
||
if (ev[i].k !== 'turn/start') continue
|
||
turns++
|
||
const t0 = ev[i].t
|
||
let end, firstChunk, firstText
|
||
for (let j = i + 1; j < ev.length; j++) {
|
||
if (ev[j].k === 'turn/end' && end === undefined) { end = ev[j].t; break }
|
||
if (firstChunk === undefined && (ev[j].k === 'assistant/chunk' || ev[j].k === 'reasoning-chunks') && ev[j].t !== undefined) firstChunk = ev[j].t
|
||
if (firstText === undefined && ev[j].k === 'text-chunks' && ev[j].t !== undefined) firstText = ev[j].t
|
||
}
|
||
const ttft = t0 !== undefined && firstChunk !== undefined ? ((firstChunk - t0) / 1000).toFixed(1) : '?'
|
||
const ttftText = t0 !== undefined && firstText !== undefined ? ((firstText - t0) / 1000).toFixed(1) : '?'
|
||
const dur = t0 !== undefined && end !== undefined ? ((end - t0) / 1000).toFixed(1) : '?'
|
||
console.log(`轮 ${String(turns).padStart(2)} ${t0 !== undefined ? fmt(t0) : '?'} TTFT=${ttft}s 首文本=${ttftText}s 轮时长=${dur}s`)
|
||
}
|
||
|
||
// 工具耗时
|
||
const tools = ev.map((e, i) => ({ e, i })).filter((x) => x.e.k === 'tool/call')
|
||
const dt = []
|
||
for (const { i } of tools) {
|
||
for (let j = i + 1; j < ev.length; j++) {
|
||
if (ev[j].k === 'tool/result') {
|
||
if (ev[i].t !== undefined && ev[j].t !== undefined) dt.push((ev[j].t - ev[i].t) / 1000)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if (dt.length > 0) {
|
||
dt.sort((a, b) => b - a)
|
||
const avg = dt.reduce((a, b) => a + b, 0) / dt.length
|
||
console.log(`\n工具调用 ${dt.length} 次;平均 ${avg.toFixed(1)}s;最慢 5 个: ${dt.slice(0, 5).map((x) => x.toFixed(1) + 's').join(', ')}`)
|
||
}
|
||
console.log(`\n合计轮数: ${turns}`)
|