DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-restart

R

dsh-restart

Manifest valid

Refresh / Restart — The two persistent buttons in the top bar of the DeepSeek Harness Desktop version (refresh + one-click restart)

UI (client)hasBundlePatchMachine translated

@ray1270/dsh-restart

CI Release

给 DeepSeek Harness 桌面版加两个常驻按钮,就在窗口顶栏「应用」「编辑」右边:

应用  编辑  刷新  重启
  • 刷新 = 重新加载这个页面。Host 不动、会话记录不动、正在跑的 agent 也不动,只把这个 Web 客户端重新装一遍。加载完仍然停在你刚才那个会话。
  • 重启 = 重启整个应用(Electron 壳 + Host)。点两下,2–4 秒后应用带着你刚才那个会话重新打开。

为什么需要它:桌面版的端口每次启动都是随机分配的(--port 0),所以每次重启等于换了一个 origin——旧页面失联、壳自动开新页面、你被扔回首页。想「重启一下看效果」原本要:关窗口 → 找图标 → 等启动 → 手动点回会话。

刷新则解决另一半问题:客户端插件的改动(slot 注册、界面代码)只需要重新加载页面就能生效,没必要为此重启 Host。但 DSH 的当前会话只活在页面里、不在 URL 上,手按 F5 会掉回首页——所以这个按钮顺手把会话也带回来了。


用起来是什么样

状态顶栏显示说明
空闲刷新 重启两个都是普通菜单项,样式与「应用」「编辑」一致
点「刷新」刷新中约 0.1 秒后整页重载;回到同一会话(2 分钟内有效)
点「重启」第一次确认重启 / 确认重启 · N 项预检;N = 此刻会被中断的 agent / job / 终端数量;4 秒不点自动撤销
点「重启」第二次重启中1–3 秒后应用退出,随即自动拉起;回来时停在同一会话(30 分钟内有效)
失败重启失败 / 刷新失败6 秒后复位;原因在 tooltip 里

为什么刷新只要一下、重启要两下:刷新不会杀掉任何服务端的东西(agent 继续跑、会话继续写),代价最大只是输入框里没发出去的草稿会丢——tooltip 里写明了。重启会真的中断活动任务,所以必须让人看清数字再决定。

顶栏那条菜单是桌面壳自己画的(preload 里的 shadow DOM),插件只能在它后面追加同级按钮——所以样式、字号、悬停、拖动区都和原生菜单项一致,窗口变窄也不会挤到右边的窗口按钮。

不是 Windows 桌面版、或者壳没有画出这条菜单时,同一对按钮回落到会话标题栏最右侧(macOS / 纯浏览器里就是这个)。两条入口共用同一套逻辑,同时在屏幕上时顶栏那份优先,标题栏那份自动让位。

没有任何弹窗,也没有控制台窗口一闪而过——重启全程屏幕上只有应用自己关掉再打开。


安装

已发布到 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 "<这个目录的绝对路径>"

装完同样要重启一次应用。


它到底怎么做的

刷新:纯客户端,Host 不参与

 [浏览器] 点「刷新」
    │  1. 把当前 sessionId 写进 localStorage(同源,重载不丢)
    │  2. location.reload()
    ▼
 [页面] 重新加载:client 插件重新拉取、重新挂载
    │  3. 读到标记 → uiWorkspace.openSession(sessionId) → 删标记
    ▼
 [浏览器] 停在同一个会话

刷新不跨 origin,所以标记放浏览器存储就够了,不必走 Host 文件——刷新这条链路上没有任何一次网络请求(harness 里用「fetch 调用次数 = 0」盯着这一点)。

