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.
- package/CHANGELOG.md +313 -0
- package/README.md +465 -40
- package/assets/icon-9c245e5b.ico +0 -0
- package/assets/icon-rescue-1902bb15.ico +0 -0
- package/assets/icon-rescue-1902bb15.png +0 -0
- package/client.js +3002 -170
- package/cordis.patch.yml +12 -0
- package/docs/README.zh.md +231 -23
- package/docs/platform-matrix.md +171 -55
- package/docs/real-machine-verification-0.8.6.md +202 -0
- package/index.js +13272 -8513
- package/lib/autostart-advice.js +31 -2
- package/lib/backup.js +2768 -21
- package/lib/checkpoint.js +214 -7
- package/lib/cookie-clear.js +260 -0
- package/lib/lifecycle.js +23 -40
- package/lib/plugin-compare.js +499 -0
- package/lib/plugin-failure.js +382 -0
- package/lib/plugin-recover.js +1557 -0
- package/lib/plugin-watch.js +213 -0
- package/lib/rescue-paths.js +982 -56
- package/lib/restart-phase.js +5 -0
- package/lib/restart-verdict.js +379 -0
- package/lib/restart.js +244 -7
- package/lib/scheduler-capability.js +19 -3
- package/lib/sleep.js +190 -29
- package/lib/watchdog-disarm.js +46 -2
- package/lib/watchdog-schedule.js +834 -17
- package/lib/watchdog-tier.js +8 -1
- package/lib/watchdog.js +1222 -47
- package/locale/en.json +175 -68
- package/locale/zh.json +175 -68
- package/package.json +3 -2
- package/screenshots.json +6 -6
- package/scripts/README.md +6 -3
- package/scripts/build-icon.mjs +998 -0
- package/scripts/clear-dsh-auth-cookies.mjs +257 -0
- package/scripts/emergency-disable.sh +69 -2
- package/scripts/emergency-toggle.ps1 +85 -69
- package/scripts/rescue-strings.zh.json +36 -18
- package/scripts/restore-profile.ps1 +182 -1
- package/scripts/watchdog.mjs +432 -79
package/docs/platform-matrix.md
CHANGED
|
@@ -1,76 +1,176 @@
|
|
|
1
1
|
# 跨环境能力矩阵
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
插件装在哪台机器上,就只有那台机器的答案 ——
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
图例:✅ 能用 | ⚠️ 能用但有条件(**每一格都写清「用户会看到什么」**)| ❌ 不支持(为什么在最后一列)
|
|
15
|
-
|
|
16
|
-
## 能力矩阵
|
|
17
|
-
|
|
18
|
-
| 能力 | Windows 桌面 | Windows web | Linux web | WSL web | 容器 | 说明(实现在哪) |
|
|
19
|
-
|---|---|---|---|---|---|---|
|
|
20
|
-
| 重启(DSH 自己把自己重启起来) | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/restart.js`:桌面用当前 exe,托管用配置的 `restartCommand` |
|
|
21
|
-
| 会话续接(重启后接着干活) | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/checkpoint.js` + `lib/lifecycle.js`:checkpoint 落在 `<DSH_HOME>/storages/dsh-autorestart/` |
|
|
22
|
-
| 重启前备份(profile 快照) | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/backup.js`:快照目录 `<stateDir>/backups/`,默认留 2 份 |
|
|
23
|
-
| **自动回滚(看门狗)** | ✅ 计划任务 | ✅ 计划任务 | ✅ systemd 定时器 | ✅ systemd(有 systemd 时)/ ⚠️ 自建后台进程:没有 systemd 时,用户在设置页会看到「看门狗的触发方式:自建后台进程」 | ⚠️ 自建后台进程:用户会看到「看门狗的触发方式:自建后台进程」(容器里没有 systemd 定时器可依赖) | `lib/watchdog-tier.js` 选档,`lib/watchdog-schedule.js` 注册,`lib/watchdog.js` 跑流程;**同一时刻只有一个监护者**(`lib/watchdog-arbiter.js`:v2 能用就是 v2,v2 用不了才回落 v1) |
|
|
24
|
-
| 自动回滚的**能力检测**(能不能建任务) | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/scheduler-capability.js`:真注册一次任务再回读、删掉、确认没了 |
|
|
25
|
-
| 自动回滚被拒时的降级链路 | ❌ | ❌ | ❌ | ❌ | ❌ | **有意不做**:被禁就是被禁,只告知(`README.md` 的「当机器不允许创建计划任务时」) |
|
|
26
|
-
| 手动还原(出事了自己救) | ✅ `restore-profile.cmd` | ✅ `restore-profile.cmd` | ✅ `restore-profile.sh` | ✅ `restore-profile.sh` | ✅ `restore-profile.sh` | `scripts/restore-profile.ps1` / `scripts/restore-profile.sh` |
|
|
27
|
-
| 摘掉本插件(急救开关) | ✅ `emergency-disable.cmd` | ✅ `emergency-disable.cmd` | ✅ `emergency-disable.sh` | ✅ `emergency-disable.sh` | ✅ `emergency-disable.sh` | `scripts/emergency-toggle.ps1` / `scripts/emergency-disable.sh` |
|
|
28
|
-
| 设置页「出事了怎么办」 | ✅ | ✅ | ✅ | ✅ | ✅ | 只显示**这台机器上真有的**脚本路径(`lib/rescue-paths.js` 按平台给 `.cmd` / `.sh`) |
|
|
29
|
-
| 桌面快捷方式(双击即救) | ✅ | ✅ | ❌ | ❌ | ❌ | 快捷方式是 Windows 的 `.cmd`;别的平台没有可双击的东西,就不显示这个按钮 |
|
|
30
|
-
| 关机倒数 | ✅ 关这台 Windows | ✅ 关这台 Windows | ✅ 关这台 Linux | ✅ **关掉整台 Windows**(不是关 WSL!) | ⚠️ 容器里没有意义:用户会看到「这台机器没有可靠的关机方式」,命令是空的、什么都不会执行 | 计划层已验证(`test/sleep-plan-posix.test.mjs`);Windows 真关电已验,**POSIX 未真关电**(用户决定不真关:一条常识证据不值得断开所有会话) |
|
|
31
|
-
| 开机自启建议 | ✅ | ✅ | ✅ | ✅ | ⚠️ 容器里没有 systemd 用户会话:用户会看到「这里不能开机自启」的说明,插件只会给建议、不会去改系统 | `lib/autostart-advice.js` + `lib/supervisor-install.js`:Windows 用计划任务或启动文件夹,Linux 用 systemd user unit |
|
|
32
|
-
| 问题反馈(诊断信息) | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/diagnostics.js`;设置页复制,诊断信息里也带 watchdog 档位 |
|
|
33
|
-
|
|
34
|
-
## 关机:会关掉哪台机器?
|
|
35
|
-
|
|
36
|
-
这一格必须说清楚 —— 猜错了代价太大。
|
|
37
|
-
|
|
38
|
-
| 你在哪 | 关机倒数按下去会关掉什么 | 界面上的原话(`host.label`) |
|
|
3
|
+
这张表只回答一个问题:**「我这台机器到底能做什么,哪一格会缺?」**
|
|
4
|
+
|
|
5
|
+
插件装在哪台机器上,就只有那台机器的答案 —— 所以每一行能力都按环境分开写,
|
|
6
|
+
每一格要么能在代码里找到对应实现(带 `文件:行号`),要么明确写「不支持 / 查不到」。
|
|
7
|
+
|
|
8
|
+
**本表的环境列只覆盖 4 类被支持的环境。** macOS、远程 SSH 无图形、容器**不在支持范围内**,
|
|
9
|
+
单独放在第四节的「范围外」里 —— 那里写清为什么不做、你会失去什么。
|
|
10
|
+
|
|
11
|
+
## 一、被支持的 4 类环境(6 列)
|
|
12
|
+
|
|
13
|
+
| 列 | 是什么 | 关键前提 |
|
|
39
14
|
|---|---|---|
|
|
40
|
-
| Windows
|
|
41
|
-
| Windows 上的 `dsh web` |
|
|
42
|
-
| Linux
|
|
43
|
-
| **
|
|
44
|
-
|
|
|
15
|
+
| **Windows 桌面** | DeepSeek Harness 桌面客户端 | `restartMode: app`(默认值,`index.js:683`) |
|
|
16
|
+
| **Windows web** | 托管在 Windows 上的 `dsh web` | 必须 `restartMode: command` + 一条真能重启该进程的 `restartCommand` |
|
|
17
|
+
| **Linux web · 有 systemd** | 托管在真实 Linux 上的 `dsh web`,有 `systemd --user` | 同上 |
|
|
18
|
+
| **Linux web · 没有 systemd** | 精简镜像 / Alpine 等没有 systemd 的 Linux | 同上 |
|
|
19
|
+
| **WSL web · 有 systemd** | WSL 里跑 `dsh web`,发行版里开了 systemd | 同上 |
|
|
20
|
+
| **WSL web · 没有 systemd** | WSL 里跑 `dsh web`,没开 systemd | 同上 |
|
|
21
|
+
|
|
22
|
+
> 「有 / 没有 systemd」只影响**自动回滚用哪种机制**(见第六节)和**开机自启怎么建议**;
|
|
23
|
+
> 其它能力在两边一样。
|
|
24
|
+
|
|
25
|
+
## 二、图例(四选一,每格都必须落在这四种里)
|
|
26
|
+
|
|
27
|
+
| 记号 | 含义 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| ✅ | **有实现,且有测试覆盖**(测试文件名写在最后一列) |
|
|
30
|
+
| ⚠️ | **有实现,但只到计划层 / 单测层,没有真机验证**,或者有条件才成立(条件写在格子里) |
|
|
31
|
+
| ❓ | **查不到或不确定**(代码里找不到对应实现,或行为依赖外部系统回答不了) |
|
|
32
|
+
| ❌ | **明确不支持**,格子(或说明)里写原因 |
|
|
33
|
+
|
|
34
|
+
## 三、能力矩阵
|
|
35
|
+
|
|
36
|
+
「实现与测试」一列是**同一行所有格子共用的证据**;格子里只写这一环境特有的差异。
|
|
37
|
+
|
|
38
|
+
| 能力 | Windows 桌面 | Windows web | Linux web · 有 systemd | Linux web · 无 systemd | WSL web · 有 systemd | WSL web · 无 systemd | 实现与测试 |
|
|
39
|
+
|---|---|---|---|---|---|---|---|
|
|
40
|
+
| **重启**(把 DSH 自己重启起来) | ✅ 插件用一条**脱离进程树**的命令 kill + 重新 start 应用(WMI 主、`schtasks` 兜底) | ✅ 走 `restartCommand`;**没配就什么都不拉起**,只留检查点并如实报告 | ⚠️ 走 `restartCommand` + POSIX 脱离守护(`setsid`):单测覆盖,**真实 Linux 实例未验** | ⚠️ 同左 | ✅ 走 `restartCommand`(`/home/fg/restart-web.sh`)+ WSL 里的 `supervise-web.sh` 守护;**真机验过(2026-11-03)**:`generation` 29 → 30、pid 73512 → 114454、`last-restart-verdict.json` 的 `result: "ok"` | ⚠️ 同左(本机 WSL 开着 systemd,无 systemd 那一档未验) | `lib/restart.js:504`(命令拼装)、`:538`(WMI)、`:871`(POSIX 脱离守护)、`:996`(Windows 调度)、`:1182`(执行 `restartCommand`);`lib/supervisor.js:1174`。测试:`restart.test.mjs`、`restart-phase.test.mjs`、`restart-safety.test.mjs` |
|
|
41
|
+
| **会话续接**(重启后接着干) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 检查点 `lib/checkpoint.js:1047`(`beginBoot`)、`index.js:7493`(启动恢复)、`lib/lifecycle.js:73`(`bootPlan`)/`:423`(续接提示)。**前提**:宿主真的被拉起来了(桌面靠自带重启,其余靠你的 `restartCommand` / 守护)。测试:`checkpoint.test.mjs`、`lifecycle.test.mjs`、`integration.test.mjs` |
|
|
42
|
+
| **重启前备份**(profile 快照) | ✅ | ✅ | ✅ | ✅ | ✅ 真机验过(2026-11-03):`snapshot.taken: true`,落在 `<stateDir>/backups/2026-11-03T00-09-28-780Z`(6 756 009 字节,含 `node_modules-link/dsh-autorestart`) | ✅ | `lib/backup.js:1585`(`createBackup`)、`:915`(`<stateDir>/backups/`);默认上限 **20 份**(`lib/backup.js:96`,配置项 `index.js:734`),**不是早期文档写的 2 份**。测试:`backup.test.mjs`、`hosted-backup.test.mjs`、`backup-hygiene.test.mjs` |
|
|
43
|
+
| **备份完整性把关**(半份快照不许冒充备份) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/backup.js:575`(`backupIsComplete`:要有可读 manifest **且** 至少一个配置文件)、`:2029`(**发布前**校验,不完整就删掉、不给正式名字)、`:746`(`latestKnownGood`:还原只用身份为 `known-good` 的那份 —— 即拍完之后有过一次启动成功)。测试:`backup-completeness.test.mjs`、`backup-known-good-boot.test.mjs`、`backup-identity.test.mjs` |
|
|
44
|
+
| **开机清残骸** | ⚠️ 只清插件自己留下的 | ⚠️ 同左 | ⚠️ 同左 | ⚠️ 同左 | ⚠️ 同左 | ⚠️ 同左 | 启动时**撤防**会删掉残留的计划任务 / systemd unit / pending 标记,并清掉能力探测留下的一次性任务(`index.js:6468`、`lib/watchdog-disarm.js:455`/`:261`/`:223`)。启动还有一次备份目录清扫 `removeIncompleteBackups`(`index.js:2088`、`lib/backup.js:2205`),但它**按设计跳过所有 `.` 开头的目录**(`lib/backup.js:2234`),所以真正的「半份快照」`*.staging-*` 残留**不会被启动清掉**:`cleanupStaleStaging`(`lib/backup.js:2295`)已经写好,但**没有任何生产代码调用它**(只有测试调用)。脱离的后台进程也没有孤儿回收。测试:`disarm.test.mjs`、`watchdog-disarm.test.mjs`、`watchdog-probe-cleanup.test.mjs`、`backup-hygiene.test.mjs`、`backup-staging.test.mjs` |
|
|
45
|
+
| **自动回滚 · 选档**(一台机器只有一个监护者) | ✅ `scheduled-task` | ✅ `scheduled-task` | ✅ `systemd-run` | ⚠️ `detached-process`(要有 `setsid`) | ✅ `systemd-run` | ⚠️ `detached-process`(要有 `setsid`) | `lib/watchdog-tier.js:110`(`pickWatchdogTier`,唯一决定档位的地方)、`:177`(`tierWithProof`)、`lib/watchdog-arbiter.js:46`(v2 / v1 只选一个)。测试:`watchdog-tier.test.mjs`、`watchdog-arbiter.test.mjs` |
|
|
46
|
+
| **自动回滚 · 注册** | ✅ 一次性计划任务(建完回读校验、`IgnoreNew`、`EndBoundary` 必填) | ✅ 同左 | ✅ `systemd-run --user --on-active=<秒>s` | ⚠️ `setsid nohup sh -c 'sleep N; 看门狗'`,pid 写进 stateDir 并回读 | ✅ `systemd-run` | ⚠️ 同 `detached-process` | `lib/watchdog-schedule.js:476`(注册任务)、`:545`(删除任务)、`:576`(`systemd-run`)、`:876`(脱离进程)、`:920`(回收)。失败会先清理再重试一次并如实报告(`index.js:4830`-`4865`)。测试:`watchdog-launcher.test.mjs`、`watchdog-systemd-arm.test.mjs`、`watchdog-strict-mode.test.mjs` |
|
|
47
|
+
| **自动回滚 · 撤防** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/watchdog-disarm.js:455`(`disarmWatchdog`,多次重试 + 退避),启动路径与健康路由都会触发(`index.js:6468`)。测试:`disarm.test.mjs`、`watchdog-disarm.test.mjs` |
|
|
48
|
+
| **自动回滚 · 报告** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 看门狗流程 `lib/watchdog.js:1224`、选还原点 `:406`、写报告 `:712`、把「结束(撤防)」也写进同一份报告 `:746`。测试:`watchdog.test.mjs`、`watchdog-report-order.test.mjs`、`watchdog-notice.test.mjs` |
|
|
49
|
+
| **自动回滚 · 能力探测** | ✅ 真注册一个探测任务 → 回读 → 删掉 | ✅ 同左 | ⚠️ 只看 `systemd-run --version` 在不在,**不做真注册回读** | ⚠️ 只看 `setsid --version` | ⚠️ 只看 `systemd-run` | ⚠️ 只看 `setsid` | `index.js:4605`(只探一次,结果缓存进 `capabilities.json`,设置页打开时不会重复注册任务)、`lib/scheduler-capability.js:165`(`probeSchedulerCapability`)、`lib/watchdog-tier.js:83`(命令存在性探测)。测试:`scheduler-capability.test.mjs` |
|
|
50
|
+
| **手动还原**(出事了自己救) | ✅ `restore-profile.cmd` | ✅ 同左 | ✅ `restore-profile.sh` | ✅ 同左 | ✅ 同左 | ✅ 同左 | `scripts/restore-profile.ps1` / `scripts/restore-profile.sh`(+ `.cmd` 外壳 `scripts/restore-profile.cmd`);脚本按平台选名字 `lib/rescue-paths.js:39`。测试:`scripts-encoding.test.mjs`、`rescue.test.mjs`;演练 `scripts/drill-rescue.ps1` / `drill-rescue.sh` |
|
|
51
|
+
| **急救摘除**(把插件从 profile 里摘掉) | ✅ `emergency-disable.cmd` | ✅ 同左 | ✅ `emergency-disable.sh` | ✅ 同左 | ✅ 同左 | ✅ 同左 | `scripts/emergency-disable.cmd` / `scripts/emergency-disable.sh` + `scripts/emergency-toggle.ps1`;`lib/rescue-paths.js:39`。测试:`rescue.test.mjs`、`rescue-ui.test.mjs` |
|
|
52
|
+
| **关机倒数**(收工关机) | ✅ 真关过电 | ✅ 真关过电 | ⚠️ `systemctl poweroff`(退 `shutdown -h now`):**POSIX 没真关过电**(用户决定不真关) | ⚠️ `systemctl` 通常不存在 → 只剩 `shutdown -h now`,**两条都可能失败**,而界面仍说「关闭这台 Linux 主机」 | ⚠️ 走 `/mnt/c/Windows/System32/shutdown.exe`,**关的是整台 Windows**(Windows 侧真关电已验,WSL 分支只到计划层) | ⚠️ 同左 | `lib/sleep.js:149`(`detectShutdownPlan`)、`:176`-`:212`(WSL / Linux 分支)。测试:`sleep-plan-posix.test.mjs`、`sleep.test.mjs`、`sleep-choice.test.mjs`、`sleep-intent.test.mjs` |
|
|
53
|
+
| **开机自启建议** | ✅ 桌面版自带重启,页面直接说「这里不用装守护」 | ✅ 建议 + 可安装(计划任务;「任务被拒时退启动文件夹」只到单测) | ✅ systemd user unit | ✅ 只给两条出路(在 WSL 里打开 systemd / 让 Windows 拉起发行版),**不替用户改系统** | ✅ 真装真卸过 | ✅ 同左(无 systemd 的那两条出路) | `lib/autostart-advice.js:68`(`planAutostartAdvice`)、`lib/supervisor-install.js:44`起(任务 / 启动文件夹 / systemd unit / compose 片段)、`lib/supervisor-config.js:585`(容器与无 init 的处理)、`lib/startup-mode.js:342`。测试:`autostart-advice.test.mjs`、`supervisor-install.test.mjs`、`supervisor-systemd.test.mjs`、`startup-mode.test.mjs` |
|
|
54
|
+
| **桌面快捷方式**(双击即救) | ✅ `.lnk`,带 DSH 图标,指向 `restore-profile.cmd`;**只有用户点了按钮才写** | ⚠️ 同左,但运行账户没有桌面目录时写不进(页面会说明失败原因) | ❌ 没有可双击的 `.lnk`;页面**不显示这个按钮**,改为直接列出 `.sh` 路径 + 复制按钮 | ❌ 同左 | ❌ 同左 | ❌ 同左 | `lib/rescue-paths.js:502`(`createDesktopShortcut`)、`:417`(`.lnk` 生成脚本)、`:591`(**只有 `win32` 才算桌面目录**)、`:460`(三种失败原因);写入由用户动作触发 `index.js:10325`。测试:`rescue-ui.test.mjs`、`rescue.test.mjs` |
|
|
55
|
+
| **设置页可见性**(五组 + 备份块) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 五组:重启相关设置 / 关机相关设置 / 开机自启 / 遇到问题 / 桌面救援入口(`client.js:283`、`:284`、`:300`、`:444`、`:445`,英文版 `:753`、`:754`、`:770`、`:910`、`:911`);备份整块折叠成「备份说明及手动备份操作」(`client.js:295`、`:6244`)。全部走普通 HTTP 路由,浏览器在哪台机器上看都一样。测试:`settings-save-notice.test.mjs`、`settings-startup-backup.test.mjs`、`rescue-ui.test.mjs` |
|
|
56
|
+
| **单插件回退**(某个插件起不来) | ✅ 从备份里挑**一个**插件退回上一个版本 | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/plugin-recover.js:1420`(`restoreOnePlugin`)、`:1188`(`detectPluginRecovery`),入口 `index.js:6407`。**注意**:启动时的自动检测**已经被拿掉**(宿主拒绝 boot 期工具调用,`index.js:6357`-`6366`),现在只有「磁盘上的报告」+「手动回退」。测试:`plugin-recover.test.mjs`、`manual-rollback.test.mjs`、`plugin-compare.test.mjs` |
|
|
57
|
+
| **一次性告知**(升级 / 首装弹一次) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 新功能公告按**功能版本**而不是包版本记账(`index.js:499`、`:6257`、`:6272`),首装提问与插件失败面板同属这一层。测试:`announcement.test.mjs`、`onboarding-card.test.mjs`、`plugin-failure-panel.test.mjs` |
|
|
58
|
+
| **诊断信息**(报告问题) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | `lib/diagnostics.js:153`(`buildDiagnostics`);**不含访问令牌、不含密钥**(`lib/diagnostics.js:52`)。测试:`diagnostics.test.mjs`、`tool-output-schema.test.mjs` |
|
|
59
|
+
|
|
60
|
+
> 行号以本仓当前提交为准。改代码之后请回来同步这一列 —— 一张比代码旧的表比没有表更坏。
|
|
61
|
+
|
|
62
|
+
### WSL web · 有 systemd 的真机验收(2026-11-03)
|
|
63
|
+
|
|
64
|
+
隔离实例(按 `docs/web-dev-instance.md` 搭起,**没碰 Windows 桌面版**):
|
|
65
|
+
|
|
66
|
+
| 项 | 实测 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| 端口 / profile / 状态目录 | `5601` / `/home/fg/.dsh/profiles/web` / `/home/fg/.dsh/storages/dsh-autorestart` |
|
|
69
|
+
| 加载的代码 | 插件以软链指向工作区 `plugins/dsh-autorestart`(不是 npm 旧版);`health.version = 0.8.6` |
|
|
70
|
+
| 重启模式 | `restartMode: command`、`restartCommand: /home/fg/restart-web.sh`(+ `supervise-web.sh` 守护) |
|
|
71
|
+
| ① 重启真的发生 | `generation` **29 → 30**;pid **73512 → 114454**;守护日志 `dsh web exited at 2026-11-03T08:09:31+08:00, relaunching in 2s`,随后打印新 token |
|
|
72
|
+
| ② 版本 | `version = 0.8.6` |
|
|
73
|
+
| ③ 残留 | systemd user unit **0**、timer **0**、cron **0**、`*.lock` **0**;`supervise-web.sh` 1 个、`dsh web` 1 个(守护本来就该在跑,属预期值) |
|
|
74
|
+
| ④ 记录 | `last-restart-verdict.json`:`result: "ok"`、`mode: "command"`、四项自检 `listener` / `routes`(15 条) / `plugin` / `recovery` 全部 `ok: true`;`last-restart-request.json`:`snapshotTaken: true`、`snapshotDir: /home/fg/.dsh/storages/dsh-autorestart/backups/2026-11-03T00-09-28-780Z` |
|
|
75
|
+
| web 端救援入口 | 渲染的是**脚本路径**(`rescue.restoreProfile = …/scripts/restore-profile.sh`),**没有 `.lnk`** —— 与矩阵里「桌面快捷方式」那一行的 ❌ 一致 |
|
|
76
|
+
| 看门狗档位 | `capabilities.json.tier = "systemd-run"`(WSL 里 `systemctl --user is-system-running` = `running`);看门狗报告 `tier: "systemd-run"`、`arbiter: "v2"` |
|
|
77
|
+
| 未验 | Windows web、真实 Linux、WSL **无 systemd** 三档仍为 ⚠️(本次没跑,不用推断冒充) |
|
|
78
|
+
|
|
79
|
+
## 四、范围外:macOS / 远程 SSH 无图形 / 容器
|
|
80
|
+
|
|
81
|
+
**这一节是结论,不是待办。** 用户已裁定:插件只对上面 4 类环境负责;
|
|
82
|
+
下面三种环境**未实测、不在支持范围**,装上去可能「看着能跑」,但没有任何一项承诺。
|
|
83
|
+
|
|
84
|
+
为什么不做:这三类环境缺的是**同一个东西** —— 一台「插件能自己动、且承诺会回来」的机器。
|
|
85
|
+
macOS 上没有任何一处代码做过真机验证;远程 SSH 无图形环境里没有桌面、也没有能被双击的
|
|
86
|
+
救援入口;容器里「谁把宿主拉回来」由编排器说了算,插件连承诺的资格都没有。
|
|
87
|
+
|
|
88
|
+
### 4.1 macOS(`process.platform === 'darwin'`)
|
|
89
|
+
|
|
90
|
+
- 代码里只认得出它(`lib/host-kind.js:141`),关机计划里有一条 `osascript` 分支(`lib/sleep.js:215`-`:224`)——
|
|
91
|
+
**这条分支从未在真机上跑过**,也没有任何测试覆盖真机行为。
|
|
92
|
+
- **你会失去什么**:
|
|
93
|
+
- 没有桌面救援快捷方式(`.lnk` 是 Windows 的东西,`lib/rescue-paths.js:591`);
|
|
94
|
+
- 自动回滚只能退化成「自建后台进程」(要靠 `setsid`,macOS 上没有这个命令 → `lib/watchdog-tier.js:135` 走不到,
|
|
95
|
+
实际会落到 `tier: none`,也就是**没有自动回滚**);
|
|
96
|
+
- 关机倒数不可靠:那条 `osascript` 分支未经真机验证;
|
|
97
|
+
- 会话续接、备份、诊断这些与平台无关的部分理论上照常工作,但**没有任何人验证过**。
|
|
98
|
+
|
|
99
|
+
### 4.2 远程 SSH 无图形环境
|
|
100
|
+
|
|
101
|
+
- 插件不检测「我在 SSH 里」,也没有为它做任何分支。它看到的只是「一个不是桌面客户端的宿主进程」。
|
|
102
|
+
- **你会失去什么**:
|
|
103
|
+
- 没有桌面、没有桌面快捷方式(页面上「桌面救援入口」那一组只能给出 `.sh` 路径);
|
|
104
|
+
- 「开机自启建议」给出的 systemd unit 在断线后会继续存在,但**它拉起的是宿主,不是你的 SSH 会话**,
|
|
105
|
+
别把它当「登录后自动接上」;
|
|
106
|
+
- 会话续接仍然会发生(新宿主起来后会恢复会话),但**什么时候起来取决于你或你的守护**,不取决于有人登录;
|
|
107
|
+
- 真正的救援只能靠 `.sh` 脚本 —— 也就是说,你得先能 SSH 上去。
|
|
108
|
+
|
|
109
|
+
### 4.3 容器(Docker / Compose / Kubernetes)
|
|
110
|
+
|
|
111
|
+
- 插件**只在一个地方**认容器:开机自启的推导(`lib/supervisor-config.js:312` 的 `detectContainer`、
|
|
112
|
+
`lib/startup-mode.js:342` 与 `lib/autostart-advice.js:89`),答案是「由编排器负责,这里没有『登录时自启』」。
|
|
113
|
+
- **关机倒数不认识容器**:`lib/sleep.js:176`-`:212` 只按 `platform === 'linux'` 分支,
|
|
114
|
+
所以容器里拿到的计划和一台普通 Linux 一模一样(`systemctl poweroff`,退 `shutdown -h now`)。
|
|
115
|
+
**早期文档写的「容器里命令是空的、什么都不会执行」与代码不符** —— 实际是「会真的去关这台 Linux」,
|
|
116
|
+
在容器里可能只是失败,也可能连带影响宿主的 init 进程,**没有验证过,不要依赖**。
|
|
117
|
+
- **你会失去什么**:
|
|
118
|
+
- 没有「开机自启」这回事:容器由编排器拉起来,插件只会在页面上这么写;
|
|
119
|
+
- 自动回滚退化成插件自己的后台进程(`setsid` 在精简镜像里常常没有 → 直接 `tier: none`,**没有自动回滚**);
|
|
120
|
+
- 关机倒数不可靠(见上),**别在容器里按那个按钮**;
|
|
121
|
+
- 容器重建后,`<DSH_HOME>/storages/dsh-autorestart/` 下的检查点与备份可能一起消失 —— 那是卷挂载的事,不是插件能保证的;
|
|
122
|
+
- 没有桌面救援入口。
|
|
123
|
+
|
|
124
|
+
> 一句话:**在这三种环境里装它,等于自己承担全部风险。** 出了问题请按 `.sh` 脚本手工还原,
|
|
125
|
+
> 或者别装。
|
|
126
|
+
|
|
127
|
+
## 五、关机:会关掉哪台机器?
|
|
128
|
+
|
|
129
|
+
这一格必须说清楚 —— 猜错了代价太大。原话来自 `lib/sleep.js` 的 `host.label`。
|
|
130
|
+
|
|
131
|
+
| 你在哪 | 关机倒数按下去会关掉什么 | 界面上的原话 | 代码 |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| Windows 桌面 | **这台 Windows 电脑** | 「关掉这台 Windows 电脑」 | `lib/sleep.js:166` |
|
|
134
|
+
| Windows 上的 `dsh web` | **这台 Windows 电脑** | 「关掉这台 Windows 电脑」 | `lib/sleep.js:166` |
|
|
135
|
+
| Linux 主机上的 `dsh web` | **那台 Linux 主机** | 「关闭这台 Linux 主机」 | `lib/sleep.js:206` |
|
|
136
|
+
| **WSL 里的 `dsh web`** | **整台 Windows 主机**(不是那个 WSL 小环境!) | 「WSL 里的 DSH → 关掉整台 Windows」 | `lib/sleep.js:187` |
|
|
137
|
+
| 容器 | 按 Linux 分支执行(**不是空命令**) | 「关闭这台 Linux 主机」 | `lib/sleep.js:206`(容器不检测,见 4.3) |
|
|
138
|
+
| macOS | 那条 `osascript` 分支(未真机验) | 「关闭这台 Mac」 | `lib/sleep.js:218` |
|
|
45
139
|
|
|
46
140
|
> ⚠️ **在 WSL 里看到关机倒数,意味着你的整台机器会关机** —— 不是只停那个 Web 服务。
|
|
47
141
|
> WSL 是 Windows 上的一个 Linux 小环境,关掉它只会停掉那个发行版;要让**主机**关机,
|
|
48
142
|
> 插件走的是挂载盘里的 `shutdown.exe`(`/mnt/c/Windows/System32/shutdown.exe /s /f /t 0`),
|
|
49
143
|
> 备选是 `powershell.exe -Command "Stop-Computer -Force"` 和
|
|
50
|
-
> `cmd.exe /c shutdown.exe /s /f /t 0
|
|
144
|
+
> `cmd.exe /c shutdown.exe /s /f /t 0`(`lib/sleep.js:193`-`:197`)。也就是说:**关的是主机**。
|
|
145
|
+
>
|
|
146
|
+
> WSL 的判定是**有证据的**:`WSL_DISTRO_NAME` / `WSL_INTEROP` / `/proc/version` 里有 "microsoft" / 挂载盘可读
|
|
147
|
+
> (`lib/host-kind.js:60`)。只靠挂载盘认出来时,界面会明说「疑似 WSL(仅凭 /mnt/c 探测)」而不是假装确定
|
|
148
|
+
> (`lib/sleep.js:186`)—— 因为一台手工挂了 C: 盘的真实 Linux 看起来一模一样。
|
|
51
149
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
用户决定不真关:为一条常识证据把所有人的会话都断掉不划算。
|
|
150
|
+
**验到什么程度**:计划层有单测覆盖(`test/sleep-plan-posix.test.mjs`:WSL 的计划必须走「关整台 Windows」那条分支、
|
|
151
|
+
纯 Linux 的计划里不许出现任何 Windows 命令、不支持的平台命令为空且**什么都不执行**)。
|
|
152
|
+
**Windows 真关电在本机跑过**;**POSIX 没有真关电** —— 用户决定不真关:为一条常识证据把所有人的会话都断掉不划算。
|
|
56
153
|
|
|
57
|
-
##
|
|
154
|
+
## 六、三档调度是怎么选的
|
|
58
155
|
|
|
59
|
-
`lib/watchdog-tier.js`
|
|
156
|
+
`lib/watchdog-tier.js:110` 的 `pickWatchdogTier()` 是**唯一**决定档位的地方
|
|
60
157
|
(设置页的警告、diagnostics 里的说明、真正注册看门狗,问的都是它):
|
|
61
158
|
|
|
62
159
|
| 档 | 机制 | 什么机器 |
|
|
63
160
|
|---|---|---|
|
|
64
|
-
| `scheduled-task` | 一次性 Windows
|
|
65
|
-
| `systemd-run` | `systemd-run --user --on-active=<秒>s --unit=dsh-autorestart-watchdog
|
|
66
|
-
| `detached-process` | `setsid nohup sh -c 'sleep <秒>; <看门狗>' >/dev/null 2>&1
|
|
67
|
-
| `none` | 没有可用机制 → **看门狗不注册**,`tier: 'none'`,设置页与 diagnostics 都明说「没有自动回滚」 |
|
|
161
|
+
| `scheduled-task` | 一次性 Windows 计划任务(`lib/watchdog-schedule.js:476`:建完回读校验、用完全删、`IgnoreNew`、`EndBoundary` 必填) | Windows(桌面与 web) |
|
|
162
|
+
| `systemd-run` | `systemd-run --user --on-active=<秒>s --unit=dsh-autorestart-watchdog`(`lib/watchdog-schedule.js:576`),结束后 `systemctl --user reset-failed` | Linux、WSL(有 systemd 时) |
|
|
163
|
+
| `detached-process` | `setsid nohup sh -c 'sleep <秒>; <看门狗>' >/dev/null 2>&1 &`(`lib/watchdog-schedule.js:894`),pid 写进 stateDir 并回读确认还活着 | Linux / WSL(没有 systemd,但有 `setsid` 时) |
|
|
164
|
+
| `none` | 没有可用机制 → **看门狗不注册**,`tier: 'none'`,设置页与 diagnostics 都明说「没有自动回滚」 | 被策略禁用的机器、没有 `schtasks` 的 Windows、既没有 systemd 也没有 `setsid` 的机器、macOS |
|
|
165
|
+
|
|
166
|
+
**一台机器同时只会有一个监护者**:`lib/watchdog-arbiter.js:46` 先看插件自己的守护(v2)能不能用,
|
|
167
|
+
能用就用它,用不了才回落到「脱离进程 + 计划任务」那一套(v1)。两个一起上会互相打架。
|
|
68
168
|
|
|
69
|
-
##
|
|
169
|
+
## 七、手工还原脚本在每个平台上做什么
|
|
70
170
|
|
|
71
171
|
两个版本是**同一套规则**(`scripts/restore-profile.ps1` 与 `scripts/restore-profile.sh`):
|
|
72
172
|
|
|
73
|
-
1. 默认取 `<stateDir>/backups/`
|
|
173
|
+
1. 默认取 `<stateDir>/backups/` 里**最新、完整、且身份为 `known-good`** 的那份(`lib/backup.js:746`,身份 = 拍完之后有过一次启动成功);
|
|
74
174
|
2. 还原前把现在的 profile 整份挪到 `<profileDir>.broken-<UTC 时间戳>`(**不删**);
|
|
75
175
|
3. 还原后回读校验:`package.json` 能解析、`dsh.profile.bundles` 非空、
|
|
76
176
|
**声明了 `dsh-autorestart` 就必须能在 `node_modules` 里读到版本号**;
|
|
@@ -81,7 +181,16 @@
|
|
|
81
181
|
中文文案都来自 `scripts/rescue-strings.zh.json`(UTF-8 **无 BOM**);没有 node 的
|
|
82
182
|
机器上退回英文。
|
|
83
183
|
|
|
84
|
-
##
|
|
184
|
+
## 八、旧版本文档里已经订正的说法
|
|
185
|
+
|
|
186
|
+
| 旧说法 | 实际行为 | 依据 |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| 「设置页只显示这台机器上真有的脚本路径」 | 页面**每个平台都会列出两条脚本路径**并带复制按钮;路径是**按平台名拼出来的**,代码不校验文件是否真在磁盘上;只有「桌面快捷方式」是 Windows 专属 | `lib/rescue-paths.js:39`、`:614`、`:615`;`client.js:7710`-`7760`、`:7743` |
|
|
189
|
+
| 「重启前备份默认留 2 份」 | 默认上限 **20 份**(`maxBackups`) | `lib/backup.js:96`、`index.js:734` |
|
|
190
|
+
| 「容器里关机命令是空的、什么都不会执行」 | 容器**不被识别**,走普通 Linux 分支(`systemctl poweroff` → 退 `shutdown -h now`) | `lib/sleep.js:176`-`:212` |
|
|
191
|
+
| 「自动回滚被拒时的降级链路」 | 仍然**有意不做**:被禁就是被禁,只告知,不偷偷换别的机制 | `lib/watchdog-tier.js:117`-`:147`(Windows 没有 `schtasks` 直接 `none`) |
|
|
192
|
+
|
|
193
|
+
## 九、怎么自己验一遍
|
|
85
194
|
|
|
86
195
|
| 平台 | 命令 |
|
|
87
196
|
|---|---|
|
|
@@ -90,3 +199,10 @@
|
|
|
90
199
|
|
|
91
200
|
演练在**自己的临时目录**里造假 profile、故意弄坏、还原、断言,然后清理干净 ——
|
|
92
201
|
它不碰任何正在用的 profile。
|
|
202
|
+
|
|
203
|
+
想确认「这台机器被认成什么」:打开设置页看「开机自启 / 启动方式 / 看门狗档位」三处,
|
|
204
|
+
它们与 `GET <healthPath>` 报的是同一份答案。
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
**事实核对日期**:2026-11-02(对照仓库当时提交的源码逐个函数核对;行号只在这一天成立)。
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# 0.8.6 真机实测读数(原始值,供 release 引用)
|
|
2
|
+
|
|
3
|
+
这份文档只做一件事:**把真机跑出来的原始读数照抄下来**,供发版说明逐条引用。
|
|
4
|
+
它不写结论性的形容词,也不替读者判断。凡是没验的、没观察到的,都留在表里并写明「未验 / 未观察到」。
|
|
5
|
+
|
|
6
|
+
采集时间:**2026-11-02 18:50–19:05**(主采窗口);被测版本:`0.8.6`。
|
|
7
|
+
另有两处更早的采样(17:33、18:28)用于看「未完成备份目录」的生命周期,已在该行单独标注时间,不并入主窗口。
|
|
8
|
+
**本轮读数取代早前一轮的读数**:早前一轮的 `generation` / 端口 / pid 等数字不再作为 0.8.6 的证据引用。
|
|
9
|
+
|
|
10
|
+
| 环境编号 | 环境 | 结论口径 |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| E1 | Windows 桌面客户端(用户日常那台,真 profile) | 已实测(见第 1 节) |
|
|
13
|
+
| E3 | WSL `dsh web`,Ubuntu-22.04,**有** systemd | 已实测(见第 2 节) |
|
|
14
|
+
| E2 | Windows 上的 `dsh web` | **未验**(见第 4 节) |
|
|
15
|
+
| E4 | WSL 无 systemd | **未验**(见第 4 节) |
|
|
16
|
+
| E5 / E6 | 原生 Linux(systemd / 无 systemd) | **未验**(见第 4 节) |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. E1 — Windows 桌面客户端(真 profile,应用模式)
|
|
21
|
+
|
|
22
|
+
| 环境 | 真触发的动作 | 读数(原始值) | 判定 |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| E1 | 重启**前**读 `health`(旧实例,端口 `52601`) | `pluginVersion = 0.8.6`、`backups.count = 2`、`backups.incomplete = 0`、`watchdog.tier = scheduled-task`、`restart.mode = app` | 动手前加载的是本次版本;盘上没有半份备份 |
|
|
25
|
+
| E1 | **真触发一次重启**(`continuum_restart`,排队后执行) | 端口 `52601` → **`50519`** | 宿主换了一个进程(应用模式自己拉起自己) |
|
|
26
|
+
| E1 | 重启**后**读 `health`(新实例,端口 `50519`) | `pluginVersion = 0.8.6`、`generation = 264`、`restart.mode = app`、`lastRestart.result = ok`、`lastRestart.durationMs = 26067`、`watchdog.tier = scheduled-task`、`backups.count = 2`、`backups.incomplete = 1` | 新实例报的版本仍是 `0.8.6`(新代码确实被加载);重启判定 `ok`、用时 `26067` ms;此刻盘上有 **1 份未完成备份**(窗口见第 5 节观察①) |
|
|
27
|
+
| E1 | 重启后**立刻**读 `backupHygiene` | `{"skipped":false,"removed":[],"kept":["2026-11-01T13-33-42-271Z","2026-11-01T23-14-17-160Z","2026-11-02T11-00-17-754Z"],"scanned":3,"bytes":0,"error":null}` | 清扫**没有删**任何东西(`removed: []`),扫到 3 个、保留 3 个、释放 0 字节、无错误——年轻目录被年龄保护挡住(见观察①) |
|
|
28
|
+
| E1 | **17:33 采样**:数备份目录 | 3 个目录;其中 `2026-11-02T09-31-54-480Z` = **460 个文件 / 152.8 MB / 没有 manifest** | 盘上确实存在一份没有 `manifest` 的半成品 |
|
|
29
|
+
| E1 | **18:28 复查**:同一台机器再数一次 | 该目录已不存在;磁盘回到 **2 个**目录、两个都有 `manifest`;`health` 变成 `count = 2`、`incomplete = 0` | 半成品在窗口过后被清扫掉了;`health` 的 `incomplete` 回到 `0` |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. E3 — WSL `dsh web`(Ubuntu-22.04,有 systemd,命令模式)
|
|
34
|
+
|
|
35
|
+
| 环境 | 真触发的动作 | 读数(原始值) | 判定 |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| E3 | 起托管实例 | `/home/fg/supervise-web.sh`;进程 `node /home/fg/.npm-global/bin/dsh web --port 5601 --no-open` | 托管实例由外部脚本拉起(`supervisor mode: external`) |
|
|
38
|
+
| E3 | 重启**前**读 `health` | `pluginVersion = 0.8.6`、`restart.mode = command`、`restart.command = /home/fg/restart-web.sh`、`watchdog.tier = systemd-run`、`watchdog.disarm.ok`;`diagnostics` → `host kind: wsl`、`desktop client: no`、`supervisor mode: external` | 加载的是本次版本;重启走命令模式且命令已记下;看门狗档位是 `systemd-run` |
|
|
39
|
+
| E3 | **真触发一次命令模式重启**(`POST /__dsh_autorestart/park`,body 含真实 `sessionId` 与 `restart: true`) | 请求 `state = queued`;旧 pid **`1503175`** → 新 pid **`1532646`** | 重启命令确实把进程换掉了 |
|
|
40
|
+
| E3 | 重启**后**读 `lastRestart` | `result = ok`、`durationMs = 25158`、`failedChecks = []`、`generation = 28` | 命令模式重启完成、用时 `25158` ms,四项启动检查无失败项 |
|
|
41
|
+
| E3 | 查 systemd 残留 | `systemctl --user list-timers \| grep autorestart` = **0**;`systemctl --user list-units \| grep autorestart` = **0** | 该清掉的清掉了(两处均为 0) |
|
|
42
|
+
| E3 | 读备份状态 | `backups.incomplete = 1` | 与 E1 同一种窗口:重启刚结束、半成品还没到年龄(见观察①) |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. 桌面端救援开关 `emergency-toggle.ps1`(**早前一轮**的读数,本轮未复采)
|
|
47
|
+
|
|
48
|
+
这一节的读数来自同一版本(`0.8.6`)的**更早一轮**实测,本轮 18:50–19:05 没有重跑;保留它是为了让发版说明仍能引用,但不要读成「本轮采到」。
|
|
49
|
+
试跑对象是**真实 profile 的副本**,不是真 profile 本体。
|
|
50
|
+
|
|
51
|
+
| 环境 | 真触发的动作 | 读数(原始值) | 判定 |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| 真实 profile 副本 | `-Action disable` | `exit 0`;`bundles` 26 → 25 | 成功摘掉一个 bundle |
|
|
54
|
+
| 真实 profile 副本 | `-Action enable` | `exit 0`;`bundles` 25 → 26;JSON 合法;`name` 行未被污染;生成 `package.json.bak-emergency-*` | 成功恢复,且恢复前留了备份 |
|
|
55
|
+
| 真实 profile 副本 | 对比 enable 后的位置 | 追加到 `bundles` 末尾,**不是原位置** | **已知偏差**(见第 6 节,功能可用但顺序变化) |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 4. 未验项(留在表里,不许删)
|
|
60
|
+
|
|
61
|
+
| 环境编号 | 环境 | 真触发的动作 | 读数(原始值) | 判定 |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| E2 | Windows 上的 `dsh web` | —(本轮没有起该实例) | 无 | **未验**:本轮没有在这台机器的 Windows 上跑托管(`dsh web`)实例 |
|
|
64
|
+
| E4 | WSL **无** systemd | — | 无 | **未验**:这台 WSL 默认带 systemd,构造不出「无 systemd」的 WSL |
|
|
65
|
+
| E5 | 原生 Linux(有 systemd) | — | 无 | **未验**:手头没有原生 Linux 机器 |
|
|
66
|
+
| E6 | 原生 Linux(无 systemd) | — | 无 | **未验**:手头没有原生 Linux 机器 |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 5. 本次观察(不是缺陷,但必须留档)
|
|
71
|
+
|
|
72
|
+
| 观察 | 事实(原始读数) | 说明 |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| ① 每次重启都会**短暂**留下一个未完成备份目录 | 本轮两次重启(E1 桌面、E3 WSL)各留一份。E1 上实测到的是 `2026-11-02T09-31-54-480Z`:460 个文件 / 152.8 MB / **没有 manifest**;17:33 采样时它在盘上,18:28 复查时已不存在(磁盘回到 2 个目录、两个都有 manifest、`health` 的 `count=2`、`incomplete=0`)。重启后立刻读 `backupHygiene` 得到 `removed: []`、`scanned: 3`、`kept: 3`、`bytes: 0`、`error: null` | 备份是**先写成暂存目录、落盘完成才改名**,所以任何中断(包括正常重启把进程换掉)都会短暂留下一个暂存目录。它在 **2 分钟年龄保护窗口**内被保留,出了窗口才被清扫;也就是说 `health` 在窗口内会如实报 `incomplete = 1`,窗口过后回到 `0`。**这不是缺陷,是窗口期的如实上报**——但引用时不要把窗口内的 `incomplete=1` 写成「备份坏了」 |
|
|
75
|
+
| ② `health?state=1` 在托管实例上**单次响应约 16.5 秒** | E3(WSL `dsh web`)上单次 `health?state=1` 约 16.5 秒返回 | 该路由会**遍历备份目录**(份数、每份时间、总占用都要真读),备份越大越慢。桌面端(E1)没有这个体感;引用 `health?state=1` 的数字时要连这个耗时一起说,不要让人以为是瞬间返回 |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 6. 已知偏差 / 未证实(如实记录)
|
|
80
|
+
|
|
81
|
+
| 项 | 事实 | 状态 |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| 看门狗真武装时的 `armUnits` 输出 | 本轮**没有观察到**——武装窗口太短,来不及抓到该字段 | **未观察到,不得写成已验证** |
|
|
84
|
+
| `emergency-toggle.ps1 -Action enable` 的 bundle 位置 | 恢复时追加到 `bundles` 末尾,而不是回到原来的位置(真实 profile 副本实测) | 功能可用(`exit 0`、JSON 合法、`name` 行未污染),顺序与原始不同 |
|
|
85
|
+
| 重启等待里 `reason` 曾被丢弃 | 桌面 `reason` 字段曾被 state 白名单裁掉,导致「为什么等」读不回来 | **已修**(早前一轮修复,白名单已补该字段) |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 7. 这份表能证明什么、不能证明什么
|
|
90
|
+
|
|
91
|
+
- **能证明**(本轮 2026-11-02 18:50–19:05 的读数):
|
|
92
|
+
- E1 与 E3 两台真机上,`0.8.6` 的代码**确实被加载**——两台在重启前后读到的 `pluginVersion` 都是 `0.8.6`(桌面新实例还报了 `generation = 264`、WSL 报了 `generation = 28`)。
|
|
93
|
+
- **两条重启路径都在真机上走通了**:E1 应用模式(端口 `52601` → `50519`,`durationMs = 26067`)、E3 命令模式(pid `1503175` → `1532646`,`durationMs = 25158`),两边 `lastRestart.result` 都是 `ok`。
|
|
94
|
+
- **该清掉的清掉了**:E3 的 `systemctl --user list-timers` / `list-units` 匹配 `autorestart` 都是 **0**;E1 的 `backupHygiene` 报告 `error: null`。
|
|
95
|
+
- **该写下的写下了**:E1 的 `backupHygiene` 读回真实目录清单、`health` 读回 `backups.count` / `incomplete`;E3 读回 `watchdog.tier = systemd-run`、`disarm.ok`。
|
|
96
|
+
- **不能证明**:
|
|
97
|
+
- **E2 / E4 / E5 / E6 四格是空的**——Windows 上的 `dsh web`、无 systemd 的 WSL、原生 Linux(有/无 systemd)本轮一个都没跑,不能因为「同一份代码」就推断它们也行。
|
|
98
|
+
- 看门狗真武装时的 `armUnits`:本轮仍未观察到。
|
|
99
|
+
- `emergency-toggle.ps1 -Action enable` 后 bundle 回到原位置:已知是追加到末尾,且这一节还是早前一轮的读数。
|
|
100
|
+
- 窗口期的 `incomplete = 1` 不代表备份损坏;数据能证明的是「窗口内如实上报、窗口后回到 0」,不是「每一刻都是 0」。
|
|
101
|
+
- `health?state=1` 在托管实例上要约 16.5 秒,不能读成「健康检查是瞬时的」。
|
|
102
|
+
- 引用这段话时,请把它读成「**两台真机、两条重启路径验过;另有 4 个环境未验、2 项未证实或有偏差**」,不能当作普适结论引用。
|
|
103
|
+
|
|
104
|
+
## 2026-11-02 21:00 界面改版后的复核(第二轮)
|
|
105
|
+
|
|
106
|
+
这一节只追加本轮读数,上面任何历史条目都没有改动。**每条都标了证据强度**:`【实测】` = agent 本轮自己读出来的原始值;`【队长目视】` = 队长在真机界面上看到/触发的,agent 没有独立复现;`【未验】` = 明确没有证据。
|
|
107
|
+
|
|
108
|
+
1. **【队长目视】宿主已重启加载新代码,界面能打开。** `Ctrl+,` → 设置 →「自动重启与关机」可正常打开;该屏**未见**「安全份」「已验证」「可放心回退」「认证依据」任何字样。(agent 本轮没有独立操作界面。)
|
|
109
|
+
2. **【未验】折叠块「备份说明及手动备份操作」在自动化点击下不展开。** 队长试了 4 次(点三角、点标题文字、点整行;先后用后台 UIA/PostMessage 与前台 SendInput 两种投递),每次点完仍是收起状态。因此折叠块展开后的**两行份数文案、下拉框右侧提示、窄窗口下的换行,全部没有被 agent 目视确认**。用户此前提供的展开截图是改动**之前**的版本,**不能当作改动后的证据**。
|
|
110
|
+
3. **【未验】「份数 2→3 界面生效」没有真机点击证据。** 现有证据只是单元测试与断言:t2 新增 2 条回归(`test/settings-startup-backup.test.mjs` 的「② 2 → 3 really writes the limit: one POST goes out and the number moves」与「the write route survives every shape the Host reports health in」),断言了「恰好 1 次 POST、body `{maxBackups:3}`、重渲染后 select 值为 3、提示出现」。**真机上点下拉框这一步未验。**
|
|
111
|
+
4. **【实测】测试读数。** 本轮在插件根目录跑 `node --test`(DSH 自带 node v24.21.0):`ℹ tests 1373 / ℹ pass 1373 / ℹ fail 0 / exit 0`。相对上一轮的 1363 条,增量 10 条:t3 的 4 条、t2 的 2 条、t4 的 4 条。
|
|
112
|
+
5. **【单测实测 / 真机未验】回退触发的那次重启不写备份。** 证据只有单元测试:在磁盘上前后数目录、`deepEqual` 对比并带防空断言。**真机从未执行过回退**——回退会把现有插件与配置换成备份内容,用户未批准,agent 不擅自做。
|
|
113
|
+
6. **【实测】版本与仓库状态。** `package.json` = `"version": "0.8.6"`;`index.js` = `const PLUGIN_VERSION_FALLBACK = '0.8.6'`;`README.md:265` = `Current version: **0.8.6**`;`docs/README.zh.md:415` = `当前版本:**0.8.6**` —— 四处一致。本地 `main` 的 HEAD = `54da5a6`,工作树干净(`git status --porcelain` 输出 0 行);**相对 `origin/main` 领先 49 个提交**(`git rev-list --count origin/main..main` = **49**,`origin/main` 停在 `ef6a445`)。派单里写的是 48,这一行以 agent 本轮实测的 49 为准——差 1 很可能是数字被记在最后一次提交之前。**尚未推送。**
|
|
114
|
+
|
|
115
|
+
> **agent 未能展开折叠块,故界面排版证据缺失,需用户目视确认后补记。**
|
|
116
|
+
|
|
117
|
+
## 2026-11-03 测试侧证据(非真机)
|
|
118
|
+
|
|
119
|
+
这一节只追加本轮读数,上面任何历史条目都没有改动。**本节的每一条都是测试侧证据,不是真机证据**:它证明的是「某条断言真的抓得住某处回归」,不证明「真宿主上跑得对」。
|
|
120
|
+
|
|
121
|
+
### 证伪实验:排队重启的回归测试是不是空断言
|
|
122
|
+
|
|
123
|
+
方法:把 `plugins/dsh-autorestart` 整目录复制到 `%TEMP%` 下的临时副本,**在副本里**分别回退两处修复点,看 `test/restart-queue-gate.test.mjs` 是否变红;每轮结束从真树恢复副本,副本用完删除。**真树全程只读**(结束时 `git status --short` 与开工前逐行一致)。
|
|
124
|
+
|
|
125
|
+
两处修复点(都在 `index.js`):
|
|
126
|
+
|
|
127
|
+
- **A** = `queuedRestartRecord()` 改直读 `readRestartRequestRecord(stateDirOf())`,认 `queued` / `deferred` 为 pending,不再问 `restartRequestInfo()`;
|
|
128
|
+
- **B** = `summarizeRestartRequest()` 在 `live === null` 但记录非终态时回 `pending: true`。
|
|
129
|
+
|
|
130
|
+
| 副本里的状态 | `restart-queue-gate.test.mjs` | 说明 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| 原样(基线) | **3 / 3 pass** | 副本本身是绿的,后面才有意义 |
|
|
133
|
+
| 只回退 **B**(脱钩时不回 pending) | **① 变红**:`AssertionError … actual: false, expected: true`(断言 `runtime.restartRequestInfo().pending === true`) | **B 被钉住**:这条断言真的抓得住这次回归 |
|
|
134
|
+
| 只回退 **A**(`queuedRestartRecord` 改回走 `restartRequestInfo()`) | **仍 3 / 3 全绿** | **A 没有被任何测试覆盖** |
|
|
135
|
+
| 两处都恢复 | 3 / 3 pass,副本已删除 | — |
|
|
136
|
+
|
|
137
|
+
**为什么只回退 A 仍然全绿**:B 让 `restartRequestInfo()` 自己就能答出 pending,于是旧写法的 A 也照常工作——A 与 B 是同一事实的两条独立实现,测试走的是 B 那条。
|
|
138
|
+
|
|
139
|
+
**裁决(队长):A 保留,不删、也不补专门测试。** 理由写在这里以免日后被误读成「A 冗余可删」:A 的价值不在「再算一遍同一件事」,而在**不让轮询器依赖一个「汇报用」的函数**——当初那个 bug 恰恰就是轮询器去问汇报器、而汇报器答错(意图一没就答 null,轮询器于是自我停止)。
|
|
140
|
+
|
|
141
|
+
### 尚未做的
|
|
142
|
+
|
|
143
|
+
- **排队修复的真机复验尚未做。** 上面全是单元测试层面的证据:真机上「点重启 → 等会话静默 → 重启真的发生」这一步还没有人跑过;本节的绿不能读成「真机已通过」。
|
|
144
|
+
- 设置页在真机上的排版(折叠块能否展开、两行份数文案、下拉框右侧提示在窄窗口下的换行)仍然只有上一节的「未验」结论,没有被补上。
|
|
145
|
+
|
|
146
|
+
## 2026-11-03 01:20 真机复核(第三轮)
|
|
147
|
+
|
|
148
|
+
这一节只追加本轮读数,上面任何历史条目都没有改动。**每条都标了证据强度**:`【真机读数】` = 在本机真实 profile / 真实桌面上读到的值;`【单测+证伪】` = 只有单元测试层面的证据,且做过去掉修复看断言是否变红的证伪。
|
|
149
|
+
|
|
150
|
+
1. **【真机读数】桌面快捷方式的图标从来自愈不了。** `.lnk` 的 `IconLocation` 停在旧图标 `…\shortcut-icon-9c245e5b.ico`,写入时间 `2026-11-02 23:02:33`——宿主已重启、新代码已加载,它**仍然指着旧图标**。根因:`createDesktopShortcut` 全仓只有一个调用点(`index.js` 的 `POST action === 'shortcut'`),启动路径从不写 `.lnk`,所以重启多少次都不会自愈。修法(t33):`readShortcutLink()` + `syncShortcutIcon()` 读回 `TargetPath` / `IconLocation`、「不同才写」、写完 `SHChangeNotify` 刷壳、`TargetPath` 原样保留,启动路径用 `try/catch` 包住校准。修后读数:`IconLocation = …\storages\dsh-autorestart\shortcut-icon-1902bb15.ico,0`,`TargetPath` 仍是 `restore-profile.cmd`,第二次 sync 返回 `already-current`。
|
|
151
|
+
2. **【真机读数】更糟的一件:跑全量测试会改写用户真实桌面。** 在加 guard 之前,测试把真 `.lnk` 的图标改成了 `%TEMP%` 里的路径(实测读到 `…\Temp\dsh-autorestart-it-recover-now-idle-…\shortcut-icon-1902bb15.ico`)。已加 `isInsideTempDir()`:临时目录里的图标永不写进真快捷方式。
|
|
152
|
+
3. **【真机读数】排队重启被作废后用户看不见。** `2026-11-02 23:04:38` 请求的重启等了 `8473292 ms`(约 2 小时 21 分)从未执行,宿主重启后 boot-recovery 判为 `cancelled`(理由含 `stale-never-executed`),页面上什么都没有。修法(t34):设置页顶部一条 `role=status` 的可见提示,只在 `state==='cancelled' && by==='boot-recovery'` 时出现,带请求时间与人话时长(「约 2 小时 21 分」),不让任何内部标识符上屏,其余五种结局都不画。
|
|
153
|
+
4. **【单测+证伪】`health.backups.available` 恒为 0。** 真机 health 报 `available: 0`,而磁盘上两份备份都在、其中一份 `intervention:false` + `certifiedBy:full-checks` 确实可还原。根因(t36):`available` 在一份「给页面看的投影」上判资格,而投影里没有 `boot` / `trigger` 两个键 → 判据拿不到证据 → **常数 0**。修法:先取完整 `identities`,投影仍由它派生(投影字段一个未增),`available` 改在 `identities` 上统计,与看门狗 / 回退助手同一个函数。已补 4 条测试并证伪(改回旧写法 → 3 条变红)。**注意:这一条尚未在真机上重读 health 复核。**
|
|
154
|
+
5. **【真机读数 / 教训】一个可以长期复用的观察**:这条缺陷长期没被发现,不是因为难,而是因为**没有任何一条测试断言过 `available` 的取值**——一个数字只要没人断言,它错成常数也能全绿。
|
|
155
|
+
|
|
156
|
+
### 尚未做的(本轮明确没做完的事)
|
|
157
|
+
|
|
158
|
+
- **排队修复的真机复验**:本轮未做,**已在下一节(第四轮)补上** —— 在新代码上真触发了一次重启,四项读数全过。
|
|
159
|
+
- **测试套件泄漏临时目录(t37):已完成。** 本节先前写的 `34448 个 / 4.17 GB` 是**只数了一个前缀**的错估,真实规模与收尾见下一节末尾「测试临时目录泄漏的收尾」。
|
|
160
|
+
|
|
161
|
+
## 2026-11-03 08:01 真机核验(第四轮 · 重启四项读数全过)
|
|
162
|
+
|
|
163
|
+
这一节只追加本轮读数,历史条目一字未改。**触发动作是「在新代码上真触发一次重启」**,不是手动重开客户端。
|
|
164
|
+
|
|
165
|
+
### ① 真触发一次它管的动作
|
|
166
|
+
|
|
167
|
+
`generation` **269 → 270**,全程 `durationMs = 42611`(约 42.6 秒),不需要人动手。
|
|
168
|
+
|
|
169
|
+
### ② health 自报的版本
|
|
170
|
+
|
|
171
|
+
`pluginVersion = 0.8.6`,与这次要发的版本一致 —— 证明**新代码真的被加载了**(不是跑在旧代码上)。
|
|
172
|
+
|
|
173
|
+
### ③ 该清掉的清掉了
|
|
174
|
+
|
|
175
|
+
- 计划任务匹配 `*dsh*` 数量 **0**;
|
|
176
|
+
- 看门狗报告 `armResidualUnits=0`、`disarm.probesRemoved=0`、`disarm.probesFailed=0`;
|
|
177
|
+
- 上一轮留在桌面的四个探针文件 `probe-icon-test.lnk`、`_probe-g.lnk`、`probe-desk-2.lnk`、`_probe-icon-copy.ico` 逐个实测**均不存在**。
|
|
178
|
+
|
|
179
|
+
### ④ 该写的写对了
|
|
180
|
+
|
|
181
|
+
- `last-restart-verdict.json`:`result=ok`、`generation=270`、四项自检 `listener` / `routes` / `plugin` / `recovery` **全 true**、`failedChecks` **0**;
|
|
182
|
+
- `last-restart-request.json`:`result=cancelled`、`reason=consumed-by-boot (the previous instance had already started this restart (attempts 1))`、`attempts=1`、`snapshotTaken=true`、`snapshotDir` 指向**真实存在**的目录。
|
|
183
|
+
|
|
184
|
+
### 两条「反着也能对」的证据(本节的重点)
|
|
185
|
+
|
|
186
|
+
1. **裁剪方向对了。** 重启前的备份目录是 `[2026-11-01T13-33-42-271Z(0.1 MB,首装种子)、2026-11-01T23-14-17-160Z(148 MB)]`;重启后是 `[2026-11-01T23-14-17-160Z、2026-11-03T00-01-04-974Z(166,828,489 字节)]`。**新快照留下了,被删的是最老的那份种子**——而修复前那次的真实行为恰恰相反(写好的 161 MB 新快照被删掉)。这是 t9 那个修复在真机上的直接证据。
|
|
187
|
+
2. **`snapshot.missing` 双向都对。** 修复前那次 health 报 `taken=false` + `missing=true`(目录确实被删了);这次报 `taken=true` 且不再报 missing(目录真的在)。**同一个核对逻辑,两个方向都如实。**
|
|
188
|
+
|
|
189
|
+
### 桌面快捷方式未被这次重启改坏
|
|
190
|
+
|
|
191
|
+
`IconLocation` 仍是 `…\storages\dsh-autorestart\shortcut-icon-1902bb15.ico,0`,`TargetPath` 仍是 `restore-profile.cmd`。
|
|
192
|
+
|
|
193
|
+
### 测试临时目录泄漏的收尾(t37)
|
|
194
|
+
|
|
195
|
+
真实规模(不是先前那个单前缀的 `34448 个 / 4.17 GB`):
|
|
196
|
+
|
|
197
|
+
- **六种前缀**:`dsh-autorestart-*`、`dsh-restart-request-*`、`dsh-ar-*`、`dsh-ledger-*`、`dsh watchdog <label> `(含空格)、`ar-*`;
|
|
198
|
+
- 共 **131,151 个目录 / 1,219,987 个文件 / 14,350 MB**;
|
|
199
|
+
- **修法**:新增 `test/_tmp-guard.mjs`(包装 `fs.mkdtempSync` / `fs.mkdtemp`,把落在 `os.tmpdir()` 下的结果记进本进程集合,`process.on('exit')` 逐个删;另导出 `cleanupTempDirsNow()` 兜底),67 个测试文件首行 `import './_tmp-guard.mjs'`;
|
|
200
|
+
- **干净窗口实测**:`131151 → 跑完全量 → 131151`,**新增 = 0**;失败场景也清(副本里加一条必失败的测试,delta = 0);
|
|
201
|
+
- **历史残留已清理**:两轮共删约 **12.9 万个**目录(123,788 + 5,215 + 2),失败 0;可用空间 **92.7 GB → 108.17 GB(释放 15.5 GB)**;`%TEMP%` 顶层目录 **5,942 → 725**;本插件六种前缀剩余 **0**。
|
|
202
|
+
|