FuriousKinght/dsh-fk-rollback

dsh-fk-rollback

DSH 配置快照与回滚:危险操作前自动留痕,附带独立还原脚本,DSH 起不来时也能救回。仅 Windows;还原脚本为 .bat,需 Node.js。

미분류backupconfig-managementdeepseek-harness

dsh-fk-rollback — FK 回滚设置

DSH 配置的快照与回滚。自动留痕、出问题能查、坏了能退回。

保证 DSH 本体能启动、能输入输出、功能正常。 不管会话聊天内容和用户自己装的插件(那些不在快照范围内)。

它解决什么

DSH 的三次真实事故(2026-10-13 循环依赖、2026-10-17 两次崩溃)都是配置文件被改坏:

事故 · 形态 · 本插件能救吗 10-13 · cordis.patch.yml 抄错,形成循环依赖 · ✅ 能 10-17 · 插件缺 exports["./client"] · ✅ 能 10-17 · patch 被截断成 828 B · ✅ 能(已实测)

实测数据(2026-10-19):

破坏后:bundles 12 → 3,patch 60712 B → 828 B
        DSH 启动失败:YAMLException,输出 2,247 B

回滚后:5/5 文件 SHA256 全部恢复
        DSH 启动正常,输出 83,254 B,零错误

快照了什么

只有 5 个配置文件(共约 119 KB):

pnpm-workspace.yaml
compatibility.json
pnpm-lock.yaml
package.json
cordis.patch.yml        ← 热重载的,回滚时最后写

明确不碰:node_modules/、插件源码、会话记录、cordis.yml(每次启动被重写)、$DSH_HOME/cordis.patch.yml(home 层,只检测不修)。

三类快照

类型 · 何时建 · 保留 · 计入 50 配额 每日 daily/ · 每天第一次启动 · 3 份 · ❌ 独立 滚动 rolling/ · 每轮对话结束 + 距上次 ≥2h + 内容变了 · 3 份 · ❌ 独立 手动/回滚前/装插件前后 checkpoints/ · 用户点按钮 / 回滚前 / 装插件前后自动 · 50 份 · ✅

保留 3 份每日的理由:每天的 daily 是在「启动成功」后才建的。 如果今天启动就崩了,今天的快照根本没建成 —— 只能靠昨天、前天的兜底。

🎯 装插件会自动留痕(关键能力)

这是最实用的功能:不管用户装什么插件,装前装后各存一份。

怎么做到的

DSH 的 dsh-plugin-manager 有两个事件,时序如下(源码已核实):

用户点「安装插件」
  ↓
this.change(async (result) => {
    const files = await this.readRestoredFiles()   ← DSH 自己也在备份
    announce("installing", {...})                  ← ★ 磁盘还干净!我在这里存「装前」
    run = await this.runPnpm(["add", spec])        ← 真正改磁盘
    announce("applying")
})
emit("plugin-manager/changed")                     ← 全部完成,我在这里存「装后」+ 体检

事件 · 时机 · 我做什么 `plugin-manager/install-state`(phase=installing) · pnpm add 之前 · 存 「装插件前 HH:mm」 `plugin-manager/changed` · 安装完成后 · 存 「安装插件后 HH:mm」 + 立刻体检

效果

📅 每日快照 2026-10-19
📌 装插件前 2026-10-19 19:55      ← 装之前,磁盘干净的状态
📌 安装插件后 2026-10-19 19:55    ← 装之后,可能已经坏了

装完发现 DSH 起不来 → 一键回滚到「装插件前」→ 精确恢复。

两个细节

  • 同一个安装请求只存一次:多 registry 重试时会重复触发 installing,用 requestId 去重
  • 连装多个插件各存一份:第二份会自动编号(装插件前 …19:55②)

两种用法

① 面板(DSH 正常时)

设置 → 「FK回滚设置」

  • 快照列表(星级 = 健康等级)
  • 创建快照 / 一键回滚
  • 体检问题(三层分级:真问题 / 提示 / 正常状态)
  • 改动溯源(谁在什么时候改过哪个配置文件)
  • 设置(保留策略 / 体检历史 / 报错日志 + 诊断报告)

② 脚本(DSH 崩了时)

双击 还原DSH配置.bat —— 不依赖 DSH,只依赖 node.exe。

脚本内置 5 级 node 探测链(本机 PATH 里没有 node):

