dsh-autorestart 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +465 -40
  3. package/assets/icon-9c245e5b.ico +0 -0
  4. package/assets/icon-rescue-1902bb15.ico +0 -0
  5. package/assets/icon-rescue-1902bb15.png +0 -0
  6. package/client.js +3002 -170
  7. package/cordis.patch.yml +12 -0
  8. package/docs/README.zh.md +231 -23
  9. package/docs/platform-matrix.md +171 -55
  10. package/docs/real-machine-verification-0.8.6.md +202 -0
  11. package/index.js +13272 -8513
  12. package/lib/autostart-advice.js +31 -2
  13. package/lib/backup.js +2768 -21
  14. package/lib/checkpoint.js +214 -7
  15. package/lib/cookie-clear.js +260 -0
  16. package/lib/lifecycle.js +23 -40
  17. package/lib/plugin-compare.js +499 -0
  18. package/lib/plugin-failure.js +382 -0
  19. package/lib/plugin-recover.js +1557 -0
  20. package/lib/plugin-watch.js +213 -0
  21. package/lib/rescue-paths.js +982 -56
  22. package/lib/restart-phase.js +5 -0
  23. package/lib/restart-verdict.js +379 -0
  24. package/lib/restart.js +244 -7
  25. package/lib/scheduler-capability.js +19 -3
  26. package/lib/sleep.js +190 -29
  27. package/lib/watchdog-disarm.js +46 -2
  28. package/lib/watchdog-schedule.js +834 -17
  29. package/lib/watchdog-tier.js +8 -1
  30. package/lib/watchdog.js +1222 -47
  31. package/locale/en.json +175 -68
  32. package/locale/zh.json +175 -68
  33. package/package.json +3 -2
  34. package/screenshots.json +6 -6
  35. package/scripts/README.md +6 -3
  36. package/scripts/build-icon.mjs +998 -0
  37. package/scripts/clear-dsh-auth-cookies.mjs +257 -0
  38. package/scripts/emergency-disable.sh +69 -2
  39. package/scripts/emergency-toggle.ps1 +85 -69
  40. package/scripts/rescue-strings.zh.json +36 -18
  41. package/scripts/restore-profile.ps1 +182 -1
  42. package/scripts/watchdog.mjs +432 -79
package/cordis.patch.yml CHANGED
@@ -60,12 +60,24 @@
60
60
  backupIncludePlugins: true
61
61
  # Keep at most this many snapshots; older ones are deleted automatically.
62
62
  maxBackups: 2
63
+ # THE PLUGIN WATCH: after the host has settled, take one reading of what
64
+ # the loaded plugins actually provide (tools, interface modules,
65
+ # registered packages) and compare it with the previous reading. It
66
+ # reports only what is NO LONGER there; a newly installed plugin is not
67
+ # news and is never reported.
68
+ watchEnabled: true
69
+ # How long (ms) the watch waits after boot before it reads, so plugins
70
+ # that load late are not mistaken for plugins that disappeared.
71
+ watchDelayMs: 8000
63
72
  # The WATCHDOG: the process that waits for the relaunched host and
64
73
  # puts the snapshot back when it never answers. It has to be triggered
65
74
  # LATER, and the machine decides how (a one-off Windows scheduled task,
66
75
  # a systemd user timer, or a detached process of our own). Set this to
67
76
  # false to switch the automatic rollback off entirely; the settings
68
77
  # page then says that there is no automatic rollback.
78
+ # ON by default: during a restart it watches the new version and rolls the
79
+ # backup back when that version never comes up. The user is asked once
80
+ # whether to keep it, and may answer no.
69
81
  watchdogEnabled: true
70
82
  # How long the detached watcher waits for the relaunched host (ms) before
71
83
  # it restores the snapshot. The watcher NEVER launches a second copy of
package/docs/README.zh.md CHANGED
@@ -32,6 +32,41 @@
32
32
  | Agent 有一个持久 goal(`create_goal`)跨重启 | goal 的 activation 是进程内的,重启后变回 `disarmed`,自动续跑停止 | 启动时重新 `goals.resume()`,goal round driver 立刻开始下一轮 |
33
33
  | Agent 想装/升级插件但"重启必打断" | 只能等或赌 | 调 `continuum_restart`:等所有 agent idle(当前这一轮跑完)再重启,回来自己接着做 |
34
34
 
35
+ ## 支持范围(Supported platforms)
36
+
37
+ 一句话判断:**你属于下面哪一种环境,以及有没有人真的在那上面跑过?**
38
+
39
+ | 你的机器 | 真机跑过 | 你需要知道什么 |
40
+ | --- | --- | --- |
41
+ | **Windows 桌面**(DeepSeek Harness 客户端) | **跑过 —— 已验** | 不用配任何东西 —— `restartMode: app` 就是默认值 |
42
+ | **WSL 上的 `dsh web`,有 `systemd --user`** | **跑过 —— 已验** | 必须自己设 `restartMode: command`,**并且**给一条真能重启这个宿主的 `restartCommand`;注意那个关机按钮关的是**整台 Windows**,不是那个 WSL 发行版 |
43
+ | **WSL 上的 `dsh web`,没有 `systemd --user`** | **还没跑过** | 配置与上一行相同;自动回滚退化成插件自己拉起的脱离进程 |
44
+ | **Windows 上的 `dsh web`** | **还没跑过** | 配置与上一行相同 |
45
+ | **Linux 上的 `dsh web`**(不是 WSL) | **还没跑过 —— 这里也没有这样的机器** | 配置与上一行相同 |
46
+
47
+ 最后三行**代码写了、单元测试也盖住了,但没有人真在那种宿主上跑过**:它们走的是与已验 WSL 行同一
48
+ 条代码路径(`restartMode: command` + 你自己的重启命令),可是「应该能行」不等于「验过了」,所以
49
+ 不写成支持。**有 / 没有 `systemd --user`** 只影响**自动回滚用哪种机制**(systemd user 定时器,还是
50
+ 插件自己的脱离进程),其它能力一样。设置页永远会告诉你这台机器拿到的是哪一种,也绝不会承诺一个
51
+ 它给不了的自动回滚。
52
+
53
+ ### 范围外:未实测 / 不支持
54
+
55
+ 下面三类**不在**支持范围内。插件装上去可能「看着能跑」,但没有任何承诺:没有真机验证,
56
+ 有些能力根本给不了。
57
+
58
+ | 环境 | 状态 | 你会失去什么 |
59
+ | --- | --- | --- |
60
+ | **macOS** | **未实测 / 不支持** | 没有桌面救援快捷方式;自动回滚退化成插件自己的后台进程(机器上没有 `setsid` 时进一步退化成**完全没有自动回滚**);关机倒数从未在真 Mac 上验过 |
61
+ | **远程 SSH 无图形环境** | **未实测 / 不支持** | 没有桌面、没有桌面救援入口,唯一出路是 `.sh` 脚本 —— 也就是说你得先能 SSH 上去;「开机自启建议」拉起的是**宿主**,不是你的 SSH 会话;会话续接只在**有人**把宿主重新拉起来之后才发生 |
62
+ | **容器**(Docker / Compose / Kubernetes) | **未实测 / 不支持** | 没有「开机自启」这回事(由编排器负责);自动回滚退化成插件自己的后台进程(精简镜像里常常没有 `setsid` → **没有自动回滚**);关机倒数**不认识容器** —— 它真的会去关这台主机,所以别在容器里按;容器重建后检查点与备份可能一起消失 |
63
+
64
+ 如果你就在这三类环境里:当实验装、风险自负;出问题请用 `scripts/restore-profile.sh` /
65
+ `scripts/emergency-disable.sh` 自救。
66
+
67
+ 完整的能力矩阵(每种能力 × 每个环境,每一格都带它依据的 `文件:行号`)见
68
+ [platform-matrix.md](platform-matrix.md)。
69
+
35
70
  ## 三分钟看懂(普通用户读这一节就够)
36
71
 
37
72
  ### 重启为什么不会丢活
@@ -42,7 +77,7 @@
42
77
  2. **排队等所有会话空闲** —— 谁还在跑就等谁,等齐了才动手;
43
78
  3. **换新进程** —— 老进程退出,新进程起来(桌面版由应用自己拉起自己;Web 托管端见下一节);
44
79
  4. **新进程自检** —— 起来之后核对四项:端口通了没、路由齐了没、插件加载了没、会话恢复完没;
45
- 5. **没起来就回滚** —— 自检没过,就把重启前的快照(配置 + 已装插件)放回去,并且把这次重启的结论如实写在状态里。
80
+ 5. **没起来就回滚** —— 自检没过,就从**身份为 `known-good` 的那一份**快照(配置 + 已装插件)放回去,并把这次重启的结论如实写在状态里。**没有任何一份带这种身份时,它不会拿「最新的一份」顶上**,而是如实报告「没有带身份的可还原点」。
46
81
 
47
82
  所以"重启丢活"变成了"重启后接着干":你不用再补一句"继续"。
48
83
 
@@ -110,7 +145,7 @@
110
145
 
111
146
  适合:经常因为装插件、升级、应用崩溃而丢掉正在进行的长任务,希望重启后自动接续的人。
112
147
 
