@local/dsh-plugin-guard: Introduction to DSH Plugin Operations Platform

前言

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。

它回答的问题分三类:

  1. 装了为什么没反应:哪些 bundle 解析失败、peer 不满足、装了没启用、tool id 冲突。
  2. 改之前能不能先看清楚改什么:逐条列出动作、目标文件、改前改后内容、是否可逆、是否需要重启,由你勾选后才执行。
  3. 装之前这个包写得对不对:对第三方包做静态体检。

核心能力

能力 回答什么问题 什么时候用
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 有三条例外,必须一起照抄:

  1. 声明了 engines.dsh 就只看它,peerDependencies 整段忽略;
  2. peer 名只看 @deepseek-ai/dsh 与 @deepseek-ai/dsh-* 前缀(@deepseek-ai/cordis、@deepseek-ai/schemastery 不参与判定);
  3. 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 会退回上游写法,故障随之回来。

典型用法

  1. 插件「装了没反应」:先跑 guard_audit,它会区分无效装载、bundle 解析失败、peer 不满足、装了没启用、tool id 冲突。
  2. 确认要修:跑 guard_plan,拿到逐条改动清单;在面板上只勾选同意的那些,再 guard_apply(planId, approved[])。任何一条校验不通过都会返回 stale-approval 并拒绝执行。
  3. 不想手动管:保持默认 selfHeal: true 启动自愈,或在面板上跑 guard_heal。
  4. 想自己开发插件或装第三方包前:跑 guard_check_bundle,它会检查 manifest、patch、语法、client 包装器、locale、slot、副作用声明、skill 预算等项。
  5. 想先只看看不写盘:把 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

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye