dsh-restart:DSH 桌面端的重启、Cookie 清理与窗口尺寸外挂

前言

DSH 的桌面端是 Electron 壳,插件装完、停掉,或者想调窗口和右侧栏尺寸时,常规做法都是手动退出进程再打开。官方没有内置「重启」入口;桌面端 Cookie 里的 dsh-auth-* 条目会不断新增、永不覆盖,累积到一定程度请求头超限,服务端回 431,客户端只显示一句「认证失败」,看不出原因。窗口默认偏小、右侧栏默认偏宽,也没有设置项暴露出来。

dsh-restart 就是针对这三件事的外挂插件,由 Abin1020 维护,分类属客户端。它不做 asar 补丁,不改官方安装包,nightly 更新不会把它冲掉。

这是什么

一句话定位:一个挂在 DSH profile 上的客户端插件,提供页面内重启按钮、Cookie 清理、窗口与右侧栏尺寸自定义三项能力。

许可证 MIT,package.json 里版本号 0.1.1,engines.node 要求 >=18。入口分两段:lib/index.js 是 Host 侧 main,client.js 是客户端 bundle 入口,通过 cordis.patch.yml 注入。

它解决的核心问题是:桌面端没有安全的「重启并顺手清理」路径,也没有不改 app.asar 的窗口尺寸入口。

核心功能

原因是 dsh-client-connection 生成 cookie 名时把 Host 的随机端口也算进了哈希,而 cookie 的作用域只认 host、不认端口:

// node_modules/@deepseek-ai/dsh-client-connection/lib/index.js
const COOKIE_PREFIX = "dsh-auth-";
function cookieName(authority) {
  return COOKIE_PREFIX + encodeBase64Url(createHash("sha256").update(authority).digest());
}

于是每次启动拿到新端口 → 新 cookie 名 → 新增一条、永不覆盖。README 给出的实测值是累积到 71 条时请求头约 16.1 KB,超过 Node 默认的 16384 字节上限。上游已有 #10802、#10828、#11095 三份报告。

运行期没法清理:Electron 活着的时候 Network/Cookies 被 Chromium 独占锁定。README 记了四条尝试路径的实测结果:

方式 结果
fs.openSync(p, 'r') EBUSY
fs.openSync(p, 'r+') EPERM
new DatabaseSync(p, { readOnly: true }) unable to open database file(errcode 14)
file:...?immutable=1 同样失败

所以清理挂在应用退出之后,由 detached helper 执行,SQL 只有一句:

DELETE FROM cookies WHERE name LIKE 'dsh-auth-%'

几条约束值得记住:

1、删之前先备份到 Network/dsh-tools-backup/Cookies.<时间戳>.bak,滚动保留最近 3 份。
2、备份失败就不删,宁可不清也不裸删。
3、删完 VACUUM 并重读校验,实际删除条数写进 last-run.json。
4、只匹配 dsh-auth- 前缀,其它站点的 cookie 一条不动。

功能二:页面重启按钮

标题栏右上角、最小化按钮左边有个循环箭头,点一下触发重启;设置页的「桌面维护」里三项功能都在一起。

重启链路的状态式如下:

页面按钮 → POST /dsh-restart/restart → Host 返回 202
                                            → detached helper 启动
                                            → 等 800ms(让响应先回页面)
                                            → taskkill /PID <Electron> /F
                                            → 清 cookie
                                            → 等 1200ms
                                            → 重新拉起 DeepSeek Harness.exe

几个关键取舍:

1、不在 Host 里直接 process.exit()。Host 自己退出会被主进程判成崩溃,弹原生崩溃对话框(DesktopHostProcess.fail → reportFatal)。
2、只杀 Electron 主进程、不加 /T。helper 是 Host 的子进程,Host 又是 Electron 的子进程,/T 会把 helper 一起杀掉。只杀主进程是安全的:它退出后 Host 的 IPC 断开,desktop-host 里的 process.once("disconnect", stop) 自行收敛。
3、动手前先验身份。tasklist /FI "PID eq <pid>" 查到镜像名不等于 deepseek harness.exe 就中止,防止 PID 被复用时误杀无关进程。