113
- 不适合:无法接受应用窗口关闭再重开的人;非 Windows 桌面且未配置 restartMode 为 command 的 web 部署(重启功能不可用);正在跑关键任务的 profile(建议先在测试 profile 验证)。
148
+ 不适合:无法接受应用窗口关闭再重开的人;web 部署但没配 `restartMode: command` + `restartCommand`(重启功能不可用);**macOS、远程 SSH 无图形环境、容器**(见上面「[支持范围](#支持范围supported-platforms)」:未实测、不支持);正在跑关键任务的 profile(建议先在测试 profile 验证)。
114
149
 
115
150
  > **包名说明**:原先的包名在公共 npm registry(npmjs.com)上已被其他作者占用,因此本包更名为 **dsh-autorestart**。
116
151
 
@@ -143,12 +178,16 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
143
178
 
144
179
  - **每次重启前**先给 profile 拍快照:`cordis.patch.yml`、`package.json`、`pnpm-lock.yaml`,以及(默认开启)已安装插件目录 `node_modules`。本机实测:**2.76 秒 / 117.6 MB / 5,562 个文件**。
145
180
  - 快照放在 `storages/dsh-autorestart/backups/<时间戳>/`,带 `manifest.json`;**只保留最新 2 份**,写新快照时自动删旧的。
181
+ - **链接安装的插件现在也进备份**(2026-10-30 实测缺口):本机有 5 个插件是用 `link:` / 联接装的(`dsh-autorestart` 自己就在里面),旧逻辑只把**链接**拷进快照、链接背后的内容一个字节都没有 —— 于是「负责救命的那个插件」自己也救不回来。现在 `manifest.json` 多了 `links: [{name, target, kind, exists, readable, copied, files, bytes}]`(外加 `linkFiles` / `linkBytes`),每个**已启用**链接插件的内容另存 `node_modules-link/<name>/`(本机这五个:**1,216 个文件 / 69.5 MB**)。还原时逐个插件给一行结论:`kept`(目标还在,链接保留,开发目录照旧「改完即生效」)、`replaced`(用备份内容顶替)、`target-missing`、`no-backup`;`restore-profile.ps1 -ForceLinkPlugins` 可以无条件全顶替;看门狗用同一套判定(`decideLinkRestore`),但**只给链接一次机会**(`attempts >= 2` 就顶替)。
146
182
  - 看护脚本跑在**宿主进程之外**(所以宿主被杀它也活着),等**新实例**最多 **90 秒**(`rollbackTimeoutMs`):新实例把自己的端口和 boot id 写进 `access.json`,看护脚本据此找到**新实例**的健康端点(桌面版每次 `--port 0` 端口随机,旧端口没有意义)。正常回答就静默退出,**什么都不动**。
147
183
  - 宿主**一直不回应**时:把快照拷回去 → 写 `last-rollback.json`(含"本次重启之后"的崩溃日志尾部,旧日志不会被误当证据)→ **不会再拉起第二个宿主**(已经起来的那一个由它自己继续,回退分支绝不 `Start-Process` 第二个 app)。
148
184
  - 拷回本身也有**时间上限**(`rollbackCopyTimeoutMs`,默认 120 秒/步):拷不完就放弃,并把原因写进报告。
149
185
  - **托管模式(`restartMode: command`)也有同一张安全网**:重启命令通常会把插件自己所在的进程一起杀掉,所以看护脚本在命令**之前**就启动了,由它来确认新实例有没有起来。命令起不来或以非零码退出时,插件**如实报错**(带退出码和输出)并把快照拷回去,绝不静默当成功。
150
186
  - 下次启动时 `continuum_status` 会报 `lastRollback`(时间、等了多久、是否回滚、**失败原因**、快照路径、崩溃日志名与尾部摘要)。
151
- - 开关:`backupOnRestart`(默认 `true`)、`backupIncludePlugins`(默认 `true`,关掉就只备配置文件)、`maxBackups`(默认 `2`)、`rollbackTimeoutMs`(默认 `90000`)、`restartCommandTimeoutMs`(默认 `30000`)、`rollbackCopyTimeoutMs`(默认 `120000`)。
187
+ - **回滚目标是「有一次启动跟在它后面的那份」,不是「最新的一份」**(用户决定,2026-10-30)。每份备份旁边有一张 `meta.json`,**它只记事实**:什么时候拍的(`takenAt` / `at`)、由什么动作触发(`trigger`——`first-install` / `pre-restart` / `manual-rollback`)、这份是多少版本(`version`)、内容指纹(`contentFingerprint`)、以及那次启动的结果(`boot`:`started` 主机起来没有、`contentMatched` 跑的是不是这份内容、`loadFailures` 有没有插件加载失败)。**身份不写在磁盘上**:`known-good` / `snapshot` / `unknown` 是宿主**读取时按这些事实派生**的——有启动记录(或它是首装那份)→ `known-good`;后面还没有任何一次启动 → `snapshot`;读不到 `meta.json`、旧记录里也没有可用的启动记录 → `unknown`。**名字和「能不能被自动还原」是两道判据**:名字只看有没有启动记录;能不能被自动还原是更窄的第二道——那次启动**起来了**(`started`)**且跑的就是这份内容**(`contentMatched`),所以叫 `known-good` 不等于能自动还回去。这个身份记的是「拍完之后发生了什么」,**不是**对里面插件能不能用的评价。**这次重启前拍的那份永远不是回滚目标**,因为它可能正是坏掉的那一份(「改坏了插件再点重启」就是这个场景),要等后面那次启动证明过它才算数。第一次安装时会种子一份首装副本(`trigger: first-install`);每次启动后(四项自检全过,或托管模式探活答了、且跑的就是这份内容)把那次重启前的快照记上 `boot`。
188
+ - **没有可回滚的身份就一份都不还原**:找不到最新的 `known-good`(即上一次启动成功之后回写的那份)时,回滚什么都不做,报告里写 `no-known-good: <原因>`,profile 保持失败重启后的原样,看门狗也**不挂**(没有可还原的目标,挂上去只会再把坏配置写一遍)。
189
+ - **可以手动回退到任意一份备份**(含更早、后面没有启动记录的那份,用户决定 2026-10-30):设置页每行都有「回退到这一份」,点了先弹**二次确认**(写清还原哪一份、当前状态会被覆盖、还原后会自动重启),确认后才动,取消则什么都不发。服务端 `POST /__dsh_autorestart/rollback-to` 只收**备份目录名**(必须是本机列表里的那一份,路径 / 路径穿越直接拒绝),顺序是先存一份当前快照(`reason: before-manual-rollback`,所以这次回退本身还能再退回来)→ 再还原选中的那份 → 写 `last-manual-rollback.json` 回执 → 走既有重启流程请求重启(插件自己绝不 kill 进程);当前状态存不下来、或看门狗正持有回滚锁时直接拒绝(409),不会出现还原了一半。
190
+ - 开关:`backupOnRestart`(默认 `true`)、`backupIncludePlugins`(默认 `true`,关掉就只备配置文件)、`maxBackups`(默认 `3`:当前一份 + 之前两份;身份为 `known-good` 的那一份永远不删)、`rollbackTimeoutMs`(默认 `90000`)、`restartCommandTimeoutMs`(默认 `30000`)、`rollbackCopyTimeoutMs`(默认 `120000`)。
152
191
 
153
192
  ### 这台电脑不让建计划任务时(自动回滚就没了)
154
193
 
@@ -183,7 +222,12 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
183
222
 
184
223
  **由一个函数决定**(`lib/watchdog-tier.js`),设置页、diagnostics 和看门狗自己问的都是这同一个函数 —— 所以"这台机器没有自动回滚"这句警告**不可能**和插件实际做的事对不上。
185
224
 
186
- 三档跑的是**同一套**流程(`lib/watchdog.js`):等新宿主写下 ready 标记 → **再问一次**(起得慢不等于死了)→ 有实例活着吗(活着就没什么好还原的)→ **直接问宿主自己的 health 路由**(只要它在应答就绝不还原;一个端口都问不到时,再等一轮、复查标记,仍然没动静才允许还原)→ 抢共用的锁(`rollback.lock`,保证只有一个在还原)→ 还原快照 → **再拉起宿主** → 再等一次 ready。最多**一次**还原 + **一次**拉起;然后看门狗停下,把结果写进 `<stateDir>/last-watchdog-report.json`(用的是哪一档、`healthCheck` 是什么、为什么)。
225
+ 三档跑的是**同一套**流程(`lib/watchdog.js`):等新宿主写下 ready 标记 → **再问一次**(起得慢不等于死了)→ 有实例活着吗(活着就没什么好还原的)→ **直接问宿主自己的 health 路由**(只要它在应答就绝不还原;一个端口都问不到时,再等一轮、复查标记,仍然没动静才允许还原)→ 抢共用的锁(`rollback.lock`,保证只有一个在还原)→ **这一轮的顺序是固定的:还原快照 → 清掉过期的 `dsh-auth-*` 通行证 cookie → 再拉起宿主 → 等 ready**。
226
+
227
+ 每一轮最多试 **`maxAttempts` 次**(默认 3 次,代码里有硬上限,**不会无限重试**):一轮没起来就先退避(5s / 10s / 20s,封顶),然后在**下一轮开始前重新问那三个问题**——宿主可能只是起得慢(那就 `not-needed`,绝不再拉一次),也可能已经有实例活着(那就 `abandoned`,绝不造成双开)。快照只在**第一次**还原(同一个快照再拷一遍不会改变任何事),**但每次都先清通行证再拉起**:带着过期 cookie 拉起来就是再死一次(71 个 cookie 就够把 Cookie 头撑过 16 KB)。
228
+
229
+ **失败也一定留报告**:无论成功失败、早退还是放弃,每一轮都会写进 `<stateDir>/last-watchdog-report.json`(用的是哪一档、`healthCheck` 是什么、试了几次、每次的 `restored` / `cookieClear` / `relaunched` / `ready` / 退避时间、以及为什么)。**报告不落盘就等于没发生** —— 这是 0.8.6 修掉的三个毛病之一("只许跑一次"、"拉起前不通行证"、"报告不落盘")。
230
+
187
231
 
188
232
  **撤销看门狗是宿主的责任,而且开机就做。** 插件一加载就会写下 ready 标记、删掉 `watchdog-pending.json`、并把自己注册的调度项删掉(带退避重试,删完回查计划任务,过程写进 `<stateDir>/watchdog-disarm.json`)。它**不等任何人来打 health 接口** —— 没人开界面时看门狗也必须被撤掉;撤销失败会出现在 `health`、诊断文本和那份记录里,而不是悄悄消失。这就是 0.8.2 修的那个线上缺陷:一个被 health 路由吞掉的异常,让一个健康宿主背后留着一只看门狗。
189
233
 
@@ -191,6 +235,34 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
191
235
 
192
236
  `watchdogEnabled: false` 可以整个关掉它。
193
237
 
238
+ ### 看门狗**不覆盖**什么(重要,别指望错)
239
+
240
+ 这套东西是一个**窗口**,不是常驻保安:
241
+
242
+ - 看门狗只在**我们自己要重启宿主**的那一刻才布防:它是被杀掉之前注册的一个一次性项,宿主起来后还会把它撤掉(删 `watchdog-pending.json`、写 ready 标记、删调度项);
243
+ - **窗口之外没有任何人看着。** 凌晨被系统杀掉的宿主、手动双击启动即崩的客户端、早上来了发现 0 个进程 —— 这些时候**没有看门狗会把它拉回来**:插件本身已经随着宿主死了,也没有任何一次性项被注册过。这种情况只剩 `scripts/restore-profile.cmd`(自己双击)能用;要是连 profile 都被官方清空了,那就什么都没有;
244
+ - 看门狗只**还原一份快照 + 把宿主拉起来**,它修不了宿主自己的 bug:每次都因为同一个原因死的宿主,最后就是 `failed` 加一份写明试了几次的报告;
245
+ - `watchdogEnabled: false`、或者这台机器没有档位(`none`),就**完全没有自动回滚**,设置页会明说。
246
+
247
+ ### 报错型故障:插件没加载成功时,只提示,不自动修
248
+
249
+ 「坏法」有两种,**两种由两个机制分别负责,互不插手**:
250
+
251
+ | 故障 | 谁负责 |
252
+ |---|---|
253
+ | **进程级**:宿主根本起不来(端口冲突、启动即崩) | 重启前快照 + 看门狗:回滚一份快照、把宿主拉起来 |
254
+ | **报错型**:宿主起来了,但某个插件 entry **静默没激活**(功能直接 404) | 本节:检测 → **只提示** → 用户点一下才恢复**那一个**插件 → 写报告 |
255
+
256
+ 启动日志里其实写着答案(`<plugin>: failed to import: <错误>`、`<plugin>: import failed …`、`- <plugin>: loading`,以及总括行 `dsh: warning: N entry did not activate`),原来没有任何人读它。现在每次启动、写 ready 标记**之前**都会读**上一次启动**那份日志:
257
+
258
+ - **只检测,绝不自动改文件、绝不自动重启。** 实测证明 DSH 自己就会剔除坏插件并照常起来(桌面版 22 秒正常启动、坏插件路由 404、配置不动;WSL 托管版 2.0–2.6 秒起来、`package.json` md5 五次一致、守护脚本 62.9 秒内只 +1 次启动)。所以启动路径上**不调用**恢复、**不请求**重启:它只读日志、只写自己的报告。单测钉住了这件事:恢复前后 profile 里每个文件的 sha256 不变、没有任何 `<plugin>.broken-*` 证据目录、`state.restart` 仍是空。
259
+ - **报告**:`<stateDir>/last-plugin-recovery.json`(`{ at, atIso, failures[], actions[](恒为空:什么都没做), restartRequested: false, needsHuman: false, backupKind, restoredAt }`),其中 `restoredAt` / `backupKind` **取自备份自己的 `meta.json`**——那张文件只记事实(何时拍的、什么动作触发的、内容指纹、那次启动的结果),`backupKind` 是宿主按这些事实**派生**出来的身份,磁盘上并没有这个字段——回答"有没有备份可恢复、恢复到哪个时点"。同一个对象也出现在 health 路由的 `pluginRecovery` 字段和 `continuum_status` 工具里(没发生过就是 `null` / 不返回)。
260
+ - **前台弹窗**:界面上直接说「有插件没加载成功」,列出是哪个插件、报了什么错(最多 3 条,其余只报数量),并给**一个**按钮「只恢复 <plugin>」。没有可用备份时按钮不出现,直接说只能手动处理。
261
+ - **按需恢复只动那一个插件**(`POST /__dsh_autorestart/plugin-restore`,body `{ "plugin": "<name>" }`):备份里有 `node_modules/<name>` 真副本 → `overwrite`:现场目录**改名**成 `<name>.broken-<ts>`(**留证据,不删任何文件**),副本拷回去;备份里只有 `node_modules-link/<name>`(链接安装的内容)→ `replace-link`:现场软链同样改名留证据、备份内容顶替。别的插件、以及 profile 的 `package.json` / `cordis.patch.yml` **一个字节都不动**(单测逐字节钉住)。
262
+ - **幂等**:现场已经等于备份 → `already-consistent`,**不写任何文件、不要求重启**;按钮点两次是安全的。
263
+ - **失败也是个答案**:没有备份 / 拷不进去 → `ok:false` + 原因,而且 **HTTP 200**(JSON 报错体)。用 500 会让浏览器分不清"没有备份"和"宿主崩了"。
264
+ - **历史**:每次尝试写 `<stateDir>/plugin-restore-history.json`(最近 20 条,最新在前);health 的 `pluginRestore` 字段带最近一条,所以弹窗能说"已经恢复过、重启后生效"。
265
+
194
266
  ### 开机自启:插件自带的守护
195
267
 
196
268
  手动启动的托管实例(`dsh web`)挂掉之后不会自己回来——没人拉起来的重启,就等于要用户自己再启动一次。插件可以装一个**守护**:一个独立的小进程,负责拉起宿主、看住它、它掉了再拉起。
@@ -328,9 +400,22 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
328
400
  - **续接消息是普通 user message**,`source.kind='continuum'`(非 human authority)、内容明确标记
329
401
  `<continuum_resume>`,模型能看到完整历史后自行判断中断点。
330
402
 
403
+ ## 对宿主版本的要求
404
+
405
+ **需要 DSH ≥ `0.1.6-alpha.2`。**
406
+
407
+ 从 `0.1.6-alpha.2` 起,宿主把「有插件没起来」分成两类处理:
408
+
409
+ - **普通插件**(你自己写的、从 npm 装进来的)加载失败**不会阻断启动**:宿主照常起来,只记一条 `warning: N entry did not activate`,其余插件继续运行。本插件的「这些功能这次没起来」提示,读的就是这条日志。
410
+ - **必需插件**(宿主自己的零件,例如网页服务)加载失败**仍然会让启动失败**:日志是 `startup failed: N required plugins did not activate`。看门狗负责的正是这一种 —— 宿主起不来时,用一份身份为 `known-good` 的备份还原,再把宿主拉起来。
411
+
412
+ `0.1.6-alpha.1` 及更早的版本没有这个区分:任何一个插件加载失败都会直接抛错、宿主起不来,本插件依赖的那条检测信号在那类宿主上根本不存在。
413
+
414
+ **这条结论是怎么来的(如实说明)**:逐版核对 DSH 官方 npm 包 `@deepseek-ai/dsh-app-boot` 的实现得出,对比了 `0.1.0-rc.2` / `0.1.3-alpha.2` / `0.1.5-rc.3` / `0.1.6-alpha.1` / `0.1.6-alpha.2` / `0.1.7-rc.2` / `0.2.0-rc.1` / `0.2.1-rc.3` / `0.2.2-alpha.2`,分界落在 `0.1.6-alpha.2`。**DSH 官方发布说明里没有一条明写这件事** —— `0.1.6-alpha.2` 的相关条目只说「启动失败时分类显示错误和等待中的服务,并将完整诊断保存到日志文件」。所以这不是官方声明,是源码核对加本机实测。
415
+
331
416
  ## 安装
332
417
 
333
- 当前版本:**0.8.4**。
418
+ 当前版本:**0.8.6**。
334
419
 
335
420
  ```powershell
336
421
  # 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
@@ -345,15 +430,21 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
345
430
 
346
431
  **先分清是哪种情况**:整个环境坏了(改了配置、装了别的插件、升级崩了)→ 先回到上一次好状态;**只有这个插件可疑** → 直接走下面三步,把这个插件摘掉就够了。(两个脚本各自的边界见 [`scripts/README.md`](../scripts/README.md)。)
347
432
 
348
- **用你自己平台的脚本** —— Linux 机器上没有 `.cmd`,Windows 机器上也没有 `.sh`:
433
+ **用你自己所在宿主的脚本** —— Linux 机器上没有 `.cmd`,Windows 机器上也没有 `.sh`。按你面前的宿
434
+ 主挑,不要按「平台清单」挑:
349
435
 
350
- | 你的机器 | 回到上一次好状态 | 摘掉本插件 |
436
+ | 你的宿主 | 回到上一次好状态 | 摘掉本插件 |
351
437
  |---|---|---|
352
438
  | **Windows**(桌面客户端、Windows 上的 `dsh web`) | 双击 `scripts\restore-profile.cmd` | 双击 `scripts\emergency-disable.cmd` |
353
- | **Linux / WSL / macOS**(托管端) | `sh scripts/restore-profile.sh` | `sh scripts/emergency-disable.sh` |
439
+ | **WSL / Linux**(托管端) | `sh scripts/restore-profile.sh` | `sh scripts/emergency-disable.sh` |
440
+ | **macOS** | `sh scripts/restore-profile.sh` | `sh scripts/emergency-disable.sh` |
441
+
442
+ macOS 用同样的两个 POSIX `sh` 脚本;从未在真 Mac 上跑过,属于未验,不作承诺。
354
443
 
355
444
  每个平台到底有什么、没有什么,看 [平台能力矩阵](platform-matrix.md);设置页的「出事了怎么办」只显示**你这台机器上真有的**那两个路径。
356
445
 
446
+ **桌面救援快捷方式(只有 Windows 有)。** 设置页可以一键在桌面放一个救援快捷方式,双击它就是跑 `restore-profile.cmd`。**它的图标用的是插件随包的 `assets/icon.ico`,不是 DSH 自己的可执行文件** —— 这是有意的:DSH 起不来的那台机器上,一个长着 DSH 脸的快捷方式读起来像「这就是那个程序」,而它恰恰是坏掉的那个。包内没有这个图标文件时,`.lnk` 照样创建,只是不写 `IconLocation`(不借用别的程序的脸,运行期也不生成图标)。其它平台不提供桌面快捷方式,设置页给的是可复制的还原脚本命令。
447
+
357
448
  **不用等 DSH 跑起来**就能把这个插件摘掉 —— 插件目录里带了一个一键开关:
358
449
 
359
450
  1. **Windows**:双击 `scripts\emergency-disable.cmd`;**Linux/WSL**:`sh scripts/emergency-disable.sh` —— 它会写清楚做了什么;
@@ -366,14 +457,20 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
366
457
 
367
458
  设置页里关于「备份」「回执」和「启动方式」的几块,先说清楚它们看的是什么:
368
459
 
369
- - **现在备份了什么**(在两个备份勾选的下面):宿主真的去读 `<DSH_HOME>/storages/dsh-autorestart/backups/`,所以份数、每份的**本地时间**、大小、有没有含已装插件、以及存放路径都是真的。只有目录元信息被读,备份里的内容**不遍历**(一份就可能 150 MB)。没有备份时写的是「还没有备份;每次重启前会自动生成一份」。每份备份是**两行带标签的写法**,不再挤成一句:
460
+ - **现在备份了什么**(在两个备份勾选的下面):宿主真的去读 `<DSH_HOME>/storages/dsh-autorestart/backups/`,所以份数、每份的**本地时间**、这些备份**总共占多少磁盘**、以及存放路径都是真的。只有目录元信息被读,备份里的内容**不遍历**(一份就可能 150 MB)。没有备份时写的是「还没有备份;每次重启前会自动生成一份」。每份备份就是一行,只带两样东西:它的**备份时间**,和这一行唯一能做的那件事(「回退到这一份」)。**行上没有大小**——总占用只在列表上方出现一次,写作「实际占用 156 MB,当前 2 份」。列表**不判定哪一份安全**——插件不再替读者下这个结论(用户决定,2026-11-02):一行就是一个时间和一个按钮,判断权在使用者:
370
461
 
371
462
  ```
372
- 备份时间 2026-10-29 22:08
373
- 备份大小 156 MB(含已装插件)
463
+ 实际占用 156 MB,当前 2 份
464
+
465
+ 备份时间 2026-10-29 22:08 [回退到这一份]
466
+ 备份时间 2026-10-29 21:44 [回退到这一份]
374
467
  ```
375
468
 
376
- 时间就是拍快照那台机器的本地时间,**精确到分、不带时区偏移**(`+08:00` 只会添乱——看的人本来就看自己的钟)。大小自动选单位(`92 KB` / `156 MB`),只在有意义时保留一位小数(`117.7 MB`,永远不写 `156.0 MB`);「(含已装插件)」紧跟在大小后面,因为它正是这份备份为什么这么大。两份备份就是两组,组与组之间留空隙。
469
+ 某一行还可能多出一句中性说明,它不是结论:**最近一次启动时拍的那一份**会写「这是最近一次启动时的状态。DSH 能启动不代表插件都正常——坏掉的插件会被排除,主机照样起来。要回退到哪一份,请自己判断。」这句话按该份自己的记录挂上去,不按它在列表里的位置。
470
+
471
+ 身份仍然存在,但**磁盘上不存**:每份旁边的 `meta.json` 只记事实——`at` / `takenAt` / `trigger` / `version` / `contentFingerprint` / `boot`(那次启动起来没有、跑的是不是这份内容、有没有插件加载失败),而 `kind` / `certifiedBy` / `certificationEvidence` 这些身份字段是宿主**读取时按这些事实派生**出来的,写盘时不再出现。派生出来的身份有三种取值——`known-good`(有一次启动跟在它后面,或者它是首装那份)、`snapshot`(每次重启前刚拍的,后面还没有任何一次启动)、`unknown`(身份层之前的旧拷贝,或者没有身份文件,不参与自动回滚)。**名字叫 `known-good` 不等于能自动还回去**:能不能被自动还原是更窄的第二道判据——那次启动**起来了**(`started`)**且跑的就是这份内容**(`contentMatched`)。这个取值记的是「拍完之后发生了什么」,**不是**对里面插件能不能用的评价;浏览器读不到 `meta.json`,宿主给的 `backups.items[].kind` 仍是它派生后递过来的值,行上不打印任何身份。
472
+
473
+ 时间就是拍快照那台机器的本地时间,**精确到分、不带时区偏移**(`+08:00` 只会添乱——看的人本来就看自己的钟)。整个备份目录的占用自动选单位(`92 KB` / `156 MB`),只在有意义时保留一位小数(`117.7 MB`,永远不写 `156.0 MB`);它只在列表上方那段汇总里出现一次,**不在任何一行上**,也不带「(含已装插件)」这类括注。两份备份就是两组,组与组之间留空隙。
377
474
  - **设置改动有回执**:每个开关/下拉都是改了就写,所以面板必须说清写没写进去——宿主回 `ok: true` 之后,**设置面板内部顶部**(面板自己的 `sticky` 条,往下滚动也一直可见)出现绿色**「已生效」** + 小字时间(`已生效 21:40:12`),3 秒后淡出;被拒绝时是红色**「没保存成功」** + 宿主给的原因,不自动消失,直到下一次改动成功或你自己点 × 关掉。**宿主没回话之前一律不显示**:首次渲染、轮询刷新、点控件都不会亮。
378
475
  - **启动方式**(Web 托管端才有):默认只给**一行大白话**(例如「我记下的启动方式:由你的脚本拉起(重启交给它,我不插手)」)加两个小动作(重新检测 / 撤销);命令原文收在**「技术细节」**折叠里,默认收起,展开才出现。**桌面客户端这一整块不渲染** —— 它自己拉起自己,也不该写 `restartCommand`。宿主说不清是哪一端时,宁可整块不显示。
379
476
 
@@ -391,10 +488,10 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
391
488
  | `maxSessions` | `4` | 一次启动最多恢复几个会话 |
392
489
  | `maxResumeAttempts` | `2` | 每个会话最多尝试恢复几次(跨启动累计,防重启循环) |
393
490
  | `deferPollMs` | `3000` | 等 idle 的轮询间隔 |
394
- | `deferSoftMs` | `300000` | 软期限:还在忙就告警 |
395
- | `deferHardMs` | `900000` | 硬期限:超过后告警;只有 `forceRestartWhenBusy` 打开时才强制重启 |
396
- | `deferPolicy` | `auto` | `auto` = 硬期限后按 `forceRestartWhenBusy` 决定(默认继续等);`wait` = 一直等 |
397
- | `forceRestartWhenBusy` | `false` | 硬期限到了但还有会话在跑时是否强制重启;默认 `false` = **不打断任何会话**,一直等到其他会话结束 |
491
+ | `deferSoftMs` | `300000` | **已弃用——读入但完全无效。** 重启等待没有截止时间;仅为兼容旧配置文件保留 |
492
+ | `deferHardMs` | `900000` | **已弃用——读入但完全无效。** 重启等待不会放弃;仅为兼容旧配置文件保留 |
493
+ | `deferPolicy` | `auto` | 仅为兼容旧配置保留:无论取什么值,等待行为都一样——一直等闸门放行,没有截止时间 |
494
+ | `forceRestartWhenBusy` | `false` | 是否允许**打断正在运行的会话**强行重启;默认 `false` = 不打断任何会话,一直等到其他会话结束。这不是截止时间 |
398
495
  | `restartMode` | `app` | `app` = 重启 DeepSeek Harness 桌面应用;`command` = 跑 `restartCommand` |
399
496
  | `restartCommand` | `''` | 自定义重启命令,支持 `{pid}` `{ppid}` `{app}` 占位符 |
400
497
  | `relaunchDelaySeconds` | `2` | 脱离的 helper 在杀掉应用**之前**等几秒(`ping -n 3`,约 2 秒) |
@@ -407,7 +504,7 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
407
504
  | `continuationText` | `''` | 覆盖默认续接提示文本 |
408
505
  | `backupOnRestart` | `true` | 每次重启前给 profile 拍快照;重启后起不来时能自动回退 |
409
506
  | `backupIncludePlugins` | `true` | 快照里是否带上已安装插件目录(本机约 117 MB、多约 3 秒);关掉就只备配置文件 |
410
- | `maxBackups` | `2` | 保留几份快照;写新的时自动删旧的 |
507
+ | `maxBackups` | `3` | 保留几份快照(当前一份 + 之前两份);写新的时自动删旧的——但身份为 `known-good` 的那一份永远不删 |
411
508
  | `rollbackTimeoutMs` | `90000` | 看护脚本等新宿主多久(毫秒),超时就回退快照(**不会再拉起第二个宿主**) |
412
509
  | `restartCommandTimeoutMs` | `30000` | 重启命令最多让插件等多久(毫秒)。前台跑着宿主的命令永远不返回——那会报「还在运行」,不算失败 |
413
510
  | `rollbackCopyTimeoutMs` | `120000` | 拷回快照的**每一步**最多花多久(毫秒):拷不完就放弃,并把原因写进回退报告 |
@@ -445,9 +542,11 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
445
542
  - **`0` 就是「和以前一样」**:`queuedGraceMs: 0` 时,没人认领的消息永远挡着重启,和 0.6.2 之前完全一致。
446
543
  - **在哪看**:`GET /health?state=1`、`GET /health?gate=1`、`continuum_status` 都会报 `queuedGraceMs`。
447
544
 
448
- ### 长期等待的时候,用户看到什么
545
+ ### 等待的时候,用户看到什么
546
+
547
+ 重启只等一件事:**所有会话把活干完**。没有截止时间、没有任何东西会过期——等多久都等;只有两种情况会结束它:闸门放行(重启执行),或者你自己撤回请求。
449
548
 
450
- 硬期限(`deferHardMs`)到了、还有会话在跑的时候,插件**不再装作「一切正常,只是在等一会儿」**,但也**不会放弃**:
549
+ 等待从第一秒就看得见(不是过了某个阈值才肯说):
451
550
 
452
551
  - **请求留着**:待重启仍然是待重启,请求方的会话仍然记在检查点里,闸门一 quiet 就照常重启。**不会在用户背后把请求悄悄丢掉**(不 cancel、不 releaseRequesters)。
453
552
  - **等待变得看得见**:日志每分钟最多一条(不是每 3 秒刷屏)——「已等待 17 分钟,XXX 一直占着;可以取消或等它收工」;`GET /health?state=1` 里多一个 `longWait` 对象(`since` / `reason` / `blockedBy`)。
@@ -552,6 +651,110 @@ restart gate: action=wait reason=agents-busy-within-soft-window busyCount=1 quie
552
651
  `{ "sessionId": "…", "scope": "mine" }`):别的会话照样重启。面板里**没有**"全撤"——那种操作只留给
553
652
  工具显式调用(`continuum_cancel_restart` 带 `scope: "all"`)。
554
653
 
654
+ **浮层不会悬停,也不说假话**:重启被新会话打断时,浮层立刻从「正在重启…」换成「**有新会话在跑,重启已暂缓**」,第二行小字「等它结束会自动继续 · 已等 N 个会话」——这时候说「正在重启」就是假话。宿主每 350ms 复查一次空闲门,那句假话最多只存在一个心跳;备份一结束、进入「正在重启」阶段之前也会当场复查一次,被挡就在同一次 tick 退回等待。阶段超过 15 秒无更新视为过期,不再显示,胶囊仍显示等待数。
655
+
656
+ ## 0.8.6:备份了什么、三层信号能说明什么、自动救援的边界(中文)
657
+
658
+ > 本章是 0.8.6 发版口径的中文原文,英文对照见 [README.md](../README.md)。它只回答普通用户会问的
659
+ > 四个问题:**备份什么、信号有几层、自动救援能救什么、出事之后按什么顺序做**。实现细节在上面各节。
660
+
661
+ ### ① 备份什么、不备份什么
662
+
663
+ | | 进不进备份 | 为什么 |
664
+ |---|---|---|
665
+ | **备份** | profile 配置(`cordis.patch.yml` / `package.json` / `pnpm-lock.yaml`)+ **你自己开发的、以链接(`link:`)方式安装的插件**(内容另存在 `node_modules-link/<name>/`;**不含 `.git`** —— 历史能从远端重建,而它才是体积主因:单个链接插件就 962 个文件 / 约 74 MB。另外每轮会清掉「既无 manifest 又无配置文件」的空壳目录,单次上限 32 个 / 2 GB,清掉了什么都会写进日志与返回结果。不完整的备份**根本不会挂上正式名字**:先写成 `.staging-<stamp>`,只有 manifest 与配置都落盘后才改名,所以任何中断只留下一个暂存目录,而不是一个「看起来能还原」的目录;**这类 `.staging-*` 残留不会被启动清扫掉**(启动清扫按设计跳过所有 `.` 开头的目录,以免删到正在写入的那一份),它们既不会被列出、也不会被拿来还原,手动删掉是安全的;而老版本留在盘上那些「长得像备份、其实没有 manifest / 配置」的目录,启动时会被扫掉 | 自己写的插件**没有确定的来源和版本**:删掉就找不回来,只能靠备份 |
666
+ | **不备份** | **第三方插件**(从 npm 装的) | 第三方**有确定的来源和版本**:`pnpm-lock.yaml` 把每个包的版本和完整性哈希都钉住了,重装装回来的是**备份时那个版本,不是最新版**——所以备份是多余的,重装比备份可靠 |
667
+
668
+ - 实测(2026-11-01 起,精简备份是默认行为):一次备份从 308 MB 降到约 100 MB。
669
+ - `backupIncludePlugins: false` → 只备份配置(约 66 KB,瞬间完成):这种配置下**插件出问题无法回退,只能手动重装**,设置页会直说。
670
+ - 想回到老的全量备份行为,用 `slimBackup: false`。
671
+
672
+ ### ② 三层检测信号:两层「确定」、一层只是「线索」
673
+
674
+ 每次启动,插件在启动路径上把三层清单跟前一次启动记下的清单(`plugin-watch.json`)比一次:
675
+
676
+ - **工具名**(`tools`)—— 提供工具的插件(codekg、vault、release-board 这类);
677
+ - **界面模块**(`clientModules`)—— 在界面上画东西的插件;
678
+ - **注册的包**(`packages`)—— 注册了 schema 的包。
679
+
680
+ | 信号 | 强度 | 能说明什么 | 不能说明什么 |
681
+ |---|---|---|---|
682
+ | **工具名消失** | **确定** | 这些功能这次真的没起来 | 是哪个插件导致的 |
683
+ | **注册的包消失** | **确定** | 这个包这次没加载 | 是文件坏了,还是配置把它禁用了 |
684
+ | **插件文件跟备份不一样** | **线索** | 这个插件可能被改过(也可能就是你刚改的、或者升级过) | **不等于坏了** —— 文件全对、加载时才报错的插件,这一层看不到 |
685
+
686
+ 还有三条边界,一起写在这里:
687
+
688
+ - **界面模块里对不上任何已安装插件的那些**,单独计数并单独说(「另外有 N 个界面模块这次没出现」),
689
+ **不硬塞给某个插件** —— 认不出主人,就不猜主人。
690
+ - **一层读不到,就记作「没有消息」,绝不记作「消失了」**:升级宿主让某个接口变了形状,
691
+ 结果只能是「这次看不到这一层」,不能变成「你的插件没了」。
692
+ - **文件比对只比「相对路径集合 + 每个文件大小」**,不哈希整棵树(真实 profile 约 150 MB)。
693
+ 所以它找得到「文件被改了 / 删了 / 备份里没有」,**找不到「文件全对但加载失败」的插件**(缺 peer 依赖、
694
+ 初始化抛错、宿主拒绝某个版本)。
695
+
696
+ ### ③ 「不一致 ≠ 坏了」—— 面板文案的硬口径
697
+
698
+ 浮窗和设置页只允许出现两种话:
699
+
700
+ - 「**这些功能这次没起来**」(确定的事,已经发生,不是猜测);
701
+ - 「**这些插件的文件跟备份不一样**」(只是差异清单,不下结论)。
702
+
703
+ **不许写「插件坏了」**:跟备份不一样,可能只是你刚改过、或者升级过。设置页每行的状态字只有
704
+ `正常` / `跟备份不一样` / `备份里没有` / `没装` / `没法判断(没有可比对的备份)`,没有「坏」这个状态。
705
+
706
+ ### ④ 自动救援(看门狗):它做什么
707
+
708
+ - **可配置**:`watchdogEnabled`,**默认开**;关掉之后新版本起不来就要自己处理。
709
+ - **只在一个窗口里值班**:这次重启是**本插件**发起的。宿主被换掉之后,它等新实例、问**新实例自己的**
710
+ health 路由(不等旧实例、不看旧端口),然后按固定顺序做:**还原最近一份身份为 `known-good` 的备份
711
+ → 清掉过期的 `dsh-auth-*` 握手 cookie → 再把宿主拉起来 → 等 ready 标记**。
712
+ - **三种触发方式,按这台机器的能力选一种,并如实告诉你选了哪种**:Windows 计划任务 /
713
+ systemd user timer / 自建后台进程;**三种都没有 → 这台机器没有自动回滚**,设置页直接写「不可用」。
714
+ - **有上限地重试**:默认 3 次(硬顶 10 次),两轮之间退避,每轮开始前重新问那三个问题
715
+ (起没起来 / 有没有实例活着 / health 答不答)。
716
+ - **每次尝试都落盘**:`<stateDir>/last-watchdog-report.json` —— 哪一档、试了几次、每轮的
717
+ 还原 / 清 cookie / 拉起 / ready、以及为什么停。没写进盘的报告等于没发生。
718
+ - **顺顺利利的重启也要写结局**:新宿主起来了、看门狗撤防了,报告会被补写成这个结局
719
+ (`result` / `stage` / `outcome: 'disarmed'`、`hostReady`、`disarmed`、`disarmedAt`,以及一段
720
+ `disarm` 明细)。在这之前,报告会一直停在 `armed`——一次早就结束了的重启,却写着自己还在待命,
721
+ 而 health 明明答的是 `ready:true`。原有字段一个不动,`previousResult` 还留着"它之前说的是什么"。
722
+ - **能力探测留下的计划任务会被清掉**:要证明这台机器能建一次性计划任务,就得真建一个——探测的最后
723
+ 一步是删掉它,而崩溃正好会把它留下(真机上就翻到 2 个)。现在开机撤防会清掉所有名字以本插件自己
724
+ 探测前缀(`dsh-autorestart-capability-probe-`)开头的任务,**绝不匹配别的任务**;根目录下的任务在
725
+ `schtasks` 输出里名字前面会带一个 `\`(形如 `\任务名`),所以比对前先剥掉**一个**前导反斜杠——剥完
726
+ 还带 `\` 的说明它在子文件夹里,一律拒绝、绝不删除;清了哪些、哪些没
727
+ 清掉,都写进撤防记录与日志,清不掉也绝不影响宿主起来。
728
+
729
+ ### ⑤ 自动救援的盲区(必须明说,别指望错)
730
+
731
+ **它能救的**:**插件自己出了问题、导致 DSH 起不来** —— 还原一份快照,再把宿主拉起来。
732
+
733
+ **它救不了的**(这一条比上一段更重要):
734
+
735
+ - **DSH 本体或系统环境出问题** —— (宿主升级坏了、环境依赖缺了)。它不修宿主的 bug,
736
+ 同一个原因每次都死,结果只会是 `failed` 加一份点明尝试次数的报告;
737
+ - **第三方插件的问题** —— 它只看自己那一份快照,既不诊断是哪个插件引起的,也不会去修第三方插件的 bug;
738
+ - **磁盘满、权限问题、系统更新**带来的变化 —— 还原快照解决不了这些;
739
+ - **重启窗口之外的一切** —— 凌晨被系统杀掉、手动双击起不来的客户端、早上发现 0 个进程:
740
+ 那个时刻**没有人值班**(插件随宿主一起死了,一次性任务根本没注册过),这时只剩
741
+ `scripts/restore-profile.cmd` 可以手动双击;
742
+ - **`watchdogEnabled: false`,或这台机器的档位是 `none`** → 没有自动回滚,设置页会这么写。
743
+
744
+ ### ⑥ 出问题之后的四步
745
+
746
+ 1. **看浮窗** —— 重启后自动出现,它会分成两段说:哪些功能这次没起来、哪些插件的文件跟备份不一样;
747
+ 2. **或者去设置页的「插件」里点「立即检查」** —— 列出每个插件现在的状态
748
+ (`正常` / `跟备份不一样` / `备份里没有` / `没装` / `没法判断`);
749
+ 3. **自己开发的插件 → 点「恢复」** —— 只把点名的**那一个**插件换回备份里的版本:现场目录改名成
750
+ `<name>.broken-<ts>` 留证据(`broken` 只是这个证据目录的名字,不是结论),**绝不删除**;
751
+ 其他插件和 `package.json` / `cordis.patch.yml` 一个字节都不动;
752
+ 4. **第三方插件 → 按提示的版本号重装** —— 面板会给出那条命令,装回来的是**备份时记下的那个版本
753
+ (不是最新版)**;备份里没记版本时,面板会如实说「只能按插件名重装」。
754
+
755
+ 四步之外还有一个前提:**没有可用备份时,插件什么都不会替你改** —— 它会直说「只能手动处理」,
756
+ 而不是装作恢复过。
757
+
555
758
  ## 收工关机(Graceful shutdown)
556
759
 
557
760
  **它干什么。** 左侧栏最底部一个月亮按钮:点一下 → 等**所有**会话都收工 → 屏幕上倒数(一个巨大的数字 + 一只蓝色小鲸鱼,每秒换一句中文)→ **关掉整台电脑**。倒数期间随时可以取消:卡片上有按钮,月亮按钮本身在关机期间也变成了"再点一下 = 取消"。倒数期间要是有会话又开始干活,倒计时会退回"等待",**绝不关机**。
@@ -921,10 +1124,12 @@ aborts the plugin logs an error and schedules one ungated restart instead of
921
1124
  retrying forever. Even that ungated fallback re-checks the same gate before
922
1125
  anything is scheduled, so a busy conversation is never killed.
923
1126
 
924
- The hard deadline (deferHardMs) only warns: it forces a restart over a running
925
- conversation only when forceRestartWhenBusy is enabled; by default it waits for
926
- the other conversations, because a restart must never kill a conversation that is
927
- still working.
1127
+ A restart waits until every conversation is idle, with NO deadline: nothing ends
1128
+ the request except the gate opening or the user withdrawing it. The only setting
1129
+ that can act on busy conversations is forceRestartWhenBusy — it restarts over a
1130
+ running conversation, i.e. it interrupts it (off by default), because a restart
1131
+ must never kill a conversation that is still working unless the user asked for
1132
+ exactly that.
928
1133
 
929
1134
  Inspect the live verdict with continuum_status (gateAllow, quietForMs, blockedBy)
930
1135
  or by requesting the health path with ?gate=1.
@@ -1007,6 +1212,9 @@ or by requesting the health path with ?gate=1.
1007
1212
 
1008
1213
  ## 平台与部署
1009
1214
 
1215
+ > 本节讲**怎么配**;**哪些环境被支持、哪些不支持**看上面「支持范围」那一节,
1216
+ > 每种能力 × 每个环境的完整矩阵(带 `文件:行号`)见 [platform-matrix.md](platform-matrix.md)。
1217
+
1010
1218
  | 部署形态 | `restartMode` | 怎么重启 |
1011
1219
  | --- | --- | --- |
1012
1220
  | DeepSeek Harness 桌面应用 | `app`(默认) | 用宿主内置的**应用重启**能力:脱离进程树地杀掉并拉起桌面端 |