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
羽毛球分组比赛记分
小程序二维码

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

小夜