Files
dsh_ai1net_server/dsh-server-docs/04-调整方案/70-anysearch插件不兼容致崩溃循环.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

126 lines
9.6 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.
# 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 的预期结果。