9 月 12 日,本机的 dsh(DeepSeek Harness,运行 web GUI 的 agent 宿主,监听 127.0.0.1:3080)发生连环故障:六轮报错、一次降级回退、一次误删插件、一次从会话历史恢复源码。以下按事件顺序完整记录发现问题和修复问题的全过程。
总览:
| 阶段 | 事件 |
|---|---|
| 起因 | dsh 为挂载 dcp 插件,自行将核心从 0.1.1-rc.2 升级到 0.1.5-rc.2,未重启进程,GUI 无法打开 |
| 修复一 | 重启进程、修复启动脚本;被新报错推翻 |
| 修复二 | 升级 2 个插件、禁用 3 个插件;写简报交 dsh 自修 |
| dsh 自修 | 改动正确但未重启;人工重启验证通过,再次被新报错推翻 |
| 修复三 | 重写三个插件的模块引用;被推翻 |
| 修复四 | 禁用 file-changes;boot 层正常,对话功能仍不可用 |
| 定位原因 | dcp 与新版事件流 API 不兼容;确定回退 |
| 回退与重建 | 降级、快照恢复、依赖重装;误删的两个插件从会话历史复原 |
背景
dsh 的核心版本原为 0.1.1-rc.2,运行稳定。当天,dsh 在一次对话中为挂载 dsh-dcp(社区开发的确定性上下文压缩插件,要求核心版本不低于 0.1.5),自行执行核心升级:全局的 @deepseek-ai/dsh 替换为 @next 频道的 0.1.5-rc.2,profile 的 package.json 写入 dcp 依赖,pnpm 更新 node_modules。升级完成后,运行中的 web 进程未重启,GUI 无法打开。
升级前,机器上有一份 ~/.dsh 全量 tar 快照(82MB)。这份快照是全程唯一起作用的预防措施。
第一轮:进程是旧的,磁盘是新的
现场检查:
1 | # 版本、进程、HTTP |
关键观察:
- 磁盘上的核心已是 0.1.5-rc.2,运行中的进程为 9 月 11 日启动,内存中仍是 0.1.1 的代码。
curl返回 200,HTTP 层正常,浏览器白屏。- 浏览器端报错为
boot manifest batches must be an array。
根因:旧进程按旧逻辑读取已被 pnpm 更新为新版的 bundle 元数据,生成的 boot manifest 格式错误,前端解析失败,插件全部无法加载。curl 只能探测 HTTP 层,无法反映前端 JS 的状态,200 是假象。
会话数据以 JSONL 持久化于 ~/.dsh/sessions,终止进程不丢失数据。执行修复:
1 | pkill -f "dsh web"; sleep 1 |
重启后逐项验证:HTTP 200、manifest 中 batches 为数组、client.js 与 index.js 静态资源均返回 200。第一层故障消除。
另发现一个问题:0.1.5 引入 auth token 机制,首次访问需通过 /?token=xxx 登录并写入 30 天 cookie,而 ~/.dsh/dsh-launch.sh 中的 open 127.0.0.1:3080 将直接返回 401。按本机守则先备份为 .bak-20260912-*,再做修改:
1 | # 修改前 |
命令行层面的验证方式(后续各轮反复使用):
1 | # 从日志取 token → 带 cookie 落地首页 HTML |
第二轮:/client 子路径失效
浏览器端报错:
1 | Failed to load plugins |
本次故障与进程无关:0.1.5-rc.2 重构了 client 模块系统,旧插件编译产物中 require("@deepseek-ai/dsh-client-runtime/client") 这种带 /client 子路径(无 .js)的写法全部失效。
排查做了四件事:
- 列出
node_modules里所有@deepseek-ai包,确认dsh-client-runtime包本体是否存在及其结构; - 扫描全部插件目录(
dsh-*、@aiwayds/*、@baconbao/*、@liustack/*)的 require 语句,按三种形式分类:带/client"的(会崩)、裸名、/client.js(两种都兼容); npm pack拉取dsh-ui-appearance@0.1.9、dsh-better-sidebar@0.19.1、dsh-at-file@0.6.3、dsh-writing-pad@1.1.3四个最新包解开对照,确认上游适配情况;- 解析首页 HTML 里的 boot manifest,确认新版认可的模块名的实际形式。
其中第二步的扫描脚本:
1 | cd ~/.dsh/profiles/web/node_modules |
结论是五个插件受影响:
| 插件 | 当前 → 最新 | 上游状态 |
|---|---|---|
| dsh-ui-appearance | 0.1.6 → 0.1.9 | 新版本已适配 |
| dsh-better-sidebar | 0.15.0 → 0.19.1 | 已适配 |
| dsh-at-file | 0.6.3(已是最新) | 未适配 |
| dsh-writing-pad | 1.1.2 → 1.1.3 | 未适配(后来证明该判断有误) |
| dsh-file-changes | github 源 | 未适配 |
两个可选方案:A,降级回 0.1.1-rc.2,一条命令恢复全部功能,放弃 dcp(推荐);B,继续使用 @next:升级两个已适配插件,禁用三个未适配插件保住启动,后续补齐。采用方案 B,剩余插件的源码修改交由 dsh 执行,改动内容记录备查。
执行:
1 | pnpm add dsh-ui-appearance@^0.1.9 dsh-better-sidebar@^0.19.1 |
随后在 cordis.patch.yml 中禁用其余三个插件。此步骤出现一个问题:disabled 条目按包名书写(dsh-writing-pad、dsh-file-changes),重启后 manifest 中三个插件仍然存在,禁用未生效。原因为 bundle id 与包名不一致:dsh-writing-pad 的 id 是 writing-pad,dsh-file-changes 的 id 是 file-changes,均无 dsh- 前缀。经 dsh --profile web --dump-config 核对真实 id 后改写,重启,三个插件在 manifest 中清零。
重启后的标准验证流程:
1 | # 等待就绪并取 token |
验证结果:禁用的三个插件出现 0 次,升级的两个插件与 dsh-client-store seed 均存在,旧 /client 引用残留 0。
本轮验证仅到达 manifest 层,未做浏览器实测,该缺陷在后续轮次反复出现。
简报与 dsh 自修
为让 dsh 自行修复剩余三个插件,整理简报一份,内容包含四点:
- 改法:把
require("@deepseek-ai/dsh-client-runtime/client")的/client后缀去掉; - bundle id 坑:重新启用时 disabled 必须写 bundle id 而不是包名,写错不报错但不生效;
- 直接改 node_modules 会被 pnpm 重装覆盖,要用
pnpm patch固化;dsh-file-changes是github:mixin-ai/dsh-file-changes的 git 依赖,patch 或 fork 二选一; - 完成后的重启与验证命令。
dsh 按简报完成修复,故障再次出现。
第三轮:dsh 的修复正确,但未重启
检查现场,还原 dsh 的四项改动:
pnpm patch了dsh-at-file与dsh-file-changes,将/client改为裸名,patch 文件内容正确;dsh-writing-pad升级到 1.1.3,该版本 dist 的实际代码为合规写法(此前”未适配”的判断有误,原因是 sourcemap 中残留的旧引用);- 删除三个 disabled 条目;
- 建立
.dsh-module-fallback/目录存放 mermaid 依赖,用于修复 mermaid 插件的依赖解析,与本次故障无关。
改动方向正确,问题在于未重启,浏览器中呈现的仍是旧进程状态。
重启后验证。manifest 层之外,这次加了一层全量 JS 扫描(排除 .map,sourcemap 中的旧引用不影响运行但会干扰判断):
1 | cd ~/.dsh/profiles/web/node_modules |
manifest 中三个插件各 5 处(已加载)。本轮验证仍限于 boot manifest 与静态扫描层。
第四轮:runtime 模块被整体移除
浏览器端报错变为:
1 | failed to import loader entry 62d6fc81 (dsh-file-changes): |
裸名同样 miss。本轮排查最深入。
第一步解决一个矛盾:首页 HTML 的 manifest 中存在 6 处 @deepseek-ai/dsh-client-runtime 字符串,但无法 require。逐条检查上下文,这 6 处均位于各插件的 inject 声明中,属于服务端 Cordis 的服务名,不是浏览器端可 require 的模块。可 require 的 seed word 为 @deepseek-ai/dsh-client-store、dsh-client-ui-primitives、dsh-client-ui-slots 等。结论:dsh-client-runtime 客户端模块在 0.1.5-rc.2 中被整体移除,不是改名。dsh 的”去掉 /client 后缀”替换在方向上无效。
第二步逐插件检查实际使用的 runtime API,直接读源码:
1 | # 每个插件到底用了 runtime 的哪些导出 |
结论与处理:
dsh-at-file/lib/client.js:只用了createSnapshotStore。查新版核心,该 API 移入dsh-client-store。改 require 目标即可,整处改动只有一行:
1 | - var import_client = require("@deepseek-ai/dsh-client-runtime"); |
dsh-file-changes/lib/client.js:用了isAppendSurfaceEvent和resolveWorkspacePath,新版无客户端侧替代。解法是把这两个函数的实现从新版核心源码中取出,内联进插件,用一个 IIFE 替换原来的 require:
1 | // 修改前 |
dsh-writing-pad:1.1.3 本来就只 requireui-primitives,无需改动。
内联函数的实现分别位于核心的 dsh-session/lib/index.js(isAppendSurfaceEvent,含 SURFACE_EVENT_TYPES 判断)与 dsh-util-workspace-path/lib/index.js(resolveWorkspacePath,含 Windows 风格路径分支),逐行确认语义后移入插件。
修复方式:先直接修改 node_modules 验证方向,重启确认可加载后,用 pnpm patch 固化(否则下次 pnpm install 会被覆盖):
1 | pnpm patch dsh-at-file@0.6.3 |
期间清理了一次 .pnpm_patches 的残留状态(含上次 dsh 操作留下的 state.json)。patch 目录内的修改以 sed 完成后提交:
1 | TMP="node_modules/.pnpm_patches/dsh-at-file@0.6.3" |
dsh-file-changes 为 github tarball 源,流程相同,只是临时目录名为一整串 codeload URL,在其中完成两个函数的内联后 patch-commit。最终 patches/ 下生成 dsh-at-file@0.6.3.patch 与 dsh-file-changes@0.1.0.patch,pnpm-lock.yaml 与 pnpm-workspace.yaml 写入 patchedDependencies 记录。
重启,最终验证:三个插件各 5 处、旧 /client 残留 0、裸名 runtime 残留 0、日志无报错。
第五轮:conversationEvents 服务被移除
报错方向改变:
1 | Failed to load plugins |
require 层全部修复,故障转移到服务层。dsh-file-changes 的 exports.inject 声明了 conversationEvents 这个 Cordis 短服务,插件通过 ctx.conversationEvents.register(fileChangesDefinition) 注册文件变更事件定义。定义本体位于源码 113 行:
1 | const fileChangesDefinition = { |
0.1.5-rc.2 将该事件系统整体重构,conversationEvents 服务不存在,插件永久处于 pending。
对照确认波及范围:dsh-at-file 的 inject 为 ["inputTriggers", "sessions", "connection", "remote", "slots", "locale"],dsh-writing-pad 为 ["slots", "layout", "remote", "locale"],这些服务在新版中均存在,因此仅 file-changes 无法激活。
处理:再次禁用 file-changes。该插件提供”文件变更展示”功能,非核心,彻底修复需等待上游适配新版事件系统。备份 patch.yml、修改、YAML 校验、重启。
验证:file-changes 0 处、at-file 5 处、writing-pad 5 处、日志无报错。
boot 层至此正常:dsh 可打开,可查看历史会话,但对话功能仍不可用。
第六轮:events is not iterable,对话不可用
故障现象:每发送一条消息即弹出”本轮运行失败 events is not iterable UNKNOWN”,连续复现,无法对话。
本次报错位于 turn 运行时,由前端弹出。服务端日志无对应错误,排查转入会话存储。dsh 的 session 日志为 zstd 压缩的 JSONL 事件流,解压检索:
1 | S=~/.dsh/sessions/--Users-gwen-kaoyan--/session-3857b294-d67a-469a-a8cf-c9f0b4727a1f |
命中 turn 20 到 23 连续复现的 turn/end 失败记录:
1 | { "type": "turn/end", "error": { "message": "events is not iterable", "code": "UNKNOWN" } } |
继续排查该 session 中唯一影响 turn 生命周期的组件 @aiwayds/dsh-dcp:
1 | DCP=~/.dsh/profiles/web/node_modules/@aiwayds/dsh-dcp |
确认它 override 了核心 BasicCompactionEngine.summarize(input) 并订阅 session/event。再看 dcp 的 README,标注为 verified against dsh 0.1.2-alpha.3。
核心版本为 0.1.5-rc.2,与 dcp 验证版本间隔三个版本。新版事件流 API 变更,dcp 的 summarize 接收的 input.events 不再可迭代,每轮 turn 开始即失败。
处理:禁用 dsh-dcp,compaction 回退核心默认的 compaction-basic(LLM 摘要)。boot 无报错。
此时情况已经明确:
- 升级核心的唯一目的是挂载 dcp;
- dcp 未适配该核心版本;
- 升级导致 client 模块、conversationEvents 服务与一批社区插件连锁故障。
升级未产生任何可用收益,且破坏了原有环境。唯一合理的选项为降级回 0.1.1-rc.2。确定执行回退。
回退
回退依据升级前的 tar 快照。步骤:
1 | # 1. 降级核心 |
第三步经过四轮尝试:
- 第一遍
pnpm install失败。patch 记录不只写在 lockfile,还写在pnpm-workspace.yaml的patchedDependencies字段里,只恢复 lockfile 的话 install 会因为找不到patches/下的文件报错。从 tar 把pnpm-workspace.yaml也恢复了; - 手动删掉
node_modules/dsh-at-file、node_modules/dsh-file-changes和.pnpm_patches残留后重装,这两个包未恢复; - 试
pnpm install --force,又试pnpm add手动补,patches/目录再次出现; - 最后从 tar 重新解包全部配置文件,
rm -rf node_modules patches,完整地重新执行pnpm install,得到干净状态。
回退完成后以 0.1.1-rc.2 重启:HTTP 200,旧版无 token 机制,核心与全部插件回到升级前版本(ui-appearance 0.1.6 / better-sidebar 0.15.0 / writing-pad 1.1.2 / at-file 0.6.3),dcp 移除,lockfile 与 workspace yaml 中无 patch 记录。
随后发现新问题:bundles 列表中有 dsh-theme-atelier 与 dsh-ui-theme-switch,但 node_modules 中不存在这两个目录,rm -rf node_modules 时被连带删除。两者不在 npm 上(npm view 返回 404),为 9 月 2 日手动安装,tar 快照不含 node_modules,本地无备份。
先将两者从 bundles 中临时移除以保证启动,dsh 恢复正常对话。恢复思路:theme-switch 的结构设计有记录(exports.inject=["theme","slots"],循环切换主题),完整源码需从历史中查找。
从会话历史重建插件
重建从结构学习开始:dsh-icon-theme 与 dsh-theme-whalegirl 为两个正常运行的主题类插件,逐一检查其 package.json、lib/client.js、lib/index.js、cordis.patch.yml,确认最小可用插件所需的文件、exports.inject 的声明方式、设置卡片向 settings.plugin.item 槽位的挂载方式。
其次查找历史。dsh 的会话存储位于 ~/.dsh/sessions/,其中 --Users-gwen-.dsh-claudecode-- 目录下为此前导入的 Claude Code 会话,每个 session 为一份 zstd 压缩的 JSONL 事件流。逐个解压检索插件名:
1 | for f in ~/.dsh/sessions/--Users-gwen-.dsh-claudecode--/*/session*.zstd; do |
命中多个 session,其中 import-4bf65167(9 月 2 日的会话)出现次数最多,即创建这两个插件的原始会话。
接下来从 JSONL 中提取源码。agent 写文件通过 write 工具调用完成,事件中含 file_path 与 content 字段;同一文件可能被多次写入,需取最后一次写入的 content 作为最终版本。提取脚本经过多轮迭代达到稳定(先是字段名不匹配,继而是命中格式问题,再是同名文件分布在多个 session 中需要去重排序),最终版本的核心逻辑:
1 | import sys, json, subprocess |
由该脚本得到完整文件清单。
dsh-ui-theme-switch,恢复 4 个文件:
package.jsonlib/index.jscordis.patch.ymllib/client.js(最终版,含中文标签「浅色/深色/跟随系统」、settings 卡片、3 个 chip 按钮)
验证:node --check 通过,package.json 为合法 JSON,加回 bundles(dsh-icon-theme 之后),重启后 manifest 中出现 2 处,/plugins/dsh-ui-theme-switch/client.js 返回 200。
同一 session 中存在 dsh-theme-atelier 的全部源码。继续恢复 5 个文件:
package.jsoncordis.patch.ymllib/index.jslib/client.js(13813 字符,含 ramp 配色引擎)README.md
bundles 顺序恢复原样:dsh-icon-theme → dsh-theme-atelier → dsh-ui-theme-switch → dsh-theme-whalegirl。同一套验证全部通过。
至此全部完成。最终状态:核心 0.1.1-rc.2,插件全部回到升级前版本,dcp 移除,patch 记录清零,两个手动插件从会话历史完整复原,对话正常。
复盘
事故根源:将尚未被插件生态适配的候选版核心当作稳定版使用。@next 频道的破坏性变更分层暴露:进程新旧不一致、client 模块移除、Cordis 服务移除、事件流 API 变更,修复一层暴露下一层。全程修复五次宣告完成,四次被新报错推翻。
几条具体教训:
- verified against 不等于最低版本要求。 dcp 的 package.json 声称要求核心不低于 0.1.5,README 的 verified against 写的是 0.1.2-alpha.3。挂载任何插件先看它实际验证过哪个版本,而不是它声称支持哪个版本。版本号跨了 alpha/rc 线的,先观望。
- 一次重启窗口只上一个改动。 核心升级和 dcp 挂载挤在同一个窗口,出事后爆炸半径翻倍,归因困难。先升核心、验证稳定、再单独挂插件,每一步都可回退可归因。
- manifest 层验证不等于运行时验证。 前几轮的修复结论都只验证到 boot manifest 和静态资源 200,真正的判据是浏览器里发一句话能正常走完一个 turn。验证清单应包含”打开页面发一条消息”这一项。
- grep 判断插件兼容性必须排除 sourcemap。
dsh-writing-pad被误判未适配,是.map文件里残留的旧源码引用造成的。要扫就扫实际的.js。 - bundle id 不等于包名。
dsh-writing-pad的 id 是writing-pad。disabled 写错名字不报错、不生效,静默失败。 - pnpm 的 patch 记录写在两个地方。 lockfile 和
pnpm-workspace.yaml的patchedDependencies,回退时要一起处理。 - 混装目录清理前先做依赖审计。 profile 的 node_modules 里混着 pnpm 管理的依赖和手动塞进去的插件,
rm -rf node_modules前必须对比 package.json 的 dependencies,把非 pnpm 管理的插件单独备份。 - 会话历史是最后防线。 两个 npm 上 404、快照里没有、本地无备份的插件,最终从 zstd 压缩的对话日志里按 write 工具调用找回。agent 写过的每个文件,日志里都留着全文。
最后记一笔:全程对 ~/.dsh 的每次改动均先备份为 .bak-时间戳 并追加至 ~/.dsh/CHANGELOG.md。该守则无法阻止事故发生,但保证了每一步可回退,这也是最终能够整体回退的原因。
附:环境变更守则
将上述教训整理为一份规则,放入本机 agent 的指令文件长期生效。同样的规则也适用于其他会自主修改环境的 agent(CLAUDE.md、AGENTS.md 或系统提示词均可承载):
1 | # 环境变更守则(对 agent 生效,违反任何一条即停下重来) |
守则无法阻止错误发生,其作用是阻止错误叠加:单次失误可回退、可归因;失误连续叠加,才造成不可收拾的损失。这也是将其长期保留在 agent 指令文件中的原因。