# 92-官方推荐插件列表按 dsh 版本过滤(2026-09-14 落地) - 日期:2026-09-14 | 状态:✅ **已上线**(插件 **0.3.16** + 平台后端已构建重启) - 触发:用户原话 ——「**设置选项下插件管理中的官方推荐插件列表 只能显示匹配当前dsh版本 和 超过当前版本的插件(超过的要明确标注)**」 > **TL;DR** > 官方目录(`awesome-dsh-plugin.com/plugins.json`)**不带 dsh 版本字段** ⇒ 只能**逐包去 npm 查声明** > (精简 packument 的 `peerDependencies / engines`,不下载 tarball),再与**平台真实版本**比。 > 分类 `match / none / newer / older / unknown`;**默认隐藏 `older`**(只声明了更旧版本), > `newer`(要求更高的 dsh)**橙色显著标注**并给出「需 ≥ X,当前 Y」。 > ⚠️ **两个"静默降级"陷阱(本轮实测踩到,都必须钉住)**: > ① `satisfies` **必须带 `includePrerelease: true`** —— 平台是 prerelease(`0.1.5-rc.1` / 子包 `0.1.5-rc.2`), > 默认语义下 `^0.1.2` 甚至 `*` 都不满足 ⇒ TOP300 会把 **`dsh-univer-office`、`dshmarket` 这些正在跑的** > 判成「只兼容更旧」(`match 97 / older 139` → 容忍后 `match 214 / older 22`); > ② **伞包 `@deepseek-ai/dsh` 必须单独解析** —— 它**不在自己的 `node_modules` 里**, > 而实测的 3 条 `newer` **全部只写在伞包上**(复用它 ⇒ 这 3 条永远判不出来)。 > 结果:TOP300 里 `match 231 / none 66 / newer 2 / unknown 1`,**无 `older`**;`dsh-zotero`、`dsh-md-notes` 被标注为「需 dsh ≥ 0.1.5-rc.2」。 --- ## 一、需求拆解与判据选择 | 问题 | 结论 | 依据 | |---|---|---| | 官方目录有没有现成的 dsh 版本字段? | **没有**。3632 条只有 `name/owner/url/page/category/description/npm/version/stars/downloads/install/added/tarball/screenshots` | 实抓 `plugins.json` 统计字段出现次数(`version` 是**插件自身**版本) | | 那版本要求写在哪? | 写在各 npm 包的 **`peerDependencies`(少数用 `engines.dsh`)**,且**分布在几十个 `@deepseek-ai/dsh-*` 子包上**,不是只写在 `dsh` 伞包 | 抽样 TOP60 + 随机 40:`NONE` 26 / 其余全是各种 `peer:@deepseek-ai/dsh-*` | | 拿什么跟它比? | **平台该包的真实版本**(`dshScopeDir()` 下各包的 `package.json`),与 `plugin-compat.ts` 判据 A 同源 | §三 的陷阱 ② | | 成本? | 一条 = 1 次**精简 packument**(`accept: application/vnd.npm.install-v1+json`,十几 KB)。结果按 `@<版本>` 落盘缓存 7 天 | 首访 5.6s(冷)→ 命中缓存 **143ms** | **分类规则**(`src/web/plugin-dsh-compat.ts`): | kind | 含义 | 展示 | |---|---|---| | `match` | 声明范围(prerelease 容忍)覆盖当前版本 | 绿色「匹配」 | | `none` | 没声明任何 `@deepseek-ai/*` 依赖 | 灰色「未声明」(**不等于不兼容**) | | `newer` | 有范围要求**比平台更高**(`minVersion(range) > 平台版本`) | **橙色「需更高版本 ≥ X」** ← 用户点名要标注的一类 | | `older` | 不满足且不要求更高(只允许更旧的) | **默认隐藏**,灰色「仅兼容更旧」 | | `unknown` | 取不到元数据 / semver 不可用 / 范围不可解析 | 灰色「未验证」,**照常显示** | > 判定顺序:**先判 `newer`**(只要有一条要求更高就归 `newer`)—— 它比"只兼容更旧"更重要。 ## 二、为什么默认过滤 + 为什么留逃生口 - 用户要求「**只能显示**匹配 + 超过」⇒ 默认隐藏 `older`。 - 但**隐藏必须可见**(本库反复强调的"别静默"):说明行会写「已按 dsh {cur} 过滤:**本次候选中**隐藏 {n} 个仅兼容更旧版本的插件」,并给出勾选框「**含仅兼容更旧版本的**」(= 后端 `dshCompat=all`,只标注不过滤)。 - `unknown` **不隐藏**:一次网络抖动 ≠ 不兼容。首访冷缓存时可能出现若干「未验证」,说明行同时给出「兼容判定仍在后台预热」提示。 ## 三、两个"静默降级"陷阱(实测数据) ``` 平台:伞包 0.1.5-rc.1 / 子包 0.1.5-rc.2(240 个 @deepseek-ai/* 包) TOP300 抽样: 严格语义(默认) match 97 | none 60 | older 139 | newer 3 | unknown 1 ① prerelease 容忍 match 214 | none 60 | older 22 | newer 3 | unknown 1 ② 再把伞包单独解析后 newer 3 条全部认出(否则全是 unknown) ``` - **① 的代价**:默认语义下 `older 139` 里包含 `dsh-univer-office`(我们**正在跑**)、`dshmarket`(插件市场本身)、 `dsh-context`、`dsh-cost-meter`… ⇒ 照着默认语义过滤会把半张列表连**能用的**一起藏掉。 根因:semver 默认**不允许 prerelease 命中不含 prerelease 的范围**(`0.1.5-rc.2 ∉ ^0.1.2`,`∉ *`)。 - **② 的代价**:`plugin-compat.ts` 的 `platformPkgDir()` = `/node_modules/@deepseek-ai/<短名>`, 而伞包 `dsh` **不在自己的 `node_modules` 里** ⇒ 它对伞包恒返回 `null`,被当作"平台没这个包,不算不兼容"。 于是只写在伞包上的那 3 条(`dsh-zotero` `^0.1.5-rc.2`、`dsh-any-background` `>=0.1.5-rc.2`、`dsh-md-notes` `>=0.1.5-rc.2 <0.1.6`) 全部判不出来,而它们**正是用户要的那一类**。 > ⚠️ **不要顺手把 `plugin-compat.ts` 也"统一"成 prerelease 容忍**:那里用默认语义是**有意的** > (注释原文:「与 pnpm 的实际安装判定一致」)—— 它是上传/导入的**闸门**,语义不同是正确的。 > 因此本功能**另起模块** `src/web/plugin-dsh-compat.ts`,**零改动** `plugin-compat.ts`。 > > 📌 **顺带发现(未改,属别人 lane)**:`plugin-compat.ts` 判不到伞包 ⇒ 声明 `@deepseek-ai/dsh: ^0.1.6` > 的插件在**上传闸门**处会被放行。是否收紧要另立单(改它就是改既有闸门行为)。 ## 四、改了什么 | 层 | 文件 | 内容 | |---|---|---| | 新模块 | `src/web/plugin-dsh-compat.ts`(199 行) | 平台版本表(**含伞包**)、semver 软载入(`minVersion` + `includePrerelease`)、`platformDeclsOf()`、`judgeDshCompat()` | | 路由 | `src/web/routes/whitelist.ts` | `GET /api/plugins/whitelist` 新增 `dshCompat` 参数(默认过滤,`=all` 只标注);响应新增 `dshVersion` / `hiddenByDsh` / `countAll` / `compatFiltered`,`plugins[]` 新增 `dshCompat` / `dshRequires`;判定落盘缓存 `whitelist-cache/npm-dsh-compat.json`(TTL 7 天,结构版本号 `COMPAT_VERSION`);**后台预热**(进程内一次,并发 6);请求内**限并发 12 + 6s 预算**,超预算判 `unknown` 并照常显示;判定窗口 360(> 页面上限 300,过滤后仍能填满) | | 前端 | `poc/business-plugins/lib/client.js`(**0.3.16**) | 表格新增「dsh 版本」列(+1 列 ⇒ `paemptyRow` 6→7);`wlCompat()` 徽章(`newer` 用 `pending` 橙色);工具条新增「含仅兼容更旧版本的」勾选;说明行给出过滤说明 + 隐藏计数 + 预热提示;`pa.pl.hintOfficial` 文案补充过滤规则 | | 校验 | `scripts/verify-dsh-compat.mjs`(新) | 17 条**回归锚点**:prerelease 语义、伞包解析、判定顺序、只隐藏 `older`、`hiddenByDsh`/`dshVersion` 存在、落盘缓存、预热、前端徽章/逃生口/说明行 | | 校验 | `scripts/verify-models-dict.mjs` | 引用键扫描**从 `ms.*` 扩到全词典**(`pa.*` 一样会漏)+ 新增 `package.json` 合法性断言(本轮真把 JSON 写坏过一次) | | 校验 | `package.json` | `npm run verify` 链追加上述两个脚本 | ## 五、验证 | 层 | 手段 | 结果 | |---|---|---| | L1 类型/语法 | 本机 `npm run typecheck` + `node --check` | ✅ | | L2 判据锚点 | `verify-dsh-compat.mjs` 17 项 + `verify-models-dict.mjs`(全词典) | ✅ 全绿 | | L3 算法预演 | 上服务器用**真平台版本表**跑 dry-run,对比两种语义分布 | ✅ §三 的两组数字即此产出 | | L4 服务器构建 | `scp` 源码(**LF**)→ `bash scripts/ci.sh` | ⚠️ typecheck ✅、build ✅;**单测 1 项失败但与本次无关**(见 §六) | | L5 接口真调 | 临时 session(直插 DB、用完即删)→ `GET /api/plugins/whitelist` | ✅ 13 项断言全绿:**默认响应不含 `older`**、`dshVersion=0.1.5-rc.1`、`hiddenByDsh=30`、`newer` 2 条带 `dshRequires`;`dshCompat=all` 时 `older` 23 条**出现** | | L6 上线实证 | 产物 sha1 比对 + 两 profile 装到 0.3.16 + 运行中实例启动时刻**晚于**包落地 | ✅ manifest 里 `wlCompat`/`pa.pl.compatCol` 均在 | **铺发**:后端 = `tar`(LF)→ `/opt/dshs` → `npm run build` → `systemctl restart dshs`(重启后 portal 200); 插件 = `npm pack` → `/opt/dsh/artifacts/business-plugins-0.3.16.tgz` → `ensure-biz-plugins.cjs --all --restart`(admin `bundles=7` / guest `bundles=6`)。 ## 六、⚠️ 与本单无关但必须上报:`ci.sh` 里 1 项单测长期红 `test/db.test.mjs` 的 **「sqlite: concurrent setCredentialKey keeps exactly one enabled」** 失败: 断言 `exactly one key enabled`,实际 `5 !== 1`。 **根因 = 档案 87 的有意行为变更**:口径①「条目**各自开关、可同时启用**」已删掉互斥 ⇒ 该测试的期望值**过时**,不是本次改动引起(本次只碰 `whitelist.ts` / 新模块 / 插件客户端,均不进 `db.test.mjs` 的依赖图)。 📌 **建议**:由 档案 87 的 lane 把该断言改为「允许并存的 N 把 key 均可启用」,或直接改造为按新口径断言。**本单未动**(跨 lane)。 ## 七、回滚 - **插件**:删 `/opt/dsh/artifacts/business-plugins-0.3.16.tgz` → 再跑 `ensure-biz-plugins.cjs --all --restart`(会自动选回 0.3.15)。 - **后端**:备份在 `/opt/dsh/backups/pre-92-20260914-204352/`(`src/web/routes/whitelist.ts` + `package.json`) ⇒ `cp` 回去 + `npm run build` + `systemctl restart dshs`。 原文件 sha256:`whitelist.ts 2fb4bf79…`、`package.json 2bc1f453…`。 - 新增文件 `src/web/plugin-dsh-compat.ts` 与缓存目录 `whitelist-cache/npm-dsh-compat.json` 可直接删(无依赖)。 - **无数据迁移、无 DB 变更**。 ## 八、遗留 - ⚠️ 代码侧与文档库本轮仍未提交(工作树留痕)。 - 「`older` 是否该彻底隐藏」是**产品判断**:当前是「默认隐藏 + 勾选可看」。若用户希望**永久不给看**,去掉勾选框即可。 - 判定窗口 360 / 页面 300 / 预算 6s 都是**可调常量**(在 `whitelist.ts` 顶部成组),若日后官方目录暴涨需要重新标定。 - 未做「按需重新判定单个插件」(现在只能等 TTL 过期或删缓存文件)。