dsh-autorestart 0.6.2 → 0.7.0

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/cordis.patch.yml CHANGED
@@ -61,8 +61,44 @@
61
61
  # Keep at most this many snapshots; older ones are deleted automatically.
62
62
  maxBackups: 2
63
63
  # How long the detached watcher waits for the relaunched host (ms) before
64
- # it restores the snapshot and launches the host again.
64
+ # it restores the snapshot. The watcher NEVER launches a second copy of
65
+ # the host: it restores the files and writes the report.
65
66
  rollbackTimeoutMs: 90000
67
+ # How long the restart command may run (ms) before the plugin stops
68
+ # waiting for it. A command that starts the host in the foreground never
69
+ # returns — that is not a failure, but it is reported as "still running"
70
+ # instead of "restarted".
71
+ restartCommandTimeoutMs: 30000
72
+ # How long ONE restore step may take (ms): the configuration files, then
73
+ # the installed plugin tree. A restore that does not finish in time is
74
+ # given up and the reason is written into the rollback report.
75
+ rollbackCopyTimeoutMs: 120000
76
+ # The self-starting supervisor: who starts this host.
77
+ # auto = nobody decided yet, installed = the supervisor of this plugin,
78
+ # external = your own way (docker, systemd, your own script),
79
+ # off = no automatic start at all.
80
+ supervisorMode: auto
81
+ # Which form of the self-start record was installed. Empty = the
82
+ # default of this platform (a scheduled task on Windows, a systemd user
83
+ # unit on Linux).
84
+ supervisorKind: ''
85
+ # How long the supervisor waits before it starts the host again after it
86
+ # died. The delay DOUBLES with every failed start.
87
+ supervisorRestartDelayMs: 3000
88
+ # The upper bound of that delay: the supervisor never waits longer.
89
+ supervisorRestartDelayMaxMs: 30000
90
+ # How long the supervisor waits for the host to answer its health route
91
+ # before it restores the snapshot.
92
+ supervisorStartTimeoutMs: 90000
93
+ # How long the supervisor waits between asking the host to stop and
94
+ # killing it.
95
+ supervisorKillGraceMs: 15000
96
+ # The port the supervisor waits for to become free before it starts the
97
+ # host (0 = the port of the current instance).
98
+ supervisorPort: 0
99
+ # Whether the one-time question may install the supervisor without
100
+ # asking again.
101
+ supervisorAutoInstall: false
66
102
  # 收工关机:Show the graceful-shutdown button at the foot of the left
67
103
  # sidebar, next to the Settings row (root scope: it is there even when
68
104
  # no conversation is open).
package/docs/README.zh.md CHANGED
@@ -30,6 +30,75 @@
30
30
  | Agent 有一个持久 goal(`create_goal`)跨重启 | goal 的 activation 是进程内的,重启后变回 `disarmed`,自动续跑停止 | 启动时重新 `goals.resume()`,goal round driver 立刻开始下一轮 |
31
31
  | Agent 想装/升级插件但"重启必打断" | 只能等或赌 | 调 `continuum_restart`:等所有 agent idle(当前这一轮跑完)再重启,回来自己接着做 |
32
32
 
