前言¶
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 的窗口尺寸入口。
核心功能¶
功能一:Cookie 清理¶
原因是 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