# 93-兼容判定收口:伞包版本 + prerelease 语义(2026-09-14 落地) - 日期:2026-09-14 | 状态:✅ **已上线**(平台后端已构建重启;服务器 `ci.sh` 转绿) - 触发:用户原话 ——「**能都把这些问题都解决,同步项目到仓库**」(接档案 92 结尾上报的两个问题) > **TL;DR** > ① **"单测长期红"根本不是代码问题** —— 本机 `48 通过 / 0 失败`,是**服务器源码落后于 git** > (服务器 `test/db.test.mjs` 还是旧版断言)⇒ 同步该文件后**服务器 CI 立即转绿**。 > ② **伞包版本收口**:`@deepseek-ai/dsh` **不在自己的 `node_modules` 里** ⇒ 原查询对它恒判 `null`, > 被当作「平台没这个包,不算不兼容」而放行;收口到 `dsh-install.platformPackageVersion()`, > 闸门与列表**共用同一份版本表**。 > ③ ⚠️ **收口带出一个必须一并修的副作用**:闸门判据 A 用 semver **默认语义**,而平台版本是 prerelease > ⇒ **声明 `*`(任意版本)都会被判成不兼容**。实测抽样 300:`real-newer 3 | narrow-pin 22 |` > `**prerelease-artifact 117** | match 98` —— 即闸门原本约 **39% 假阳性** > (`dshmarket`、`dsh-context` 这些**在平台上跑得好好的**都在其中)。 > 只收口不修语义 = **让假拦截变多**,所以同轮加**prerelease 兜底**:先按默认语义判, > 不满足时再看 `includePrerelease` —— 容忍下能满足的**只计数不阻断**。 --- ## 一、问题①:单测"长期红"的真相(服务器源码落后) | 事实 | 判据 | |---|---| | 本机单测 | `node --test` 五个文件 → **49 tests / 48 pass / 0 fail**(1 skip) | | 本机 `test/db.test.mjs` | 断言已是新口径:`档案 87:条目口径从「互斥单选」改为「各自开关、可同时启用」⇒ 老断言整体改写`,期望 `every entry stays enabled`(5 个) | | 服务器同文件 | 仍是旧断言 `exactly one key enabled`,实际 `5 !== 1` ⇒ **失败** | | 服务器是否落后 | 用 `git ls-files src test`(60 个文件)做 **LF 归一化后的 sha256 逐文件比对**:**只有 2 个文件不同** —— `test/db.test.mjs`(服务器旧)与 `src/supervisor/orchestrator.ts`(**别人在途**,见 §四) | ⇒ **修法 = 同步该文件**(不是改代码)。同步后服务器 `bash scripts/ci.sh` → **CI OK ✅(48 pass / 0 fail)**。 > 📌 **教训(本库已在 `CODEBUDDY.md` 写过,这次又踩)**:「服务器能跑」≠「服务器源码 == git」。 > 这次的具体形态更隐蔽:**你没改的东西,服务器就是旧版** —— 所以"某个单测红"未必是代码坏了, > 先做一次**逐文件哈希比对**再动手。方法见 §五。 ## 二、问题②:伞包版本收口(含"共用版本表") ### 病根 `plugin-compat.platformPkgDir(pkg)` = `/node_modules/@deepseek-ai/<短名>`, 而**伞包 `@deepseek-ai/dsh` 不在自己的 `node_modules` 里** ⇒ `platformVersion('@deepseek-ai/dsh')` 恒 `null` ⇒ 判据 A 那句 `if (v === null) continue // 平台没这个包:不算不兼容(插件自带)` 把它放行。 **代价**(实测):官方目录里那 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`) ⇒ 一条都认不出来。 ### 修法(单一来源) - **新增** `src/web/dsh-install.ts` → `export function platformPackageVersion(pkg)`: 伞包读 `/package.json`,子包读 `/<短名>/package.json`;进程内缓存; `resetInstallPathsCache()` 一并清缓存。 - **消费方两处改为共用**:`plugin-compat.platformVersion()` 与 `plugin-dsh-compat.platformVersionOf()` ⇒ 两处不再各写一份版本表(原先 `plugin-dsh-compat` 有自己的 `versionTable`、`plugin-compat` 有自己的 `versionCache`)。 ## 三、问题③(收口带出):闸门判据 A 的 prerelease 假阳性 ### 为什么要一起修 只把伞包接进判据 A,会让**声明 `*` 的插件**第一次被拦(`satisfies('0.1.5-rc.1', '*')` 在**默认语义**下为 `false`) —— 即"修好一处,制造一批新假拦截"。所以必须把语义一并处理。 ### 实测分布(抽样 TOP300,`@deepseek-ai/*` 全量声明逐条判) | 分类 | 数量 | 例 | 处置 | |---|---|---|---| | `match` | 98 | `dsh-vision-router` | 满足 | | `none` | 60 | `@linxin666/*` | 没声明平台依赖 | | **`real-newer`** | **3** | `dsh-zotero` `dsh-any-background` `dsh-md-notes` | **真不兼容,必须拦** | | `narrow-pin` | 22 | `dsh-univer-office` `@nanmicoder/dsh-agent-teams` | 声明里只列了更旧具名版本;**仍拦**(有 trust 逃生口) | | **`prerelease-artifact`** | **117** | `dshmarket` `dsh-context` `dsh-find-plugin` | **平台效应,不该拦** | ### 修法 判据 A 逐条改成两步(`plugin-compat.ts`): ```ts ok = semver.satisfies(v, range) // ① 默认语义(与 pnpm 一致)—— 保持原判据 if (!ok && semver.satisfies(v, range, { includePrerelease: true })) { ok = true; prereleaseOnly = true // ② prerelease 兜底:**只计数不阻断** } ``` `CompatResult` 新增 `prereleaseOnly?: number` —— **不计入 `findings`、不影响 `level`**,只留可观测计数。 > ⚠️ 这一改**放宽**了上传闸门(少拦 ~39%),是本轮唯一一处"安全网变松"的改动, > 依据是**实测数据**(那 117 条里包含本平台正在运行的 `dshmarket`);判据 B(平台包导出符号缺失) > **未动**,真正的 API 破坏仍会被拦。**若认为该保持严格,把第二节的兜底删掉即可回退**(锚点在 `verify-dsh-compat.mjs`)。 ## 四、⚠️ 未动(属别人 lane,仅上报) - `src/supervisor/orchestrator.ts`(**在途**):`BASE_MEM_MB 160→448`、`MIN 384→512`、`MAX 1024→1536`、 `dsh-plugin-mcn-suite 128→256`(注释标注 2026-09-14、为 0.1.5 调);而 `poc/business-plugins/lib/client.js` 的对应常量**还是旧值** ⇒ 本机 `verify-mem-model.mjs` **4 项红**(它正是为「两份事实漂了」建的断言)。 **我没有动任何一边**:这组数字直接决定实例 cgroup 配额,是**产品决策**。 ⚠️ 注意:**提交后仓库树两边都是旧值 ⇒ 该断言在 HEAD 上自然一致**(红只出现在当前工作树)。 - 文档库 `CODEBUDDY.md` / `BRIEF.md` / `DEPLOY-本部署.md` / `skills/dsh-*` / `04-调整方案/{76,82}` 等 仍是**别人的在途改动**,本轮提交**未包含**。 - 文档库 `scripts/docs-archive-index.py` 在 Windows 上会把 `INDEX.md` 的行尾 **CR 翻倍** (见档案 91 §八 的完整记录)—— 该脚本是别人新增的,**未改**。 ## 五、方法沉淀:判断"服务器源码是否等于 git" ```bash # 本机:列出仓库里的源码文件,逐个算「LF 归一化后的 sha256」 git ls-files src test > files.txt # 服务器:同一份清单,逐个算(tr -d '\r' 抹平行尾差异) cd /opt/dshs && while read f; do [ -f "$f" ] && printf "%s %s\n" "$(tr -d '\r' <"$f" | sha256sum | cut -c1-16)" "$f"; done < files.txt # 两边 sort 后 diff ⇒ 差异**精确到文件**(本轮 60 个文件里只 2 个不同) ``` > ⚠️ 两个坑:① 两侧都要**行尾归一化**(服务器 LF / 本机可能是 CRLF); > ② `diff <(a) <(b)` 的**进程替换在本机 Git bash 里不可用**(`/dev/fd` 不存在)⇒ 落临时文件再 diff。 ## 六、验证 | 层 | 手段 | 结果 | |---|---|---| | 本机测试 | `node --test` 五文件 | ✅ 48 pass / 0 fail | | 本机 verify | `npm run verify` | ⚠️ 仅剩 §四 的 4 项(别人在途),其余**全绿** | | 本机断言 | `verify-platform-admin-section.mjs`(**本轮更新**:旧断言断的是档案 91 已替换掉的 UI) | ✅ 全绿(含模拟点击两个新增入口 + 选中目录厂家) | | 本机断言 | `verify-dsh-compat.mjs`(**新增 4 条锚点**:共用版本表 / 伞包单独解析 / prerelease 兜底 / 只计数不阻断) | ✅ 全绿 | | 服务器 CI | `bash scripts/ci.sh` | ✅ **CI OK**(48 pass / 0 fail)—— 修前 1 项红 | | 服务器功能 | 造临时插件目录**真跑闸门** | ✅ 13 项全绿:伞包/子包"要求更高"→`incompatible`;`*`/`^0.1.0-rc.7` → 不拦截且 `prereleaseOnly` 计数正确;满足→`ok`;未声明→`unknown` | | 服务器接口 | 重启后复验白名单 | ✅ `dshVersion=0.1.5-rc.1`、默认不含 `older`、`newer` 2 条、`dshCompat=all` 时 `older` 出现 | **铺发**:tar(LF)→ `/opt/dshs` → `npm run build` / `bash scripts/ci.sh` → `systemctl restart dshs`(重启后门户 200)。 备份:`/opt/dsh/backups/pre-93-20260914-210458/`(含 `test/db.test.mjs` / `src/web/{dsh-install,plugin-compat,routes/whitelist}.ts` / `package.json` / `poc/business-plugins/{lib/client.js,package.json}`)。 ## 七、回滚 - 后端:`cp -a /opt/dsh/backups/pre-93-*/` 覆盖回 `/opt/dshs/` → `npm run build` → `systemctl restart dshs`。 - 单点回退"闸门放宽":删 `plugin-compat.ts` 判据 A 里的 prerelease 兜底分支(`verify-dsh-compat.mjs` 会立刻报警)。 - 无数据迁移、无 DB 变更。