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 一律写「远程服务器」。
79 lines
7.0 KiB
Markdown
79 lines
7.0 KiB
Markdown
# 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 启用(天然就是一次"可能失败的启用")合并验收,不必单独造场景。
|
||
|