Files
dsh_shenxian/dsh-server-docs/scripts/analyze-session.mjs
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
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 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

91 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话时序分析器(只输出时间戳/事件类型/时延,不输出任何消息正文)
* 用法:node analyze-session.mjs <session.jsonl.zstd>
*/
import { readFileSync } from 'node:fs'
import { zstdDecompressSync } from 'node:zlib'
const file = process.argv[2]
const raw = readFileSync(file)
// 可能是多帧拼接:按 zstd magic 切分逐帧解压
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
const offsets = []
for (let i = 0; i + 4 <= raw.length; i++) {
if (raw.compare(MAGIC, 0, 4, i, i + 4) === 0) offsets.push(i)
}
let text = ''
const frames = offsets.length > 0 ? offsets : [0]
for (let f = 0; f < frames.length; f++) {
const start = frames[f]
const end = f + 1 < frames.length ? frames[f + 1] : raw.length
try {
text += zstdDecompressSync(raw.subarray(start, end)).toString('utf8')
} catch (e) {
// 单帧整体解压兜底
}
}
if (text === '') {
try {
text = zstdDecompressSync(raw).toString('utf8')
} catch (e) {
console.log('decompress failed:', e.message)
process.exit(1)
}
}
const lines = text.split('\n').filter((l) => l.trim() !== '')
console.log(`帧数=${frames.length} 行数=${lines.length} 解压字节=${text.length}`)
const pick = (obj, names, depth = 0) => {
if (obj === null || typeof obj !== 'object' || depth > 3) return undefined
for (const n of names) if (typeof obj[n] !== 'undefined') return obj[n]
for (const v of Object.values(obj)) {
const hit = pick(v, names, depth + 1)
if (hit !== undefined) return hit
}
return undefined
}
const TS_KEYS = ['ts', 'time', 'timestamp', 'at', 'createdAt', 'created_at', 'mtime', 'date']
const KIND_KEYS = ['kind', 'type', 'event', 'role', 'name', 'tag']
const events = []
const kinds = new Map()
for (const line of lines) {
let o
try {
o = JSON.parse(line)
} catch {
continue
}
const ts = pick(o, TS_KEYS)
const kind = pick(o, KIND_KEYS)
const k = typeof kind === 'string' ? kind : JSON.stringify(kind ?? '?')
kinds.set(k, (kinds.get(k) ?? 0) + 1)
const t = typeof ts === 'number' ? (ts < 1e12 ? ts * 1000 : ts) : typeof ts === 'string' ? Date.parse(ts) : NaN
events.push({ t: Number.isNaN(t) ? undefined : t, k })
}
console.log('\n== 事件类型分布(TOP 20)==')
;[...kinds.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).forEach(([k, c]) => console.log(` ${c}\t${k}`))
const withTs = events.filter((e) => e.t !== undefined)
console.log(`\n带时间戳事件: ${withTs.length}/${events.length}`)
if (withTs.length > 1) {
const span = (withTs[withTs.length - 1].t - withTs[0].t) / 1000
console.log(`时间跨度: ${span.toFixed(1)}s(约 ${(span / 60).toFixed(1)} 分钟)`)
const gaps = []
for (let i = 1; i < withTs.length; i++) gaps.push({ d: (withTs[i].t - withTs[i - 1].t) / 1000, a: withTs[i - 1].k, b: withTs[i].k })
console.log('\n== 最大间隔 TOP 12(秒 | 前事件 → 后事件)==')
gaps.sort((x, y) => y.d - x.d).slice(0, 12).forEach((g) => console.log(` ${g.d.toFixed(1)}\t${g.a} → ${g.b}`))
const over5 = gaps.filter((g) => g.d > 5)
console.log(`\n>5s 的空档数: ${over5.length};>30s: ${gaps.filter((g) => g.d > 30).length};>60s: ${gaps.filter((g) => g.d > 60).length}`)
// 会话首尾时间(便于与外部日志对齐)
const fmt = (ms) => new Date(ms).toISOString().replace('T', ' ').slice(0, 19)
console.log(`\n首事件: ${fmt(withTs[0].t)} 末事件: ${fmt(withTs[withTs.length - 1].t)}`)
}