DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wodaimnd /

wodaimnd/dsh-plugin-guard

Verified

DSH 插件运维台:兼容性体检 + 装载行修复(patch-bundle-row)+ 授权闸门(plan/apply/undo)+ 装前静态体检与装后复验账本。把 dsh-compat-vet 的审查修复层与 dsh-forge 的锻造台合到一处。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@db0876db

@local/dsh-plugin-guard —— 插件运维台

把 dsh-compat-vet 的审查修复层与 dsh-forge 的锻造台合成一个插件, 并补上两者都没有的那一格:装载行的 name 能不能被 loader 解析。

一个侧栏入口、一块工作台,三件事:

能力 回答什么问题 什么时候用
体检 guard_audit 已装的插件里,哪些装上了却挂不上?哪些 bundle 解析失败 / peer 不满足 / 装了没启用 / tool id 冲突? 插件「装了没反应」、面板不见了、设置页少了东西、升级 dsh 之后功能消失
计划 + 授权 guard_plan → guard_apply 要改哪几个字节、可不可逆、要不要重启;你勾选哪些才执行哪些 准备修,但不想让工具擅自动别人的包
自愈 guard_heal 把「name 解析不了」就地修好,幂等、可 dry-run、置信度不足不猜 想彻底不用管
装前体检 guard_check_bundle 我准备装的这个包写得对不对(manifest / patch / 语法 / client 包装器 / locale / slot / 副作用 / skill 预算) 自己写插件、或装第三方包之前
撤销 guard_undo 按 journal 逐字节写回 修完发现不对

1. 它补的那一格:装载行解析

这是旧 compat-vet 查不出来的一类故障,也是本插件存在的理由。

故障长什么样

某个插件装在 profile 里、dsh.profile.bundles 里也有它、node_modules 里文件齐全, 但:

  • 设置页里没有它的那一项
  • 输入框的模型选择器里没有它的分组
  • 它该有的数据目录从未被创建
  • dsh 日志里一行都不提

为什么

它自带的 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 解析:

  • cordis-plugin-loader/src/config/entry.ts → this.parent.tree.import(name, baseUrl)
  • dsh-app-boot 的 HostResolvedRootInclude.import:裸名按 dsh 安装目录解析, scoped 名 / 相对路径按 profile 目录解析

裸名在 profile 的 node_modules 与 dsh 安装目录里都不存在(profile 里只有 @ebony-vinyl/ 这一层), 于是 MODULE_NOT_FOUND —— 宿主半身与浏览器半身一起挂不上。

为什么 profile 层 YAML 改不了它

dsh-app-boot/lib/index.js 的 applyEntryPatches:

const { id, insert, name, ...overrides } = patch
...
if (name && name !== target.name) {
  warn('patch: name mismatch for %C (expected %C, got %C), skipping', ...)
  continue                                   // ← name 只作身份断言
}
for (const [key, value] of Object.entries(overrides)) target[key] = value

name 不是可覆盖字段,它只用来断言「你改的是你以为的那一行」。 所以「往 profile 的 cordis.patch.yml 追加三行就能修好」这条路不存在, 必须落到 bundle 自己的 patch 文件上。

因此自愈分两级

一级(默认):就地改字节。 只替换那一行 name 的那一段,其它字节、注释、引号风格全不动。

-      name: 'dsh-our-free-model'
+      name: '@ebony-vinyl/dsh-our-free-model'

二级(包目录只读时):不碰 bundle 字节,改用 profile 层补丁。

这条路径的合法性来自 applyEntryPatches 的另外两条语义:

if (insert) { data.push(...insert); buildMap(insert) }   // 插入的行会进索引
...
target[key] = value                                       // disabled 可覆盖

于是可以「停掉坏行 + 补一条好的」:

- id: our-free-model
  disabled: true                       # 停掉原行
- insert:
    - id: our-free-model--guard
      name: '@ebony-vinyl/dsh-our-free-model'
      config: {}