第一次安装后没有这个按钮可用,因为 button 本身这时才被加载进来,所以首次生效必须手动退出重开。

功能三:窗口尺寸与右侧栏宽度

两项都不需要改 app.asar。

右侧栏走真实布局:

ctx.slots.entries("root") 找到 layout 注册的那条 entry,
用 entry.store.create().actions.setRightbar(px) 直接改布局

上限是 min(视口 × 0.7, 视口 − 280 − 400),下限 300,超了被 clampWidth 截断。为什么不走 ctx.layout:LayoutController 只暴露 openRightbar / closeRightbar / toggleSidebar,没有 setRightbar,宽度被写进内联 gridTemplateColumns,CSS 也盖不掉。为什么不 slots.inject("root"):root 是 single slot,catalog 明确写了二次注册会 shadow 整个 AppFrame,只能 entries("root") 读取已有条目。左侧栏不动,保持默认 280。

窗口侧真正的活由 lib/winagent.exe 干。渲染层的 window.resizeTo / moveTo 只当兜底。原因是 client bundle 在导航开始后约 10.4 秒才执行,那时窗口早就 show() 了;而 Electron 里 document.visibilityState 对隐藏窗口同样报 visible,拿它判断「还没显示」是无效的。Host 的 apply() 一定早于 navigateMain() 完成,所以从 Host 侧拉起独立进程操作窗口,有约十秒余量。

main.js:12889  createWindow(appPreload, false, true)      ← show = false
main.js:12937  enterWorkspace(): await navigateMain(url) 之后才 window.show()
main.js:13128  mainWindow = createMainWindow()
                  └── 里面 new DesktopHostProcess(...)   ← Host 先起来,页面后加载

winagent.exe 的实现要点(源码在同目录 winagent-src.cs):

1、按 --pid 找窗口:EnumWindows + GetWindowThreadProcessId,只认类名前缀 Chrome_WidgetWin_1,多个候选取面积最大的(同进程还有个隐藏的 Chrome_WidgetWin_0)。
2、每 40 ms 轮询一次,最多 30 秒;父进程先消失就写 parent-gone 直接退出,不留孤儿。
3、尺寸换算不写死边框:先 DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS) 取可见边界,用 GetWindowRect.Width - visible.Width 算出该窗口此刻的不可见边框,再加上它。所以 --width 1690 得到的可见宽度就是 1690。
4、最多两轮 SetWindowPos 纠偏,|实际 − 目标| ≤ 2 算到位。

它是 detached + unref 的一次性进程,摆好就退出,不会常驻。README 记了两次实测:真实重启记录里 waitedMs 为 3–5 ms;Host 侧 spawn 本身 20 次采样为 min 12.2 ms、p50 17.4 ms、max 27.9 ms、avg 17.8 ms。对比主窗口从创建到 show() 要等约 10.4 秒,开销占比约 0.17%。

界面上的「启动恢复」那一行优先显示 agent 记录:visibleAtApply === false 表示在窗口显示前完成、没有可见抖动;true 表示赶到时窗口已经显示了。没有代理记录时回落到渲染层的 boot.log 诊断,两者都没有就提示「还没有启动记录」。

安装与启用

推荐走插件页。打开 设置 → 插件,点 添加插件,在「包名或地址」里填入:

https://github.com/Abin1020/dsh-restart

点 安装,装完后完全退出 DSH 再打开。

装的时候 DSH 会自动把本插件登记进 profile 的 dependencies 与 dsh.profile.bundles,不需要手改任何文件。装完重启生效,但第一次重启只能手动退出重开——重启按钮这时才被加载进来。

离线包或要改代码时用手动安装:

# 1. 放进插件目录
git clone https://github.com/Abin1020/dsh-restart.git ~/.dsh/plugins/dsh-restart

