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 一律写「远程服务器」。
11 KiB
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 过期或删缓存文件)。