DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

NOOBnoob163 /

NOOBnoob163/dsh-self-restart

Verified

Let a DSH agent restart its own host and resume afterwards. Windows desktop only.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@913bb1c9

dsh-self-restart

让 DSH 里的 Agent 能「自己重启自己」,并且重启完自己醒过来接着干。

DSH 桌面版是个常驻进程:改了插件、改了配置、内存涨了、偶尔卡住了,都得关掉再打开。 但这里有个死结 —— 关掉 DSH 就等于关掉正在干活的 Agent 自己,它没法一边关自己一边把自己拉起来。

这个插件提供那个「替它做」的东西。

平台:仅 Windows 桌面版。它靠 WMI 创建脱离进程、靠 taskkill / 窗口消息关闭应用, 换平台要另写一套。


核心:一次重启,多个会话各自续命

如果只记一件事,记这个:

它把「重启」做成一个可并发复用的动作。 几个会话可以把各自的活挂到同一次重启上, 重启完再把它们各自叫醒、各自拿回自己的理由、各自判断要不要接着干。

这不是「A 重启完再轮到 B」,而是并成一趟:

会话 A:restart_self(reason="改完插件要验证")  ┐
会话 B:restart_self(reason="等配置生效")      ├─→ 同一趟车(最多 8 人,谁先来谁先上)
会话 C:restart_self(reason="清理内存")        ┘   ← 还可以在等待途中随时加入
                          ↓
              等**所有人**都不忙 → 关一次 → 拉起一次 → 验证端口
                          ↓
   并发唤醒:A 拿"改完插件要验证" / B 拿"等配置生效" / C 拿"清理内存"
                          ↓
        A、B、C 各自跑起来,互不等待 —— 一台机器上三条活同时重新开始

为什么这是"并发"而不是"排队处理" —— 两个阶段都成立,但要分开说:

阶段 行为 是不是并发
排队 任意时刻、任意会话都可以把自己的活挂到同一趟车上;等待途中也能随时加入 ✅ 并发加入
重启 只关一次、只拉一次 —— 不管车上有 1 人还是 8 人 一次动作,不是 N 次
唤醒 逐条投递(一条失败不影响其余,也不用等上一个人跑完) 投递是串行的
醒后 每个会话独立跑自己那一轮,互不等待、互不阻塞 ✅ 真并发执行

⚠️ 一个容易写错的点,这里说清楚:唤醒消息是逐条投的,不是同时群发。 但那只是投递动作的先后,投完就各自跑了 —— 不是"排队执行",后醒来的不用等先醒的干完。

这带来五个别处没有的性质:

性质 意味着什么
只关一次、只拉一次 3 个人排队,不是重启 3 次。整台机器只中断一次,中位几十秒
每人各拿各的理由 唤醒消息是按 成员 × 自己的 reason 逐条投的,不是群发一句通用话
每人各自决定续不续接 同一趟车里 A 选 resume: true、B 选 resume: false 都行 —— 逐人判断,不是一刀切
一个失败不拖累其余 逐人记账(resume:<单号>:<会话id>):B 投递失败,A 和 C 照常被叫醒
可以各自下车 名单里永远是"发车之后要叫醒谁"。谁不想被叫醒就摘掉自己,其余人照常 —— 见下

两种退出方式,别用错:

调用 影响范围 什么时候用
restart_status(leave: true) 只有你自己 "这车跟我没关系了"——车照开,不叫醒我;我是最后一个人则整趟不发车
restart_status(cancel: true) 整趟车所有人 "这趟别开了"——包括别人的重启一起作废

⚠️ 只是自己不想要了,永远该用 leave。用 cancel 会把别人的重启一起打掉。

为什么值得这样设计:多个会话同时要重启是常态(改完插件,几个窗口都在等); 而且重启期间整台机器是停的,每多一次重启就多一次中断。 所以正确的模型不是"每个会话各排各的",而是"一次中断,服务所有人"。

⚠️ 代价也要说清:一趟车里只要还有人没空闲,整趟都会等。人越多,等到空档越难。 所以「取消」是整趟取消 —— 这也正是它必须让你先看名单的原因。 (只想让自己退出、不想连累别人的,用「下车」,见下面对两者区别的说明。)


什么时候你不需要它

  • 只是想手动重启一次 → 不需要,直接重启 DSH 更快。
  • 不跑长任务、也不在意会话被打断 → 不需要,它解决的是"重启完要接着干"。
  • 不是 Windows 桌面版 → 用不了,见「已知边界」。

它真正省事的是这个场景:你自己不在电脑前,但希望 Agent 把活干完、需要重启时自己排上、 重启完自己醒过来继续 —— 而不是第二天回来发现卡在某个报错上。