# 2. 在 profile 的 package.json 里登记
#    ~/.dsh/profiles/desktop/package.json
#    - dependencies 加 "dsh-restart": "file:../../plugins/dsh-restart"
#    - dsh.profile.bundles 末尾加 "dsh-restart"

# 3. 建链接(profile 用 pnpm 的 nodeLinker: hoisted,它不会替你建这个链接)
#    Windows PowerShell:
#    New-Item -ItemType Junction `
#      -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-restart" `
#      -Target "$env:USERPROFILE\.dsh\plugins\dsh-restart"

同样要重启一次 DSH。

确认装上:浏览器或 PowerShell 打开 http://127.0.0.1:<当前端口>/dsh-restart/status,返回 200 即加载成功。端口在启动日志里,或看地址栏。

卸载走插件页 卸载,DSH 会自己清掉 dependencies 和 dsh.profile.bundles 里的登记,之后要再重启一次才真正卸载干净。手动装的版本需自己清理:

# 1. 从 profile 移除(把 ~ 换成你的用户目录)
#    ~/.dsh/profiles/desktop/package.json
#      删 dependencies 里的 "dsh-restart"
#      删 dsh.profile.bundles 里的 "dsh-restart"

# 2. 删链接
rm ~/.dsh/profiles/desktop/node_modules/dsh-restart

# 3. 删插件与数据(可选)
rm -rf ~/.dsh/plugins/dsh-restart ~/.dsh/dsh-restart

备份的 cookie 库在 %APPDATA%\@deepseek-ai\dsh-desktop\Network\dsh-tools-backup\,需要时手动删。

典型用法

调好窗口和右侧栏,保存布局。布局写到 $DSH_HOME/dsh-restart/layout.json:

{ "window": { "width": 1600, "height": 1000, "x": 40, "y": 30 }, "rightbar": { "width": 520 } }

任一项为 null 或缺失,表示不覆盖、沿用应用默认。

配置项在 profile 的 cordis.patch.yml 里:

- insert:
    - id: dsh-restart
      name: 'dsh-restart'
      config:
        cleanOnEveryRestart: true
        keepBackups: 3
        restartLaunchDelayMs: 1200
        killDelayMs: 800
        dryRun: false

三项功能的工作方式可以归纳成一句:重启是触发点,清理是副作用。

1、每次点重启,默认清一次 cookie,保证库里永远只有 1 条。
2、不做定时器。运行期 cookie 库被 Chromium 独占,清不了;而「每次重启必清」已经保证不会有第二条累积。
3、清理一定发生在「进程已退出」和「新进程启动」之间,这是唯一能安全打开 SQLite 的窗口。
4、布局偏好也走同一条链:调好 → 保存为默认 → 点重启 → 新进程起来时 client 读 layout.json,agent 在 show() 前套用。

适用场景与注意

适合这几类情况:

1、遇到过「认证失败」但一直找不到 431 这条线索。
2、需要频繁装、停插件或改配置,手动退出重开很频繁。
3、觉得默认窗口偏小、右侧栏偏宽,又不想改 app.asar。

几条注意:

1、插件以当前 DSH 进程权限运行,能读写你的 user-data 目录。安装前建议先看一遍源码和 MIT 许可证。
2、它依赖 lib/winagent.exe,这是构建产物;scripts/build-winagent.mjs 是构建脚本。核对版本时以源码与 winagent-src.cs 为准。
3、重启会杀掉 Electron 主进程,请先保存对话框里未保存的内容。
4、本插件属于社区目录,与 DeepSeek / 幻方无官方从属关系;DSH 生态主张「一切皆插件」,这里的目录是独立站点,不是官方应用商店。

结尾

它的价值不在单看某一个功能,而在把「重启」变成一条可复用的链路:重启天然带着一次清理和新布局的套用,且全程不碰 app.asar,nightly 更新不会冲掉。

目录页与源码:

  • https://www.skillhub.cn/plugins/Abin1020/dsh-restart
  • https://github.com/Abin1020/dsh-restart
羽毛球分组比赛记分
小程序二维码

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

小夜