buildMap(insert) 保证后一条 patch 能引用前一条插入的行,所以两段必须按顺序放。


2. 授权闸门:为什么确认必须自建

这台机器上 DSH 的 user-approval 被改成了 decide() => 'allowed-once', DSH 原生的确认对话不会弹出来。在这样的环境里,「点一下就改别人包」的按钮等于没有护栏。

所以确认建在协议层,不是界面上的一个弹窗:

plan(只读)─→ planId + 每条:动作 / 目标文件 / 改前改后 / 可逆性 / 是否需重启
                    │
                    ├─ 用户在面板上逐条勾选 ← 这就是授权
                    ▼
apply(planId, approved[]) ─→ 校验① planId 有效(未过期、未执行)
                              ② 指纹未变
                              ③ 只有 approved 里的条目执行
                              └→ 任何一条不满足:拒绝,错误码 stale-approval
                    ▼
复检 ─→ 本次批准的那条 issue 是否还在?不在了才算成功
                    ▼
                    └→ 不在就自动回滚本次改动的全部字节
                    ▼
undo(journalId) ─→ 从备份逐字节写回

指纹为什么要带目标文件的字节哈希

sha256(report.checkedAt + report.summary + package.json 字节 + cordis.patch.yml 字节 + 每条目标文件的字节哈希 + 已装版本)

只校验「报告有没有变」不够:报告证明「当时判过」,字节哈希证明「现在改的还是同一个东西」。 中间可能有人手动改了那个 patch,也可能有别的安装动作重写了 node_modules。

实测:报告一变 → 指纹变 → stale-approval → 不执行。


3. 与 dsh-compat-vet / dsh-forge 的关系

旧插件 保留了什么 补了什么 / 改了什么
dsh-compat-vet 三层复检(磁盘清单 + 自身 reaudit + 轮询 dsh 判定)、backupProfile/restoreBackup、explainErrorCode 含 stale-approval、回环 + 同源校验 新增 patch-bundle-row / activate 两个动作;新增 plan/apply/undo 协议;修掉内置包误报
dsh-forge 装前静态体检全部判据(manifest / exports / patch / syntax / client / locale / icon / slot / effects / lifecycle / skill 预算)、账本与 diff 改名为 auditBundle;工具名换成 guard_* 前缀(与 forge 并存时不撞 id)
@local/market-verdict 字面量口径的 tool/service 冲突扫描 冲突按包对聚合(82 个同名 tool 摊成 82 条会把真问题淹掉)

修掉的两个误报(这两个误报会让用户学会忽略报告)

① 内置包被报成未解析。 @deepseek-ai/dsh-base、@deepseek-ai/dsh-web-app 在 app.asar 里, createRequire 解析不到,但它们确实是内置 bundle、确实能挂上。 旧 compat-vet 只按 createRequire 判定,所以每次体检都带两条噪音。

正解是 读安装体的 package.json dependencies(app-boot 自己就这么做)。 它在 asar 里,所以要能读 asar —— lib/asar.js 是最小实现(只读 header 索引,不写、失败返回 undefined)。

实测:本机 version=0.2.1-rc.5,dependencies 305 项,@deepseek-ai/dsh-base 在内。

② peer 判定口径写错会误报所有插件。 unmetRequirements(dsh-app-boot/lib/index.js:279-299)有三条例外,必须一起照抄:

  1. 有 engines.dsh → 只看它,peerDependencies 整段忽略
  2. peer 名只看 @deepseek-ai/dsh 与 @deepseek-ai/dsh-* 前缀 (@deepseek-ai/cordis、@deepseek-ai/schemastery 不参与判定)
  3. range 对着 runtimeVersion 求值,不是对着 peer 包自己的版本 (这是 dsh 的独特约定);workspace:^ / ~ / * 视为当前运行时

按自己直觉写会得出「dsh-plugin@2.2.7 与运行时冲突」的假结论 (它声明 @deepseek-ai/cordis: "4.0.1",运行时是 4.0.5 —— 但 cordis 根本不在判定范围内)。