它提供什么

一句话定位:把「重启宿主」变成一个可被多个会话并发复用的动作。 不是"每个会话各重启一次",而是"一次中断,服务所有排队的会话"。

工具 干什么
restart_self 排一次「延时重启」:等真正空闲后,由一个脱离进程的助手把 DSH 关掉、重新拉起、并验证端口真的回来了
restart_status 看有没有待执行的重启、现在忙不忙、上次重启成不成、这台机器重启要多久;也能「下车」或「取消」
① Agent 调 restart_self
② 还可以有别的会话陆续排进来(同一趟车,最多 8 人;等待途中也能随时加入)
③ 助手(脱离进程)等所有人都不忙了 → 关掉 DSH → 重新拉起 → 验证端口
④ 起来后插件按名单**逐条**把唤醒消息投进每个会话(各拿各的理由)
⑤ 每个会话被唤醒后就**各自跑起来**,互不等待 —— 一次重启,N 条活同时续上

几条设计取舍(会影响你怎么用它)

  • 绝不强行打断别人。 有别的会话在忙就排队等,不是作废、也不是硬闯。 默认一直等(maxWaitMinutes: 0),超过 notifyAfterMinutes(默认 20 分钟) 还没发车会走一次通知钩子(见「外部通知」)。

  • 不想被叫醒就「下车」,不要用「取消」。 这是两个不同的出口:

    • restart_status(leave: true) —— 只影响你自己:把你这一个会话从叫醒名单里摘掉。 车上还有别人 → 车照开,只是不会叫醒你;摘完没人了 → 这趟本来就没人为它等了,直接不发车。
    • restart_status(cancel: true) —— 影响整趟:车上所有人的这次重启一起作废。 所以取消之前先调一次 restart_status() 看名单(它会列出每个人 + 各自的理由 + 你在不在里面)。

    一句话区分:leave 是"这车跟我没关系了",cancel 是"这趟别开了"。 只是自己不想要了,永远该用 leave —— 用 cancel 会连累别人。

  • 不弹窗打扰,但有一张不阻塞的状态卡片。 卡片上有三个出口: 「不等了,立刻重启」(只跳过"等家务")、「打断」按钮(取消整趟)、右上角的 ✕。 「打断」和 ✕ 写的是同一个取消标记;「不等了」写的是另一个标记(force.flag), 只影响"还要不要等家务"。

  • 排队时会在会话标题前挂 ♻️ ,一眼能看出谁在等;上次重启失败的会话挂 ⭕ (2 小时后自动消失)。 ⚠️ 从 IM 通道来的会话,图标排在频道前缀后面(QQ · ♻️ 原标题)。 原因:有插件会按「开头是不是 QQ · 」判断要不要补前缀,图标抢到最前面会让它反复补。 兼容范围(实测):认 16 个常见渠道标签,清单内收敛;清单外若对方只加一次也幂等。

  • 一趟车结束就封口。 取消 / 失败 / 成功三条路都会删掉计划文件、清掉取消标记, 否则会出现"已取消"和"待执行"同时成立的假状态。

  • 「关掉了却没人拉起来」是唯一不可接受的后果。 所以除了主助手,还有一个保险丝进程: 万一主助手在关机途中死了,保险丝负责把 DSH 拉回来。


安装

本插件是一个标准的 DSH bundle(package.json 里带 dsh.bundle.patch,自带 cordis.patch.yml)。

方式一:用 DSH 的插件管理(推荐)

让 Agent 调用它自带的 plugin_manager 工具:

plugin_manager(action="install_bundle", target="<本目录的绝对路径>")

install_bundle 会自己完成「装包 + 选中 bundle」两件事。

⚠️ 不要用 shell 命令模拟这两步(拷目录、手改 profile 的 package.json / cordis.patch.yml、在 profile 目录里跑 pnpm)—— 这是 DSH 官方文档明确禁止的做法。

方式二:本地开发时用 workspace 链接

把本目录登记进 profile 的 package.json:

{
  "dependencies": { "dsh-self-restart": "workspace:*" },
  "dsh": { "profile": { "bundles": [ "dsh-self-restart" ] } }
}

然后在 profile 目录里装一次依赖、重启 DSH。 (仓库自带的 deploy.ps1 就是给这种方式配套的:源码 → 实际加载位置 的同步 + 对账。)

依赖

  • Node 内置模块(fs / path / os / child_process / url / crypto);
  • @deepseek-ai/schemastery —— 由 DSH 宿主提供(所以同时列为 peerDependencies)。

「重启完自己醒过来」用的是 DSH 自带的正式服务 sessionController (随发行版默认挂载)—— 不需要实验 bundle,也不需要额外安装任何东西。


配置

