前言¶
DSH 桌面版的端口每次启动都是随机分配的(--port 0),所以每次重启都等于换了一个 origin:旧页面失联、壳自动开新页面、你被扔回首页。想「重启一下看效果」,原本要关窗口、找图标、等启动、手动点回会话。客户端插件的改动(slot 注册、界面代码)本来只需要重新加载页面就能生效,但手按 F5 会让当前会话掉回首页——因为 DSH 的当前会话只活在页面里、不在 URL 上。
下面介绍的 @ray1270/dsh-restart 就是解决这两件事的客户端插件。
这是什么¶
维护者 Ray1270(package.json 里 author 为 Ray_lv)发布在 npm 上的客户端插件(dsh.client.platform = web),当前版本 0.3.7,MIT 协议。它给 DeepSeek Harness 桌面版的窗口顶栏加两个常驻按钮:
应用 编辑 刷新 重启
刷新只重装浏览器这一半,Host、会话记录、正在跑的 agent 都不动;重启会重启整个应用(Electron 壳 + Host),但回来时停在你刚才那个会话。
两个按钮分别做什么¶
刷新:重新加载当前页面。Host 不动、会话记录不动、正在跑的 agent 也不动,只把 Web 客户端重新装一遍。加载完仍然停在刚才那个会话(会话标记 2 分钟内有效)。代价是输入框里没发出去的草稿会丢,已发送的对话、正在生成的回复、后台 job 都不受影响。由于不跨 origin,刷新这条链路上没有任何一次网络请求。
重启:重启整个应用。第一点是预检,按钮显示「确认重启 · N 项」,N 是此刻会被中断的 agent / job / 终端数量,4 秒不点自动撤销;第二点是执行,1–3 秒后应用退出并自动拉起,回来时停在同一会话(30 分钟内有效)。确认过就是真中断:会话记录是追加写,丢的是进行中那一轮和 job 输出。
之所以刷新一下、重启两下:刷新不杀服务端任何东西,代价最大只是草稿;重启会真的中断活动任务,必须让人看清数字再决定。
点失败(重启失败 / 刷新失败)时按钮 6 秒后复位,原因在 tooltip 里。
安装与启用¶
已发布到 npm,一行命令:
dsh plugin --profile desktop add @ray1270/dsh-restart
装完重启一次应用才会加载(这一次只能手动重启,鸡生蛋问题),之后就靠这两个按钮了。
dsh plugin add 走的是 pnpm add 到 profile,再把包名追加进 dsh.profile.bundles,不用手改任何文件。应用里的「插件 → 添加插件」走的是同一条路,只是多一个填路径的输入框。
不走 npm 的话,到 Releases 下载最新的 ray1270-dsh-restart-<版本>.tgz,在「插件 → 添加插件」里填它的绝对路径。两条路装的是同一个文件——release 附件与 npm 上那份 tgz 的 sha256 逐字节相同。想自己核对:
Get-FileHash .\ray1270-dsh-restart-<版本>.tgz -Algorithm SHA256 # 与同名 .sha256 文件比对
从源码装(开发):
dsh plugin --profile desktop add "<这个目录的绝对路径>"
装完同样要重启一次应用。
配置¶
写在 profile 的加载行 config: 里(也可以直接改 cordis.patch.yml):
| 键 | 默认 | 含义 |
|---|---|---|
enabled |
true |
重启总开关;false 时重启报「不可用」并拒绝一切重启请求(刷新不受影响) |
delayMs |
1200 |
动手前的等待,用来保证 HTTP 回执先到浏览器 |
hostGraceMs |
8000 |
等 Host 自己优雅退出的上限,超时才强杀 |
killTimeoutMs |
15000 |
等旧进程彻底消失的硬上限 |
pendingTtlMinutes |
30 |
「重启后回哪个会话」标记的有效期 |
刷新没有配置项:它不碰 Host,也就没有可配置的危险动作。
刷新是怎么做的¶
纯客户端,Host 不参与:
- 把当前
sessionId写进 localStorage(同源,重载不丢); location.reload();- 页面重新加载时 client 插件重新拉取、重新挂载,读到标记后
uiWorkspace.openSession(sessionId),再删掉标记。
刷新不跨 origin,所以标记放浏览器存储就够了,不必走 Host 文件。
重启是怎么做的¶
由 Host 侧排程,helper 由 WMI 服务启动,绕过应用自身的进程树。整体流程:
- 浏览器
POST /api/dsh-restart/restart { sessionId, force }; - Host 做活动工作检查(
force才放行),写pending-restore.json(重启后要回的会话); - 起一个短命的 powershell,调用
Win32_Process.Create→ 由WmiPrvSE.exe创建helper.ps1(用Win32_ProcessStartup的ShowWindow = 0让它生来没有窗口); - 等 helper 写出 boot 状态后才回 200;
- helper 复核 ShellPid 的镜像路径仍等于目标 exe(防 PID 复用),
taskkill /F只杀 Electron 壳(不加/T),等 Host 自己优雅退出(最多 8 秒),再Start-Process重新拉起 exe(失败重试一次并校验新进程还活着),结果写restart-state.json; - 壳一死,Host 的 IPC 管道断开,Host 走自己的
application.shutdown.shutdown(0)优雅退出(5 秒上限)。
几个关键点:
- 只杀壳,不杀树。 Host 子进程注册了
process.once("disconnect", () => stop()),壳一死它就自己走完整 dispose 流程,所以「硬杀」只硬在壳这一下。 - helper 必须活着跨过壳死掉那一刻。 这个应用的子进程树不活过壳,普通子进程做不到(表现就是「只有关、没有开」)。
Win32_Process.Create由WmiPrvSE.exe执行,helper 的父进程是 WMI 服务,壳死时它毫发无损;helper.log的 boot 行会记下parent=…(WmiPrvSE)。 - 不用
detached: true。 Windows 上 Node 用detached启动powershell.exe会让它立刻以 0 退出,一行都不执行;真正让 helper 活下来的是「由谁创建」。 - WMI 启动失败就放弃重启。 短命 launcher 把结果写进
launch-result.json;Host 等不到 helper 的boot或 WMI 明确拒绝,就回 500 并写cancel-restart标记,还在延迟中的 helper 看到会立刻放弃。 - 创建的进程要生来隐藏。
-WindowStyle Hidden救不了(那是窗口存在之后才去隐藏),必须构造Win32_ProcessStartup交给Create。
生态里现成的安全重启是 ctx.appExit(0),但它在桌面版等于让 Host 自杀,壳会判定异常终止并弹出「应用意外停止」恢复框,多一次点击、日志里留一次假崩溃。这个插件选的是「杀壳 + 重新拉起」,所以能一次点完。
发生问题时按可能性排序自查:
- 点了没反应、应用还在——helper 没起来(WMI 被禁用/拦、脚本被删)。此时应用完全没受影响。先看
~/.dsh/dsh-restart/launch-result.json,再看helper.log。 - 应用退出了但没回来——helper 在杀之前就死了,或
Start-Process被拦。手动双击D:\dsh\DeepSeek Harness.exe;restart-state.json写着停在哪一步。 - 点「刷新」后回到首页——会话标记没能写下(无痕模式、存储被策略关闭)或会话已被删,刷新链路失败一律静默退回首页。
- 想彻底绕过这一切——托盘右键退出,再双击图标。
状态文件都在 ~/.dsh/dsh-restart/:restart-state.json(最近一次重启阶段)、helper.log(带时间戳的过程日志)、launch-result.json(WMI 创建 helper 的结果)、pending-restore.json(待恢复会话标记,被消费后删除)、cancel-restart(仅在 Host 放弃启动时短暂出现)。
适用场景与注意¶
适合在 DeepSeek Harness 桌面版里调试客户端插件、或常需要重启应用看效果的人。刷新在任何环境都可用;重启只支持桌面版,CLI / headless 的 Host 没有壳可重启,按钮会明确报「不支持」。
注意几点:
- 插件以当前 dsh 进程的权限运行,安装前建议先看源码与许可证(MIT)。
- 顶栏入口依赖桌面壳自己画的菜单条(
preload用data-windows-menu标记 + open shadow root),插件只能在它后面追加同级按钮;探测不到就静默不挂载,回落到会话标题栏最右侧。两条入口共用一套逻辑,顶栏那份优先。 - 同一时刻只有一个入口在屏幕上:顶栏挂载成功时标题栏那对渲染成空,但仍挂着(它是唯一拿得到
sessionId的地方,用于刷新/重启后的会话回填)。 - 重启依赖 WMI 服务。WMI 被策略关掉、或
Win32_Process.Create被安全软件拦住时,重启会明确拒绝(应用不受影响),错误和返回码写在launch-result.json。 - 杀软可能好奇:「隐藏窗口的 PowerShell 杀掉应用再拉起」是 EDR 的经典启发式。helper 是明文、带完整注释、固定路径、日志可查,没有做任何编码混淆。
- 极端情况下仍可能看到一个控制台窗口:某台机器上
Win32_ProcessStartup构造失败时 helper 会退回可见窗口启动(重启本身照常),launch-result.json里windowHidden会是false。
小结¶
刷新解决「客户端改动只需重装页面、又不想丢当前会话」,重启解决「重启后被扔回首页」。两者都以会话为中心,代价提示和失败原因都写在界面上和日志里。
目录页:https://www.skillhub.cn/plugins/Ray1270/dsh-restart
仓库:https://github.com/Ray1270/dsh-restart
npm:https://www.npmjs.com/package/@ray1270/dsh-restart