Files
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(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/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

91 lines
3.4 KiB
JavaScript
Raw Permalink 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)}`)
}