所有配置项都有默认值,写在 lib/index.js 的 Config schema 里; 要改就在自己 profile 的 cordis.patch.yml 里覆盖同名 id 的 config:

- id: self-restart
  config:
    idleSeconds: 12
    notifyAfterMinutes: 20
配置项 默认 说明
enabled true 总开关
port 19387 兜底端口;实际端口每 10 秒从宿主现取
dshExe '' DSH 主程序路径;留空 = 自动找
stateDir '' 状态目录;留空 = %DSH_HOME%\self-restart
idleSeconds 12 「自己这一轮说完了」要连续静默多少秒
othersWindowSeconds 120 「别人还在忙」的判定窗口
confirmBufferSeconds 5 排上之后留给回复落盘的缓冲
confirmSeconds 10 动手前的确认窗(卡片上有倒计时,仍可打断)
maxWaitMinutes 0 最多等多久;0 = 一直等(另有 12 小时硬上限)
checkSeconds 10 助手的轮询间隔
bootTimeoutSeconds 90 拉起后等端口回来的上限
maxAttempts 3 拉起失败后的重试次数
notifyAfterMinutes 20 等这么久还没发车就通知一次;0 = 不通知
notifyRepeat false 要不要重复通知
notifyRepeatMinutes 60 重复通知的间隔
noticeCommand '' 外部通知命令(见下节);空 = 完全不通知
noticeTimeoutMs 30000 通知命令超时,超时就杀掉
waitForImDirect true IM 私聊算不算「有人在用」
waitForAllSessions false 所有会话都 idle 才动手(含群聊)
queueMaxMembers 8 同一趟车最多几个人
wakeMessage true 重启成功后要不要把唤醒消息投回原会话
wakeMessageMaxChars 600 唤醒消息里原样带回 reason 的字数上限
wakeDelaySeconds 3 起来后等几秒再投唤醒消息(等服务就位)
resumeHint '' 唤醒消息里附一句自定义提示(留空 = 不提)

外部通知(notice hook)

本插件不内置任何通知渠道。 它只在特定事件发生时,执行你自己配置的一条命令, 把事件内容通过环境变量交给它 —— 发不发、发给谁、用什么发,全在那条命令里。

为什么这样设计

通知渠道属于使用者环境,不属于插件。写死某个渠道等于要求每个使用者都装同样的东西; 没装就静默失败,那比没有通知更糟。所以这里只留一个接口,默认关闭。

怎么配

- id: self-restart
  config:
    noticeCommand: 'node C:\tools\my-notify.mjs'

命令里用环境变量取内容:

环境变量 内容
DSH_SR_EVENT 事件名,见下表
DSH_SR_TEXT 一句话正文(已经是单行,可直接发)
DSH_SR_PLANID 这趟重启的单号
DSH_SR_COUNT / DSH_SR_ELAPSED / DSH_SR_WAITEDMIN 等 各事件附带的标量字段(字段名大写归一)
DSH_SR_EVENT 什么时候触发
restart-queued-long 等了 notifyAfterMinutes 还没发车
restart-done 重启成功且之前发过"还在排队"通知(配对,不刷屏)
wake-failed 重启成功,但没能叫醒排队的会话
restart-failed 重启失败或被取消

安全边界

  • 插件只执行 noticeCommand 这一条你自己写的命令, 不执行 DSH_SR_TEXT 等 payload 里的任何内容 —— payload 只作为环境变量传入,不参与解释。
  • 命令由 shell 执行(所以可以带参数、管道、重定向)。 这意味着命令本身有完整权限 —— 只写你信任的命令,别把会话内容拼进去。
  • 有超时(noticeTimeoutMs)、有并发上限(同时最多一条在跑)、失败只记日志。
  • 通知失败绝不影响重启:重启是该做的事,通知是锦上添花。

重启完自己醒过来接着干

重启成功后,插件会把一条消息投进原会话(不是定时提醒、不是系统提示), 会话因此被驱动起来跑一轮 —— 带回来的正是你当初写的 reason。

  • resume: false 的重启(收尾型)不叫醒。
  • 投递是**「至多一次」的:宁可偶尔"这次没叫醒、需要手动开", 也绝不允许同一次重启投出两条 —— 两条一样的「接着干」会让同一件活被干两遍。 所以顺序是先记账、再投递、投不进去才回滚**。
  • 消息投出去就撤不回了:restart_status(cancel: true) 只能拦住还没投递的。

「闲下来」是怎么判断的:两段式

  1. 第一段:等自己这一轮说完。 连续 idleSeconds 没有新动静。
  2. 第二段:等别人。 再看一遍全部会话,othersWindowSeconds 内写过会话的算「在动」。

不是所有"别人"都一样:

