Files
dsh_ai1net_server/dsh-server-docs/scripts/sess-analyze.mjs
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

112 lines
5.7 KiB
JavaScript
Raw 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))