1. $DSH_HOME/dsh-runtimes/*/dependencies/node/bin/node.exe
2. 盘符根目录扫描(找 resources/runtime/primary-runtime/...)
3. 注册表反查安装路径
4. PATH 里的 node
5. 询问用户输入

找不到就退出,绝不修改任何文件。

⚠️ 怎么让改动生效(DSH 桌面端没有 F5)

DSH 是 Electron 桌面应用,`F5` / `Ctrl+R` 不会重新加载页面。 (那是浏览器行为;Electron 在这里没注册 reload 快捷键。)

你想做的事 · 正确做法 让回滚后的配置生效 · 点面板里的「立即重启 DSH」按钮,或关掉窗口重开 让插件面板的代码改动生效 · 同样是重启 DSH 只是想刷新面板数据 · 点面板里的「刷新」按钮(不需要重启)

本插件的面板里,所有需要「重启才生效」的操作都会明确告诉你,并提供「立即重启」按钮。

v1.3.0 起:重启能力已内置,不需要额外安装任何重启插件。
实现见 lib/restart.mjs(WMI 创建助手 → taskkill /T 杀进程树 → 自动重开)。
自动重启只在 Windows 桌面版 DSH 下可用(需要 Electron 的 IPC 通道)。
命令行启动的 dsh web、或其他平台,按钮会明确提示「请手动重启」并说明原因。

命令行

node dsh-rollback.mjs <命令> [选项]

⚠️ 如果你的 PATH 里没有 node,可以用 DSH 自带的运行时(路径随安装位置不同):

<DSH安装目录>\resources\runtime\primary-runtime\dependencies\node\bin\node.exe

不确定的话,直接双击同目录下的 还原DSH配置.bat —— 它会自动探测 node 和 DSH 目录。

命令 · 作用 list · 列出快照(按类型分组) show · 快照详情 status · 当前状态摘要 check · 静态体检(--json / --home-layer) diagnose · 诊断(含 home 层排查顺序) snapshot · 手动创建(--name / --note / --daily) restore · 还原到指定快照 restore --last-good · 自动选最佳 restore --dry-run · 只显示会改什么 trace [文件] · 改动溯源(--days N) sessions · 列出最近会话 prune · 清理过期快照 oplog · 查看操作日志

退出码:0 成功 · 4 无可用快照 · 5 权限/占用 · 7 需人工 · 8 部分成功

健康分级

快照的可靠性分 5 级。按「配置指纹」记录启动历史(不是按快照对象,这样内容相同的快照共享验证):

等级 · 判据 verified-strong · 同指纹启动成功 ≥3 次且 0 失败 verified · 同指纹启动成功 ≥1 次 static-ok · 静态体检通过,但从未被启动验证 failed · 曾启动失败 broken · 静态体检有 fatal

--last-good 按这个顺序降级选点:verified-strong → verified → static-ok → failed。

一个必须知道的死角

$DSH_HOME\cordis.patch.yml(home 层,实测 132 B,由第三方插件 dsh-skin 自动生成) 叠加在 profile 层之后,优先级更高。它坏了 DSH 起不来,而:

  • ❌ 本插件救不了(已排除在快照外)
  • ❌ DSH 弹窗也救不了(只备份 profile patch)

本插件能检测(home-layer-unparsable fatal),但不修。

所以:profile 层体检全绿但 DSH 起不来时,第一件事查这个文件。

node dsh-rollback.mjs check --home-layer

安全设计

红线 · 原因 绝不自动回滚 · 会掩盖真问题 不改 DSH 本体 · 更新即失效 不碰 node_modules · pnpm 的领地 会话日志绝对只读 · 那是用户的核心数据 回滚前必须先快照 · 防点错 两阶段写入 + 固定顺序 · patch 最后写,避免中间态触发热重载 apply() 全包 try/catch · 本插件出问题不能拖垮 DSH session/event 带 { global: true } · 否则收不到别的会话的事件 异常绝不冒泡 · 不能因为体检打断用户对话

验收标准:故意破坏 lib/client.js、entry.js,或直接删掉本插件 → DSH 仍能正常启动。

目录结构

dsh-data/plugin-src/dsh-fk-rollback/
├── package.json          插件清单(exports 含 ./client)
├── cordis.patch.yml      bundle patch(name 必须等于包名)
├── entry.js              Host 半边(钩子 + HTTP 端点)
├── lib/
│   ├── core.mjs          快照/还原/指纹/保留
│   ├── check.mjs         体检(自实现 YAML 子集)
│   ├── trace.mjs         会话溯源(只读)
│   └── client.js         Client 半边(设置面板)
├── dsh-rollback.mjs      命令行脚本(零第三方依赖)
├── 还原DSH配置.bat       一键还原(5 级 node 探测)
└── README.md

快照存放:$DSH_HOME/rollback-snapshots// (在 profile 外面 —— profile 内会跟着一起坏)

技术要点

  • 零第三方依赖:只用 node:fs/path/crypto/zlib
  • YAML 自实现子集:不引入 js-yaml(cordis.patch.yml 里有 !!js 自定义标签,完整解析器反而要额外注册 tag)
  • 多帧 zstd 必须逐帧解:直接解压 13 MB 文件只能拿到第一帧 244 B;逐帧解压得 13,510 条事件
  • 导入路径解析:@deepseek-ai/* 这类 install-scope 包在 profiles/node_modules(共享层),不在 profile 层;那层里可能有指向 DSH 安装目录的悬空符号链接 —— DSH 自身能从 asar 解析,所以不是错误
  • 幽灵检测按 `dsh` 字段过滤:hoisted 树下 node_modules 有 150 个顶层包,而带 dsh 字段的只有 14 个

.bat 固化副本的更新策略(2026-10-23 修订)

用户指出的原设计缺陷:
「用户肯定希望的是,将23号启动快照固化为.bat,而不是继续保留22号旧的固化脚本」

旧行为(错的)

22号:用户手动「设为 .bat」固化 B 快照
      → meta.fixedBy = 'user'

23号:开机,自动建当天 daily
      启动钩子调 autoFixDaily()
      → 看到 fixedBy === 'user'
      → 直接跳过,永不覆盖

结果:.bat 一直还原 22 号那份陈旧配置
      用户早就忘了自己固化过,也不知道为什么 .bat 是旧的

问题本质:把「用户手动操作过一次」当成了「永久意图」,但这个意图会随时间失效。

新行为(对的)

默认:每次启动都跟着当天 daily 走
      .bat 永远还原「今天启动时的那份配置」  ← 这才是用户真实期望

可选:显式钉住(pin=true)
      用户明确说「我就要这一份,别动」时才保留

字段约定

字段 · 值 · 含义 fixedBy · 'user' / 'auto-daily' · 谁写的这份副本(仅记录) pin · true / false · 是否钉住(决定启动时会不会被替换) replacedFrom · 对象 / null · 上次被替换掉的副本,供追溯

⚠️ 关键:判断是否跳过,只看 `pin`,不再看 `fixedBy`。

命令