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/;交接单不入库(政策)。
112 lines
5.7 KiB
JavaScript
112 lines
5.7 KiB
JavaScript
/**
|
||
* 会话取证 · 画像:元信息/权限档位/用户消息/12 类错误模式扫描/工具清单(不输出正文以外内容)
|
||
* guest 会话问题分析(只读)
|
||
* 用法:node analyze-guest.mjs <session.jsonl.zstd> [--full]
|
||
*/
|
||
import { readFileSync } from 'node:fs'
|
||
import { zstdDecompressSync } from 'node:zlib'
|
||
|
||
const file = process.argv[2]
|
||
const raw = readFileSync(file)
|
||
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 ? 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 {}
|
||
}
|
||
const evs = []
|
||
for (const l of text.split('\n')) {
|
||
if (!l.trim()) continue
|
||
try { evs.push(JSON.parse(l)) } catch {}
|
||
}
|
||
const ts = (ms) => new Date(Number(ms)).toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' })
|
||
|
||
// ── 1) 元信息 ─────────────────────────────────────────
|
||
const meta = evs.find((e) => e.type === 'session')
|
||
console.log('=== 元信息 ===')
|
||
console.log(' id:', meta?.id, '| cwd:', meta?.cwd)
|
||
console.log(' 创建:', ts(meta?.createdAt), '| 事件数:', evs.length, '| 解压:', text.length, '字符')
|
||
const presets = evs.filter((e) => e.type === 'permission/preset').map((e) => e.data?.preset)
|
||
const modes = evs.filter((e) => e.type === 'sandbox/mode').map((e) => e.data?.mode)
|
||
const pols = evs.filter((e) => e.type === 'approval/policy').map((e) => e.data?.policy)
|
||
console.log(' permission preset:', presets.join(','), '| sandbox mode:', modes.join(','), '| approval:', pols.join(','))
|
||
const turns = evs.filter((e) => e.type === 'turn/start').map((e) => Number(e.data?.turn))
|
||
console.log(' turn 数:', turns.length, '| 最后事件:', ts(evs[evs.length - 1]?.time ?? evs[evs.length - 1]?.time0))
|
||
|
||
// ── 2) 类型直方图(正确格式)──────────────────────────
|
||
const hist = new Map()
|
||
for (const e of evs) hist.set(e.type, (hist.get(e.type) ?? 0) + 1)
|
||
console.log('\n=== 事件类型(Top 25)===')
|
||
for (const [t, n] of [...hist].sort((a, b) => b[1] - a[1]).slice(0, 25)) console.log(' ' + String(n).padStart(5) + ' ' + t)
|
||
|
||
// ── 3) 用户消息 ───────────────────────────────────────
|
||
const textOf = (d) => {
|
||
if (typeof d === 'string') return d
|
||
if (Array.isArray(d)) return d.map(textOf).join('')
|
||
if (d && typeof d === 'object') {
|
||
if (typeof d.text === 'string') return d.text
|
||
if (d.content) return textOf(d.content)
|
||
return ''
|
||
}
|
||
return ''
|
||
}
|
||
console.log('\n=== 用户消息(共 ' + evs.filter((e) => e.type === 'user/message').length + ' 条)===')
|
||
for (const e of evs.filter((e) => e.type === 'user/message')) {
|
||
const t = textOf(e.data?.content).replace(/\s+/g, ' ').slice(0, 260)
|
||
console.log(` [${ts(e.time)}] ${t}`)
|
||
}
|
||
|
||
// ── 4) 错误/拒绝模式扫描 ──────────────────────────────
|
||
const PATTERNS = [
|
||
['沙箱后端不可用', /no sandbox backend is usable/],
|
||
['拒绝无沙箱运行', /refusing to run the command unconfined/],
|
||
['权限被拒', /(?:\bdenied\b|Permission denied|EACCES|not permitted)/],
|
||
['文件不存在', /\bENOENT\b|No such file or directory|command not found/i],
|
||
['只读文件系统', /Read-only file system|EROFS/],
|
||
['超时', /\btimed? ?out\b|ETIMEDOUT|timeout/i],
|
||
['解析失败/DNS', /Could not resolve host|EAI_AGAIN|getaddrinfo/],
|
||
['401/鉴权', /401|unauthorized|authentication required/i],
|
||
['崩溃/熔断', /crash-restart|circuit-open/],
|
||
['Python 缺失', /python3?: (?:command )?not found|python3 不存在/],
|
||
['工具失败关键词', /tool (?:call )?failed|Tool failed|工具执行失败/],
|
||
['中文报错词', /报错|失败|不可用|无法执行|被拒绝|不允许/],
|
||
]
|
||
console.log('\n=== 错误/限制模式扫描 ===')
|
||
for (const [name, re] of PATTERNS) {
|
||
const hits = []
|
||
for (const e of evs) {
|
||
const s = JSON.stringify(e)
|
||
if (re.test(s)) hits.push(e)
|
||
}
|
||
if (!hits.length) { console.log(` ${name}: 0`); continue }
|
||
const kinds = new Map()
|
||
for (const h of hits) kinds.set(h.type, (kinds.get(h.type) ?? 0) + 1)
|
||
console.log(` ${name}: ${hits.length} 次 → ${[...kinds].map(([k, v]) => k + '×' + v).join(', ')}`)
|
||
for (const h of hits.slice(0, 2)) {
|
||
const m = JSON.stringify(h).match(re)
|
||
const i = JSON.stringify(h).indexOf(m[0])
|
||
console.log(` · [${ts(h.time ?? h.time0)}] …` + JSON.stringify(h).slice(Math.max(0, i - 130), i + 130).replace(/\\n/g, ' '))
|
||
}
|
||
}
|
||
|
||
// ── 5) 工具调用清单(tool 相关事件的 name/command)────
|
||
console.log('\n=== 工具调用(含 tool 的事件,取 name/command/tool)===')
|
||
const toolEvs = evs.filter((e) => /tool/i.test(e.type))
|
||
const names = new Map()
|
||
const fails = []
|
||
for (const e of toolEvs) {
|
||
const s = JSON.stringify(e)
|
||
const nm = (s.match(/"name":"([^"]{2,40})"/) ?? [])[1] ?? (s.match(/"tool":"([^"]{2,40})"/) ?? [])[1] ?? '(?)'
|
||
names.set(nm, (names.get(nm) ?? 0) + 1)
|
||
if (/"(?:isError|ok|error|status)":(?:true|false|"[^"]*")/.test(s)) {
|
||
if (/"isError":true|"ok":false|"status":"(?:error|failed)"/.test(s)) fails.push(e)
|
||
}
|
||
}
|
||
console.log(' 工具事件类型数:', toolEvs.length)
|
||
for (const [n, c] of [...names].sort((a, b) => b[1] - a[1]).slice(0, 20)) console.log(' ' + String(c).padStart(4) + ' ' + n)
|
||
console.log(' 标记失败的工具事件:', fails.length)
|
||
for (const f of fails.slice(0, 5)) console.log(' · [' + ts(f.time ?? f.time0) + '] ' + JSON.stringify(f).slice(0, 300))
|