dsh-autorestart 0.6.2 → 0.8.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.
Files changed (47) hide show
  1. package/README.md +460 -12
  2. package/assets/screenshot-1.png +0 -0
  3. package/assets/screenshot-2.png +0 -0
  4. package/assets/screenshot-3.png +0 -0
  5. package/assets/screenshot-4.png +0 -0
  6. package/assets/screenshot-5.png +0 -0
  7. package/client.js +6329 -3863
  8. package/cordis.patch.yml +44 -1
  9. package/docs/README.zh.md +297 -9
  10. package/docs/platform-matrix.md +92 -0
  11. package/docs/rescue-drill.md +606 -0
  12. package/index.js +2867 -199
  13. package/lib/access.js +212 -0
  14. package/lib/autostart-advice.js +198 -0
  15. package/lib/backup.js +131 -4
  16. package/lib/blockers.js +228 -0
  17. package/lib/checkpoint.js +147 -0
  18. package/lib/diagnostics.js +188 -0
  19. package/lib/health.js +264 -0
  20. package/lib/host-kind.js +145 -0
  21. package/lib/node-runtime.js +144 -0
  22. package/lib/rescue-paths.js +253 -0
  23. package/lib/rescue.js +596 -0
  24. package/lib/restart-verdict.js +327 -0
  25. package/lib/restart.js +823 -56
  26. package/lib/scheduler-capability.js +300 -0
  27. package/lib/sleep.js +46 -49
  28. package/lib/startup-mode.js +491 -0
  29. package/lib/supervisor-config.js +735 -0
  30. package/lib/supervisor-install.js +557 -0
  31. package/lib/supervisor.js +1276 -0
  32. package/lib/watchdog-arbiter.js +117 -0
  33. package/lib/watchdog-schedule.js +310 -0
  34. package/lib/watchdog-tier.js +182 -0
  35. package/lib/watchdog.js +479 -0
  36. package/locale/en.json +274 -64
  37. package/locale/zh.json +274 -64
  38. package/package.json +7 -2
  39. package/screenshots.json +6 -4
  40. package/scripts/README.md +65 -0
  41. package/scripts/drill-rescue.ps1 +508 -0
  42. package/scripts/emergency-disable.cmd +27 -0
  43. package/scripts/emergency-toggle.ps1 +373 -0
  44. package/scripts/restore-profile.cmd +44 -0
  45. package/scripts/restore-profile.ps1 +608 -0
  46. package/scripts/run-tests.ps1 +46 -0
  47. package/supervisor/supervisor.js +884 -0
package/locale/zh.json CHANGED
@@ -1,48 +1,99 @@
1
1
  {
2
- "meta": {
3
- "title": "Autorestart — 重启续接",
4
- "description": "让 Agent 的工作跨 DSH 重启不中断:持久检查点、启动时重新武装目标或注入续接消息,以及 Agent 可自行请求的“空闲即重启”。"
5
- },
6
- "backupChoice": {
7
- "title": "重启前自动备份:要不要把已安装的插件也一起备份?",
8
- "body": "每次重启前会快照当前配置,万一重启后起不来就能自动回退。",
9
- "withPlugins": "配置 + 已安装插件",
10
- "withPluginsHint": "多约 3 秒、约 117 MB;插件把启动搞坏时也能回退。",
11
- "configOnly": "只备份配置",
12
- "configOnlyHint": "瞬间完成、约 66 KB;插件问题无法回退,需要手动重装。",
13
- "footnote": "以后可在 设置 → 自动重启与关机 修改",
14
- "pickPrimary": "用这个",
15
- "pickSecondary": "只备份配置",
16
- "recommended": "推荐",
17
- "dismissAria": "关闭这个一次性提示"
18
- },
19
- "settings": {
20
- "nav": "自动重启与关机",
21
- "groups": {
22
- "restart": "重启相关设置",
23
- "shutdown": "关机相关设置"
24
- },
25
- "seconds": {
26
- "label": "关机倒数秒数",
27
- "immediate": "不倒数(立即关机)",
28
- "value": "{n} 秒",
29
- "hint": "所有会话收工后,屏幕上会显示这段倒数(可点「不关了」取消),数完才关机;想要更多落盘时间就把倒数调大。"
30
- },
31
- "restartGrace": {
32
- "label": "重启前等待",
33
- "immediate": "不等(立即重启)",
34
- "value": "{n} 秒",
35
- "hint": "重启没有倒数环节,会话一收工就会立刻重启;如果你装了“会话结束后才写”的记忆/总结类插件,把这里调大(默认 0 秒)。"
36
- },
37
- "backup": {
38
- "label": "重启前备份",
39
- "hint": "每次重启前先快照一次当前配置;万一重启后起不来,能自动回退。"
40
- },
41
- "backupPlugins": {
42
- "label": "备份包含已安装插件",
43
- "hint": "关闭后只备份 DSH 自身状态,不含 profile 里的插件目录。"
44
- },
45
- "error": "没能保存设置:{message}"
2
+ "label": "待重启",
3
+ "busyPrefix": " · 等待 ",
4
+ "busySuffix": " 个任务结束",
5
+ "idle": " · 空闲后自动重启",
6
+ "goalActive": " · 被某个自动续跑的 goal 占着",
7
+ "goalExhausted": " · 某个任务的 goal 轮次额度已用完,无法续跑",
8
+ "settling": " · 还在等这段安静期",
9
+ "parkedGoals": " · 已停泊 {n} 个 goal,重启后自动继续",
10
+ "parkButton": "停泊并重启",
11
+ "parkButtonAria": "停泊该会话的 goal,并立即请求重启",
12
+ "parkBusy": "停泊中…",
13
+ "parkError": "停泊失败:{message}",
14
+ "tooltip": "等待其他会话空闲后自动重启;重启后会自动继续本会话进度",
15
+ "tooltipBusy": "等待其他任务结束(计数含“排队但还没开始”的任务)后自动重启;重启后本会话会自动继续进度",
16
+ "tooltipWithError": "等待其他会话空闲后自动重启;重启后本会话会自动继续进度。{message}",
17
+ "dotAria": "待重启",
18
+ "panelHeading": "待重启",
19
+ "panelStateBusy": "等待其他会话空闲后重启",
20
+ "panelStateIdle": "等待空闲窗口后重启",
21
+ "panelStateGoalActive": "等待其他会话停泊它自动续跑的 goal",
22
+ "panelStateGoalExhausted": "等待其他会话的 goal(轮次额度已用完)",
23
+ "panelBlockedTitle": "占用会话:{title}",
24
+ "panelBlockedGeneric": "有一个任务正在运行",
25
+ "panelBlockedSession": "会话「{label}」正在运行",
26
+ "panelBlockedSubagent": "{name}正在运行",
27
+ "panelBlockedSubagentGeneric": "有一个子智能体正在运行",
28
+ "panelBlockedSubagentGroup": "会话「{label}」的 {n} 个子智能体正在运行",
29
+ "panelBlockedSubagentGroupGeneric": "有 {n} 个子智能体正在运行",
30
+ "panelBlockedRestartPending": "等这次重启完成后再关机",
31
+ "panelGoalActiveLine": "占用会话「{title}」:goal 正在自动续跑(goal-active)",
32
+ "panelGoalExhaustedLine": "占用会话「{title}」:goal 轮次额度已用完,无法继续(goal-exhausted)",
33
+ "panelParkedTitle": "已停泊的 goal(重启后自动继续):",
34
+ "panelParkedGoal": "会话 {session} · {goal}",
35
+ "panelWaiting": "已等待 {duration}",
36
+ "panelResume": "重启后本会话会自动继续进度",
37
+ "longWaitPill": " · 已等 {duration} · {n} 项任务在跑",
38
+ "longWaitTooltip": "已等 {duration}。在跑:{blockers}",
39
+ "blockerGeneric": "有会话",
40
+ "blockerJoin": "、",
41
+ "blockerGroupSubagents": "{label}({n} 个子智能体)",
42
+ "blockerGroupSubagentsGeneric": "{n} 个子智能体",
43
+ "blockerMore": "…等 {n} 项",
44
+ "blockersTitleLine": "占用会话:{blockers}",
45
+ "durationSeconds": "{n} 秒",
46
+ "durationMinutes": "{n} 分 {m} 秒",
47
+ "durationHours": "{n} 小时 {m} 分",
48
+ "durationMinutesShort": "{n} 分钟",
49
+ "durationHoursShort": "{n} 小时",
50
+ "durationHoursMinutesShort": "{n} 小时 {m} 分",
51
+ "cancelPanelTasks": "这次重启还在等 {n} 个任务结束",
52
+ "cancelPanelOthers": "还有 {n} 个会话也在等这次重启",
53
+ "cancelPanelOthersAfter": "还剩 {n} 个会话在等",
54
+ "cancelPanelAria": "待重启详情",
55
+ "cancelButton": "取消我的这次重启",
56
+ "cancelButtonAria": "取消我的这次重启",
57
+ "cancelConfirmTitle": "取消你的这次重启?",
58
+ "cancelConfirmOnlyMe": "取消后这次重启就不会发生了。",
59
+ "cancelConfirmOthers": "还有 {n} 个会话也在等这次重启——取消只影响你自己,他们仍会重启。",
60
+ "cancelConfirmUnknown": "当前会话未知,无法只撤自己。",
61
+ "cancelConfirmButton": "确认取消",
62
+ "cancelConfirmButtonAria": "确认取消这次重启",
63
+ "cancelDismissButton": "再想想",
64
+ "cancelDismissButtonAria": "算了,不取消",
65
+ "cancelFailed": "没能取消这次重启。",
66
+ "cancelFailedWith": "没能取消这次重启:{message}",
67
+ "cancelNotRequester": "这个会话没有请求这次重启,所以什么都没取消。",
68
+ "cancelNothingPending": "现在没有待重启的请求。",
69
+ "cancelUnknownSession": "当前会话未知,无法只撤自己。",
70
+ "cancelNoRoute": "找不到取消接口(宿主版本太旧)。",
71
+ "backupChoiceTitle": "重启前自动备份:要不要把已安装的插件也一起备份?",
72
+ "backupChoiceBody": "每次重启前会快照当前配置,万一重启后起不来就能自动回退。",
73
+ "backupChoiceWithPlugins": "配置 + 已安装插件",
74
+ "backupChoiceWithPluginsHint": "多约 3 秒、约 117 MB;插件把启动搞坏时也能回退。",
75
+ "backupChoiceConfigOnly": "只备份配置",
76
+ "backupChoiceConfigOnlyHint": "瞬间完成、约 66 KB;插件问题无法回退,需要手动重装。",
77
+ "backupChoiceFootnote": "以后可在 设置 → 自动重启与关机 修改",
78
+ "backupChoicePickPrimary": "用这个",
79
+ "backupChoicePickSecondary": "只备份配置",
80
+ "backupChoiceRecommended": "推荐",
81
+ "backupChoiceDismissAria": "关闭这个一次性提示",
82
+ "backupChoiceError": "保存失败:{message}",
83
+ "supervisor": {
84
+ "title": "让 DSH 自己把自己拉起来?",
85
+ "body": "现在这个 DSH 托管实例是手动/外部启动的——它挂了不会自己回来。我可以给你装一个自带的守护:登录时自动启动,进程掉了自动拉起,启动失败自动回滚上次快照。守护不会碰你的会话数据,只做「拉起 + 看住 + 回滚」三件事。",
86
+ "changes": "会改什么:新增 <stateDir>/supervisor/(守护脚本 + 配置 + 日志 + 锁);Windows 注册一条登录自启记录(计划任务 dsh-autorestart-web),Linux 注册一个 systemd 用户服务。不改:会话数据、模型配置、已装插件。",
87
+ "rights": "需要什么权限:Windows 不需要管理员(用「登录时运行」;若该机策略禁止普通用户建计划任务,会自动降级到「启动文件夹」,同样不需要);Linux 需要能写自己的家目录(systemctl --user,普通用户即可)。",
88
+ "uninstall": "怎么卸:设置页「开机自启 → 卸载」一键;或手动 schtasks /Delete /TN \"dsh-autorestart-web\" /F(Linux:systemctl --user disable --now dsh-autorestart-web.service && rm ~/.config/systemd/user/dsh-autorestart-web.service),两者都要再删掉 <stateDir>/supervisor/ 目录。",
89
+ "install": "帮我装(推荐)",
90
+ "installHint": "会写一个守护脚本到 <stateDir>/supervisor/,并在 Windows 注册一条登录自启记录 / Linux 注册一个 systemd 用户服务。不需要管理员。之后宿主进程退出,守护会在几秒内拉起它;起不来会自动回滚到重启前的快照。随时可以在设置里一键卸载。",
91
+ "keep": "保持我现在的启动方式",
92
+ "keepHint": "什么都不改。你现在用什么启动就继续用什么。代价:宿主进程退出后不会自动回来——重启功能会退化成「只能在有外部守护时生效」。",
93
+ "later": "以后再说",
94
+ "laterHint": "这张卡片不再出现。你可以随时在「设置 → 自动重启与关机 → 开机自启」里手动装。",
95
+ "error": "操作失败:{message}",
96
+ "busy": "正在处理…"
46
97
  },
47
98
  "sleep": {
48
99
  "button": {
@@ -61,26 +112,26 @@
61
112
  },
62
113
  "countdown": {
63
114
  "caption": {
64
- "20": "鲸鱼娘开始收网了",
65
- "19": "备份已经替你打包好",
66
- "18": "没说完的话,再讲一句",
67
- "17": "大家都在慢慢安静下来",
68
- "16": "最后再巡一圈房间",
69
- "15": "海面开始变暗了",
70
- "14": "灯一盏一盏熄了",
71
- "13": "房间已经安静下来",
72
- "12": "还有一小会儿",
73
- "11": "再看一眼屏幕吧",
74
- "10": "鲸鱼娘在数最后几秒",
75
- "9": "灯已经关了一半",
76
- "8": "声音越来越小",
77
- "7": "马上就到道别的时候了",
78
- "6": "最后一口海水",
79
- "5": "晚风越来越轻",
80
- "4": "说声晚安吧",
81
- "3": "真的只剩三秒啦",
82
- "2": "还来得及说声再见",
83
115
  "1": "晚安 🌙",
116
+ "2": "还来得及说声再见",
117
+ "3": "真的只剩三秒啦",
118
+ "4": "说声晚安吧",
119
+ "5": "晚风越来越轻",
120
+ "6": "最后一口海水",
121
+ "7": "马上就到道别的时候了",
122
+ "8": "声音越来越小",
123
+ "9": "灯已经关了一半",
124
+ "10": "鲸鱼娘在数最后几秒",
125
+ "11": "再看一眼屏幕吧",
126
+ "12": "还有一小会儿",
127
+ "13": "房间已经安静下来",
128
+ "14": "灯一盏一盏熄了",
129
+ "15": "海面开始变暗了",
130
+ "16": "最后再巡一圈房间",
131
+ "17": "大家都在慢慢安静下来",
132
+ "18": "没说完的话,再讲一句",
133
+ "19": "备份已经替你打包好",
134
+ "20": "鲸鱼娘开始收网了",
84
135
  "default": "晚安 🌙"
85
136
  },
86
137
  "remaining": "还剩 {n} 秒",
@@ -117,11 +168,170 @@
117
168
  "error": "保存失败:{message}"
118
169
  }
119
170
  },
171
+ "nav": "自动重启与关机",
172
+ "settings": {
173
+ "groups": {
174
+ "restart": "重启相关设置",
175
+ "shutdown": "关机相关设置",
176
+ "supervisor": "开机自启",
177
+ "report": "报告问题",
178
+ "rescue": "出事了怎么办"
179
+ },
180
+ "seconds": {
181
+ "label": "关机倒数秒数",
182
+ "immediate": "不倒数(立即关机)",
183
+ "value": "{n} 秒",
184
+ "hint": "所有会话收工后,屏幕上会显示这段倒数(可点「不关了」取消),数完才关机;想要更多落盘时间就把倒数调大。"
185
+ },
186
+ "restartGrace": {
187
+ "label": "重启前等待",
188
+ "hint": "重启没有倒数环节,会话一收工就会立刻重启;如果你装了“会话结束后才写”的记忆/总结类插件,把这里调大(默认 0 秒)。",
189
+ "value": "{n} 秒",
190
+ "immediate": "不等(立即重启)"
191
+ },
192
+ "backup": {
193
+ "label": "重启前备份",
194
+ "hint": "每次重启前先快照一次当前配置;万一重启后起不来,能自动回退。",
195
+ "list": "现在备份了什么",
196
+ "count": "当前 {n} 份",
197
+ "keep": "最多保留 {n} 份,超出自动删最旧的那份。",
198
+ "empty": "还没有备份;每次重启前会自动生成一份。",
199
+ "path": "存放路径",
200
+ "copyPath": "复制路径",
201
+ "copied": "路径已复制",
202
+ "copyFailed": "复制失败,请手动选中上面的路径",
203
+ "withPlugins": "含已装插件",
204
+ "withoutPlugins": "只有配置"
205
+ },
206
+ "backupPlugins": {
207
+ "label": "备份包含已安装插件",
208
+ "hint": "关闭后只备份 DSH 自身状态,不含 profile 里的插件目录。"
209
+ },
210
+ "supervisor": {
211
+ "status": "当前由",
212
+ "installed": "插件守护",
213
+ "external": "你自己的方式",
214
+ "unknown": "未知",
215
+ "off": "关闭",
216
+ "capable": "重启能力",
217
+ "capableYes": "可用:宿主退出后会自动回来",
218
+ "capableNo": "不可用:宿主退出后不会自动回来",
219
+ "install": "装上守护",
220
+ "uninstall": "卸载守护",
221
+ "hint": "守护是一个独立进程:登录时自动启动,宿主进程掉了自动拉起,启动失败自动回滚上次快照。不需要管理员。",
222
+ "noInstallDesktop": "桌面版自带重启,这里不用装守护。",
223
+ "noInstallExternal": "已经有别的守护在管这个宿主:再装一个会两个一起拉起、抢同一个端口,所以这里不提供安装。",
224
+ "noInstallUnknown": "宿主没说清是谁在拉起它,所以这里不提供安装。",
225
+ "access": "当前访问链接",
226
+ "accessHint": "重启后 token 会变,链接也会变。这个链接一直是最新的。",
227
+ "pending": "卸载没有完成,请手动执行:{commands}",
228
+ "reason": "说明",
229
+ "command": "重启命令",
230
+ "derive": "推导重启命令",
231
+ "apply": "就用这条命令",
232
+ "revert": "撤销",
233
+ "noCommand": "推不出可重放的重启命令",
234
+ "plan": "干跑预览",
235
+ "reason.ok": "可以用这条命令重新拉起这个 DSH。",
236
+ "reason.desktop-app": "桌面版自带重启,不用管这条命令。",
237
+ "reason.not-a-web-host": "这个 DSH 不是网页型宿主,没有端口也没有监听器。",
238
+ "reason.argv-unavailable": "读不回启动命令行(它是被一个特殊方式拉起来的),所以不猜。",
239
+ "reason.external-supervisor": "已经有别的守护在管这个进程:装第二个会抢端口。改为「优雅退出,让它拉起我」。",
240
+ "reason.container-no-init": "这台机器没有开机自启机制(容器),所以不承诺自动拉起。",
241
+ "reason.port-not-fixable": "端口每次都变,浏览器会被踢出去;得先把端口固定下来。",
242
+ "reason.supervised": "插件自己的守护在管这个进程,重启直接交给它。",
243
+ "status.external": "我记下的启动方式:由你的脚本拉起(重启交给它,我不插手)",
244
+ "status.bareWritten": "我记下的启动方式:按你确认过的那条命令重启",
245
+ "status.bareUnknown": "我还不知道怎么把你拉起来",
246
+ "status.unknown": "我看不出你是怎么启动的(我不猜,也不会自己乱拉)",
247
+ "status.selfSupervised": "我记下的启动方式:插件自己的守护在管(重启交给它)",
248
+ "status.container": "这里没有\"登录时自启\"这回事:由编排器负责",
249
+ "redetect": "重新检测",
250
+ "deriveHelp": "帮我看看怎么重启",
251
+ "technical": "技术细节",
252
+ "technicalHint": "以下是插件实际会执行的命令",
253
+ "previous": "撤销后会还原成",
254
+ "app": "桌面版自带重启",
255
+ "startup": "启动方式",
256
+ "startup.bare": "手动启动(没人管它)",
257
+ "startup.external": "由别的守护在管",
258
+ "startup.container": "容器里(由编排器管)",
259
+ "startup.desktop": "桌面版自带重启",
260
+ "startup.selfSupervised": "插件自己的守护在管",
261
+ "startup.unknown": "认不出是谁拉起它的",
262
+ "autostart": "开机自启怎么做"
263
+ },
264
+ "report": {
265
+ "label": "遇到问题了?",
266
+ "github": "复制诊断信息并去 GitHub 提问题",
267
+ "hint": "诊断信息里没有你的访问令牌,也没有任何密钥;把它贴进 issue,我们就能看出问题出在哪。",
268
+ "email": "没有 GitHub 账号?发邮件到 {email}",
269
+ "copy": "复制诊断信息",
270
+ "copyFailed": "复制失败,请手动复制这段",
271
+ "issueTitle": "重启 / 开机自启相关的问题",
272
+ "issueBody": "请把刚才复制的诊断信息粘贴到下面。\n\n## 发生了什么\n\n\n## 期望什么\n\n\n## 怎么复现\n"
273
+ },
274
+ "rescue": {
275
+ "hint": "DSH 打不开的时候,用下面这两个脚本救自己(都不用 DSH 跑起来)。",
276
+ "restoreProfile": "回到上一次好状态",
277
+ "emergencyDisable": "只把这个插件摘掉",
278
+ "path": "路径",
279
+ "copyPath": "复制路径",
280
+ "copied": "路径已复制",
281
+ "copyFailed": "复制失败,请手动选中上面的路径",
282
+ "reveal": "打开所在文件夹",
283
+ "which": "怎么选:整个环境坏了(改了配置、装了别的插件、升级崩了)→ 用「回到上一次好状态」;只怀疑这个插件 → 用「只把这个插件摘掉」。",
284
+ "warning": "重要:这两个脚本是在 DSH 打不开的时候用的。DSH 正开着的时候用「回到上一次好状态」,会把你现在正在用的配置覆盖掉。",
285
+ "shortcut": "在桌面放一个快捷方式",
286
+ "shortcutDone": "已在桌面",
287
+ "noDesktop": "这台机器上没有桌面目录,所以放不了快捷方式(脚本的路径在上面,可以手动复制过去)。",
288
+ "shortcutFailed": "快捷方式没放成:{message}",
289
+ "revealFailed": "文件夹没打开:{message}",
290
+ "rollbackOk": "自动回滚:可用(重启后起不来会自动回到上一个好状态)",
291
+ "rollbackBlocked": "自动回滚:**不可用** —— 这台电脑不允许创建计划任务。**会丢失:重启后 DSH 起不来时不会自动恢复**;出问题时请双击「出事了怎么办」里的还原脚本。",
292
+ "rollbackUnknown": "自动回滚:还不知道(这台机器的能力还没检查过)",
293
+ "watchdogTier": "看门狗的触发方式:{tier}",
294
+ "watchdogLast": "上一次看门狗的结果:{result}",
295
+ "tierScheduledTask": "Windows 计划任务",
296
+ "tierSystemdRun": "systemd 定时器",
297
+ "tierDetachedProcess": "自建后台进程",
298
+ "tierNone": "(这台机器没有可用机制)"
299
+ },
300
+ "error": "没能保存设置:{message}",
301
+ "autostartKind": {
302
+ "other": "其他方式",
303
+ "windows-task": "Windows 计划任务",
304
+ "windows-startup": "Windows 启动文件夹",
305
+ "systemd-user": "systemd 用户服务",
306
+ "systemd-system": "systemd 系统服务",
307
+ "wsl-systemd": "WSL 里的 systemd",
308
+ "wsl-no-systemd": "WSL(没有 systemd)",
309
+ "container": "容器",
310
+ "desktop": "桌面版自带重启",
311
+ "unknown": "认不出",
312
+ "pm2": "pm2(进程守护工具)"
313
+ },
314
+ "startupWhy": {
315
+ "desktop": "这个进程就是桌面客户端本体,它自己会重启自己",
316
+ "bare": "它是被人在终端里手动起的,没人管它",
317
+ "external": "它是由另一个守护进程拉起来的",
318
+ "container": "它跑在容器里,由编排器负责",
319
+ "unknown": "看不出它是怎么被拉起来的",
320
+ "self-supervised": "它由本插件自己的守护在管"
321
+ }
322
+ },
323
+ "meta": {
324
+ "title": "dsh-autorestart — 重启不断线",
325
+ "description": "让 DeepSeek Harness 重启后接着干活:落盘的检查点、启动时重新武装 goal 或给一条续接提示,以及一次等人闲下来的优雅重启。"
326
+ },
327
+ "text": {
328
+ "missing": "(这条文案还没写)"
329
+ },
120
330
  "announcement": {
121
331
  "title": "新功能:收工关机",
122
332
  "line1": "左侧栏最底部多了一个 🌙「收工关机」按钮。",
123
333
  "line2": "点一下 → 所有会话收工后倒数 {n} 秒关机。",
124
- "line2Generic": "点一下 → 所有会话收工后倒数几秒关机。",
334
+ "line2.generic": "点一下 → 所有会话收工后倒数几秒关机。",
125
335
  "line3": "想改行为,去「设置 → 自动重启与关机」。",
126
336
  "disabled": "按钮目前关闭,可在设置里打开。",
127
337
  "button": "知道了",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-autorestart",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "DeepSeek Harness plugin: restart without losing work. Waits until every conversation is idle, restarts the host, then automatically resumes the interrupted sessions. AI-generated, not human-reviewed: use with caution.",
6
6
  "license": "MIT",
@@ -31,6 +31,9 @@
31
31
  "index.js",
32
32
  "client.js",
33
33
  "lib/**/*.js",
34
+ "supervisor/supervisor.js",
35
+ "scripts/*.ps1",
36
+ "scripts/*.cmd",
34
37
  "cordis.patch.yml",
35
38
  "locale/*.json",
36
39
  "icon.svg",
@@ -46,7 +49,9 @@
46
49
  "client": {
47
50
  "platform": "web",
48
51
  "immediately": true,
49
- "inject": ["@deepseek-ai/dsh-client-ui-conversation"]
52
+ "inject": [
53
+ "@deepseek-ai/dsh-client-ui-conversation"
54
+ ]
50
55
  },
51
56
  "compatibility": {
52
57
  "dshReleases": {
package/screenshots.json CHANGED
@@ -1,5 +1,7 @@
1
- [
2
- "assets/screenshot-1.png",
3
- "assets/screenshot-2.png",
4
- "assets/screenshot-3.png"
1
+ [
2
+ "assets/screenshot-1.png",
3
+ "assets/screenshot-2.png",
4
+ "assets/screenshot-3.png",
5
+ "assets/screenshot-4.png",
6
+ "assets/screenshot-5.png"
5
7
  ]
@@ -0,0 +1,65 @@
1
+ # 自救包(scripts)
2
+
3
+ DSH 起不来的时候,这个文件夹里的东西**不用 DSH 自己**就能救自己。
4
+
5
+ ## 按平台挑文件(**别拿错平台的**)
6
+
7
+ | 你的机器 | 还原 profile | 摘掉本插件 |
8
+ |---|---|---|
9
+ | **Windows**(桌面客户端 / Windows 上的 `dsh web`) | 双击 `restore-profile.cmd` | 双击 `emergency-disable.cmd` |
10
+ | **Linux / WSL / macOS**(托管端) | `sh scripts/restore-profile.sh` | `sh scripts/emergency-disable.sh` |
11
+
12
+ Linux/WSL 的机器上**没有** `.cmd` 文件,Windows 的机器上**没有** `.sh` —— 设置页的
13
+ 「出事了怎么办」只显示**这台机器上真有的**那两个路径,不会给你一条走不通的死路。
14
+ 每个平台到底有什么能力,看 [平台能力矩阵](../docs/platform-matrix.md)。
15
+
16
+ - **双击哪个**:DSH 起不来(插件升级把 profile 弄坏了)→ 还原;只是想把本插件摘掉 → 摘插件。
17
+ - **找不到它们?**:设置页里有一组「出事了怎么办」,上面写着这两个脚本的**绝对路径**(可以一键复制、可以一键打开所在文件夹;Windows 上还能放一个桌面快捷方式)—— DSH 真打不开、连 README 都打不开的时候,那一栏就是给你指路的。
18
+ - **它会做什么**:`restore-profile.cmd` / `restore-profile.sh` 把最新的那份快照(`<stateDir>/backups/` 里带 manifest 的)放回 profile —— 先把现在的 profile 整份挪到 `<profile>.broken-<时间戳>`,再复制快照里的配置和插件树,最后回读校验(package.json 能解析、`dsh.profile.bundles` 有内容、插件版本号读得到);校验不过就如实报错,并告诉你原来那份还在哪个目录。**它不会替你重启 DSH**(重启由你自己来,免得"救一次挂一次")。
19
+ - **会留下什么**:一个 `<profile>.broken-<yyyyMMdd-HHmmss>` 目录(出事之前的那份 profile 原封不动地留着,确认没问题后可以自己删掉),加上最后一行"已还原到 …… 现在可以重新打开 DSH"的结论。
20
+
21
+ ## 两条硬规矩(有意收紧,不是 bug)
22
+
23
+ - **目标 profile 目录必须已经存在**:还原器把快照放回一个**已经存在**的 profile,**不会凭空新建**一个 profile(也不会替你建 `profiles` 目录)。路径写错的时候,宁可报错,也不留一个空壳 profile 在那儿。
24
+ - **插件版本号的校验看配置说话**:profile 的 `dsh.profile.bundles` 里**没有** `dsh-autorestart` → 这一项**跳过**;**声明了**、但 `node_modules/dsh-autorestart` 不在 → **必须失败**(配置说一套、磁盘是另一套,这是真坏,不能放过)。
25
+
26
+ ## 演练(不碰你正在用的 profile)
27
+
28
+ | 平台 | 命令 |
29
+ |---|---|
30
+ | Windows | `powershell -NoProfile -ExecutionPolicy Bypass -File .\drill-rescue.ps1` |
31
+ | Linux / WSL | `sh scripts/drill-rescue.sh` |
32
+
33
+ 两个演练是**同一套断言**(Windows 的 `.ps1` 与 POSIX 的 `.sh`):在临时目录里造一个假 profile → 故意把 `package.json` 写坏、删掉插件目录 → 调还原脚本救回来 → 断言(能解析、插件版本读得到、`broken-*` 目录存在、非 profile 目录被拒绝、目标目录不存在时被拒绝、"配置说装了却没装"时必须失败、**还原脚本的输出里真的有中文**)→ 清理临时目录。全绿时退出码是 0。
34
+
35
+ 演练的目录纪律(**并发跑也互不干扰**):每次跑用一个带完整 GUID 的**唯一**目录,目录由本次演练自己创建(已存在就报错退出),目录里放一个独占的锁文件 `.lock`;清理**只删自己那一个绝对路径**,**绝不按前缀/通配符扫临时目录** —— 那会踩到同时在跑的另一个演练。成功、断言不过、抛错中断,走的都是同一个 `finally`,一定会清理;清理不干净就把残留路径打印出来。
36
+
37
+ `restore-profile.ps1` 的参数:`-DryRun`(只打印将要做什么)、`-Snapshot <目录>`(指定某一份快照)、`-ProfileDir <路径>`(演练用的隔离目录)、`-StateDir <路径>`(插件状态目录,默认 `<DSH_HOME>/storages/dsh-autorestart`)。
38
+ `restore-profile.sh` 的参数:`--dry-run`、`--snapshot <目录>`、`--profile <路径>`、`--state-dir <路径>`。`DSH_HOME` 可以用环境变量覆盖,但目标 profile 目录必须已经在那儿。
39
+
40
+ > 编码规矩(为什么脚本是英文的):这台机器只有 Windows PowerShell 5.1,它按 ANSI 读**不带 BOM 的 UTF-8** —— 脚本里只要有一个中文字符,整份文件就变乱码、直接 ParserError,脚本连第一步都走不到。POSIX 那边同理(`/bin/sh` 也按字节读,而且中文注释会让不同的 shell 表现不一致)。这个坑一天之内踩了三次,所以现在是**根治**而不是"记得加 BOM":**所有 `.ps1` / `.sh` / `.cmd` / `.mjs` 必须是纯 ASCII(所有字节 < 128、不带 BOM)**。面向用户的中文全部放在**一个文件**里:`scripts/rescue-strings.zh.json`(UTF-8、**不带 BOM**,带 BOM 会让 `JSON.parse` 直接失败),脚本运行时读它;读不到或读坏了,脚本会用**英文兜底**继续跑,并明确打印一行英文说明(不会崩成乱码)。
41
+ >
42
+ > **以后要改文案,改 `rescue-strings.zh.json`,不要往脚本里塞中文。** 键名是语义化的(`restore.title`、`restore.brokenKept`、`drill.passed`、`emergency.failed` …),值里可以用 `{0}`、`{1}` 这类占位符。这条规矩由 `test/scripts-encoding.test.mjs` 天天盯着:它会扫目录(不是写死文件名)、断言每个 `.ps1` / `.sh` / `.mjs` 都是纯 ASCII、断言 `.sh` 以 `#!/bin/sh` 开头并开着 `set -eu`、断言 JSON 无 BOM 且能被 `JSON.parse`、断言每个脚本里用到的键在 JSON 里都存在、断言 `.cmd` 调的是 `powershell.exe` 而不是 `pwsh`。
43
+
44
+ ## 写 POSIX 脚本时踩过的三个坑(别再踩一次)
45
+
46
+ 这三个都是**真跑出来的**(不是猜的),症状和原因都写在这儿:
47
+
48
+ 1. **`/bin/sh`(dash)会数单引号里的括号。**
49
+ 一段 `$( ... )` 里如果有一段**单引号包起来的英文兜底文案**,而那个文案里有一个
50
+ 没配平的 `)`,整份脚本会直接语法报错(`Syntax error: ")" unexpected` / `expecting ")"`),
51
+ **而且报错的位置在后面很远的地方**,很难看出是哪一行害的。
52
+ → **兜底文案里不要出现括号**。要写就写成 `-` 或 `:`。
53
+ 2. **双引号包起来的 sed 表达式会被 shell 吃掉转义。**
54
+ `sed -n "s/.*\"$key\".../\1/p"` 里的 `\"` 会被 shell 先处理掉,表达式在引号那里被截断,
55
+ sed 报 `unterminated 's' command`(而且报错只给 char N,看不出是引号的问题)。
56
+ → **sed 表达式要么全单引号,要么用 `'...'"$var"'...'` 的拼接写法**。
57
+ 3. **文案 JSON 是嵌套的**(`restore.title` 是一条路径,不是文件里的键)。
58
+ 用一条平铺的 sed 去找 `"restore.title"` 永远找不到 → 脚本会静悄悄地退回英文,
59
+ **功能全对、只是没有中文**,特别难发现。
60
+ → **按路径取值**:用 node 读 JSON(`process.argv[2].split(".")` 一层层取),
61
+ 顺便把 `{0}`/`{1}` 的占位符替换也交给 node —— 中文是 UTF-8,**按字节的 sed 会把它弄坏**。
62
+
63
+ 这三个坑的共同点:**症状离原因很远**(语法错在后面、或者干脆不报错只是少了中文),
64
+ 所以每个脚本都尽量把"取值"和"拼字符串"这两件事交给 node,只在 node 不在时才退回
65
+ 那一小段 ASCII 兜底逻辑。