/** * 会话取证 · 画像:元信息/权限档位/用户消息/12 类错误模式扫描/工具清单(不输出正文以外内容) * guest 会话问题分析(只读) * 用法:node analyze-guest.mjs [--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))