来源 算不算"有人在用" 说明
本地窗口 算
IM 私聊 算(waitForImDirect 可关) 私聊那头随时可能来人
IM 群聊 不算 那是机器人在跟别人聊,重启中位只有几十秒
查不到 算(保守) 宁可多等

⚠️ 默认值下,"自动找空闲"可能经常等不到:如果你有大量 IM 私聊会话, 第一个该考虑调的就是 waitForImDirect。

「不等了,立刻重启」

等家务等到没完时,卡片上的「不等了,立刻重启」只跳过等家务这一段, 不是强制关闭别人的对话。它写的是 force.flag,和「打断」(写 cancel.flag)是两回事。


状态文件在哪

%DSH_HOME%\self-restart\(默认 ~\.dsh\self-restart\):

文件 是什么
state.json 插件每 10 秒写的实时状态(端口、在跑的会话、待执行的重启)
plan.json 当前这一趟重启的计划;没有待执行重启时不存在
last-run.json 上一次重启的结果(助手写)
history.jsonl 流水账,一行一次
reported.json 插件自己的记账(哪些已汇报、哪些已通知)
helper.log / plugin.log 助手 / 插件的日志
notify-<planId>.json 通知配对用的记账

日志会一直留着,需要时自己清理。


开发

改完怎么让它生效

  • 改了 lib/index.js → 必须重启宿主(Node 侧代码不会热加载)。
  • 改了 bin/*.ps1 → 下次排重启就生效(助手是现读现跑的)。

开发目录和 DSH 实际加载的目录是分开的,用 deploy.ps1 同步 + 对账:

pwsh -NoProfile -File deploy.ps1 -Check     # 只对账(0=一致 / 5=不一致)
pwsh -NoProfile -File deploy.ps1 -DryRun    # 预演会复制哪些文件
pwsh -NoProfile -File deploy.ps1            # 真正同步

跑测试

powershell -NoProfile -ExecutionPolicy Bypass -File test\run-all-tests.ps1
powershell ... -File test\run-all-tests.ps1 -Only 'test-helper','test-two-phase'   # 只跑指定几套

⚠️ 测试全程不碰真 DSH:所有用例都用假目标(假 exe / 假端口 / 假会话目录), 每套末尾还有"真 DSH 监听者 PID 没变"的断言兜底。


目录结构

lib/index.js              插件主体(工具注册、状态轮询、唤醒投递、标题标记)
lib/notice-hook.js        外部通知钩子(执行用户配置的命令)
bin/dsh-self-restart.ps1  助手:等待 → 关闭 → 拉起 → 验证 → 写结果
bin/dsh-restart-window.ps1 状态卡片(WPF)
cordis.patch.yml          bundle 补丁(挂载点)
deploy.ps1                开发目录 → 加载目录 的同步 + 对账
test/                     自测(ps1 + mjs)

它内部怎么做的(想改代码再看)

为什么要「脱离进程」的助手:它用 WMI 创建,父级是系统的 WmiPrvSE, 不在 DSH 的进程树里 —— 所以 DSH 被杀掉之后它照样活着,能把 DSH 拉回来。 (同一个脚本有三种身份:点火器负责把自己脱离出去,正主动手关/拉, 保险丝守「正主死在半路」这一种最坏情况。)

为什么「先确认拉得起来,才允许动手关」:曾经用了一个不存在的主程序路径做自测, 助手照样先把 DSH 杀了,杀完才发现没法拉起。所以动手前有一道硬闸: 主程序不存在 / 是 0 字节 / 读不到 → 原地退出,DSH 一根汗毛不动。

为什么宽限期只有几秒:taskkill 不带 /F 是给窗口发 WM_CLOSE, 而 DSH 的退出条件是 window-all-closed —— 它有多个顶层窗口,可见的只有一个, 其余是隐藏的功能窗口,不会被外部关闭请求关掉。 所以「礼貌关闭」在结构上就不可能让它退出,等多久都一样。


已知边界

  • 只在 Windows 桌面版上工作。 找不到主程序就明确失败,绝不乱猜路径。
  • sessionController 服务是自动唤醒的前提(DSH 自带,正常组合下都在)。 拿不到 → 不会有「重启完自动叫醒」,但重启本身照常。
  • 唤醒只在 outcome=success 时投递:重启失败 / 被取消 / 没执行(skipped-busy) 都不会叫醒 —— 失败时叫醒 Agent 继续干会把故障掩盖掉。
  • 投递是「至多一次」(刻意的),见上文。
  • 通知是「尽力而为」:noticeCommand 失败、超时、没配,都不影响重启本身。

许可

MIT


—/ 5

No ratings yet

Verified DSH bundle

Commit 913bb1c9a002

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout