# dsh 调试与修复技术路线手册 > 本文档沉淀 dsh-hub / 官方 dsh 生态**反复出现**的调试与修复技术路线,避免每次重新调研。凡遇到下列问题类型,先查本文档对应章节,再动手。 > > 配套:`docs/关键踩坑记录.md`(现象→原因→正确做法索引)、`BUG_FIX_SOP.md`(五阶段流程)、`REFERENCE.md`(dsh 官方架构/接口事实)。 > > 版本基线:2026-08-28(rc.12 连环事故后整理)。所有命令均在 Windows + Git Bash + Node v24 环境验证。 --- ## 0. 问题类型 → 章节速查 | 症状 | 章节 | |---|---| | 会话打不开 / 「历史加载失败」/ 启动卡壳 | §1 会话文件(zstd 格式) | | 会话「本轮运行失败 content.some is not a function」等运行期错误 | §2 崩溃定位 | | 插件工具输出异常 / 会话日志 tool-result content 是字符串 | §2.4 + §3 插件契约 | | dsh 插件加载失败(loader entry / TDZ / duplicate id) | §3 插件契约 | | 需要验证安装包 / 发布前门禁 | §4 验证纪律 | | UI 层问题(右键菜单 / i18n / 注入) | §5 UI 层技术路线 | --- ## 1. 会话持久化文件(session.jsonl.zstd)调查与修复 **适用**:会话损坏修复、崩溃数据排查、回退会话、任何直接操作 `~/.dsh/sessions/**/session.jsonl.zstd` 的场景。 ### 1.1 解码(只读调查,安全) dsh 会话 = **zstd 多帧容器**(`JSONL 明文 → 按批次压缩为独立 zstd 帧 → 拼接`)。Node v24 内置 zstd,无需外部工具: ```js const { zstdDecompressSync } = require('node:zlib') // scanZstdFrames: 逐帧结构扫描(magic + 帧头 + blocks + checksum),返回 { frames: [{start,end}], tornStart? } // 完整实现见 deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts // 或仓库 .tmp/dsh-session-decode.cjs(可直接复用,已含完整 scan 逻辑) ``` - 解码脚本:`deepseek-harness/.tmp/dsh-session-decode.cjs`(全目录解码到明文 .jsonl,短文件名防 Windows 路径超长) - 明文 = 每行一个 JSON(可能是 **packed storage row**,见 §1.2-③) ### 1.2 格式不变量(改文件前必须满足,缺一即崩) | # | 不变量 | 违反时症状 | 校验代码 | |---|---|---|---| | ① | **帧 1 解压后恰好一行 header**(非空、第一个 `\n` 恰在末尾) | 启动时 workspace 插件树加载失败:`corrupt Zstandard session log: first frame is not exactly one header line` | `buf.indexOf(10) === buf.length - 1`(**Buffer 字节级**,勿用 string!见 §1.4-④) | | ② | 事件帧行序列 = 完整 JSONL,事件 **seq 从 0 连续**(`event.seq === events.length`,scanner 逐行校验) | 打开会话:`complete frame contains a torn JSONL record`(seq gap → committedBytes 停行) | scanner 复刻 / 安装库 `loadStored` | | ③ | **packChunks storage rows**:`text-chunks`/`reasoning-chunks`/`tool-call-chunks` 是打包行(一行为多个 chunk 事件),普通事件行原样 | 重编码时若拆/合打包行会破坏 seq 或结构 | 保持原行原样 JSON.stringify,勿改打包行结构 | | ④ | 帧可含 checksum(`ZSTD_c_checksumFlag:1`),重压缩事件帧用同参数 | — | `zstdCompressSync(text, { params: { [constants.ZSTD_c_checksumFlag]: 1 } })` | ### 1.3 修复技术路线(安全流程,顺序执行) 1. **备份**:原文件 `copyFileSync` 到临时目录(保留原始字节 = 黄金参照)。 2. **确认无进程占用**:`tasklist | grep -i "dsh\|node"`,dsh 运行中绝不改会话文件。 3. **从备份解码**(不是从已损坏文件),逐行 parse。 4. **内容级修改**(只动事件数据): - 删崩溃尾巴:只删**尾部**事件(尾部删除不破坏 seq 连续性);若删中间事件必须全局重排 seq 并同步 `tool/result.sourceEventSeqs` 引用。 - 修畸形 block:如 tool-result 的 `content` 是字符串 → 改 `[{ type: 'text', text }]`。 5. **重建为两帧**(关键): - 帧 1 = **原文件第一帧的原始压缩字节**(`subarray(frame0.start, frame0.end)`,逐字节复用,保证合法) - 帧 2 = 事件明文(**`slice(1)` 排除 header 行**,从 `seq=0` 的 `permission/preset` 行开始)单独压缩 - `Buffer.concat([headerFrameBytes, eventFrame])` 写回 6. **双层验证**(见 §1.5)。 ### 1.4 常见错误模式与判别(全部实战踩过) - **单帧重编码**:把 header+全部事件压成一帧 → 违反 ①(启动崩)。判别:`scanZstdFrames` 帧数 = 1。 - **事件帧混入 header 行**:`decodeAll` 全部帧后 `split('\n')` 没 `slice(1)` → 事件帧首行 `seq=undefined` → 违反 ②(打开崩)。判别:事件帧解压后第一行是 `{"type":"session"...}`。 - **帧边界算错**:自写 scanZstdFrames 的 block/checksum 解析偏差 → 复用 header 帧字节时带上垃圾尾随(下一帧开头)→ `invalid frame magic at byte N`。判别:对备份文件做「解压扫描」找真实边界(从 offset 递增 `subarray(0,X)` 直到解出非空明文的最小 X)。 - **字节 vs 字符假象**:JS string 的 `indexOf/length` 是 UTF-16 code unit;会话 cwd 含中文(UTF-8 3 字节/字符)时 `Buffer.length(330) ≠ string.length(317)`,用 string 判断「一行」会误报截断。**一律用 Buffer 字节级**(`buffer.indexOf(10) === buffer.length - 1`)。 ### 1.5 验证矩阵(用安装库,不是自写复刻) ```js // ctx 只需 stub 一个方法,即可实例化安装库并走真实读取路径 import { Context } from '<安装目录>/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/cordis/lib/index.js' import { JsonlSessionPersistence } from '<安装目录>/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/index.js' const ctx = new Context() ctx.sessions = { list: () => [] } const store = new JsonlSessionPersistence(ctx, { root: '<会话根>', compression: 'zstd' }) await store.listArtifacts() // 启动扫描路径(读 header) await store.loadStored('') // 完整读取路径(readZstdPrefix + torn 校验) ``` | 层 | 验证 | 通过标准 | |---|---|---| | 全目录 header | 所有 `session.jsonl.zstd` 帧 1 恰好一行 | 8/8 | | 启动扫描 | 安装库 `listArtifacts()` | 无抛错 | | 完整读取 | 安装库 `loadStored(id)`(逐个会话) | 无抛错、事件数正确、末事件符合预期 | | 事件语义 | 无畸形 block、无 error turn/end、seq 连续 | 2/2 目标会话 | 恢复语义模拟:dsh 对「无 turn/end 的 open turn」自动合成 `turn/end (kind: interrupted)`(`interruptedTurnClosers`,`packages/core/session/src/repair.ts`)——删 error 尾巴后会话以中断状态恢复,可继续。 --- ## 2. dsh 崩溃 / 运行期错误定位技术路线 **适用**:会话「本轮运行失败 xxx」、插件加载失败、运行期 TypeError。 ### 2.1 报错文案溯源(UI → 代码) 1. UI 文案 → 找 locale 键:`grep -rn "文案" packages/client/*/src/client/locales.ts`(如「本轮运行失败」= `message.turnError`)。 2. locale 键 → 渲染节点:`grep -rn "turnError" packages/client/ui-conversation/src/` → `conversation-nodes/turn-error.ts` → 错误 message 来自 `displayFailureMessage(failure)`(`client/runtime/src/client/sessions/failure-display.ts`:`code==='AUTH'` 显示固定文案,否则 `record.message`)。 3. 错误 message 是**持久化的 failure.message** → 去会话事件流找源头(§2.2)。 ### 2.2 会话事件流分析(找崩溃现场) - 解码会话(§1.1)→ 定位崩溃前的 `assistant/chunk {"type":"finish","reason":{"kind":"error"...}}` 与 `turn/end error`。 - 回看崩溃前步骤:`tool/call` → `tool/result`(检查 `message.content[0]` 结构!)→ `step/end` → 下一步 `step/start` 立即崩。 - **畸形数据判别**:`tool-result` block 的 `content` 必须是 `ContentBlock[]`;若是字符串/对象 = 上游工具输出契约违规(§2.4)。 ### 2.3 运行时崩溃点定位(`.some()` 类) `content.some is not a function` 这类:崩溃发生在**请求重建/图像检查**等遍历消息 content 的路径: 1. 全库搜运行时 `.some(` 调用(排除 tests):`grep -rn "content.some" packages/*/src/ | grep -v tests` 2. 重点:`llm/llm/src/content.ts` 的 `contentHasImage()`(递归进入 tool-result block 调 `contentHasImage(block.content)`——**block.content 若是字符串直接崩**),调用方 = 各 provider adapter(`llm-pi-ai/src/adapter.ts`、`llm-deepseek/src/adapter.ts` 的 `messages.some(m => contentHasImage(m.content))`)。 3. 结论链条:provider → adapter 图片检查 → contentHasImage 递归 → tool-result block content 类型 → **上游工具 execute/render 输出格式**。 ### 2.4 数据契约核对清单(工具输出) | 契约 | 要求 | 违反后果 | |---|---|---| | `ToolResult.content` | `ContentBlock[]`(数组) | contentHasImage 递归崩 / 序列化异常 | | `output.render(args, value)` | 返回 `ContentBlock[]` | 字符串直接进 tool-result content → 会话毒化 | | `execute(args, exec)` | 返回 JSON 可序列化值;经 `snapshotJsonValue` 快照 | 非 JSON → `ToolOutputError` | | cordis 插件工具 | 用 `harness.defineTool`(有 `assertRenderedContent` 数组校验);**自造 defineTool 会绕过校验** | 畸形输出无人拦截 | 防御缺口(已建议官方):`core/tools/src/index.ts` 的 `snapshotProjection` 只做 JSON 快照、**不校验 render 返回数组**——畸形插件毒化整个会话而非单工具报错。 --- ## 3. 插件开发契约清单(防重复踩坑) - **身份四重相等**(AGENTS §0 铁律 2):`package.json name` == `tsdown PLUGIN_ID` == `cordis.patch.yml insert.name` == web profile `bundles` 条目;改任一必须同步全部。 - **工具输出**:`render` 必须返回数组(§2.4);`execute` 返回 JSON 值。 - **词典/模块顶层**:词典必须是纯数据(零函数调用),否则模块顶层 TDZ(`Cannot access 't' before initialization`,踩坑 #81);顶层定义顺序:常量 → 词典 → 函数。 - **加载链路验证**:改动后跑 `npm run build && npm run build:client`(SDK junction 被 `npm install` 清掉后必须重跑 build:client);发布前 `node scripts/verify-release.mjs` ALL PASS(含全新环境装配冒烟);插件独立发布前 `node scripts/verify-plugin.mjs`。 - **i18n**:语言源 = 官方 locale 插件写入的 ``(MutationObserver 订阅);文案进词典、禁硬编码。 --- ## 4. 验证与测试环境纪律 - **隔离 DSH_HOME**:一切测试/复现/安装/卸载用 `%TEMP%` 或 E:/ 下独立目录;**永不碰真实 `~/.dsh`**(除非用户明示,且先备份 + 打印路径 + 失败回滚)。 - **发布门禁**:`node scripts/verify-release.mjs` 全 PASS;lib 产物零漂移(build 后先 commit 再 publish);`npm version` bump 后必须同步 `src-tauri/tauri.conf.json` version(build:installer 有版本一致性检查);dist-tags 用 registry 直查(`curl -s https://registry.npmjs.org/-/package/@marecgents/dsh-hub/dist-tags`),`npm view` 有缓存不可信。 - **会话修复专用纪律**:见 §1.3(备份 → 确认无进程 → 从备份解码 → 双层验证)。 --- ## 5. UI 层技术路线(右键菜单 / 注入 / i18n) (来自踩坑 #76–#85 的系统化经验,完整细节见对应条目) - **右键菜单语义**:WebView2 原生菜单已在 Rust 侧禁用(`SetAreDefaultContextMenusEnabled(false)`);页面右键全由 DOM 接管——对象行 `div[role="treeitem"]`(closest 上行)→ 专属菜单(`preventDefault + stopPropagation` 双保险);空白处 → 刷新菜单;**全局刷新源只保留一个**(新增 handler 前 `grep "addEventListener('contextmenu'"` 盘点全仓)。 - **DOM 事件**:同节点多 contextmenu 监听都会执行,preventDefault 不阻止其他监听;协作不依赖他方 stopPropagation 副作用。 - **官方弹层可达性**:合成事件/真实点击都可能打不开官方 React 弹层(实测)——先做「用户直点该官方入口」判别,功能落点优先官方**数据层 API**(如 `session.rename`),UI 形态自收敛(菜单内嵌表单)。 - **i18n 语言源**:`document.documentElement.lang` + MutationObserver + `useSyncExternalStore`(React)。 - **TDZ**:词典纯数据(§3)。 --- ## 6. 常用脚本与文件速查(本仓库) | 用途 | 位置 | |---|---| | 会话 zstd 全目录解码 | `deepseek-harness/.tmp/dsh-session-decode.cjs` | | 会话格式扫描(帧 1 一行) | `deepseek-harness/.tmp/session-format-scan.cjs` | | 会话修复重建(备份→改→两帧重建→验证) | `deepseek-harness/.tmp/session-format-fix.cjs` | | 会话 scanner 复刻诊断 | `deepseek-harness/.tmp/session-scanner-diag.mjs` | | 安装库验证(listArtifacts / loadStored) | `deepseek-harness/.tmp/session-lib-verify*.mjs` | | 官方 zstd 帧扫描参考 | `deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts` | | 官方会话修复器 | `deepseek-harness/packages/core/session/src/repair.ts`(`interruptedTurnClosers`) | | 官方持久化 JSONL | 安装目录 `.../node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/index.js` | | 崩溃点 contentHasImage | `deepseek-harness/packages/llm/llm/src/content.ts` |