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 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

@@ -0,0 +1,90 @@
/**
* 会话时序分析器(只输出时间戳/事件类型/时延,不输出任何消息正文)
* 用法: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)}`)
}