/** * 会话时序分析器(只输出时间戳/事件类型/时延,不输出任何消息正文) * 用法:node analyze-session.mjs */ 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)}`) }