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 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

@@ -0,0 +1,60 @@
# 34 · 功能插件启用:探活 + 快照回滚 + 逐插件隔离标记
- 日期:2026-09-11
- 触发:用户确认「批量 + 统一重启 + 刷新 + 死循环风险」,并明确「**标记错误插件禁用并标记(让 admin 方便知晓),其余正常插件不需要回滚**」
- 状态:**✅ 已实施(commit `0283c1b`)并端到端验证通过(含「好插件保留」真实插件复核)**
## 一、暴露的既有缺陷
`/api/plugins/mine/apply` 的异步任务原本是:
```
pnpm add 各插件 → reconcileBundles → restartMain → 直接标 success
```
**全程没有任何探活** → 插件把实例搞崩(崩溃循环)时,任务照样报「✓ 已应用」,用户以为成功、实际实例起不来。档案 25 的事故(`duplicate loader entry id`)正是这一类。**排查时还发现第二个更严重的既有缺陷 → 见档案 35。**
## 二、设计(采纳用户方案)
```
① 快照 profile(package.json / pnpm-lock.yaml / cordis.patch.yml)
② 禁用项 → pnpm remove(不引入新代码,永远安全,不需探活)
③ 启用项 → 先「整批试一次」(正常路径只花 1 次重启,与旧行为一致)
探活通过 → 完成
探活失败 → ④
④ 回滚快照 → 逐插件隔离定位:能起来的保留,起不来的单独摘掉 + 记事故
结束状态 = 快照 + good 集合 ← 好插件不被坏插件连累
⑤ 事故写入 audit_log(action='plugin_incident')
⑥ 任务结果新增 rejected[],stage/error 写明「哪些已自动禁用」
```
## 三、实现
| 文件 | 改动 |
|---|---|
| `orchestrator.ts` | 新增 `restartAndProbe(userId, budgetMs=60000)` |
| `spawner.ts` | 接口加该方法 |
| `k8s-spawner.ts` | 兜底实现(k8s 未启用;返回 ok:true 保持旧行为) |
| `business-plugins.ts` | apply 任务重写 + `snapshotProfile`/`restoreProfile` |
**探活判定的两个关键点(都是实测踩出来的)**:
1. **不能只判 `restartMain` 的返回值** —— 编排器里没有该用户实例时(portal 重启清了内存态、或实例被空闲回收)`restartMain` 返回 `undefined`,会把**无辜插件**误判为不兼容。→ 改为:无实例时先按 `/api/dsh/enter` 同路径 `launch` 一个。
2. **固定短等待会把好插件判死** —— 初版用「固定 2.5s 稳定窗口」,实测好插件也报「未产出 launch token」。→ 改为**轮询**(500ms 间隔、总预算 60s),拿到 token 且 `status==='running'` 后再过 2.5s 确认没闪崩。
## 四、验证(2026-09-11,端到端)
用两个自建 fixture 插件:`good-test`(合法 bundle)、`bad-test`(`cordis.patch.yml` 插入已存在的 loader id `permission` → 必然启动失败)。
| 检查项 | 结果 |
|---|---|
| 坏插件被准确诊断 | ✅ 回传真实错误 `TypeError: duplicate loader entry id: permission`(`cordis-plugin-loader/lib/index.js:91`),非泛化文案 |
| 隔离循环执行 | ✅ 阶段依次 `整批试→定位(1/2)→定位(2/2)→恢复实例`,耗时 3s→30s→33s→42s(真实重启+探活) |
| **实例未进崩溃循环** | ✅ 全程 `instance` 存活,最终回到干净状态 |
| 快照回滚生效 | ✅ `final bundles` = 原始 5 个;`deps` 无残留 |
| 事故可追溯 | ✅ `audit_log.action='plugin_incident'` 记录 pluginId + 真实 reason |
| **好插件被保留** | ✅ **已用真实插件复核通过**(2026-09-11):从官方白名单导入真实 npm 插件 `dsh-status-rotator`,与 `bad2`(npm-pack 布局、仅 patch 有差异)一起启用 → 任务返回 `完成:1 个已启用,1 个已自动禁用`;`bundles`/`deps` 均含 `dsh-status-rotator` 且不含 `bad2`,`plugin_incident` 记录 `bad2` 的完整真实错误,实例存活。→ **「好插件不被坏插件连累」成立**。此前两轮 fixture 失败纯属 fixture 自身问题(平铺 tar 布局 pnpm 不接受 / 包名与 patch 引用不一致)。 |
## 五、后续
1. ~~用真实插件复核「好插件保留」~~ ✅ **已完成(2026-09-11)**:白名单导入 `dsh-status-rotator` + `bad2` 一起启用 → 好插件保留、坏插件被摘并标记。
2. **admin 可见性**:事故已入 `audit_log`,但**读取/聚合并未实现** —— 门户「插件管理」页应显示「⚠ N 个用户启用失败」。需要新增一个 audit 读取方法(adapter 目前只有写)。
3. **重启前预检**:应用后、重启前跑 `dsh --profile web --dump-config`,可 0 重启拦住配置类错误(未做)。
4. **前端**:`location.reload()` 与被禁用插件的展示,随批次 3(client bundle 改造)一起做。