Chuyển đến nội dung chính

dsh-guard-desktop

Đã xác minh

dsh-guard-desktop · v0.1.2 · MIT · Giao diện web

DSH Guard —— DeepSeek Harness 官方桌面端的 profile 快照 / 崩溃自动回滚 / 一键主动回滚(Windows)

Cài đặt

dsh plugin add dsh-guard-desktop

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.

Thẻ

Readme

DSH Guard 图标

DSH Guard · 守护

给 DeepSeek Harness 用的 profile 快照 / 崩溃自动回滚 / 一键主动回滚 插件(Windows)。

一句话:改插件、改配置之前先打一个快照;改崩了,一个按钮回滚回去。 如果崩到连界面都打不开,还有一个活在 DSH 之外的看门狗替你回滚(默认不替你启动 DSH,见下)。


先说清楚一件事:插件救不了你,看门狗才行

这是本项目的核心设计,也是它为什么长成这个样子:

部件 活在哪 能干什么 DSH 崩了还活着吗
守护面板 DSH 插件(进程内) 看状态、打快照、点回滚、装/卸看门狗 ❌ 跟着一起死
看门狗 Windows 计划任务(独立进程) 巡检,确认起不来且近期动过插件 → 自动回滚 ✅ 活着

所以「守护」不是一个纯插件:插件只是它的遥控器,真正能在 DSH 打不开的时候救你的是那个外挂看门狗。 (同类做法可参考 DSH-Store 在 macOS 上装的 launchd Guardian。)


它能做什么

  • 打快照:把 profile 的关键文件 + node_modules 一起镜像到一个带指纹的目录
  • 插件变更检测:比对 profile 指纹(依赖列表 + 关键文件哈希),发现变了就自动留一份变更后快照
  • 一键主动回滚:面板上每个快照都有「回滚到此快照」,两步确认,6 秒不点自动取消
  • 自动回滚(看门狗):只有"确认起不来"才回滚 —— 端口持续不在(超过宽限期 DSHGUARD_RESTART_GRACE_SEC) 且日志里有致命错误证据(插件加载失败之类)且最近动过插件。三条同时成立才动手。

    ⚠️ 2026-09-14 事故后收紧的:原来"端口不在 + 启动器进程没了"就算崩溃,而这恰恰是重启的正常过程 (先杀旧服务、再起新的),于是正常重启被判成崩溃 → 自动回滚 → 回滚又去拉同一个端口 → 撞端口 (EADDRINUSE)→ 服务再也起不来。现在端口短暂不在从来不等于崩溃。 回归夹具:tools\test-restart-collision.ps1(复现该场景,4 个用例)。

  • 回滚也留档:回滚前先把当前(坏的)状态存一份 pre-rollback-broken —— 回滚本身也有后悔药
  • 回滚后校验:还原 → 移除新增插件目录 → pnpm install → 校验 profile 指纹是否真的回到快照值,不通过就算失败

环境要求

  • Windows(看门狗基于计划任务;package.json 里已声明 os: ["win32"])
  • DSH(dsh 命令可用),Node ≥ 20
  • PowerShell 5.1(系统自带即可;DSH_GUARD_POWERSHELL=pwsh.exe 可切到 7)

安装

1. 装插件

双击 Install-Plugin.cmd(或在终端里跑 Install-Plugin.ps1)。

它会:

  1. 先给你打一个 profile 快照(装坏了能靠它回滚;不打快照就中止,除非加 -NoSnapshot)
  2. 把插件拷到 <你的 profile>\node_modules\dsh-guard
  3. 把 dsh-guard 登记进 profile 的 dsh.profile.bundles
  4. 落地自检:JS 语法、.ps1 的 BOM、profile manifest 是否合法且没有 BOM;任何一项不过就自动撤销这次安装

然后重启 DSH 才会加载。

1b. 用 npm 上的版本(推荐,升级最省事)

dsh plugin --profile desktop add dsh-guard-desktop
  • 新增 bundle 会热加载(约 18 秒,不用重启官方端);只有"改了插件内部代码"才需要重启。
  • ⚠️ 包内 cordis.patch.yml 的 insert.name 必须等于包名(dsh-guard-desktop)—— DSH 只把 profile 的 bundle 包按包名链接进模块回退目录;写成别的名字解析不到, 现象是"包装上了、/dsh-guard/* 却全 404、面板不挂载"(账本 E80)。 profile 的 cordis.patch.yml 里那条覆盖用 id: dsh-guard 定位,别写死 name:, 否则换包名时那条覆盖会被静默跳过(守护会退回默认 profile/判活方式)。

