Files
dsh_shenxian/dsh-server-docs/archive/交接单-已完成/T05-插件兼容性预检.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

138 lines
10 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.
# 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** —— 现在有了明确依据(静态不兼容),建议下架;处置属业务目标,待定。