# 79 · 插件启停的两处平台缺陷:`uninstall` 缺 `await`(把整个 orchestrator 带崩)+ `pnpm remove` 用了无效 flag(2026-09-13 发现) - 日期:2026-09-13 - 状态:✅ **已修复并部署**(2026-09-13 13:4x 逐条核实产物);仅剩**可选的**「加固 b 半应用态主动自愈」未做 —— 见 §七 - 触发:用户在 guest 的「功能管理」启用 `dsh-plugin-mcn-suite` → **安装失败**(2026-09-13 07:4x) - 影响:**P0** —— 任一次插件启停失败都可能**终止整个 `dshs` 进程**(全体用户瞬断 + 所有实例被停),并把目标 profile 留在**半应用态**(该实例此后每次启动即崩) > **TL;DR**:D1(缺 `await`)→ 未捕获 Promise 拒绝 → orchestrator 退出;D2(无效 flag)→ 让 D1 必然被触发;D3(内存不够)→ 让流程必然走进触发 D1 的分支。 ## 一、现象链(journalctl 原文,逐条可核) | 时刻 | 事件 | |---|---| | 07:41–07:43 | guest 装上 mcn-suite 后反复 `[dsh-plugin-mcn] 榜单查询失败: unable to open database file` | | 07:43:28/46/56 | 实例侧 `FATAL ERROR: Reached heap limit`(`Mark-Compact 155.3(169.1) -> 153.9 MB` 等)→ `exitCode 134`;`[crash-restart]` 触发 | | 07:43:58 | `Error: Command failed: … setpriv … pnpm remove dsh-plugin-mcn-suite --ignore-workspace-root-check …`;stderr 解出 **`ERROR Unknown option: 'ignore-workspace-root-check'`**;栈 = `runPnpmAs(…:406) ← uninstall(…:575) ← business-plugins.js:614` | | 07:43:58 | **`dshs.service: Main process exited, code=exited, status=1/FAILURE`** → systemd 4 秒后自动重启(07:44:02) | | 07:45–07:46 | 服务重启后 guest 继续被拉起 → 每次都 OOM → attempt 1..5 → **`crash-loop-circuit-open`**(熔断);两个实例均 inactive | ## 二、三个缺陷 | # | 级别 | 位置 | 内容 | |---|---|---|---| | **D1** | **P0** | `src/web/routes/business-plugins.ts:682`(部署版 `business-plugins.js:614`) | 隔离定位循环里写的是 `try { uninstall(sel.id) } catch { /* 尽力而为 */ }` —— **缺 `await`**。`uninstall` 是 `async`,其 rejection 逃出**同步** `try/catch` ⇒ **unhandled rejection ⇒ 进程退出**(Node 默认 `--unhandled-rejections=throw`) | | **D2** | P1 | 同文件 `:644`(部署版 `:575`) | `runPnpmAs(user.id, ['remove', id, '--ignore-workspace-root-check', '--reporter', 'silent'], dir)` —— 该 flag **只存在于 `add` 子命令**,`remove` 不认(pnpm 9.15.9 报 `Unknown option`,提示 `ignore-workspace`)⇒ **禁用路径必然失败**,进而**必然触发 D1** | | **D3** | P1 | 容量 | 实例 V8 堆限 160 MiB 装不下 `dsh-univer-office` + `dsh-plugin-mcn-suite`(隔离 cgroup 实测分别 **+63.7 / +38.5 MiB rss**)⇒ 探活失败 → 平台进「隔离定位」分支 → **必然走到 D1/D2** | ## 三、连带后果(本身也是缺陷) - 因进程直接退出,平台兜底的 `restoreProfile()`(在 `try/catch` 里)**没跑** ⇒ profile 停在**半应用态**(mcn-suite 已在 `dependencies` + `bundles`)⇒ 该实例此后**每次启动都 OOM**(熔断循环),用户彻底不可用。 - 本次处置:手工回滚 guest profile(从 deps + bundles 移除 mcn-suite,备份 `package.json.bak-rollback-20260913T0748`)→ 触发一次 `enter` 验证实例恢复(`dsh web:` 正常、`active running`、cgroup **186.5 / 384 MiB**、无 crash-restart)。 ## 四、修法(已备好,未落地) 1. **D1**:`try { uninstall(sel.id) } catch {}` → `try { await uninstall(sel.id) } catch {}`(**一行**)。 2. **D2**:`remove` 的参数改成 `['remove', id, '-w', '--reporter', 'silent']`(或直接去掉 `--ignore-workspace-root-check`)。 ⚠️ `add` 的 `--ignore-workspace-root-check` 是**合法** flag,**不要一起改**。 3. **加固(建议同批)**: - 路由最外层 `void (async () => {…})()` 补 `.catch()`(双保险,避免同类遗漏再崩进程); - 启动/应用前的**半应用态自愈**:检测「`bundles` 里有包但 `node_modules` 装不全」→ 自动摘除或至少审计告警。 4. **验收**:修复后在任一 profile 上重现「启用一个注定失败的插件」→ 期望 = **任务 `failed` + profile 回滚 + 服务不退出**。 ## 五、红线与影响 - 改码 + `npm run build` + `systemctl restart dshs` → **中断在线用户数秒** ⇒ 走 **R8 先知会**。 - 回滚:单文件 `git` 回退 + 重新 build + 重启;服务本身有 systemd 自动重启兜底。 ## 六、相关 - 档案 65(功能插件启停 ↔ web provider 联动)|档案 16 §2.2(危险操作确认弹窗)|档案 75 维度 B(资源成本,与 D3 同源)|档案 74(V8 堆限 256→160,D3 的背景)。 --- ## 七、修正(2026-09-13 13:4x)· **D1 / D2 / 加固 a 已落地并部署** > 触发:用户问「还有待处理的事项吗」时复核本条,发现**状态已过期** —— 三个缺陷中有两个半已经修完并上线。 **逐条实测(部署产物 `lib/web/routes/business-plugins.js`,只读核对)**: | 项 | 期望修法(§四) | 产物实测 | 结论 | |---|---|---|---| | **D1** 缺 `await` | `try { await uninstall(…) } catch {}` | `:587` 与 `:617` 均为 `await uninstall(sel.id)` | ✅ **已修** | | **D2** 无效 flag | `remove` 去掉 `--ignore-workspace-root-check` | `:575` = `['remove', id, '-w', '--reporter', 'silent']`;全仓该 flag **仅剩 `add` 一处**(合法) | ✅ **已修** | | **加固 a** 外层 `.catch()` | 路由最外层补 catch | `:613` `void (async () => {` … `:718` `})().catch((err: unknown) => {` | ✅ **已修** | | **加固 b** 半应用态主动自愈 | 检测「`bundles` 有包但 `node_modules` 装不全」→ 自动摘除/告警 | 只有**失败路径**的 `restoreProfile()`(`:567` / `:671` / `:710`)回滚到快照 | ⚠️ **未做**(可选加固,非 P0) | **D3 已消解(不再需要窗口)**:D3 的成因是「V8 堆限写死 160 MiB + `MemoryMax` 写死 384 MiB 装不下 univer + mcn-suite」。**档案 81 · R1-④ 已把两者改成「按插件集合动态计算」**(`instanceMemMb()` / `heapMbFor()`)。2026-09-13 13:3x 实测运行中实例: ``` guest(uid 100002) NODE_OPTIONS=--max-old-space-size=256 scope MemoryMax=544 MiB admin(uid 114801) NODE_OPTIONS=--max-old-space-size=256 scope MemoryMax=384 MiB 平台进程 env 仍有 DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160 ← 已被代码侧 withHeap() 覆盖,不再是生效值 ``` ⇒ **§五「需 R8 窗口重启」的前提已不成立**:不再需要为修复本条而重启。 **仍未闭环的一项**:§四.4 的端到端验收(**在某个 profile 上重现「启用一个注定失败的插件」→ 期望 任务 `failed` + profile 回滚 + 服务不退出**)**尚未跑过**。代码层面三个修法都已就位,但「服务真的不再被带崩」目前是**代码判据**、不是**实测判据**。建议与 T03 的 guest 启用(天然就是一次"可能失败的启用")合并验收,不必单独造场景。