2. 打开面板

设置 → 插件 → 守护。

3. 装看门狗(这一步才是真正的保险)

看门狗是跑在 DSH 之外的那个进程 —— DSH 崩了、白屏了、起不来,它照样活着,负责自动回滚。

两种自启方式,任选:

方式 入口 需要管理员? 说明
计划任务(默认) 双击 Install-Watchdog.cmd,或在独立窗口里点「安装看门狗」 要(弹一次 UAC) 注册名为 DSH Guard 看门狗 的计划任务:登录时启动、隐藏窗口、无时长上限、失败重试 3 次
启动文件夹 双击 Install-Watchdog-NoAdmin.cmd 不要 在启动文件夹放一个快捷方式,登录时由资源管理器拉起。不想给管理员权限时用这个,效果一样

界面上点「安装看门狗」时:计划任务被拒或失败的话,它会当场问你要不要改用启动文件夹方式, 并把安装过程的完整输出弹给你看(同时写进 <守护数据目录>\logs\guard-ui.log)。

端口不会被搞错:安装时若没显式指定端口,它会自动探测"正在监听的 DSH 实例"并把那个端口烘进自启项 (而不是想当然地用 3080)。面板上也会显示它盯的是哪个端口、以及判定依据。

⚠️ 从哪一端装,就盯哪一端。 看门狗会把"你点安装时所在的那个端口"烘进计划任务参数里。 你在哪个端口上点的安装,它就盯哪个端口。想换成另一个端口,在那个端口的面板里再装一次。


出事了怎么办

界面还能打开

面板上点「回滚到此快照」。建议选在出问题之前的那个(比如 pre-plugin-install)。

界面打不开了(白屏 / 起不来)

推荐:双击独立窗口 —— 它不依赖 DSH,DSH 打不开时照样能回滚:

Open-Guard-UI.cmd

独立窗口

(外观照 DSH Web 做的:浅灰页面底 + 白色圆角卡片 + 品牌蓝。图是 Guard-UI.ps1 -ProofSheet 渲染出来的设计基准, 改配色后先渲染一张自己看,别拿用户的眼睛当测试机。)

想让它带图标出现在桌面上(.cmd/.vbs 自己不能带图标,只有快捷方式能):

Create-Desktop-Shortcut.cmd

它建一个「DSH Guard 守护」快捷方式:目标 wscript.exe + ui\Open-Guard-UI.vbs(启动不闪控制台), 图标用本项目的 ui\guard.ico。

窗口里能做:看状态(profile / 插件指纹 / 端口在不在监听 / 看门狗 / 快照数)、列快照、 选中一行 → 回滚到选中快照(或直接双击那行)、打快照、打开数据目录、看日志、装/卸看门狗。

它不写死任何端口:判定顺序是 -WebPort > 环境变量 DSH_WEB_URL > 最近启动的那个 DSH 实例占用的端口 > 默认 3080; 判定依据会显示在窗口里,也可以手动改。

也可以在命令行做同样的事(同样不依赖 DSH):

# ① 直接卸载插件(删目录 + 从 bundles 摘掉),重启 DSH 即可
.\Uninstall-Plugin.cmd

