1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
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}`)
|