重启:Host 侧排程,helper 由 WMI 服务启动

 [浏览器] 点第二下
    │  POST /api/dsh-restart/restart { sessionId, force }
    ▼
 [Host] 1. 活动工作检查(force 才放行)
        2. 写 pending-restore.json(重启后要回的会话)
        3. 起一个短命的 powershell,让它调用
           Win32_Process.Create → 由 WmiPrvSE.exe 创建 helper.ps1
           (创建时带 SW_HIDE 启动信息:helper 生来没有窗口)
        4. 等 helper 写出 boot 状态 → 才回 200
    │
    ▼  helper 的父进程是 WmiPrvSE,不在应用的进程树里
 [helper] 5. 复核 ShellPid 的镜像路径仍等于目标 exe(防 PID 复用)
         6. taskkill /F 只杀 Electron 壳(**不加 /T**)
         7. 等 Host 自己退出(最多 8 秒,超时才补刀)
         8. Start-Process 重新拉起 exe(失败重试一次,并校验新进程还活着)
         9. 结果写 restart-state.json
    │
    ▼ 壳一死 → Host 的 IPC 管道断开 → process.once("disconnect")
 [Host]   application.shutdown.shutdown(0) —— 它自己的优雅退出,5 秒上限

关键点 1:只杀壳,不杀树。 Host 子进程注册了 process.once("disconnect", () => stop()),壳一死它就自己走完整的 dispose 流程。所以「硬杀」只硬在壳这一下,Host 侧仍然是优雅收尾。

关键点 2:helper 必须活着跨过「壳死掉」那一刻——普通子进程做不到。 这一点最初是错的,实际表现就是「只有关、没有开」:helper 写下 boot、写下 killing、taskkill 发出去了,然后它自己跟着壳一起消失,重拉那一步根本没执行。应用关掉,再也没回来。

原因:这个应用的子进程树不活过壳(壳被杀 → 整棵树一起收走)。所以 helper 不能是 Host 的子进程,也不能是壳的孙进程——它必须由进程树之外的进程创建。Win32_Process.Create 由 WmiPrvSE.exe 执行,helper 的父进程是 WMI 服务,壳死的时候它毫发无损。helper.log 的 boot 行会记下 parent=…(WmiPrvSE),这是这条链路生效的直接证据。

关键点 3:不用 detached: true,也不靠它。 在 Windows 上,Node 用 detached 启动 powershell.exe 会让它立刻以 0 退出——一行都不执行(实测)。而且 detached 本来也不提供进程树隔离(那需要 CREATE_BREAKAWAY_FROM_JOB,而这里的 job 不允许)。真正让 helper 活下来的是「由谁创建」,不是「怎么 detached」。

关键点 4:WMI 启动失败就放弃重启,不赌。 短命的 launcher 把结果写进 launch-result.json;Host 等不到 helper 的 boot、或 WMI 明确拒绝,就直接回 500 并写 cancel-restart 标记——还在延迟中的 helper 看到标记会立刻放弃,不会在界面已经说「没重启」之后再把应用关掉。

关键点 5:创建的进程要「生来就是隐藏的」,不然屏幕上会多一个 PowerShell 窗口。 WMI 的 Win32_Process.Create 不带启动信息,控制台程序(helper 是 powershell.exe)便会在创建的那一刻拿到一个可见的控制台窗口——实测它会一直留在屏幕上,直到 helper 自己退出。-WindowStyle Hidden 救不了:那个开关是在窗口已经存在之后才去隐藏,实测无效。所以 launcher 构造 Win32_ProcessStartup(ShowWindow = 0,即 SW_HIDE)一起交给 Create,进程一开始就不可见。


为什么不用 ctx.appExit(生态里现成的「安全重启」)

ctx.appExit(0) 在桌面版等于让 Host 自杀。Host 一退,壳的 child.once('close') 就判定为异常终止,弹出「应用意外停止」恢复框,要用户再点一次「重启」。

那条路能用,但:多一次点击、日志里留一次假崩溃、语义上是「应用崩了」。这个插件选的是「杀壳 + 重新拉起」,所以能一次点完。appExit 那条路留作兜底(见下面的自救)。


配置

写在 profile 的加载行 config: 里(也可以直接改 cordis.patch.yml):

