Files
dsh_shenxian/dsh-server-docs/04-调整方案/70-anysearch插件不兼容致崩溃循环.md
T

125 lines
9.6 KiB
Markdown
Raw Normal View History

# 70 · anysearch 插件与 dsh 0.1.2-rc.1 不兼容 → admin 实例崩溃循环(已止损)
- 日期:2026-09-12
- 触发:用户报障「界面上实例正在启动,计数一直 01 01 01」+「正在定位问题插件(1/2):`@anysearch/anysearch-dsh`(66s)」
- 结论一句话:**插件在 `import` 阶段就抛错**(`@deepseek-ai/dsh-llm` 未导出 `assertNever`)→ dsh 的 plugin tree 加载是**全或无** → 整个启动中止、进程退出 → 平台崩溃自愈反复重启(指数退避,5 次/10 min 逼近熔断)。**摘掉该 bundle 即恢复**;与 provider 配置、权限、网络**都无关**。
- 状态:✅ **已止损**(2026-09-12 15:32 起实例恢复)|⚠️ **待决**:AnySearch 这个能力是否保留(见 §八)
- 关联:档案 64(AnySearch 接入 · §9 曾预警「走候选池会出问题」)、档案 65(provider 托管段,**未部署**)、档案 20(崩溃自愈熔断)、档案 68(候选池启停链路)
---
## 一、现象
| 观察 | 用户侧 | 服务侧 |
|---|---|---|
| 实例启动卡住 | 「正在定位问题插件(1/2):`@anysearch/anysearch-dsh`(66s)」 | 每次重启都在同一插件抛错 |
| 进度计数不动 | 「计数一直 01 01 01」 | 启动在**插件加载阶段**中止 → 无后续进度可报 |
| 反复重启 | 页面持续停在启动态 | `[crash-restart]` 退避 1000→2000→4000→8000ms,`restartsInWindow` 1→4 |
## 二、根因(journald 原文)
```
Error: dsh: plugin tree failed to load: failed to apply loader entry include (cordis:include):
failed to import loader entry web-search-anysearch (@anysearch/anysearch-dsh):
The requested module '@deepseek-ai/dsh-llm' does not provide an export named 'assertNever'
...
at .../@deepseek-ai/dsh-tool-web/lib/index.js:5
import { assertNever } from "@deepseek-ai/dsh-llm";
```
**链条**:`@anysearch/anysearch-dsh` → 依赖 `@deepseek-ai/[email protected]` → 该包 `import { assertNever } from "@deepseek-ai/dsh-llm"` → 平台侧 `@deepseek-ai/dsh-llm`(随 dsh **0.1.2-rc.1**,官方包零改动)**没有这个导出** → ESM 实例化失败 → `plugin tree failed to load` → 进程 `exitCode: 1`。
**定性**:**插件与平台的版本兼容性问题**(API 面不匹配),不是配置错、不是权限问题、不是网络问题。
## 三、止损(2026-09-12 15:31–15:32,exec-session-C)
| 步 | 动作 | 证据 |
|---|---|---|
| 1 | 备份 profile 配置 | `home/profiles/web/package.json.bak-anysearch-<ts>`(cp -a) |
| 2 | 从 **`dsh.profile.bundles`** 与 **`dependencies`** 摘掉 `@anysearch/anysearch-dsh` 两处 | `python3 -m json.tool` → **JSON 合法**;两处均无 anysearch 残留 |
| 3 | 等平台自愈下一次拉起(无需人工重启) | 15:31:53 起**无新 `crash-restart`** |
| 4 | 验证实例健康 | `dsh web: http://127.0.0.1:34197/?token=…` 已打印;`curl` 本地 → **HTTP 401**(服务在跑、待鉴权 = 正常);scope `dsh-114801-f798a574.scope` **active** |
**未做(有意)**:未重启平台服务、未动 guest 实例(`4092b965` 全程正常)、未删 `.credentials.yaml` 里的 `ANYSEARCH_API_KEY`(留着便于换版本后复用)。
**踩坑留痕**:本轮前两次尝试用 `ssh '<内联 script>'` 拼复杂引号 → **命令被引号规则击穿**(第 3/4 次同类错误)。**再次确认技能里的硬性要求:复杂引号一律「本地写文件 → scp → 执行」**,勿在 ssh 里内联拼引号。
> ⚠️ 兼容性 **不是** provider 配置问题:`cordis.patch.yml` 里**没有** anysearch 托管段,所以**档案 65 的部署救不了这个故障**(65 解决的是「provider 指向缺失」,这里是「插件本体 import 失败」)。
## 四、为什么「自动拉起」救不了它(回答用户问题 1)
**能拉起,但不能自愈**:
- 平台的崩溃自愈**确实在工作** —— `[crash-restart]` 指数退避(1s→2s→4s→8s,上限 30s)+ 窗口熔断(10 分钟内 5 次 → `crash-loop-circuit-open`,之后停止自动重启等人工介入)。
- 但**重启的前提是「偶发故障」**。这里是**确定性失败**:配置里那个插件每次都会在同一个位置抛错 → 每次拉起都崩 → 一路撞到熔断。
- 所以判据是:**「重启后错误是否变化」**。错误一模一样 = 配置/兼容性问题,重启无用;错误随机或只出现一次 = 才可能是自愈能救的场景。
## 五、为什么进度卡在「01」(回答用户问题 2)
- 实例启动分两大段:**(a) 平台侧启动 + 探活** → **(b) dsh 进程内 plugin tree 加载**。用户看到的「正在定位问题插件(1/2)」属于 (b) 的**逐个加载**阶段。
- dsh 的 plugin tree 加载**不做部分成功**:任一条 entry `import` 失败 → `plugin tree failed to load` → **整棵树放弃**、进程退出。所以第 1 个插件就失败时,**永远走不到第 2 个**,进度计数自然停在 `01`。
- 「66s」是平台侧探活等待超时(等不到实例 HTTP 就绪)。
## 六、红线遵守
- **R8(中断用户须先知会)**:本次影响面 = **仅 admin 实例 `cce6d1cd`,且它当时已在崩溃循环、本就不可用**;**未重启平台服务、未动 guest 实例**。修复后该实例可用性**由不可用变为可用**。
- **R7(禁批量)**:只改 1 个用户的 1 个配置文件(2 行);改动前 `cp -a` 留回滚点。
- 服务器侧操作锁:已持有 `fix-anysearch-crashloop`(`/opt/dsh/state/.op-lock/`),完工释放。
- 未 push 任何仓库;未改平台代码。
## 七、回滚
```bash
P=/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde/home/profiles/web
cp -a $P/package.json.bak-anysearch-<ts> $P/package.json # 恢复含 anysearch 的版本(会再次崩溃循环,仅在需要复现时用)
```
停实例:`systemctl stop dsh-114801-*.scope`(下次访问自动拉起)。
## 八、⚠️ 待决(需用户定:这是业务目标,不是技术选型)
**问题本身是业务边界**:AnySearch 这个能力**还要不要**?两条路:
| 选项 | 做法 | 代价 |
|---|---|---|
| **A. 保留能力,换兼容版本** | 找一个与 dsh `0.1.2-rc.1` 的 `@deepseek-ai/dsh-llm` 兼容的 anysearch 版本(或让插件侧不依赖 `[email protected]`);先在**测试用户**上验证再投放 | 需要版本调研 + 一轮验证;期间候选池里的 tgz 仍是坏的 |
| **B. 下架** | 把 `_anysearch_anysearch-dsh.tgz` 从候选池下架(门户 `#/plugins`),平台继续用 DeepSeek 官方搜索(现状本来就够用) | 零风险;放弃 AnySearch 的搜索质量/成本收益 |
**在决定之前有一个必须做的动作(建议立刻做)**:候选池里 `_anysearch_anysearch-dsh.tgz` **对任何用户都是坏的** —— 谁点启用谁崩实例(`business-plugins` 候选池是「全员可见」的)。建议先下架该条,再慢慢评估 A/B。
**另附**:`.credentials.yaml` 里 `ANYSEARCH_API_KEY` 是**明文**。若选 B,建议连它一起删(删 3 行,见档案 64 §8.4);若选 A,保留待复用。
---
## 九、✅ 处置决定与执行(2026-09-12 18:46–18:56)
### 9.1 用户决定:**B —— 彻底放弃 AnySearch**
(承接 §八 的 A/B 待决;用户 2026-09-12 明确选 B。)
### 9.2 执行(持文档库全局锁 + 服务器侧操作锁;**未重启服务、未中断任何在线用户**)
| 步 | 动作 | 证据 |
|---|---|---|
| 1 | **只读核查**:候选池条目 / 凭据 / 各用户 profile 残留 | 池内 `_anysearch_anysearch-dsh.tgz`(11:16);**无任何用户 profile 引用 anysearch**;`/opt/dsh/users/main/.dsh/.credentials.yaml` 只匹配到 `client-connection` / `browser-session` → **`ANYSEARCH_API_KEY` 已不存在**(§8.4 的"删 3 行"因此无需执行) |
| 2 | **下架候选池条目**(走平台 admin API,非手工改库) | `DELETE /api/plugins/business/%40anysearch%2Fanysearch-dsh` → **HTTP 200 `{"ok":true}`**;`audit_log` id **175** `delete_business_plugin` |
| 3 | 复查 | DB `business_plugins` 无该行;池内 tgz 已移除;残留计数 **0** |
| 4 | 清临时 session(R4) | `user_agent='poc-curl2'` 的临时 session 已清 |
**下架姿势说明**:用 `mksess.cjs` 造**临时 admin session**(600 s)→ `curl -X DELETE` 打 `127.0.0.1:3080` → 用完即删。
**刻意不手工改 SQLite** —— 走 API 才会同时落到 `audit_log`,并顺带执行 `PKG_NAME_RE` 校验与 `tgzPath` 一致性检查。
### 9.3 同类排查(顺手做,未扩大范围)
用**档案 71 已部署的判据模块**(`/opt/dshs/lib/web/plugin-compat.js` → `checkPluginCompat()`)对池内**存量**两条做静态体检
(预检只覆盖"新导入",**覆盖不到存量** —— 这是本次顺带补上的盲区):
| 条目 | 体检结果 | 处置 |
|---|---|---|
| `dsh-univer-office` | **level = `ok`**(`sawPlatformRef=true`、无 findings) | **保留**(池内现存唯一条目) |
| `@liustack/modlens` | **level = `unknown`**(不声明、也不 import 任何 `@deepseek-ai` 包 → 静态判不了) | ⚠️ 且 audit 有它的 `plugin_incident`(与 anysearch 同一批)→ **AI 自主决定一并下架**(代价不对称:下架可逆、保留=谁点谁崩);tgz 已备份 `/opt/dsh/backups/plugin-pool-20260912/_liustack_modlens.tgz.20260912-185302`;`audit_log` id **176** |
### 9.4 现状与回滚
- 候选池现存 **1 条**:`dsh-univer-office`(已验兼容)。
- **回滚任一**:把备份 tgz 经门户「上传」重新导入候选池即可(DB 行随之重建)。
- 平台搜索能力:**继续用 DeepSeek 官方搜索** —— 即 §八 选项 B 的预期结果。