4. 配置

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 分钟)。超时后 apply 返回 stale-approval
maxScanBundles 64 最多扫多少个 bundle 的 patch,防超大 profile 卡死
promptSection true 往系统提示词写一段「有插件挂不上」的提醒;无问题时不占上下文

selfHeal: false + tools: true 是一个常用组合:只报告、不自动改盘, 所有改动都要经过你在面板上勾选。


5. 验证

cd "G:\AI PGRM\DSH\plugins\plugin-guard"

# ① 协议正确性(合成 fixture)
node test/e2e.mjs

# ② 真机集成(真实 profile + 真实 asar)
node test/integration.mjs

# ③ 浏览器半协议形状
node test/client-contract.mjs

期望:

测试 结果 它证明什么
e2e.mjs 25/25 审计能抓 OFM 故障、plan 不改盘、指纹变即失效、只改一行、注释全保留、undo 逐字符还原、重复 apply 被拒
integration.mjs 16/16 从 asar 读出运行时与内置包清单、内置 bundle 不再误报、自愈 dry-run 零改动、装前体检自检 ERROR=0
client-contract.mjs 25/25 id 与包名一致、factory 真跑得起来、slot 注册对、无裸写死颜色、client/host 端点一致

e2e.mjs 里有两个坑值得记下来,它们是测试本身最容易写错的地方:

  1. 故障包的目录必须是 node_modules/@ebony-vinyl/dsh-our-free-model(scoped 布局)。 放在 node_modules/dsh-our-free-model 会让裸名恰好可解析,复现不出问题。
  2. 修复只改 patch 行,不改 bundles 条目。要是测试场景把 bundles 也写成裸名, 修完 patch 之后 bundle 级仍然解析不了,复检会(正确)判失败。

6. 安装

$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。但功能重叠——建议启用本插件后停用这两个,避免同一件事有两个入口。


7. 回滚

$p = "C:\Users\Shinelon\.dsh\profiles\desktop"
# ① 停用:从 dsh.profile.bundles 删掉 "@local/dsh-plugin-guard"
# ② 撤销所有自愈改动:自愈账本里每一项都有 beforeHash / afterHash
#    按 journal 撤销面板上执行过的改动:
#      guard_undo(journalId) 或面板「记录」页的「撤销」按钮
# ③ 彻底移除依赖:从 dependencies 删掉那行,pnpm install --dir $p
# ④ 清状态目录:Remove-Item "$p\..\..\plugin-guard" -Recurse -Force

自愈改过的 bundle patch 会在 pnpm install 之后被重写回原样(node_modules 是 pnpm 的产物)。 这不是问题:下次启动自愈会再跑一遍(幂等),所以「彻底不用管」成立的前提是 本插件保持启用。停用之后,那些包的 patch 会退回上游的写法,故障随之回来。


8. 已知边界(诚实声明)

  • 只认字面量:tool id 扫描只看 defineTool({ name: 'x' }) / .register({ name: 'x' })。 拼接出来的名字(name: dynName)不在视野内。
  • !!js 表达式的 name 不判:静态判不了,标为 expression 并跳过。
  • 相对路径的 name 只查 package.json 存在性,不模拟 loader 的锚点解析完整链。
  • 不替你做决定:多包 tool 冲突时只给「隔离其中一个」的选项,不替你选留哪个。
  • 升级 / 卸载动作不实现:ACTION.UNINSTALL / UPGRADE 在 plan 里有定义但没有执行体, 面板会明说「请用官方插件页」。理由:这两个动作的正确性依赖 dsh 自己的 pluginManager, 自己 spawn pnpm 复现一遍只会多出一个故障源。
  • 自愈会改 node_modules:这是它唯一越界的地方,所以默认只在 confidence: high (包自己的 package.json name)时动手,并且每次都留字节备份。
—/ 5

No ratings yet

Verified DSH bundle

Commit db0876db5599

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout