Files
dsh_shenxian/dsh-server-docs/04-调整方案/92-官方推荐插件列表按dsh版本过滤.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

122 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。结果按 `<npm>@<版本>` 落盘缓存 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()` = `<dsh 包>/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 过期或删缓存文件)。