Files
dsh_shenxian/dsh-server-docs/04-调整方案/93-兼容判定收口-伞包版本与prerelease语义.md
T

131 lines
9.2 KiB
Markdown
Raw Normal View History

# 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)` = `<dsh 包根>/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)`:
伞包读 `<dsh 包根>/package.json`,子包读 `<scope>/<短名>/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 变更。