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

112 lines
5.7 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.
/**
* 会话取证 · 画像:元信息/权限档位/用户消息/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))