Bundle
dsh-desktop-auto-restart
DSH 桌面端(Windows)重启插件:从侧边栏按钮或默认关闭的模型工具重启宿主,重启后按会话原有预设自动唤醒;宿主繁忙时拒绝重启。
- Source
- comer2031
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# dsh-desktop-auto-restart(重启插件)
给 DSH 加一个「重启宿主」的工具和一个侧边栏按钮;**重启后自动把指定会话唤醒**,让它接着干。
它解决的是一个具体麻烦:改了 DSH 宿主侧插件(host 半边)必须重启 DSH 才生效,可重启又会把正在跑的回合、连同发起这次改动的 agent 自己一起杀掉。本插件把「重启」做成一次工具调用,并在新进程起来后把 agent 唤醒回来。
## 兼容性
**只支持 Windows 上的 DSH 桌面端**(那个 Electron App)。
- 重启脚本靠 PowerShell + WMI 分离进程、靠 user32 伪造系统会话结束 —— 非 Windows 平台没有这套东西。
- 非桌面形态(例如浏览器版)没有可重启的 App,指定端口上找不到宿主进程,脚本会如实报「端口上没有监听进程」并退出。
- 插件自己在非 Windows 平台会直接拒绝重启,不会半途而废、也不会静默失败。
- **脚本能不能跑,取决于本机 PowerShell 执行策略**:需要允许本地脚本(`RemoteSigned` 即可)。Windows 客户端的默认策略可能是 `Restricted`,那种机器上脚本**根本不会被执行**,而 WMI 照样返回 rc=0 ⇒ 工具会报「脚本已拉起」却什么都没发生。修法:`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`(不需要管理员)。
- **装了火绒等安全软件**时,重启会在它的「系统加固」里记一条 `WmiPrvSE.exe 触犯敏感动作防护规则`:用 WMI 建进程正是为了让重启器脱离 DSH 进程树,属于预期行为;对方的软件若设成询问模式会弹窗,放行即可。
## 功能
- **工具 `dsh_restart`**(⚠️ **测试性质,谨慎使用**):`{ reason, sessionIds, delaySeconds? }` —— 先写「待唤醒清单」,再调重启脚本(`delaySeconds` 不传=10 秒,留给回执落盘;`0`=立刻重启)。
该工具**默认不注册**(「设置 → 重启」里的开关默认关);打开后模型能自己重启宿主,这会**中断正在跑的回合**,请确认清楚再让它用。宿主繁忙时**直接拒绝**,且没有 `force` 之类的绕过参数(繁忙判定见下;**调用方自己那个会话不算繁忙**)。
- **侧边栏按钮**(`sidebar.footer.action`):空闲时点一下直接重启、不弹框;繁忙时先弹一个确认层(确认键红底),确认后带 `force` 放行。端点只认**本机回环地址 + 带自定义请求头的同源请求**,别的网页调不动它。
- **重启后自动唤醒**:新进程启动时读清单,对每个会话走**官方通道**、按它**原有的预设**唤醒,并注入一条「已重启完成,请继续」的 notice;唤醒失败就**保留清单**,下次启动继续重试。
- **防自杀**:脚本只用「宿主监听端口 + 精确 PID」定位进程,绝不按进程名批量杀;不改任何配置与工具面。
## 安装
包在 npm 上,名字就是 `dsh-desktop-auto-restart`。用 DSH 自带的 CLI 一条命令装(装完重启 DSH 生效):
```sh
dsh plugin --profile <profile 名> add dsh-desktop-auto-restart
```
(`npm i dsh-desktop-auto-restart` 只是把包装进当前目录,**DSH 插件要按上面那条 CLI 装**才生效。)
手动装(等同上面这条做的事):
1. 让 profile 能找到这个包:把本目录放进 `<profile>/node_modules/`,或在 `<profile>/package.json` 的 `dependencies` 里用 `file:` 路径/包名指向它。
2. 在 `<profile>/package.json` 的 `dsh.profile.bundles` 数组里加上 `dsh-desktop-auto-restart`。
3. 重启 DSH。
(`<profile>` 默认在 `~/.dsh/profiles/<profile 名>`。)
## 配置
字段定义在 `lib/index.js` 的 `Config`,默认值如下;要改就在 profile 的 `cordis.patch.yml` 里写一条 id 定向 patch。
| 字段 | 默认 | 作用 |
| --- | --- | --- |
| `agentToolEnabled` | `false` | **默认关**:关着时**不注册** `dsh_restart`(模型看不到这个工具);打开后需重启 DSH 才生效 |
| `wakeOnStart` | `true` | 启动时是否读清单执行唤醒 |
| `wakeDelayMs` | `3000` | 启动后等多久再开始唤醒(太早还拿不到会话) |
| `pendingTtlMs` | `1800000` | 清单过期时间(默认 30 分钟) |
| `scriptPath` | `''` | 覆盖重启脚本路径;留空=用插件自带脚本(自定义脚本要认 `-Port`/`-InstallRoot`/`-ResultFile` 这三个参数) |
| `dryRun` | `false` | 只落清单、不真重启(演练用) |
| `desktopPort` | `19387` | 宿主监听端口:脚本靠它认进程;宿主不在默认端口时必须改这里 |
| (无) | — | UI 端点门禁与 60 秒并发冷却**没有开关**:为安全起见不可配置 |
| `installRoot` | `''` | 宿主安装目录;留空=**自动探测**(插件自己就跑在这个 App 里,直接取当前进程 exe 所在目录,实测准确);探测不到才回落到脚本内置默认 `%LOCALAPPDATA%\Programs\DeepSeek Harness` |
⚠️ `cordis.patch.yml` 里 **id 定向的 patch 会整段替换该插件的 config**,所以要改的字段和想保留的字段都得一并写全。
## 设置页
DSH 的「设置」里会多一个**「重启」**分节(DSH 0.2 的标准做法:浏览器半边注册 `settings.section` 槽位,读写走宿主在同一前缀下开的同源 API,写回用官方 `configEditor` ⇒ 除了下面第一条工具开关,其余改完热生效、不用重启)。
- **允许 agent 调用重启工具**(默认**关**):关着时插件**不注册**这个工具,模型压根看不到它 —— 想让它能自主重启再打开,然后重启一次 DSH 才生效;运行时把它关掉,已注册的工具会**立刻**改为一律拒绝。
- **宿主监听端口 / 宿主安装目录**:安装目录留空=自动探测,探测结果直接显示在面板的状态行里(也会显示当前是「手动配置」还是「自动探测」)。
- **唤醒**:是否在重启后自动唤醒会话、唤醒前等多久。
- 面板顶部的只读状态:宿主端口、平台、当前是否繁忙、待唤醒清单条数、**上一次脚本结果**(`ok`/`failed` + 原因)。
## 工作机制
1. **工具调用**:校验重启脚本存在(不存在直接拒绝)→ 把「要唤醒谁 + 原因 + 时间」写进 DSH 数据目录(环境变量 `DSH_HOME`,默认 `~/.dsh`)下的 `restart-pending.json` → 用 WMI `Win32_Process.Create` 拉起重启脚本(父进程是 WmiPrvSE,脱离 DSH 进程树,DSH 自己被杀也不影响它),并读回 `rc`/`pid`。
2. **重启脚本**:按端口找到后端 host → 校验它的 exe 在安装目录下(不在就放弃)→ 先伪造系统会话结束、请它优雅退出,超时(默认 8 秒)才强杀这两个精确 PID → 等端口释放 → 再用原 exe 经 WMI 拉起来 → 等端口重新监听。
3. **重启后**:插件启动时若清单未过期,就每 4 秒重试唤醒(最多 30 次);全部成功才清清单,否则保留给下次启动。
4. **繁忙判定**:有正在跑的会话/有排队消息/有 running 或 stopping 的后台任务/有已武装的定时提醒 ⇒ 算繁忙;**唯独"调用方自己那个会话"不算**——agent 调工具时它自己那一轮必然是 running,算进去这个工具就永远调不动。取不到 `agents`、`jobs` 服务,或判定过程抛错 ⇒ 一律当繁忙(宁可不重启)。
5. **UI 端点门禁**:只接受「来自回环地址 + 带自定义请求头 `x-dsh-desktop-auto-restart` + `Origin` 与 `Host` 同源」的 POST —— 自定义头让跨源请求先触发浏览器预检,而宿主不给 CORS 放行 ⇒ 外部网页没法偷偷触发重启。(自定义头不是密码,挡的是浏览器,不是本机上的其它程序。)
6. **结果回读与并发闸门**:脚本每次运行都把结果写进数据目录下的 `restart-last-result.json`(先 `running`,再 `ok`/`failed`);工具拉起脚本后最多等 6 秒 —— 脚本若当场拒绝(端口上没有宿主进程、exe 不在安装目录……),工具**如实报失败并撤掉待唤醒清单**;等不到结果说明它在正常干活(成功路径要几十秒)。另外,一次重启被拉起后的 60 秒内不接受第二次请求,避免连点造成双实例。
## 目录结构
```
lib/index.js host 半边:工具、UI 端点、清单、唤醒
lib/client.js 浏览器半边:侧边栏按钮 + 繁忙确认层
scripts/restart-desktop.ps1 重启脚本(-DryRun 可空跑,不动机器)
scripts/restart-desktop.bat 转发入口(把参数传给 ps1)
.opencodereview/rule.json 本仓库的代码审查清单(错题本)
```
## 开发
```sh
npm test # host 半边的测试:自己造宿主桩,不需要装 DSH
```
测试把 `lib/index.js` 复制到临时目录、在它旁边放三个 `@deepseek-ai/*` 最小桩模块再动态 import,
用假的 ctx 与服务覆盖:工具开关与注册、繁忙拒绝、UI 端点门禁(缺头/非回环/跨源)、设置面板读写与
类型收敛、演练模式、并发冷却、待唤醒清单的认领与清除、重启后走官方通道唤醒。全程用临时 `DSH_HOME`,
不碰真实 `~/.dsh`,跑完自清。
## 限制
- 重启**必然**中断本进程与正在跑的回合;「继续」由重启后的唤醒负责。
- 安装目录通常**自动探测**(插件取自己进程 exe 所在目录,装在非默认位置也对);探测不到才回落到脚本内置默认 `%LOCALAPPDATA%\Programs\DeepSeek Harness` —— 那种情况下装在别处会被脚本的安全校验拒绝,把配置项 `installRoot` 指到实际目录即可。
- 重启始终失败时(脚本缺失、权限不足、端口被占),只能人工启动一次。
- 唤醒依赖宿主既有的会话恢复能力;宿主起不来就谈不上唤醒。
- 工具回执只能覆盖「脚本有没有被拉起」和「脚本头几秒有没有拒绝」;脚本中途失败(例如强杀失败、端口没释放)只会体现在 `restart-last-result.json` 与 `%TEMP%\dsh-restart-desktop.log` 里 —— 那时工具早已返回。
## 许可证
MIT
Install
dsh plugin --profile web add github:comer2031/dsh-desktop-auto-restart
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-desktop-auto-restart from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.