33
+ ## 三分钟看懂(普通用户读这一节就够)
34
+
35
+ ### 重启为什么不会丢活
36
+
37
+ 插件把"重启"做成一件**可恢复**的事,顺序是固定的:
38
+
39
+ 1. **先把会话状态落盘** —— 每个会话在做什么、哪些 goal 还带着,都先写进磁盘上的检查点;
40
+ 2. **排队等所有会话空闲** —— 谁还在跑就等谁,等齐了才动手;
41
+ 3. **换新进程** —— 老进程退出,新进程起来(桌面版由应用自己拉起自己;Web 托管端见下一节);
42
+ 4. **新进程自检** —— 起来之后核对四项:端口通了没、路由齐了没、插件加载了没、会话恢复完没;
43
+ 5. **没起来就回滚** —— 自检没过,就把重启前的快照(配置 + 已装插件)放回去,并且把这次重启的结论如实写在状态里。
44
+
45
+ 所以"重启丢活"变成了"重启后接着干":你不用再补一句"继续"。
46
+
47
+ ### 两种运行形态,各管各的
48
+
49
+ | | 桌面客户端 | Web 托管端(`dsh web`) |
50
+ |---|---|---|
51
+ | 谁负责把它拉起来 | **应用自己**(自己关自己、自己开自己) | 先看"是谁拉起的" |
52
+ | 要不要装系统服务 | **不要**。装了反而会双开:登录时再多开一个 DSH | 只在"没人管它"的时候才可能装 |
53
+ | 已经有人管着它(守护脚本 / systemd / 容器 / pm2 / Windows 计划任务…) | 不适用 | **只优雅退出**,交给那个守护去拉起它(不自己再起一个) |
54
+ | 认不出是谁拉起的 | 不适用 | **什么都不做,也不猜命令** —— 宁可说"我认不出",也不在第一个旁边再开一个 |
55
+
56
+ ### 开机自启:插件只给建议,不替你改系统
57
+
58
+ 插件**不会**改你的系统配置、**不会**替你注册任何开机项。它只在设置页把"这台机器上怎么做开机自启"讲清楚,要用的命令可以直接复制,要不要用由你决定。
59
+
60
+ | 你的情况 | 推荐做法 | 注意 |
61
+ |---|---|---|
62
+ | Windows 上的 `dsh web` | 登录时自启:优先用"计划任务"(不需要管理员);这台机器不允许的话,就用"启动文件夹"里的一个 .cmd | 记录里跑的是**真正的 node + 守护脚本**,不是 DSH 客户端本体(否则每次登录会再开一个 DSH) |
63
+ | Linux(不是 WSL) | systemd 用户服务 | 这是**登录后自启**;要**开机自启**(不必先登录)得另外开一次 linger |
64
+ | WSL 里有 systemd | 同上:systemd 用户服务 + linger | 同上 |
65
+ | WSL 里没有 systemd | 这里没有"登录时自启"这回事。两条路:① 在 WSL 内部开启 systemd(改发行版的配置 + 重启子系统);② 让 Windows 侧用计划任务/启动文件夹把 WSL 拉起来(更接近真正的开机自启) | 插件**不会**替你改发行版配置,也不会在 WSL 里推荐容器编排那套做法(WSL 的"编排器"是 Windows 本身) |
66
+ | 容器 / k8s | 由编排器负责(compose 的 `restart: always` 那一类) | 容器里没有"登录时自启" |
67
+
68
+ **"登录后自启"和"开机自启"不是一回事**:前者等你登录了才生效,后者是机器一开就生效(要额外开一次 linger)。插件把这句话和对应的命令一起写在设置页里。
69
+
70
+ ### DSH 起不来了
71
+
72
+ 双击插件目录里的 `scripts/emergency-disable.cmd` —— 它会把这个插件安全地摘掉并留一份备份;然后重新打开 DSH 就行(细节见下面的「起不来怎么办」)。
73
+
74
+ ### 实测状态表(哪些真机验过,哪些只有单元测试)
75
+
76
+ | 启动方式 | 真机验过? | 依据 |
77
+ | --- | --- | --- |
78
+ | 由 shell 守护脚本拉起(`supervise-*.sh` 之类) | **是** | 真机识别为 external,识别依据指到那个脚本进程,并且**拒绝再装第二个守护** |
79
+ | 裸跑:手起(前台) | **部分** | 真机确认它把自己认成"没人管"(mode `bare`,依据是父进程是 bash)并给出了安装计划;**真正的"装 → 跑 → 卸"是在"已有守护"的实例上真机跑通的**,裸跑实例上没做完整装卸 |
80
+ | 裸跑:后台(`nohup` / `setsid`) | **部分** | 真机确认能被认成裸跑(后台启动);**"重拉命令脱离原 shell"这一条只有单元测试**(当时机器上返回的并不是脱离标记) |
81
+ | WSL(有 systemd) | **是** | 真装真卸 |
82
+ | Windows 登录任务 | **部分** | 真机用系统原生方式建 → 查(State=Ready、登录触发器)→ 删(无残留)成功,证明确实不需要管理员;**"被拒后降级到启动文件夹"只有单元测试** |
83
+ | 桌面客户端 | **是** | 真机确认能自己重启回来(有快照与自检记录、没有触发回滚、不会双开);这是客户端自己的事,不在托管端的矩阵里 |
84
+ | pm2 / forever | 否,只有单元测试 | 识别与行为由单元测试钉住 |
85
+ | 容器 / compose | 否,只有单元测试 | 同上 |
86
+ | `npx` / `pnpm dlx` 包装 | 否,只有单元测试 | 同上 |
87
+ | 由别的 DSH 插件拉起 | 否,只有单元测试 | 同上 |
88
+ | tmux / screen / SSH 会话 | 否,只有单元测试 | 同上 |
89
+ | WSL 由 Windows 侧拉起 | 否,只有单元测试 | 同上(认成"认不出",什么都不做) |
90
+
91
+ **未实测 ≠ 不支持**:这些路径的行为由单元测试钉住;「没做完整装卸」「只有单元测试」都不等于不支持。若你的环境被认错,请把状态页里的「启动方式 + 识别依据」贴给我们。
92
+
93
+ ### 报告问题
94
+
95
+ 设置页最下面有一组「**报告问题**」:
96
+
97
+ - 主按钮「**复制诊断信息并去 GitHub 提问题**」:点一下,诊断信息进剪贴板,同时打开一个**已经填好标题和提纲**的新 issue 页 —— 把剪贴板里的诊断信息粘贴进去就行。
98
+ - 诊断信息里**没有**你的访问令牌,也**没有**任何密钥:它只有插件版本、平台、是不是桌面客户端、启动方式(连同判断依据)、监管器、重启模式,以及上一次重启的结果(失败时还有失败的那几项自检)。
99
+ - 浏览器不让你用剪贴板时(不是安全上下文、或没给权限),插件会把诊断信息显示在一个**可以全选**的文本框里,并提示你手动复制 —— 报告不会因为浏览器说"不"就没了。
100
+ - 没有 GitHub 账号?同一组里有一行小字给出邮箱(252170229@qq.com),外加一个「复制诊断信息」按钮和对应的邮件链接(主题已填好,正文提示粘贴)。
101
+
33
102
  ## 适合谁 / 不适合谁
