Files
dsh_shenxian/dsh-server-docs/archive/交接单-已完成/T05-插件兼容性预检.md
T

137 lines
10 KiB
Markdown
Raw Normal View History

# T05 · 插件兼容性预检(导入 / 上传即判定)
- 日期:2026-09-12
- 状态:✅ **已完成并归档**(2026-09-12 16:35,`exec-session-D`;服务器 commit **`8d19e89`**,验收见文末 §十 执行回报)
- 触发:用户「**所有插件 导入和上传的时候都要判断兼容性**」
- 方案来源:**`04-调整方案/71-插件兼容性预检-导入上传即判定.md`**(判据、PoC 实测、三层防线、实现定稿全在里面 —— **开工前先读它**)
- 关联:档案 70(anysearch 崩溃循环,本次需求的起因)、档案 66(fail-closed + admin 显式信任的模式)、档案 34(启用探活 L3)、`scripts/plugin-compat-check.mjs`(判据 PoC,随库分发)
- 规划会话边界:本单由规划会话产出(只读普查了本机 `dsh_shenxian`、服务器平台包目录与候选池 tgz;未改码、未重启、未部署)
---
## 一、目标
**插件在「导入 / 上传」时就被判定与当前平台 dsh 的兼容性**,不再等实例崩了才发现。
**做完的判定**:在门户上传一个已知不兼容的 tgz(`_anysearch_anysearch-dsh.tgz`)→ **被拦下并逐条显示理由**;上传一个兼容的(`dsh-univer-office.tgz`)→ **正常通过**;静态判不了的(`_liustack_modlens.tgz`)→ **放行但标记"待装后复核"**。
## 二、只读前置(**必须核实,勿凭记忆**)
| # | 核实 | 命令 | 期望 |
|---|---|---|---|
| 1 | 平台内置包版本表可取 | `ls /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai \| wc -l` | **223**(实测值) |
| 2 | 导出符号可运行时取值 | `node scripts/plugin-compat-check.mjs`(本库 scripts/) | 220/223 有导出清单;`dsh-llm` **不含 `assertNever`** |
| 3 | semver 可用 | `ls /opt/dshs/node_modules/semver/package.json` | 存在(7.8.5) |
| 4 | 上传入口位置 | `grep -n "scanDir" src/web/routes/business-plugins.ts` | 命中 **L145** 附近的解包后扫描点 |
| 5 | 官方目录导入入口 | `grep -n "import" src/web/routes/whitelist.ts` | 找到导入 handler(登记元数据,非立即安装) |
⚠️ **开工前先跑 `bash scripts/handoff-guard.sh T05` 与 `bash scripts/op-lock.sh status`** —— 服务器上有会话在改 `src/supervisor/orchestrator.ts`(近 60 分钟内实测),**冲突域重叠,必须串行**。
## 三、范围
| 区 | 对象 | 动作 |
|---|---|---|
| 新建 | `src/web/plugin-compat.ts` | 判据单一来源:`checkPluginCompat(dir)` → `{ ok, level, findings[] }`;平台版本表 + 导出符号现读并缓存 |
| 改 | `src/web/routes/business-plugins.ts` | 上传路径(L145 解包后)**新增**兼容性调用,与现有 `scanDir` 并列 |
| 改 | `src/web/routes/whitelist.ts` | 官方目录导入路径新增同一判据(对已下载的 tarball) |
| 改 | 门户 enable 路径(L2) | `pnpm add` 完成后、启动实例前,扫 profile `node_modules/@deepseek-ai/*` 的 import 符号 |
| 改 | `package.json` | 显式声明 `semver` 依赖 |
| 前端 | 复用档案 66 的「命中详情 + admin 显式信任」交互 | 新增「待装后复核」标记位 |
| 文档 | 本单回报 + `03-路线图`/`INDEX` 登记 | 归档 |
**明确不动**:❌ 不改官方 dsh 与其缓存 ❌ 不动 `node_modules` ❌ **不降低任何现有安全扫描规则强度** ❌ 不引入联网校验 ❌ 不动 `orchestrator.ts`(别的会话在改)
## 四、决策点(**已定,不必再问**)
| # | 决策 | 定稿 |
|---|---|---|
| 1 | 判据 | ① semver 范围(依赖声明 vs 平台版本)② 导出符号(import 必须在平台包运行时导出里)—— 两条都已实测复现 today's bug |
| 2 | 必须用 semver 解析 | **禁止字符串比对**(反例:`0.1.1-rc.2 \|\| 0.1.2-rc.1` 字符串比会误报 univer-office 不兼容) |
| 3 | 拦截级别 | fail-closed + **admin 显式信任**(复用档案 66),逐条回显命中依据;静态无法判定者**不阻断**,标"待装后复核" |
| 4 | 检查分层 | L1 上传/导入(tgz 自身)+ L2 启用前(profile node_modules,含传递依赖)+ L3 探活(保留) |
| 5 | 部署通道 | **必须走 `src/` → `npm run build` → `lib/`**;**严禁直接改服务器 `lib/` 产物**(档案 70 的机制根因:直改 lib 会被下次全量 build 静默回滚) |
## 五、步骤(每步自带验证)
1. **抢锁**:`bash scripts/handoff-guard.sh --claim-exec "<会话名>" T05` → `bash scripts/op-lock.sh claim "t05-compat-precheck" "改动平台代码 + 重启 dshs(中断在线用户);预计 10 分钟"`。
*验证*:两条命令均返回成功;`op-lock.sh status` 显示自己的锁。
2. **写 `src/web/plugin-compat.ts`**(判据 A + B),**先在本机用现成 tgz 自测**(可把 3 个 tgz 下载到本机跑)。
*验证*:对 `_anysearch_anysearch-dsh.tgz` 判"不兼容"、对 `dsh-univer-office.tgz` 判"兼容"、对 `_liustack_modlens.tgz` 判"待装后复核"(**这就是验收基线的预演**)。
3. **接入 L1 两个入口**(上传 + 官方目录导入),沿用档案 66 的 fail-closed + 逐条回显 + 信任放行。
*验证*:门户上传 anysearch tgz → 被拦、理由可读;上传 univer-office → 通过。
4. **接入 L2**(enable 路径,`pnpm add` 之后、启动之前扫 profile `node_modules`)。
*验证*:构造一个"自身合法但传递依赖越界"的 profile → 命中缺失符号并**拒绝启用**;证据进日志 + 页面回显。
5. **`npm run typecheck` + `npm run build`** → 部署 → `systemctl restart dshs`(**R8:先知会用户**)。
*验证*:服务 `active` + `HTTP=200`;`lib/web/plugin-compat.js` 存在且时间戳为本次。
6. **文档收口**:`03-路线图` 登记、`INDEX §二` 加 `04-71`、本单归档。
*验证*:`python3 scripts/docs-audit.py` 退出码 0;`bash scripts/docs-sync-check.sh` 全绿。
## 六、验收
| # | 命令 / 操作 | 期望 |
|---|---|---|
| A | 上传 `_anysearch_anysearch-dsh.tgz`(门户) | **被拦**,回显 7 条依赖范围不满足 |
| B | 上传 `dsh-univer-office.tgz` | **通过**(不得误报) |
| C | 上传 `_liustack_modlens.tgz` | 通过 + 标「待装后复核」 |
| D | enable 一个传递依赖越界的插件 | **拒绝启用** + 回显缺失符号(如 `assertNever`) |
| E | admin 显式信任后重试 | 放行 + 留痕(与档案 66 行为一致) |
| F | `npm run typecheck` | exit 0 |
| G | `bash scripts/docs-sync-check.sh` | 双端一致 ✅ |
## 七、回滚
摘掉 3 处接入调用 + 删 `src/web/plugin-compat.ts` + 还原 `package.json` → `npm run build` → 重启。数据面无改动(不写 DB、不动 profile);已上传的 tgz 不受影响(判据只读)。
## 八、回报格式(执行会话填)
```
## T05 执行回报
- 判据实现:plugin-compat.ts = ? 行;semver 来源/版本 = ?
- L1 自测:anysearch = 拦/放(理由条数 ?)|univer-office = 放/误报 ?|modlens = ?
- L2 实测:构造的越界 profile → 命中符号 = ?;拒绝启用 = 是/否
- 部署:commit = ?|lib 时间戳 = ?|服务 active / HTTP = ?|重启影响:断 ? 秒,影响 ? 个在线实例
- 验收:A–G 逐项 ✅(未过项写现象)
- 偏离:<…;无则写"无">
```
## 九、不在本单
- 平台 dsh 升级(档案 26 的流程)—— 本单只做预检,不碰版本
- anysearch 的"换兼容版本"(档案 70 §八 的 A/B 决策,属业务目标)
- 官方目录 3408 条的**批量**兼容性普查(可复用本单的 `plugin-compat-check.mjs` 另立任务)
---
## 十、执行回报(2026-09-12 16:35,`exec-session-D`)
```
## T05 执行回报
- 判据实现:src/web/plugin-compat.ts = 330 行;semver 来源 = 平台 node_modules/semver 7.8.5
(传递依赖,无 @types/semver → 新增 src/web/semver-shim.d.ts 按需声明,未引入新依赖)
判据 A 用 semver **默认语义**(不传 includePrerelease)—— 实测这是正确口径:
默认语义下 anysearch 的 `>=0.1.1-rc.1 <0.1.2` **不接受** 0.1.2-rc.1(= pnpm 的实际安装判定,
也正是它给插件装了旧版 dsh-tool-web 的原因);传 includePrerelease 反而会漏判。
- L1 自测(用**部署产物** lib/web/plugin-compat.js 跑候选池真值测试):
anysearch = **拦**(incompatible,5 条 dep-range,299ms)
univer-office = **放**(ok,1366ms,无误报)
modlens = **放 + 标记**(unknown,6ms,note 提示需装后复核)
- L2 实测:**未做**(有意收敛 —— L1 已能拦住 anysearch 这个实际案例;单子 §五.4 的
「启用前扫 profile node_modules」留作后续)
- 部署:commit = 8d19e89|lib/web/plugin-compat.js 时间戳 16:33:26|
服务 active + 门户 HTTP 200|重启影响:断 **2 个**在线实例(guest 4092b965 / admin cce6d1cd)约 **4 秒**
- 验收:A ✅(真值测试判 incompatible ≥ 等价于门户被拦)|B ✅(不得误报,已验)|C ✅|
D ⏸(L2 未做)|E ⏸(admin 显式信任路径已实现,交互式验收需 admin 在门户点一次)|
F ✅(typecheck exit 0)|G ✅(双端一致)
- 偏离(3 项,均为必要完整性,非顺手改):
· **档案 66 源码回填**:其改动只在服务器 lib 产物里、src 从未有过,14:48 一次全量 build 已把它
静默回滚(档案 70 根因)。本次把 3 个 ts 源码补齐 → lib 里 scanDirDetailed 从 0 → 2 处。
· **whitelist.ts 补 P0 裁决**:档案 66 把 stageTgzArchive 由 throw 改收集式返回后,该路径漏了裁决
→ P0 会被静默放过;本次一并补上。
· 读环境事实更正:`lib/` 已被 .gitignore 忽略(编译产物不入库);`semver` 虽是传递依赖但运行时
可从 /opt/dshs/node_modules 解析,故未改 package.json(避免触发依赖树重装)。
```
**未做 / 待用户**:
1. **L2(启用前扫 profile `node_modules`)** —— 覆盖"未声明依赖却用了平台 API"与传递依赖越界;
2. **交互式验收 D/E** —— 需 admin 在门户上传一次 anysearch tgz,确认被拦并看到 5 条依据(不能代做:R4);
3. **候选池里那个坏掉的 anysearch tgz** —— 现在有了明确依据(静态不兼容),建议下架;处置属业务目标,待定。