键默认含义
enabledtrue重启总开关;false 时重启会报「不可用」并拒绝一切重启请求(刷新不受影响)
delayMs1200动手前的等待,用来保证 HTTP 回执先到浏览器
hostGraceMs8000等 Host 自己优雅退出的上限,超时才强杀
killTimeoutMs15000等旧进程彻底消失的硬上限
pendingTtlMinutes30「重启后回哪个会话」标记的有效期

刷新没有配置项:它不碰 Host,也就没有可配置的危险动作。


出问题了怎么自救

按可能性排序:

  1. 点了没反应、应用还在。 helper 没起来(WMI 被禁用/拦、脚本被删)。此时应用完全没受影响——这是刻意的:Host 等到 helper 的 boot 状态才回 200,等不到就直接返回 500 并放弃重启。按钮会在 12 秒后变成红色提示。先看 ~/.dsh/dsh-restart/launch-result.json(WMI 那一步的返回码和错误),再看 helper.log。
  2. 应用退出了但没回来。 说明 helper 在杀之前就死了(旧版本的症状),或者 Start-Process 被拦。手动双击 D:\dsh\DeepSeek Harness.exe 即可;restart-state.json 里会写着停在哪一步,helper.log 的 boot 行会写出 helper 的父进程是不是 WmiPrvSE。
  3. 点「刷新」后回到了首页。 说明会话标记没能写下(无痕模式、存储被策略关闭)或会话已被删。刷新链路失败一律静默退回首页,不会卡住。
  4. 想彻底绕过这一切。 托盘右键退出,再双击图标——和以前一样。

状态文件都在 ~/.dsh/dsh-restart/:

文件内容
restart-state.json最近一次重启的阶段(boot → killing → killed → relaunching → relaunched / failed / aborted)
helper.log逐行带时间戳的过程日志,boot 行带 helper 的父进程
launch-result.jsonWMI 创建 helper 那一步的结果:ok / pid / returnValue / windowHidden / error
pending-restore.json待恢复的会话标记,被下一次页面加载消费后即删除
cancel-restart只在 Host 放弃一次启动时短暂出现:还在延迟中的 helper 看到它就退出,不再动手

已知限制

  • 刷新不重载 Host 侧插件。 它只是把浏览器这一半重新装一遍:客户端插件、原生菜单、样式都会更新;Host 插件(路由、服务)必须用「重启」。
  • 刷新会丢没发出去的草稿。 输入框内容只活在页面里。已发送的对话、正在生成的回复、后台 job 都不受影响。
  • 顶栏入口依赖桌面壳的菜单条。 那条菜单是 preload 用 data-windows-menu 标记 + open shadow root 画的,不是 Web 客户端的 slot。插件按「探测不到就退让」处理:找不到宿主就静默不挂载,标题栏入口顶上;壳若在未来版本改了结构,最多是回到标题栏,不会报错也不会留半个按钮。
  • 同一时刻只有一个入口在屏幕上。 顶栏挂载成功时标题栏那对会渲染成空(但仍然挂着,因为它是唯一拿得到 sessionId 的地方,用来做刷新/重启后的会话回填)。
  • 重启只支持桌面版。 CLI / headless 的 Host 没有壳可重启;检测到这种情况时按钮会明确报「不支持」。刷新在任何环境都可用。
  • 重启会中断进行中的工作。 确认框里的数字就是会断掉的东西,确认过就是真断。会话记录是追加写,丢的是进行中那一轮和 job 输出。
  • 重启后端口会变。 旧标签页一律失联,壳会开新页面;远程访问的地址也会变。刷新不会。
  • 重启依赖 WMI 服务。 helper 必须由 WmiPrvSE.exe 创建,否则它活不过壳被杀的那一刻。WMI 被策略关掉、或 Win32_Process.Create 被安全软件拦住时,重启会明确拒绝(应用不受影响),错误和返回码写在 launch-result.json。这是有意的取舍:宁愿不重启,也不要关掉一个回不来的应用。
  • 杀软可能好奇。 「隐藏窗口的 PowerShell 杀掉应用再拉起」是 EDR 的经典启发式,加上 WMI 创建进程就更容易被盯。所以 helper 是明文的、带完整注释的、固定路径、日志可查,没有做任何编码混淆;launch-result.json 连 WMI 的原始返回码都留着。
  • 极端情况下仍可能看到一个控制台窗口。 如果某台机器上 Win32_ProcessStartup 构造失败,helper 就会退回「可见窗口」启动(重启本身照常工作):launch-result.json 里 windowHidden 会是 false,Host 日志也会留一行 warn。属于「看得见、可查」,不是静默发生。