34
103
 
35
104
  适合:经常因为装插件、升级、应用崩溃而丢掉正在进行的长任务,希望重启后自动接续的人。
@@ -61,16 +130,96 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
61
130
  | 系统提示词 | 一段「重启政策」说明,告诉 agent 怎么请求重启、插件会做什么 |
62
131
  | 宿主事件 | `agent/status`(跟踪哪些会话在忙)、`tools/pre-execute`(拦下"手动杀/重启 DSH"的 shell 命令)、启动时的恢复流程 |
63
132
  | profile 行 | 一条 bundle 行:`dsh-autorestart` |
64
- | 写文件 | 只在 `<DSH_HOME>/storages/dsh-autorestart/` 下写:`state.json`(检查点)、`backups/`(重启前快照)、`restart-<代次>.cmd` / `restart-<代次>-watch.ps1`(脱离进程的重启 helper)、`last-rollback.json`(回退报告)。唯一会写进 profile 的时机是**回退**:把快照里的 `cordis.patch.yml`、`package.json`、`pnpm-lock.yaml` 和 `node_modules` 拷回去 |
133
+ | 写文件 | 只在 `<DSH_HOME>/storages/dsh-autorestart/` 下写:`state.json`(检查点)、`backups/`(重启前快照)、`restart-<代次>.cmd` / `restart-<代次>-watch.ps1`(脱离进程的重启 helper)、`last-rollback.json`(回退报告)、`restart-outcome.json`(插件自己判定的那次重启的结论)、`access.json`(当前实例的带 token 访问链接,权限 `0600`)。唯一会写进 profile 的时机是**回退**:把快照里的 `cordis.patch.yml`、`package.json`、`pnpm-lock.yaml` 和 `node_modules` 拷回去 |
65
134
 
66
135
  ## 重启前备份与自动回退
67
136
 
68
137
  - **每次重启前**先给 profile 拍快照:`cordis.patch.yml`、`package.json`、`pnpm-lock.yaml`,以及(默认开启)已安装插件目录 `node_modules`。本机实测:**2.76 秒 / 117.6 MB / 5,562 个文件**。
69
138
  - 快照放在 `storages/dsh-autorestart/backups/<时间戳>/`,带 `manifest.json`;**只保留最新 2 份**,写新快照时自动删旧的。
