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/;交接单不入库(政策)。
91 lines
3.4 KiB
JavaScript
91 lines
3.4 KiB
JavaScript
/**
|
||
* 会话时序分析器(只输出时间戳/事件类型/时延,不输出任何消息正文)
|
||
* 用法: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)}`)
|
||
}
|