# ② 或者用守护自己的命令行回滚到某个快照
powershell -ExecutionPolicy Bypass -File .\lib\guard.ps1 `
  -Action rollback -Profile web -Id 20260914-095206_pre-plugin-install -Out r.json

快照都在 %USERPROFILE%\.dsh-guard\snapshots\,直接看目录名也知道有哪些。 装过看门狗之后,<守护数据目录>\bin\Guard-UI.ps1 也会有一份 —— 即使插件目录被删了,它还在。


快照里存了什么

<profile>\package.json          <- profile 的依赖 + bundles 清单
<profile>\cordis.yml
<profile>\cordis.patch.yml      <- 你自己的补丁层
<profile>\pnpm-lock.yaml
<profile>\pnpm-workspace.yaml
<profile>\node_modules\         <- 整个镜像(含 pnpm 的 .pnpm 结构)

外加一份 meta.json:标签、时间、大小、identity.hash(profile 指纹)、identity.depCount、是否含 node_modules。

指纹是什么:profile 依赖集合 + 关键文件的哈希。它让"回滚成功了吗"变成可验证的事, 而不是"看起来文件回来了"。回滚流程最后一步就是拿它比对。

💾 磁盘占用提醒:因为镜像 node_modules,一个快照约等于你 profile 的 node_modules 大小。 不会无限涨:打完一个快照就会自动清理,只保留最近 DSHGUARD_KEEP_SNAPSHOTS 个(默认 8,被标记为 lastGood 的那个永不删);而且状态没变就不重复打(判据是"标签相同 + 指纹相同"), 所以重启 DSH 不会每次都多出一份。 界面与命令行也都能手动清:独立窗口的「清理旧快照 / 删除选中」,面板上的「清理旧快照」, 以及 guard.ps1 -Action prune [-Keep N] / guard.ps1 -Action delete -Id <快照名>。


配置(环境变量)

看门狗是独立进程,配置靠环境变量(计划任务启动时继承的是系统/用户环境,不是 DSH 的)。

变量 默认 说明
DSHGUARD_HOME %USERPROFILE%\.dsh-guard 守护自己的数据根目录(日志/快照/状态)
DSHGUARD_WATCH_INTERVAL_SEC 5 巡检间隔(秒)
DSHGUARD_QUARANTINE_SEC 90 检测到插件变更后的隔离观察期(秒)
DSHGUARD_MAX_ROLLBACK_ATTEMPTS 3 连续回滚上限(防死循环)
DSHGUARD_AUTO_START_DSH 0 端口持续不在且有致命证据时,是否擅自 dsh web 拉起
DSHGUARD_AUTO_RESTART 0 回滚后是否擅自拉起(同上)
DSHGUARD_RESTART_GRACE_SEC 90 端口刚不在时的宽限期:这段时间只观察,不重启不回滚
DSHGUARD_KEEP_SNAPSHOTS 8 快照保留个数(超出的自动清理;lastGood 永不删)
DSHGUARD_PLUGIN_CHANGE_WINDOW_SEC 900 "最近动过插件"的时间窗(秒):自动回滚只允许落在这个窗口内

为什么默认不替你启动 DSH:看门狗只能用 dsh web --port <端口> 起一个没有窗口的实例; 而桌面外壳随后启动时,它的启动器会走"快速路径"看到端口已被占用,于是只去拉起"已有窗口" —— 可那个实例根本没有窗口,用户看到的现象就是点图标打不开。所以默认只记录 + 提示,由你自己启动。 | DSH_GUARD_POWERSHELL | powershell.exe | 用哪个 PowerShell 跑守护脚本 |

⚠️ 这张表里的名字必须和 lib/guard-bootstrap.ps1 里的推导一致。这里曾经踩过一个很隐蔽的坑: PowerShell 的 -replace 默认大小写不敏感,所以 ([a-z0-9])([A-Z]) 会匹配几乎每两个字符, 生成 DSHGUARD_W_AT_CH_IN_TE_RV_AL_SE_C 这种名字 —— 于是所有配置覆盖静默失效(设了没用,还不报错)。 现在用 -creplace,并且有回归测试: powershell -ExecutionPolicy Bypass -File tools\test-config.ps1。

插件版不做 AI 诊断:桌面版那套依赖作者本机的 ask.py,不适合塞进插件。 脚本里 Invoke-AiDiagnosis 是一个返回"没诊断"的垫片,想接自己的诊断直接替换它。 (所以也没有 DSHGUARD_ENABLE_AI 这个变量 —— 别照桌面版文档去设。)


维护窗口:告诉守护"接下来别动我"

你要重启 DSH / 改配置 / 换插件时,可能会出现"端口短暂不在",再叠加"最近动过插件", 理论上仍有被判定成故障的余地。要绝对放心,就先声明一个维护窗口 —— 窗口内守护只记录、 不重启、不回滚:

# 开一个 30 分钟的窗口(-Minutes 省略则默认 30)
lib\guard.ps1 -Action maint-start -Minutes 30 -Note '我在改配置' -Out r.json
# 提前结束
lib\guard.ps1 -Action maint-stop -Out r.json

窗口写成一个标记文件 <守护数据目录>\state\maintenance.json(内容是到期时间 + 备注), 到期自动失效,不会永久关掉守护。所以任何脚本/工具都能用它,不必依赖某套界面。

和 DSHGUARD_RESTART_GRACE_SEC(默认 90 秒)的区别:宽限期是自动的模糊判断 ("刚还在跑 → 大概是有人在重启"),维护窗口是你显式声明的确定性保证。

目录结构

dsh-guard-plugin/
├─ package.json            dsh.bundle.patch + dsh.client(web, 注入 settings)
├─ cordis.patch.yml        insert: [{ id: dsh-guard, name: dsh-guard-desktop }] —— name 必须是包名(E80)
├─ Install-Plugin.ps1/.cmd 装(含装前快照 + 落地自检 + 失败自动撤销)
├─ Uninstall-Plugin.cmd    卸(= Install-Plugin.ps1 -Revert 的包装)
├─ client/client.js        设置页「守护」面板(React,两步确认回滚)
├─ Open-Guard-UI.cmd       打开独立窗口(不依赖 DSH)
├─ Create-Desktop-Shortcut.cmd  在桌面建带图标的快捷方式
├─ Install-Watchdog.cmd    装看门狗(计划任务,弹一次 UAC)
├─ Install-Watchdog-NoAdmin.cmd  装看门狗(启动文件夹,不需要管理员)
├─ ui/
│  ├─ Guard-UI.ps1         独立窗口本体(WinForms,照 DSH 观感自绘)
│  ├─ Open-Guard-UI.vbs    无控制台闪窗地拉起它(快捷方式指向它)
│  ├─ guard.ico            窗口/快捷方式图标(本项目自绘,非官方标志)
│  └─ theme-proof.png      界面设计基准(-ProofSheet 渲染出来的)
├─ lib/
│  ├─ index.js             宿主:7 条同源 HTTP 路由 + 端口解析
│  ├─ guard.ps1            CLI:status / list / snapshot / rollback / incidents
│  ├─ guard-bootstrap.ps1  配置头(所有目录 + 端口优先级)
│  ├─ guard-core.ps1       快照/回滚/健康/日志原语(25 个函数)
│  ├─ watchdog.ps1         看门狗入口(-Once 可只跑一轮)
│  ├─ watchdog-core.ps1    巡检/回滚/隔离循环(12 个函数)
│  ├─ install-watchdog.ps1 注册计划任务(自请求提权)
│  └─ uninstall-watchdog.ps1
└─ tools/                        开发/自检用,**不装进 profile**
   ├─ test-host-load.mjs         不启动 DSH,验宿主加载 + 路由 + 端口解析
   ├─ test-client-load.mjs       没有浏览器也验面板 bundle 能渲染出回滚按钮
   ├─ test-host-spawn.mjs        验宿主拼给 guard.ps1 的那套参数能拿到合法 JSON
   ├─ test-config.ps1            配置优先级 + DSHGUARD_* 环境变量覆盖
   ├─ test-cfg-keys.ps1          静态核对:核心引用的每个 $script:Cfg 键都有定义(防"判断静默失效")
   ├─ test-restart-collision.ps1 回归夹具:复现"外壳重启被判成崩溃"那次事故,6 个用例
   └─ Fix-Bom.ps1                给 .ps1 补 UTF-8 BOM(5.1 会按 ANSI 读中文,缺 BOM 报假语法错)

自检与开发

# 看装没装、bundles 有没有、JS/PS1 有没有问题(只读)
.\Install-Plugin.ps1 -Verify

# 宿主逻辑(不需要 DSH)——建议先指向影子环境,别碰真 profile
$env:DSH_HOME='D:\tmp\shadow\.dsh'; $env:DSHGUARD_HOME='D:\tmp\shadow\guardhome'
node tools\test-host-load.mjs

# 面板 bundle(不需要浏览器)
node tools\test-client-load.mjs

# 看门狗只跑一轮,把结果写成 JSON
powershell -File lib\watchdog.ps1 -Profile web -Once -Out r.json

影子环境(强烈建议):用 DSH_HOME / DSHGUARD_HOME / DSHGUARD_AUTO_RESTART=false 把一切 重定向到临时目录,再用一个假的 dsh.cmd 顶替真命令 —— 这样连"回滚后自动重启"都能演练, 而不会动到你真正在用的 profile。

⚠️ 三条实测踩出来的坑,写在这里省你几个小时:

  1. DSH 会把 DSH_WEB_URL 导出给整个进程树,第二个实例会继承第一个实例的端口。 所以守护的端口优先级是 显式传参 > 请求的 Host 头 > DSH_WEB_URL > 3080, 面板上也会显示"端口来自哪里"。猜错端口的症状是看门狗以为 DSH 死了、反复重启。
  2. DSH 读 profile 的 package.json 用 JSON.parse,不认 BOM。 用 Set-Content -Encoding UTF8(5.1 会写 BOM)改它 = 整个 DSH 起不来。
  3. 🔴 客户端插件的 inject 是「服务名」,不是「包名」。(这条是真把界面搞白过的) client/client.js 里的 exports.inject 只能写 Cordis 服务名;写成 ['@deepseek-ai/dsh-client-ui-settings'] 这种包名,客户端插件就会去等一个永远不存在 的服务 → 插件永不激活 → 客户端 boot 不完成、App 不挂载 → 界面全白 (宿主和服务都正常、服务端日志一条错都没有,只剩 defer 注入的其他挂件可见 —— 极难定位)。 包名要放在 package.json 的 dsh.client.inject(那里是"先加载哪些插件"), bundle 里只写服务名。本面板用 ctx.slots 挂设置页 Tab,所以是 ['slots'] (对照:官方 dsh-client-ui-settings-plugin-inventory 写 ["slots","locale","remote","remote.pluginInventory"];本机已安装版本里有 44 个 官方客户端 bundle 都写 inject = ["slots"])。 Install-Plugin.ps1 的落地自检与 tools/test-client-load.mjs 都会拦这条。 本项目一律用 [IO.File]::WriteAllText(..., UTF8Encoding($false))。

已知边界(不吹)

  • 只支持 Windows。看门狗依赖计划任务;macOS/Linux 需要换一套守护机制(欢迎 PR)。
  • 一次只盯一个端口。同时跑着多个 DSH 实例时,只有你安装时那一端被看门狗盯着。
  • 看门狗在登录时启动。它常驻,但如果它自己没起来(比如计划任务被禁用),就没人兜底 —— 面板顶部的「看门狗运行中/未运行」徽章就是给你看这个的。
  • 回滚 ≠ 万能。它还原的是 profile 的配置与依赖;你自己项目里的文件、会话数据不在范围内。
  • 不做云备份。快照在本地 ~/.dsh-guard,磁盘坏了就没了。

官方桌面端(healthMode: process,2026-09-26 新增)

DSH 官方桌面端(D:\dsh\)与网页端有两处硬差别,本插件为此增加了进程模式:

差别 后果 进程模式怎么解
界面端口每次启动随机(asar 里是 server.listen(0, "127.0.0.1"),实测过 19387) 按端口判活必然盯错 判活改成看 DeepSeek Harness.exe 进程;结论写进同一个 $Health.PortUp 字段,所以下游(宽限期/连续轮次阈值/隔离观察/回滚判定)一行都没改
没有 dsh web --port 这条 CLI 看门狗"自动拉起"用不了 Start-DshWeb 改拉 appExe;HTTP 探测在进程模式下跳过(否则官方端一重启、端口一换就误报"HTTP 探测失败")
插件进程的 PATH 里没有 pnpm 回滚最后一步 pnpm install 失败 $script:PnpmCmd 指向官方端自带运行时(node.exe <pnpm.cjs>)

装到官方端时必须在 profile 补丁层里给这段(不给就等于默认 profile='web' + 盯端口 = 守错对象):

- id: dsh-guard
  config:
    profile: desktop
    healthMode: process
    appExe: 'D:\dsh\DeepSeek Harness.exe'
    appProcess: 'DeepSeek Harness'
  • 默认值是上游行为:不传 healthMode 就是 port 模式,网页端那份一个字节都没变。 夹具 tools\test-process-mode.ps1 把"进程模式可用"与"不配置 = 老行为"一起钉住(含阳性/阴性对照)。
  • 顺带修掉一个会咬人的环境坑:从 PowerShell 7 会话 spawn 出来的 5.1 子进程会继承 PS7 的 PSModulePath → 5.1 命中 PS7 版 Microsoft.PowerShell.Utility、加载失败 → Get-FileHash 这类靠自动加载的 cmdlet 直接变成"无法识别的名称"。lib/guard-bootstrap.ps1 §0 现在会剔掉 PS7 目录。
  • 静态护栏:tools\test-var-collision.ps1 —— dot-source 文件里的普通变量撞入口脚本的参数名 (例如 $keep vs [int]$Keep:PowerShell 变量名不区分大小写 + dot-source 同作用域 → 把数组赋给 [int] 参数,报一句与变量名毫无关系的 Cannot convert System.Object[] to Int32)。(带阳性对照自检)
  • 看门狗脚本副本会同步到 <守护数据目录>\bin;Install-Watchdog.cmd 安装时会把 -HealthMode/-AppExe/-AppProcess 一并烘进计划任务参数(登录时环境里什么都没有)。

许可

MIT,见 LICENSE。"DeepSeek"、"DeepSeek Harness" 是其权利人的商标, 本项目为第三方插件,不代表官方、也不暗示任何官方背书。