70
- - 重启后 helper 会跑一个隐藏的看护脚本,轮询健康端点 **90 秒**(`rollbackTimeoutMs`):正常回答就静默退出,**什么都不动**。
71
- - 宿主**一直不回应**时:把快照拷回去 → 写 `last-rollback.json`(含"本次重启之后"的崩溃日志尾部,旧日志不会被误当证据)→ **自动再拉起一次**。
72
- - 下次启动时 `continuum_status` 会报 `lastRollback`(时间、等了多久、是否回滚、快照路径、崩溃日志名与尾部摘要)。
73
- - 开关:`backupOnRestart`(默认 `true`)、`backupIncludePlugins`(默认 `true`,关掉就只备配置文件)、`maxBackups`(默认 `2`)、`rollbackTimeoutMs`(默认 `90000`)。
139
+ - 看护脚本跑在**宿主进程之外**(所以宿主被杀它也活着),等**新实例**最多 **90 秒**(`rollbackTimeoutMs`):新实例把自己的端口和 boot id 写进 `access.json`,看护脚本据此找到**新实例**的健康端点(桌面版每次 `--port 0` 端口随机,旧端口没有意义)。正常回答就静默退出,**什么都不动**。
140
+ - 宿主**一直不回应**时:把快照拷回去 → 写 `last-rollback.json`(含"本次重启之后"的崩溃日志尾部,旧日志不会被误当证据)→ **不会再拉起第二个宿主**(已经起来的那一个由它自己继续,回退分支绝不 `Start-Process` 第二个 app)。
141
+ - 拷回本身也有**时间上限**(`rollbackCopyTimeoutMs`,默认 120 秒/步):拷不完就放弃,并把原因写进报告。
142
+ - **托管模式(`restartMode: command`)也有同一张安全网**:重启命令通常会把插件自己所在的进程一起杀掉,所以看护脚本在命令**之前**就启动了,由它来确认新实例有没有起来。命令起不来或以非零码退出时,插件**如实报错**(带退出码和输出)并把快照拷回去,绝不静默当成功。
143
+ - 下次启动时 `continuum_status` 会报 `lastRollback`(时间、等了多久、是否回滚、**失败原因**、快照路径、崩溃日志名与尾部摘要)。
144
+ - 开关:`backupOnRestart`(默认 `true`)、`backupIncludePlugins`(默认 `true`,关掉就只备配置文件)、`maxBackups`(默认 `2`)、`rollbackTimeoutMs`(默认 `90000`)、`restartCommandTimeoutMs`(默认 `30000`)、`rollbackCopyTimeoutMs`(默认 `120000`)。
145
+
146
+ ### 重启后的访问链接(token 是一次性的)
147
+
148
+ `dsh web` 的 boot token **每个进程都不一样**,重启后浏览器手里那条旧链接会 401,用户就被挡在门外。所以插件把当前实例的访问链接写进 `storages/dsh-autorestart/access.json`(权限 `0600`,里面是密钥),并在 `continuum_status` 与日志里报出来;前端发现手里那条链接过期时,会自己跟到新链接上。
149
+
150
+ ### 开机自启:插件自带的守护
151
+
152
+ 手动启动的托管实例(`dsh web`)挂掉之后不会自己回来——没人拉起来的重启,就等于要用户自己再启动一次。插件可以装一个**守护**:一个独立的小进程,负责拉起宿主、看住它、它掉了再拉起。
153
+
154
+ - **它是独立进程**:插件活在宿主里,宿主一死插件也没了,所以插件不可能是"把宿主拉回来"的那个东西。守护是随包发布的零依赖 Node 脚本(`supervisor/supervisor.js`),安装时被复制到 `storages/dsh-autorestart/supervisor/`。
155
+ - **它做什么**:用 `supervisor.json` 里的命令拉起宿主 → 等它退出 → 退避(1 秒起,每次翻倍,30 秒封顶;**只有真正起来过一次才清零**)→ 再拉起。宿主完全起不来时,把上次重启的快照恢复回去。
156
+ - **怎么登记自启(都不需要管理员)**:
157
+ - **Windows**:计划任务 `dsh-autorestart-web`(`/SC ONLOGON /RL LIMITED`)。若该机策略不允许普通用户建计划任务,会自动降级到「启动文件夹」里的一个 `.cmd`(同样零权限、同样是"登录时")。
158
+ - **Linux / WSL**:systemd **用户**单元(`~/.config/systemd/user/dsh-autorestart-web.service`)。没有系统服务管理器的机器(容器、没开 systemd 的 WSL)根本没有"开机自启"这回事——插件会明说,并把启动命令交给用户。
159
+ - **每个平台各有一份「开机自启怎么做」的引导**(设置页会显示,连带可复制的命令):
160
+
161
+ | 托管平台 | 插件怎么做 |
162
+ |---|---|
163
+ | **Windows** | 计划任务(`/SC ONLOGON /RL LIMITED`,不要管理员);这台机器拒绝时降级为「启动文件夹」里的 `.cmd`。注册的命令是**真 node** + `supervisor.js`,绝不是 DSH 客户端本体。 |
164
+ | **Linux(非 WSL)** | systemd **用户**服务;并提示 `loginctl enable-linger <user>`——**不开 linger 就只是"登录后自启",不是"开机自启"**。 |
165
+ | **WSL(有 systemd)** | 同样的用户单元 + linger 提示。 |
166
+ | **WSL(无 systemd)** | 这里根本没有"登录时自启":两条路 —— ① 在 WSL 内部开启 systemd(`/etc/wsl.conf` → `[boot] systemd=true` + `wsl --shutdown`;**这是系统配置改动**);② 在 Windows 侧用计划任务/启动文件夹拉起 WSL(`wsl.exe -d <distro> -e <命令>`;**需要 Windows 侧操作,但更接近真正的开机自启**)。WSL 里**永不**建议容器编排那套做法 —— WSL 的编排器是 Windows 本身。 |
167
+ | **容器 / k8s** | 不承诺开机自启:由编排器负责(compose 的 `restart: always`)。 |
168
+ | **认不出** | 不装任何东西、不猜命令:说明原因,让用户自己选。 |
169
+
170
+ 只**猜成** WSL(仅凭挂载的 Windows 盘)时,插件**什么都不替你做**:只提示"没把握,建议手动选一条"。
171
+ - **装多少次都只有一个**:任务/单元名是固定的,计划任务用 `/F` 覆盖,守护自己还持一把独占锁——两个守护不可能并存。
172
+ - **装到一半失败不会留残骸**:某一步失败时,已经写下的 unit(或计划任务)会被撤掉,回滚过程也报在步骤列表里——半装的守护会在下次登录拉起一个过期的。
173
+ - **命令名不会被拼两次**:一步里程序名放在 `file`、参数放在 `args`,所以跑的是 `systemctl --user enable --now …`,而不是 `systemctl systemctl …`(那会让 systemd 回一句 "Unknown command verb systemctl")。
174
+ - **随时可以还原**:安装前先把原来的 `restartMode` / `restartCommand` 存下来,卸载时写回去。设置页有「开机自启 → 卸载」一个按钮,手动收尾的命令也印在卡片和日志里;卸载哪一步没做完,会把待办**写下来**(`UNINSTALL-PENDING.json`,放在守护目录**旁边**的 `<stateDir>/` 下,所以写它不会把守护目录又拽回来),而且**只有真的还有残留时**才写。
175
+ - **第一次会问一次**:托管实例第一次出现时问一句"要不要装守护"。卡片写清**会改什么 / 需要什么权限 / 怎么卸**。选「保持我现在的启动方式」什么都不改——插件只记录"你已经自己有守护了"(于是永远不会装第二个)。
176
+
177
+ 桌面版**不问**:它自己就能把自己拉起来(`restartMode: app`),问了是噪声。桌面版**也装不了**守护:登录自启记录会去跑 Electron 客户端本体,那等于**每次登录再开一个 DSH**(双开)。所以桌面版上 `plan` / `install` 直接回 `ok: false`、`error: "desktop-manages-itself"`、步骤列表为空;设置页显示 `mode: "app"`(`restartCapable: true`),不会再吓唬用户"重启能力不可用"。
178
+
179
+ 守护的启动命令**必须指向一个真正的 Node**:进程本身就是 node 时用它,否则去安装目录里找 `resources/runtime/primary-runtime/dependencies/node/bin/node.exe`,再退到 PATH 里的 `node`。**绝不拿应用 exe 冒充 node**;一个都找不到就拒绝安装并说清原因。
180
+
181
+ ### 这个宿主是怎么被拉起来的(启动方式矩阵)
182
+
183
+ Web 端的启动方式有十几种,插件必须**每一种都适配对**。分类器是一个**纯函数**(`lib/startup-mode.js`):输入是父进程链 / 环境标记 / 容器标记 / 平台,输出是 `{mode, kind, evidence, selfReplay, canInstallSupervisor, suggestion}`。
184
+
185
+ | 启动方式 | `mode` | 插件怎么做 |
186
+ |---|---|---|
187
+ | 裸终端 `dsh web`(前台) | `bare` | **可以**装自家守护;不装则给出推导命令 + 确认 |
188
+ | `nohup … &` / `setsid` 后台 | `bare` | 同上,但重拉命令**脱离shell**(`setsid nohup …`),不随启动它的 shell 退出而死 |
189
+ | shell 守护脚本(`supervise-*.sh`) | `external` | 只优雅退出(`selfReplay: refused`)——脚本会把它拉回来 |
190
+ | systemd(user / system 单元) | `external` | 只优雅退出(依据:`INVOCATION_ID` 或父进程链) |
191
+ | pm2 / forever | `external` | 只优雅退出 |
192
+ | docker / compose / k8s | `container` | **不承诺开机自启**:给 compose `restart: always` 片段;容器里不装 systemd |
193
+ | Windows 计划任务 / NSSM 服务 | `external` | 只优雅退出 |
194
+ | WSL 里由 Windows 侧拉起 | `unknown` | **什么都不写**:父进程在 WSL 内看不见——建议选「裸终端装自家守护」,或者自己写一条启动命令(可选) |
195
+ | `npx` / `pnpm dlx` 包装 | `bare`(可用)或 `unknown` | `argv[1]` 是解析后的脚本路径就能用;否则拒绝,绝不猜 |
196
+ | 别的 DSH 插件拉起 | `external` | 只优雅退出——**绝不双开** |
197
+ | tmux / screen / SSH 会话 | `bare` | 走「命令重放」分支,且命令**脱离会话**(优雅退出会断开会话) |
198
+
199
+ **比表格更重要的两条规矩**:只有 `bare` 的宿主才允许写自重放命令、才允许装自家守护;`unknown` 一律 `selfReplay: 'unknown'` 并且**什么都不装**——插件宁可说"我认不出",也不在第一个宿主旁边再开一个。
200
+
201
+ 设置页会把判定和**依据**一起显示出来(「当前由 … — ancestor:1234: systemd」),让人看得懂插件为什么这么说。
202
+
203
+ ### 「保持我现在的启动方式」之后:推导重启命令(E)与干跑确认(B)
204
+
205
+ 用户选择保持现状时,插件仍然可以写下一条"能把它拉起来"的命令——但**只能靠推导,绝不猜**。猜错的命令比没有命令更糟:它看起来像重启成功了,实际留下一个起不来的宿主。
206
+
207
+ 候选命令来自进程自己知道的事实:`[process.execPath, ...process.execArgv, ...process.argv.slice(1)]` 加上工作目录。**优先级**决定接下来怎么办:
208
+
209
+ | 级 | 情形 | 写什么 |
210
+ |---|---|---|
211
+ | **E0** | 是插件自己的守护把这个实例拉起来的 | **什么都不写**——重启直接问守护(插件先写回滚请求,让守护去杀进程、重新拉起)。 |
212
+ | **E1** | 有**外部**监管器(systemd / 用户自己的脚本 / pm2) | **不写**自重放命令:`restartCommand` 变成**优雅退出**(`kill -TERM <pid>` / `taskkill /PID <pid>`),因为是那个监管器负责再拉起我们。装第二个守护会抢同一个端口。 |
213
+ | **E2** | 没有守护、也没有 init(容器) | 给出命令 + 一段 compose 片段(`restart: always`),**不承诺**开机自启。 |
214
+ | **E3** | 没有守护、有 init(Windows / systemd 可用) | 推导出的命令——但**要用户确认干跑之后**才写。 |
215
+
216
+ **拒绝**的情形(每种都有代码,也都指出路):桌面版(它自己会重启)、没有 web 服务端的宿主、读不回命令行、已有外部监管器、没有 init 的容器、以及**端口每次都变**(浏览器的 cookie 会跟着 authority 一起死——端口必须固定)。
217
+
218
+ **干跑(B)是一个承诺**:命令、工作目录、端口、健康检查 URL 在**写任何东西之前**原样展示。只有明确的确认才会把命令写进 profile 配置;写之前的原值被记下来,设置页一键就能改回去。
219
+
220
+ - 干跑:`POST /__dsh_autorestart/supervisor` 带 `{ "action": "dry-run" }`(或工具 `continuum_supervisor` 的 `action: "dry-run"`)——**什么都不写**。
221
+ - 确认:`{ "action": "apply", "confirmed": true }`——没有 `confirmed` 宿主回 `400 confirmation-required`,同样什么都不写。
222
+ - 撤销:`{ "action": "revert", "confirmed": true }`——把原来的 `restartMode` / `restartCommand` 写回去。
74
223
 
