@ray1270/dsh-restart:给 DSH 桌面版加刷新与一键重启

前言

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 不参与:

  1. 把当前 sessionId 写进 localStorage(同源,重载不丢);
  2. location.reload();
  3. 页面重新加载时 client 插件重新拉取、重新挂载,读到标记后 uiWorkspace.openSession(sessionId),再删掉标记。

刷新不跨 origin,所以标记放浏览器存储就够了,不必走 Host 文件。

重启是怎么做的

由 Host 侧排程,helper 由 WMI 服务启动,绕过应用自身的进程树。整体流程:

  1. 浏览器 POST /api/dsh-restart/restart { sessionId, force };
  2. Host 做活动工作检查(force 才放行),写 pending-restore.json(重启后要回的会话);
  3. 起一个短命的 powershell,调用 Win32_Process.Create → 由 WmiPrvSE.exe 创建 helper.ps1(用 Win32_ProcessStartup 的 ShowWindow = 0 让它生来没有窗口);
  4. 等 helper 写出 boot 状态后才回 200;
  5. helper 复核 ShellPid 的镜像路径仍等于目标 exe(防 PID 复用),taskkill /F 只杀 Electron 壳(不加 /T),等 Host 自己优雅退出(最多 8 秒),再 Start-Process 重新拉起 exe(失败重试一次并校验新进程还活着),结果写 restart-state.json;
  6. 壳一死,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 自杀,壳会判定异常终止并弹出「应用意外停止」恢复框,多一次点击、日志里留一次假崩溃。这个插件选的是「杀壳 + 重新拉起」,所以能一次点完。

发生问题时按可能性排序自查:

  1. 点了没反应、应用还在——helper 没起来(WMI 被禁用/拦、脚本被删)。此时应用完全没受影响。先看 ~/.dsh/dsh-restart/launch-result.json,再看 helper.log。
  2. 应用退出了但没回来——helper 在杀之前就死了,或 Start-Process 被拦。手动双击 D:\dsh\DeepSeek Harness.exe;restart-state.json 写着停在哪一步。
  3. 点「刷新」后回到首页——会话标记没能写下(无痕模式、存储被策略关闭)或会话已被删,刷新链路失败一律静默退回首页。
  4. 想彻底绕过这一切——托盘右键退出,再双击图标。

状态文件都在 ~/.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

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