Files
dsh_shenxian/dsh-server-docs/04-调整方案/79-插件启停的两处平台缺陷-uninstall缺await与无效pnpm-flag.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

7.0 KiB
Raw Blame History

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 启用(天然就是一次"可能失败的启用")合并验收,不必单独造场景。