75
224
  ### 首次安装只问一次:备份要不要连已装插件一起备
76
225
 
@@ -137,7 +286,7 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
137
286
 
138
287
  ## 安装
139
288
 
140
- 当前版本:**0.6.2**。
289
+ 当前版本:**0.7.0**。
141
290
 
142
291
  ```powershell
143
292
  # 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
@@ -148,8 +297,23 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
148
297
 
149
298
  安装后行 id 为 `dsh-autorestart`。本机已经装好并处于 `active`。
150
299
 
300
+ ## 起不来怎么办(装了插件之后 DSH 打不开)
301
+
302
+ **不用等 DSH 跑起来**就能把这个插件摘掉 —— 插件目录里带了一个一键开关:
303
+
304
+ 1. **双击 `scripts\emergency-disable.cmd`** —— 弹出的黑窗口里会写清楚做了什么,按任意键关掉;
305
+ 2. **重启 DSH**(关掉再打开)——这次没有这个插件,DSH 就起得来;
306
+ 3. 以后想再试它:在同一个目录里按住 Shift 右键 →「在终端中打开」,运行 `powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action enable`,然后再重启 DSH。
307
+
308
+ 它只动**一个文件**里**一个数组**:profile 的 `package.json` 里的 `dsh.profile.bundles`(就是「插件启用列表」那几行)。写之前先备份成 `package.json.bak-emergency-<时间戳>`,写完重新解析一遍 JSON 校验;校验不过就用备份回滚,并如实报错。**重复运行没有副作用**:本来就没启用时只会告诉你「已经禁用」,不会失败。
309
+
151
310
  ## 配置
152
311
 
312
+ 设置页里关于「备份」和「启动方式」的两块,先说清楚它们看的是什么:
313
+
314
+ - **现在备份了什么**(在两个备份勾选的下面):宿主真的去读 `<DSH_HOME>/storages/dsh-autorestart/backups/`,所以份数、每份的**本地时间**、大小(MB)、有没有含已装插件、以及存放路径都是真的。只有目录元信息被读,备份里的内容**不遍历**(一份就可能 150 MB)。没有备份时写的是「还没有备份;每次重启前会自动生成一份」。
315
+ - **启动方式**(Web 托管端才有):默认只给**一行大白话**(例如「我记下的启动方式:由你的脚本拉起(重启交给它,我不插手)」)加两个小动作(重新检测 / 撤销);命令原文收在**「技术细节」**折叠里,默认收起,展开才出现。**桌面客户端这一整块不渲染** —— 它自己拉起自己,也不该写 `restartCommand`。宿主说不清是哪一端时,宁可整块不显示。
316
+
153
317
  所有键都有默认值,写在 `cordis.patch.yml` 的 insert 行里;要覆盖就在 profile 的
154
318
  `~/.dsh/profiles/<profile>/cordis.patch.yml` 里加同名 `- id: dsh-autorestart` 行(**整段替换 config**)。
155
319
  插件同时导出真实的 `Config` schema(schemastery),所以 Plugin Manager / 配置编辑器里能看到键名、类型与默认值
@@ -181,7 +345,9 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
181
345
  | `backupOnRestart` | `true` | 每次重启前给 profile 拍快照;重启后起不来时能自动回退 |
182
346
  | `backupIncludePlugins` | `true` | 快照里是否带上已安装插件目录(本机约 117 MB、多约 3 秒);关掉就只备配置文件 |
183
347
  | `maxBackups` | `2` | 保留几份快照;写新的时自动删旧的 |
184
- | `rollbackTimeoutMs` | `90000` | 看护脚本等新宿主多久(毫秒),超时就回退快照并再拉起一次 |
348
+ | `rollbackTimeoutMs` | `90000` | 看护脚本等新宿主多久(毫秒),超时就回退快照(**不会再拉起第二个宿主**) |
349
+ | `restartCommandTimeoutMs` | `30000` | 重启命令最多让插件等多久(毫秒)。前台跑着宿主的命令永远不返回——那会报「还在运行」,不算失败 |
350
+ | `rollbackCopyTimeoutMs` | `120000` | 拷回快照的**每一步**最多花多久(毫秒):拷不完就放弃,并把原因写进回退报告 |
185
351
  | `showSleepButton` | `true` | 收工关机的月亮按钮是否显示在左侧栏最底部、设置按钮旁边 |
186
352
  | `sleepCountdownSeconds` | `20` | **关机倒数秒数**:收工关机在真正关机前倒数几秒——**下拉框:0 / 10 / 20 / 30 / 60 秒**,默认 20;`0` 表示**不倒数、立即关机**(仍会先等所有会话收工)。**这段看得见的倒数就是关机前的那段等待** —— 关机没有第二个看不见的预留 |
187
353
  | `sleepCommand` | `''` | 收工关机的自定义关机命令;留空 = 自动识别宿主(Windows 用 `cmd.exe /c …`,其它平台用 `/bin/sh -c …`) |
@@ -258,7 +424,7 @@ restart gate: action=wait reason=agents-busy-within-soft-window busyCount=1 quie
258
424
  | `continuum_recover_now` | **立刻重跑恢复**(不用等下一次重启):重试卡住的会话,返回每个会话的动作 + 原因。参数 `sessionId`(省略 = 所有待恢复会话) |
259
425
  | `continuum_handoff` | **会话接力**:把一个已经超出模型 context window 的会话交给一个**全新的会话**继续(带去 goal 目标、检查点里记录的接手原因和一段有界的对话尾部),旧会话保留成存档、goal 被解除。参数 `sessionId`(省略 = 调用方自己的会话)/ `maxChars`(默认 `20000`) |
260
426
  | `continuum_status` | 只读:phase、generation、是否有待重启、检查点路径、识别的重启环境、被记录的会话、每个阻塞会话的阻塞原因、被停泊的 goal |
261
- | `continuum_cancel_restart` | 取消待重启,并释放该会话的续接请求 |
427
+ | `continuum_cancel_restart` | 撤回待重启请求。默认**只撤调用方自己那一份**(`scope: "mine"`),别的会话排队的重启不受影响;`scope: "all"` 才是"所有人都别重启了"。参数 `scope`(`mine`(默认)/ `all`)/ `reason`(写进日志) |
262
428
 
263
429
  除了工具本身(模型在工具表里能看到名字/描述),插件还有两层保证:
264
430
 
@@ -284,6 +450,46 @@ restart gate: action=wait reason=agents-busy-within-soft-window busyCount=1 quie
284
450
  所以**不需要改 AGENTS.md**——这是 Host 插件钩子(工具注册 + system prompt section + tools.guard),对 profile 内所有 agent 生效;
285
451
  你当然也可以额外在 AGENTS.md 里写一条团队规范,但它只是重复。
286
452
 
453
+ ### 取消重启:只撤自己 vs 全撤
454
+
455
+ 重启请求是**合并**的:好几个会话可以同时请求同一次重启,插件把每个请求者都记在 `restart.requesters` 里,
456
+ 重启成功后**一起**被唤醒。所以"取消"必须说清楚**撤的是谁的请求**:
457
+
458
+ | scope | 会发生什么 |
459
+ | --- | --- |
460
+ | `mine`(默认) | 只撤掉**调用方自己**那一份。别的会话还在等这次重启 → 重启请求**保留**,`restart.bySessionId` 改指向剩下的第一个请求者;调用方是**最后一个**请求者时,重启才真的消失:phase 回到 `idle`,这个会话被释放。 |
461
+ | `all` | 为**所有人**取消:所有请求者都被释放,待重启请求消失。也就是"所有人都别重启了"。 |
462
+
463
+ 有两种调用会被**拒绝**,而且**不改动任何状态**——"顺手全撤"永远不是兜底行为:
464
+
465
+ - 不知道调用方是哪个会话(调用时没有会话)→ 拒绝;
466
+ - 调用方**并没有**请求这次重启(只是在别人的会话名下)→ 拒绝。
467
+
468
+ 工具会回报结果:`cancelled`(重启请求本身是否真的没了)、`remainingRequesters`(还有几个会话在等)、
469
+ `scope`、`phase`。例如只撤自己、别人的请求还在时:
470
+
471
+ ```json
472
+ { "ok": true, "cancelled": false, "scope": "mine", "remainingRequesters": 1, "sessionId": "sess-a", "phase": "deferred" }
473
+ ```
474
+
475
+ ### 小胶囊:点它看详情,也从这儿自己撤掉
476
+
477
+ 输入框那排的小胶囊只有 22px 高、一行宽,所以它**只放数字**(`待重启 · 已等 16 分钟 · 3 项任务在跑`)——
478
+ 不放名字,也不放任何"双击可以取消"之类的提示(没地方放)。**单击**它(或聚焦后按 Enter / 空格)会在上方
479
+ 弹出一个小面板:
480
+
481
+ - 这次重启还在等几个任务结束;
482
+ - 还有几个**别的**会话也在等同一次重启;
483
+ - 一个按钮:「取消我的这次重启」。
484
+
485
+ 按钮按下去弹出的是**确认框**(没有时限,也不倒计时):「取消你的这次重启?」,并说清现在取消意味着什么
486
+ (只有你,还是你退出、别人照样重启)。**只有确认框里的「确认取消」才真的发请求**;按 Esc、点面板外面、
487
+ 或者按「再想想」都只是关掉,什么都不做。
488
+
489
+ 这个请求只撤**本会话自己**那一份(`POST /__dsh_autorestart/cancel`,body 是
490
+ `{ "sessionId": "…", "scope": "mine" }`):别的会话照样重启。面板里**没有**"全撤"——那种操作只留给
491
+ 工具显式调用(`continuum_cancel_restart` 带 `scope: "all"`)。
492
+
287
493
  ## 收工关机(Graceful shutdown)
288
494
 
289
495
  **它干什么。** 左侧栏最底部一个月亮按钮:点一下 → 等**所有**会话都收工 → 屏幕上倒数(一个巨大的数字 + 一只蓝色小鲸鱼,每秒换一句中文)→ **关掉整台电脑**。倒数期间随时可以取消:卡片上有按钮,月亮按钮本身在关机期间也变成了"再点一下 = 取消"。倒数期间要是有会话又开始干活,倒计时会退回"等待",**绝不关机**。
@@ -390,6 +596,10 @@ WSL 的判定依据:`WSL_DISTRO_NAME`、`WSL_INTEROP`、`/proc/version` 里出
390
596
 
391
597
  **给代码看的状态字段。** `GET <healthPath 前缀>/sleep`(默认 `/__dsh_autorestart/sleep`)会回答阶段、倒数和"要关的是哪台机器"。其中几个字段是给**程序**看的、不是给人看的:`host.kind` 是机器类别(`windows` / `wsl` / `linux` / `macos` / `unsupported`;如果平台没认出来、但你配了 `sleepCommand`,则是 `custom`);`host.suspect` 为 `true` 表示这台机器**只凭挂着 Windows 盘**才被认成 WSL(是佐证,不是铁证);`lastRun.dryRun` 表示上一次是**试运行**,什么都没真关;`backupOnRestart` / `backupIncludePlugins` 报告重启前备份的当前范围。
392
598
 
599
+ **上一次重启的记录。** `GET <healthPath>?state=1` 和 `continuum_status` 工具都会报 `lastRestart`:重启前拍的那份快照(`snapshot.dir`、`snapshot.stamp`、`snapshot.atLocal`——**本地时间**,因为目录名是 UTC——、`snapshot.durationMs`、`snapshot.pruned`),以及**起来那个新实例**的判定(`result` 是 `pending` / `ok` / `failed`,外加 `verifiedAt`、`durationMs` 和四条开机检查 `listener` / `routes` / `plugin` / `recovery`)。新实例始终没起来时,记录停在 `pending`——这同样看得见。记录落在 `<stateDir>/last-restart-verdict.json`。
600
+
601
+ **这个宿主是谁拉起来的。** 同一个返回里的 `supervisor` 块说 `mode`(`app` / `installed` / `external` / `unknown` / `off`)、`restartCapable`,以及——因为**第二个守护会抢同一个端口**——`selfReplay`(`allowed` / `refused` / `unknown` / `n/a`);别人在管时还有 `external` 的证据(`kind`、`evidence`、`commandLine`)。Linux 上会读宿主的**父进程链**(`/proc/<pid>/cmdline`),所以用户自己的 `supervise-*.sh` 脚本能被认出来。**认不出人时是 `unknown`,绝不会默默当成"裸跑"**:这时插件拒绝自己重放自己的命令。**桌面版不会是 `unknown`**:它自己会把自己拉起来,所以报 `mode: "app"`、`restartCapable: true`、`selfReplay: "n/a"`。
602
+
393
603
  **关机命令起不来时的退路。** 宿主会等命令的**真实结果**(进程有没有真的起来)。命令起不来(找不到程序、权限不够)时,关机**不会假装成功**:状态回到 `idle`,并给出原因("关机命令没跑起来(…),电脑还开着,可以再点一次月亮按钮。"),电脑保持开机。界面上那个「正在关机」的卡片也**不会把界面锁死**:最多停留 8 秒就会给出一个"关掉这个提示"的按钮。
394
604
 
395
605
  ## 需要 goal 吗?
@@ -579,7 +789,7 @@ lib/lifecycle.js 纯决策:bootPlan / restartDecision / selectGoalDisarms
579
789
  lib/restart.js Windows 脱离重启(WMI 主、schtasks 兜底、dryRun、自定义命令)
580
790
  lib/sleep.js 收工关机:识别真宿主机(含 WSL)、关机命令、等待→倒数→关机的状态机
581
791
  lib/semver.js 版本号比较(公告去重用:只看"记录版本是否低于当前功能版本")
582
- locale/{en,zh}.json Plugin Manager 显示用标题/描述
792
+ locale/{en,zh}.json 界面文案的镜像(与 client.js 里注册给 DSH 的两份字典同键同词;键集由一条用例钉住)
583
793
  icon.svg 插件图标
584
794
  LICENSE MIT 许可(发布到插件市场需要)
585
795
  test/ node:test 用例(状态机 / 检查点 / 重启命令 / goal 解除 / 集成恢复 / recover_now)