开发与测试

没有任何构建步骤:lib/index.js(Host 半区)、lib/client.js(浏览器半区)、lib/helper.ps1(重启动作)都是可以直接读的源码。

node scripts/smoke-host.mjs      # Host 半区 37 项:路由、状态、活动工作计数、标记消费、命令行/WMI 脚本拼装(含隐藏窗口)
node scripts/smoke-client.mjs    # 浏览器半区 27 项:装载、slot 注册、两条入口、会话续接、刷新标记
node scripts/selftest-helper.mjs # helper 真机自检 23 项:直启与 WMI 启动各跑一遍杀进程/等退出/重新拉起、取消标记、PID 复用防护,并扫屏确认没有控制台窗口
node scripts/package-check.mjs   # 15 项:打包、解开、验包,再用包里那份 lib 跑一遍上面两个 smoke
node scripts/install-check.mjs   # 13 项:把 tgz 真的装进一个新建的 profile,再让 launcher 组合它(需要 PATH 上有 dsh 和 pnpm;CI 里也跑这一步)

前三个看的是源码树,后两个看的是发出去的那个 tgz。这两者会分叉:files 漏写一个文件、bundle patch 没进包、exports 指错路径——源码树全都正常,用户装上去什么都没有。package-check 把包解开、用它自带的 lib/ 再跑一遍 smoke;install-check 走得更远一步,真的让 launcher 把这个包装进 profile。两条都是「别相信源码树正常就等于包正常」。

selftest-helper.mjs 用一次性的 powershell.exe 进程冒充「壳」和「Host」,不会碰真正的 DSH,因为它自己正跑在里面。第 2 组用的是和 Host 完全相同的 WMI 启动路径,并断言 helper 的父进程是 WmiPrvSE.exe——也就是说,「helper 不随壳一起死」这一点是跑出来验证的,不是推断的。同一组还会在 helper 运行期间真的枚举屏幕上的可见窗口(scripts/find-visible-window.ps1),确认没有那个标题为 dsh-restart helper 的控制台窗口;扫描器本身先对着一个已知可见的窗口验证过「看得见才算数」,免得负断言变成一个瞎子的断言。

