前言¶
DSH 的模型选择器、设置页、数据目录,都由 profile 里挂上的 bundle 决定。插件装了,不等于挂上了。dsh.profile.bundles 里有它、node_modules 里文件齐全、日志里却一行不提——这种情况排查起来很麻烦,因为故障不报任何错误。
下面介绍的 @local/dsh-plugin-guard(维护者 wodaimnd)就是为这一类问题做的工具。它把 dsh-compat-vet 的审查修复层和 dsh-forge 的装前体检合成一个插件,补上了两者都没有的能力:判断 bundle 装载行的 name 字段能不能被 loader 解析,以及在不弄坏别人包的前提下修好它。
声明的依赖范围(见后面「安装与启用」)是本地 link: 安装,功能全部在本插件进程内完成。
这是什么¶
一个 DSH 插件,带侧栏面板和 6 个 guard_* 模型工具。分类为 admin-security。仓库地址在 GitHub,包名为 @local/dsh-plugin-guard,版本 0.1.1,private: true。
它回答的问题分三类:
- 装了为什么没反应:哪些 bundle 解析失败、peer 不满足、装了没启用、tool id 冲突。
- 改之前能不能先看清楚改什么:逐条列出动作、目标文件、改前改后内容、是否可逆、是否需要重启,由你勾选后才执行。
- 装之前这个包写得对不对:对第三方包做静态体检。
核心能力¶
| 能力 | 回答什么问题 | 什么时候用 |
|---|---|---|
guard_audit 体检 |
哪些插件装上了却挂不上 | 面板不见了、设置页少了东西、升级 dsh 后功能消失 |
guard_plan → guard_apply 计划 + 授权 |
要改哪几个字节、可不可逆、要不要重启 | 准备修,但不想让工具擅自动别人的包 |
guard_heal 自愈 |
把 name 解析不了的装载行就地修好 | 想彻底不用管 |
guard_check_bundle 装前体检 |
准备装的包写得对不对 | 自己写插件、或装第三方包之前 |
guard_undo 撤销 |
修完发现不对 | 按 journal 逐字节写回 |
六个工具(guard_audit、guard_plan、guard_apply、guard_heal、guard_check_bundle、guard_undo)都在 tools: true 时注册,关掉配置 tools 就退化为纯面板。
它补的那一格:装载行 name 解析¶
旧 dsh-compat-vet 查不出这一类故障。典型症状是:插件装在 profile 里,dsh.profile.bundles 里有它,node_modules 里文件齐全,但设置页没有它的项、模型选择器没有它的分组、数据目录从未创建,日志里一行不提。
原因在于它自带的 cordis.patch.yml 里装载行写了裸包名,而包本身是 scoped 安装:
- insert:
- id: our-free-model
name: 'dsh-our-free-model' # ← 真名是 @ebony-vinyl/dsh-our-free-model
config: {}
loader 拿这一行的 name 走 Node ESM 解析。dsh-app-boot 的 HostResolvedRootInclude.import 里,裸名按 dsh 安装目录解析,scoped 名和相对路径按 profile 目录解析。裸名在两边都不存在,于是 MODULE_NOT_FOUND,宿主半身和浏览器半身一起挂不上。
为什么不能往 profile 层的 cordis.patch.yml 追加三行解决:dsh-app-boot/lib/index.js 的 applyEntryPatches 里,name 不是可覆盖字段,只用来断言「你改的是你以为的那一行」,不匹配时 continue 跳过。所以补丁必须落到 bundle 自己的 patch 文件上。
因此自愈分两级:
一级(默认):就地改字节。 只替换那一行 name 的对应片段,其它字节、注释、引号风格全不动:
- name: 'dsh-our-free-model'
+ name: '@ebony-vinyl/dsh-our-free-model'
二级(包目录只读时):不碰 bundle 字节,改用 profile 层补丁。 停掉坏行再加一条好的:
- id: our-free-model
disabled: true
- insert:
- id: our-free-model--guard
name: '@ebony-vinyl/dsh-our-free-model'
config: {}
buildMap(insert) 保证后一条 patch 能引用前一条插入的行,两段必须按顺序放。
授权闸门:plan / apply / undo¶
在部分环境里,DSH 原生的确认对话不会弹出(例如被改成 decide() => 'allowed-once')。这时「点一下就改别人包」的按钮没有护栏。本插件把确认建在协议层,不在界面上做弹窗:
plan(只读)→ planId + 每条:动作 / 目标文件 / 改前改后 / 可逆性 / 是否需重启
│
├─ 用户在面板上逐条勾选 ← 这就是授权
▼
apply(planId, approved[]) → 校验 planId 有效(未过期、未执行)
→ 校验指纹未变
→ 只有 approved 里的条目执行
→ 任一条不满足:拒绝,错误码 stale-approval
▼
复检 → 本次批准的 issue 是否还在?不在了才算成功
▼
└→ 不在就自动回滚本次改动的全部字节
指纹由以下内容计算 sha256:报告的 checkedAt 与 summary、package.json 字节、cordis.patch.yml 字节、每条目标文件的字节哈希、已装版本。报告证明「当时判过」,字节哈希证明「现在改的还是同一个东西」。有人在两个时点之间手动改过 patch,或其它安装动作重写了 node_modules,校验都会失败并返回 stale-approval。
计划有效期默认 15 分钟(planTtlMs: 900000),超时后 apply 同样返回 stale-approval。复核不通过会自动回滚本次改动的全部字节;需要手动回滚时用 guard_undo(journalId),按 journal 从备份逐字节写回。
与 dsh-compat-vet / dsh-forge 的关系¶
本插件合并了两个旧插件的能力,并修掉了两个误报。
| 旧插件 | 保留 | 新增 / 修改 |
|---|---|---|
dsh-compat-vet |
三层复检、backupProfile/restoreBackup、explainErrorCode、回环与同源校验 |
新增 patch-bundle-row / activate 动作;新增 plan/apply/undo 协议;修掉内置包误报 |
dsh-forge |
装前静态体检全部判据、账本与 diff | 改名为 auditBundle;工具名换成 guard_* 前缀 |
@local/market-verdict |
字面量口径的 tool/service 冲突扫描 | 冲突按包对聚合,避免同名 tool 摊成几十条淹没真问题 |
误报一:内置包被报成未解析。 @deepseek-ai/dsh-base、@deepseek-ai/dsh-web-app 在 app.asar 里,createRequire 解析不到但确实挂得上。旧 compat-vet 只按 createRequire 判定,每次都产生两条噪音。正解是读安装体的 package.json dependencies(app-boot 自己就这么做的);为此插件内置了最小 asar 读实现 lib/asar.js,只读 header 索引,不写、失败返回 undefined。
误报二:peer 判定口径。 dsh-app-boot/lib/index.js 的 unmetRequirements 有三条例外,必须一起照抄:
- 声明了
engines.dsh就只看它,peerDependencies整段忽略; - peer 名只看
@deepseek-ai/dsh与@deepseek-ai/dsh-*前缀(@deepseek-ai/cordis、@deepseek-ai/schemastery不参与判定); - range 对着
runtimeVersion求值,而不是对着 peer 包自己的版本(workspace:^/~/*视为当前运行时)。
按直觉写判定会得出「插件声明的 cordis 版本与运行时冲突」的假结论,而 cordis 根本不在判定范围内。
配置¶
配置写在 cordis.patch.yml 的 config 里:
| 键 | 默认 | 说明 |
|---|---|---|
tools |
true |
注册 6 个 guard_* 模型工具,关掉后退化为纯面板(0 token) |
pageUi |
true |
挂 /api/dsh-plugin-guard 与侧栏面板 |
warnOnStartup |
true |
启动时把阻断问题打到 stderr |
selfHeal |
true |
启动自愈;关掉后退化为纯只读体检 |
selfHealDryRun |
false |
true 时只报告打算改什么,不写盘 |
syntaxCheck |
true |
装前体检是否跑 node --check |
ledgerDir |
.plugin-guard |
状态目录(计划 / journal / 自愈账本),相对工作区根 |
planTtlMs |
900000 |
计划有效期,15 分钟 |
maxScanBundles |
64 |
最多扫多少个 bundle 的 patch |
promptSection |
true |
往系统提示词写一段「有插件挂不上」的提醒;无问题时不占上下文 |
常用组合 selfHeal: false + tools: true:只报告、不自动改盘,所有改动都要面板上勾选。
安装与启用¶
README 里的安装方式是本地 link: 安装,需要先准备好插件目录,再改 profile 的 package.json:
$p = "C:\Users\Shinelon\.dsh\profiles\desktop"
# ① 依赖(link: 安装,改动即时生效)
# package.json → dependencies 里加:
# "@local/dsh-plugin-guard": "link:G:/AI PGRM/DSH/plugins/plugin-guard"
pnpm install --dir $p
# ② 启用(bundles 末尾加一项)
# package.json → dsh.profile.bundles 里加 "@local/dsh-plugin-guard"
# ③ 重启 DSH Desktop
与 dsh-compat-vet、dsh-forge 并存安全,工具名用 guard_* 前缀不会撞 id;但功能重叠,建议启用本插件后停用那两个,避免同一件事有两个入口。
回滚:
$p = "C:\Users\Shinelon\.dsh\profiles\desktop"
# ① 停用:从 dsh.profile.bundles 删掉 "@local/dsh-plugin-guard"
# ② 撤销:guard_undo(journalId) 或面板「记录」页的「撤销」按钮
# ③ 彻底移除依赖:从 dependencies 删掉那行,pnpm install --dir $p
# ④ 清状态目录:Remove-Item "$p\..\..\plugin-guard" -Recurse -Force
有一点需要注意:自愈改过的 bundle patch 会在 pnpm install 之后被重写回原样,因为 node_modules 是 pnpm 的产物。这不是问题——下次启动自愈会再跑一遍(幂等)。「彻底不用管」成立的前提是本插件保持启用;停用之后那些包的 patch 会退回上游写法,故障随之回来。
典型用法¶
- 插件「装了没反应」:先跑
guard_audit,它会区分无效装载、bundle 解析失败、peer 不满足、装了没启用、tool id 冲突。 - 确认要修:跑
guard_plan,拿到逐条改动清单;在面板上只勾选同意的那些,再guard_apply(planId, approved[])。任何一条校验不通过都会返回stale-approval并拒绝执行。 - 不想手动管:保持默认
selfHeal: true启动自愈,或在面板上跑guard_heal。 - 想自己开发插件或装第三方包前:跑
guard_check_bundle,它会检查 manifest、patch、语法、client 包装器、locale、slot、副作用声明、skill 预算等项。 - 想先只看看不写盘:把
selfHealDryRun设为true,或用面板上的 dry-run 查看自愈计划。
验证¶
README 提供了三套测试命令:
cd "G:\AI PGRM\DSH\plugins\plugin-guard"
# ① 协议正确性(合成 fixture)
node test/e2e.mjs
# ② 真机集成(真实 profile + 真实 asar)
node test/integration.mjs
# ③ 浏览器半协议形状
node test/client-contract.mjs
README 中记录的期望结果:e2e.mjs 25/25,integration.mjs 16/16,client-contract.mjs 25/25。其中 e2e.mjs 有两个容易踩的坑:故障包目录必须是 scoped 布局,否则裸名恰好可解析、复现不出故障;修复只改 patch 行、不改 bundles 条目,否则 bundle 级仍然解析不了。
适用场景与注意事项¶
适合这几类人:在 DSH 里维护多个插件、遇到过「装了没反应」排查成本高的人;自己写插件或装第三方包之前想做一轮静态检查的人;不想每次手工改 bundle patch 的人。
几点要留意:
- 插件以当前 DSH 进程的权限运行,会注册 6 个模型工具和面板接口,写文件只落在两类位置——bundle 的
cordis.patch.yml(仅授权或自愈时,带字节备份)和你自己工作区下的.plugin-guard状态目录。安装前应先读一遍源码与仓库声明,确认没有问题再启用。 - 装前体检的判据是静态的,不是运行时模拟。
- 自愈依赖本插件保持启用;停用后上游写法会回归。
- 这些事在 README 里标注的 Python/Node 验收脚本是开发者侧的,普通使用者不需要跑。
已知边界¶
README 自己列的诚实声明:
- tool id 扫描只认字面量(
defineTool({ name: 'x' })/.register({ name: 'x' })),拼接出来的动态名字不在视野内; !!js表达式的 name 静态判不了,标为expression并跳过;- 相对路径的 name 只查
package.json存在性,不模拟 loader 锚点解析完整链; - 多包 tool 冲突时只提供「隔离其中一个」的选项,不替你决定留哪个;
- 升级 / 卸载(README 因截断未尽列,完整列表见 GitHub 原文)。
结尾¶
@local/dsh-plugin-guard 的价值在于把「插件明明装了却挂不上」这类无日志故障的排查和修复收敛到一个入口,并且把改动前的计划、授权、回滚都留在协议层,而不是靠界面弹窗。如果你想在装插件前先做一轮静态检查,或想让自愈在启动时顺手修好装载行,这个插件可以直接用。
目录页:https://www.skillhub.cn/plugins/wodaimnd/dsh-plugin-guard
GitHub:https://github.com/wodaimnd/dsh-plugin-guard