手工打开的浏览器 harness(file:// 打开即可):

文件断言覆盖
scripts/caption-harness.html23挂载时机、按钮顺序、字号/高度继承、两击重启契约、sessionId 进入 POST body
scripts/refresh-harness.html10真重载:点刷新 → 页面真的重载 → 标记存活 → 回到同一会话 → 标记被消费

refresh-harness.html 分两阶段跑(结果和「已点击」状态放 sessionStorage,跨重载存活),因为要测的行为本身就是「页面会消失」。

浏览器半区的 slot 位置来自 DSH 自带的 slot 目录(dsh-cordis-client-runner 里的 conversation.session.header.utilities:右对齐、可叠加、注入 sessionId)。顶栏那条菜单条则来自 app.asar 的 lib/preload-app.cjs(installWindowsMenu)。


发布(维护者)

打 tag 就算发布,没有手工上传的那一步:

# 1. 改 package.json 的 version(tag 必须与之一致,否则 release 流程第一步就红)
# 2. 提交
git tag v0.3.7
git push origin main --tags

release.yml 随后在 windows-latest 上按这个顺序做六件事:

  1. 比对 tag 与 package.json 的 version(不一致就红,不打包);
  2. 跑 npm test;
  3. 用同一个 checkout 打 npm pack,附上 .sha256;
  4. 连着验两次这个 tgz。package-check 把它解开:files 承诺的、main / exports 指向的、bundle patch 和 lib/helper.ps1 都必须在包里,最后把包里那份 lib/index.js 和 lib/client.js 拉起来跑各自的 smoke(37 项 / 27 项)。install-check 再往前一步:把它真的装进一个刚建的 profile,让真 launcher 组合一遍,再读回它组合出来的东西(13 项);
  5. 删掉这个 tag 上可能已有的 release,重新建一个公开的,正文由 scripts/release-notes.mjs 从上一个 tag 之后的提交生成。
  6. 把同一个 tgz(不是重新打包)npm publish 到 npm。默认走 trusted publishing:这一步拿 runner 的 OIDC 身份换一个只够发一次的凭据,--provenance 顺带给 tarball 签一份来源证明,仓库里没有任何 npm 密钥。发完等 registry 真的能看到这一版(问的是 version 文档、带 nonce,绕过所有缓存;最多两分钟),再把 npm 提供的那份下载回来、和本地 .sha256 比一遍——字节一致才算过。只有「既没配 trusted publisher、也没有 NPM_TOKEN」(比如本地打 tag)才打印一行跳过;该发而没发出去是红的。

trusted publishing 已经配好了,不用再去网页上点设置。当前关系随时可复核:

npm trust list @ray1270/dsh-restart
# type: github / id: 7414ac6a-742e-44c2-aa23-866885d25bed
# file: release.yml / repository: Ray1270/dsh-restart
# permissions: publish, stage publish

它是用 CLI 建的,代价只有一次 OTP(2FA),不需要开浏览器:

npm trust github @ray1270/dsh-restart --file release.yml \
  --repo Ray1270/dsh-restart --allow-publish -y

要复刻到别的包上,前提和约束是:

  • npm 上 @ray1270 得先是你的(注册同名账号即自动持有这个 scope);
  • --file 填 workflow 的文件名(release.yml),不是路径;environment 留空就是不绑定 environment;发布这一步必须和它写在同一个文件里(复用 workflow 会改变 npm 看到的那个文件名);
  • 仓库里不用加任何 secret:release.yml 已经有 permissions: id-token: write,发布步骤靠 runner 的 OIDC 身份换凭据。npm CLI 得 ≥ 11.5.2(node 24 自带的够用),发布步骤会先查版本,太老就带着明确信息红掉,而不是丢一句「认证失败」。

NPM_TOKEN secret 仍然被支持(有就优先用它):Classic 的 Automation,或者 Granular 里勾 Read and write、只给这一个包。但 npm 已公布 bypass-2FA token 的直接发布会在 2027 年 1 月收回(changelog),所以默认路径是 OIDC。

之后打的 tag 会自动多一份 npm 镜像;已经发过的版本不会被重复发布(重跑同一个 tag 会走「已经有了」那条路,并且仍然比一遍字节,不会盲跳),所以 workflow_dispatch 那条恢复路径仍然安全。

判断「npm 上有没有这一版」不能读 packument:它走 CDN,发布会滞后一两分钟。v0.3.7 就是这么红过一次的——05:12:59 发布成功,05:13:18 的检查判红,而 registry 上 05:14:35 才出现。所以现在读的是 /@ray1270%2fdsh-restart/<version>?t=<nonce> 这个 version 文档,并且最多等两分钟。

第 4 步里的 install-check 需要一个真的 launcher,而在一台 windows runner 上 npm install --global pnpm @deepseek-ai/dsh 量到 13 分 16 秒(654 个包)。第一次把它加进流程时,发布就是卡在这里,然后被我拿掉了。

后来发现同一台镜像上换成 npm i -g --prefix "$RUNNER_TEMP/dsh-launcher",同样 654 个包 64 秒(又一次 78 秒)——区别只是写到 C: 盘的默认前缀,还是写到临时盘。所以现在用的是后者,.github/actions/launcher/action.yml 里就这一段加一行 PATH;CI 里也给它单开了一个 install job,整轮约 1 分半。

中间还试过按 launcher 版本缓存那个前缀:197 MB,恢复 40 秒,对比 64 秒的安装——省下半分钟,代价是一个会失效的缓存、一段「缓存里的东西还跑得起来吗」的探测,以及两处要同步的版本号。删了。

顺序是刻意的:先验版本、再跑测试、再验包、再验装、最后才发。任何一步红了都什么都发不出去,所以 Release 里那个 tgz 一定是「跑绿了、包里那份代码真的能起来、而且真能装进 profile」的那份。

两条容易踩的坑,都是这台流水线自己踩出来的:

  • 发布说明不能用 --generate-notes。 那个功能只列「合并的 PR」,而这个仓库是直接往 main 推的,v0.3.3 第一版发出来时正文就只有一行 Full Changelog。
  • 不能用 gh release edit 就地改。 tag 被移到新提交之后,旧 release 对外就查不到了(GET /releases/tags/vX 返回 404),但带 token 的 gh release view 还找得到它——于是编辑「成功」了三次,公开 API 里始终只有一个更老的版本。所以现在是删掉重建。

这两条的共同点,也是第 3 步存在的理由:流水线的绿灯说的是「命令返回 0」,不是「东西对了」。 所以每一步都回头看结果——读回 API 看 release 是不是公开的、正文够不够长;解开包看里面是不是真的齐。

发布完还会读回 API 校验:isDraft 必须是 false、正文长度必须够;不对就用 ::error:: 让这一步红掉。想重发一个已存在的 tag(比如某步一时抽风),用 workflow_dispatch 再跑一次即可——它会重建 release,说明也一起重算。


卸载

dsh plugin --profile desktop remove @ray1270/dsh-restart

或者从 profile 的 dsh.profile.bundles 里删掉那一行,再重启。

残留物只有 ~/.dsh/dsh-restart/ 这个目录,删掉即可。

Comments

Loading…

Similar plugins

dsh-restart

by mwk719

给 DeepSeek Harness Desktop(Windows)的标题栏加一个「重启」按钮。点一下弹出确认框,确认后应用关闭并重新打开。

Development & InfrastructureManifest valid

★ 0

MIT

JavaScript

Nov 2, 2026

dsh plugin --profile web add dsh-restart

by PPsz-qqq

DeepSeek Harness plugin: one-click restart button in the top-right of the session header (Windows desktop + dsh web). 一键重启 DeepSeek Harness 客户端。

UI & ExperienceDevelopment & InfrastructureManifest valid

★ 5

↓ 104/wk

MIT

JavaScript

Oct 3, 2026

dsh plugin --profile web add dsh-restart-button

by momiji42

Restart button in the DSH desktop title bar + Start with Windows switch, in one plugin.

Manifest valid

★ 0

JavaScript

Nov 3, 2026

dsh plugin --profile web add dsh-desktop-tools

by 13520rygvv

DSH desktop plugin: a one-click restart button in Settings, plus a customizable shortcut.

Tools & CapabilitiesUI & ExperienceManifest valid

★ 1

JavaScript

Oct 27, 2026

dsh plugin --profile web add @local/dsh-restart-button

by StabCut

DSH Desktop sidebar restart button: orderly relaunch of DSH Desktop from the sidebar settings row (based on desktopActions.requestRestart).

Manifest valid

★ 0

MIT

JavaScript

Aug 21, 2026

dsh plugin --profile web add dsh-plugin-restart-desktop

by IHS470

DeepSeek Harness restart button in the DSH desktop window title bar

Development & InfrastructureManifest valid

★ 3

↓ 167/wk

MIT

JavaScript

Oct 3, 2026

dsh plugin --profile web add dsh-plugin-restart