dsh-session-delete:給 DSH Web 界面補上會話刪除

前言

DSH 的會話日誌就是磁盤上的一個目錄,但 DSH 本身沒有刪除的能力:SessionPersistence 只有 create/open/flush/stat/list,WorkspaceRegistry 只有 archive/unarchive。歸檔只是軟刪除,日誌目錄、註冊表條目和子會話(subagent)的日誌都留在磁盤上。長期跑下來,會話越積越多,卻沒有從界面真正清掉的入口。

dsh-session-delete 就是解決這個問題的第三方插件,維護者 Neruviko,屬於客戶端類插件,目前在社區目錄 SkillHub 上星標爲 0。它給側邊欄會話行的「…」菜單加了「刪除會話」,帶確認對話框和備份引導。下面介紹它做了什麼、怎麼裝、刪除過程的安全設計,以及使用上的注意事項。

這是什麼

一句話:在 DSH Web 界面裏,把一個會話連同磁盤日誌和它的子會話子樹真正刪掉,並清理工作區註冊表。

它的目標運行環境是 DSH 0.2.0-rc.2 的 Web GUI(作者實測可用),Node 版本要求 ^22.19.0 || >=24.0.0,與 DSH 本體一致。插件包版本 0.2.2,MIT 許可,兩個半身(Host 與 Browser)都是手寫的,沒有構建步驟。

核心功能

功能清單來自插件 README,已在 GitHub 與 SkillHub 目錄頁覈對:

  1. 側邊欄任意會話行的「…」菜單裏新增「刪除會話」,位置在「歸檔會話」之後。
  2. 可以刪除任意會話,包括正在運行或正打開着的會話;刪除前會先派發停止事件,停掉它的運行中工作(輪次、子代理、任務與提醒都會被取消)。
  3. 刪除父會話時連帶刪除整棵子會話子樹。子會話在側邊欄不可見,無法單獨刪除,只刪父會話會留下再也訪問不到的孤兒日誌。
  4. 確認對話框顯示會話標題、創建時間、日誌大小、子會話數量;有子會話時明示「將同時刪除 N 個子會話及其後代」。
  5. 對話框內嵌「下載此會話的 ZIP 歸檔」鏈接,引導先備份。
  6. 必須勾選確認框才能點刪除。
  7. 刪除後側邊欄行立即消失,不需要刷新頁面。

值得單獨說明的是它不加隱藏掃描:會話目錄不是枚舉出來的,而是由 JSONL 後端的目錄佈局確定性算出來的:

<DSH_HOME>/sessions/<projectKey(cwd)>/<encodeSegment(sessionId)>/session.v4.jsonl.zstd

index.js 裏逐字符復刻了兩個路徑變換函數,encodeSegment(把任意字符串編碼成單個安全路徑段)和 projectKey(把 cwd 變成項目目錄名,分隔符摺疊成 -,兩端加 --,例如 /home/me/projects/demo → --home-me-projects-demo--)。子樹則來自 DSH 自己的 traceSession(),或 sessionPersistence.list() 裏 header 的 parentSession 鏈接,同樣不碰磁盤目錄枚舉。

安裝與啓用

下面是 README 給出的安裝命令,是手寫 git+https 形式,不是 dsh plugin add github:owner/repo。裝到 web profile(其他 profile 同理,把 web 換掉即可):

dsh plugin --profile web add git+https://github.com/TonyZiYuan/dsh-session-delete.git

dsh plugin add 轉發給 pnpm,裝完會自動把新裝的 bundle 追加到 profile 的 dsh.profile.bundles(依據包裏 dsh.bundle 字段),不需要手工改 package.json。

如果安裝報錯說某個依賴的構建腳本被攔截,pnpm 默認不運行已安裝依賴的 prepare 腳本,而 git 依賴需要先構建。此時按提示把包加進 profile 的 pnpm-workspace.yaml(在 ~/.dsh/profiles/web/ 下):

packages:
  - .

allowBuilds:
  dsh-session-delete: true

然後重跑上面的安裝命令。pnpm ≥ 11 把 onlyBuiltDependencies 等設置合併成了這個 allowBuilds 映射,詳見 pnpm v10 → v11 遷移說明。

更新和卸載:

dsh plugin --profile web update dsh-session-delete
dsh plugin --profile web remove dsh-session-delete

也可以從本地目錄安裝,路徑必須是絕對路徑:

dsh plugin --profile web add link:/absolute/path/to/dsh-session-delete

link: 的相對路徑是相對 profile 的 manifest 目錄(即 ~/.dsh/profiles/web/)解析的,寫成 ./dsh-session-delete 會指向 ~/.dsh/profiles/web/dsh-session-delete,裝不上。

裝完之後必須重啓 dsh web,bundle 與瀏覽器半身只在啓動時加載(patchReload: live 只能熱更新 patch,加載不了新的瀏覽器代碼)。用這條確認掛載成功:

dsh --profile web --dump-config   # 應能看到 `dsh-session-delete` 條目

典型用法

插件不帶命令行入口,裝好後操作全在 Web 界面裏。一個會話的刪除流程,按 README 描述的順序:

  1. 停止運行中工作。派發 workspace/session-stop(與 DSH 歸檔時 stopActivity: true 是同一事件),各 provider 取消該會話的輪次、子代理、任務與提醒,隨後短暫等待讓取消落地。provider 拋錯只記日誌,不阻斷刪除。
  2. flush 屏障。調用 sessionPersistence.flush(),確保沒有緩衝事件。失敗不阻斷,後續的白名單校驗仍然守住字節。
  3. 定位目錄。從 header 自帶的 cwd 算出目錄,不掃描 sessions 根目錄。
  4. 枚舉後代子樹。用 Session Query 的 traceSession() 拿到任意深度的子會話樹;沒有 query 服務時回退爲 sessionPersistence.list() 裏 header 的 parentSession 鏈接。默認連帶刪除整棵子樹。
  5. 整樹預檢。對父會話與每個後代逐個定位目錄、檢查是否逃出 sessions root、校驗內容白名單。全部通過才繼續;任一失敗則整體中止,一個都不刪。
  6. 遞歸刪除。逐個 fs.rm(dir, { recursive: true, force: true })。操作系統句柄鎖擋住時(Windows EPERM/EBUSY,POSIX EACCES/ETXTBSY)返回 session/in-use,提示關掉該會話後重試。
  7. 清理註冊表。對每個已刪 id 執行 workspaceRegistry 的 unarchiveSession / unpinSession / detachSession,同時更新內存緩存、持久化表與已連接瀏覽器的推流。沒有 registry 組成時回退爲原子改寫 workspace.json(臨時文件 + rename,留一份 .bak)。此步失敗不阻斷刪除,僅在響應裏標記 registryCleaned: false。
  8. 發佈移除公告。對每個已刪 id 發出 api-session/removed,DSH 既有的轉發鏈路會把該行從所有已連接瀏覽器的側邊欄移除。不發這一步,行會一直留到下次刷新。
  9. 記入墓碑。被刪 id(含整個子樹)記入本 Host 代的內存墓碑集,供瀏覽器半身的「幽靈收割器」使用。

HTTP 層面有兩個路由,寫請求額外要求來源是迴環地址:

方法 路徑 作用
GET /api/session-delete/sessions 可刪會話清單 + tombstones(本 Host 代已刪 id,供收割器用)
POST /api/session-delete/delete 刪除,body {sessionId, cascade?},cascade 默認 true

刪除響應裏有 descendants 摘要,便於確認級聯結果:

{
  "ok": true,
  "deleted": true,
  "absent": false,
  "freedBytes": 8192,
  "registryCleaned": true,
  "descendants": { "requested": 3, "deleted": 3, "ids": ["child-1a", "child-1b", "grand-1a"] }
}

兩條路由都套 ctx.connection.requestRejection() 信任柵欄(Host/Origin 僞造與未認證請求會被拒絕)。這是 README 裏明確寫的內容,可以直接覈對。

有一個坑:刷新後幽靈會復活

這是 README 裏一段比較關鍵的說明。聊過天的會話,其 Agent/Session 對象會常駐 Host 內存(空閒會話要能被繼續對話,是 DSH 的既定設計)。刪除銷燬了磁盤日誌,但沒有公開 API 能銷燬別的 fiber 持有的 live 會話對象;客戶端列表基線合併 live 會話又是無條件的(dsh-session-query 的 listSessions)。於是刷新瀏覽器後,基線從內存重建,已刪的行會復活:出現在「未分組」下(註冊表成員已清理),點開仍有完整文本(歷史讀取 live 優先,不碰磁盤)。

插件用兩層配合來對沖:

  • Host 側:每次成功刪除把 id 記入內存墓碑集,GET /sessions 響應帶 tombstones 數組。墓碑與幽靈同生共死,Host 重啓後兩者都不存在。
  • Browser 側:訂閱客戶端會話列表快照,任何 id 若「在墓碑裏卻出現在列表中」,自動再發一次刪除,走 absent 冪等路徑,觸發註冊表清理與重公告,行隨即掉落。只收割墓碑內的 id,新開的 blank 會話不受影響。

效果是刷新後幽靈即使閃現,也會在亞秒級被收掉。README 也明說,毫秒級的閃現窗口無法在插件層根除,需要上游在基線合併處支持「已刪除會話排除」。手動兜底方式是對幽靈行再點一次「刪除會話」,對話框會顯示「日誌已不在磁盤」狀態,確認即可清除。

安全設計

README 列出的安全設計直接轉述如下:

  • 目錄必須在 sessions root 之內,且 basename 必須等於 encodeSegment(sessionId),父會話與每個後代都各自校驗。
  • sessionId 必須匹配 /^[A-Za-z0-9._-]{1,128}$/。
  • 內容白名單:用戶放在會話目錄裏的文件不屬於本插件該銷燬的對象,寧可拒絕。
  • 級聯也是全有或全無:整棵子樹先全部預檢,任一成員不通過則一個都不刪。
  • 先停後刪:刪除前派發 workspace/session-stop,避免在活躍寫入者下面撕裂日誌;真正被句柄擋住時操作系統自己會拒絕,映射爲 session/in-use。
  • 循環血緣:枚舉後代時帶 seen 集合,畸形血緣不會無限遞歸。
  • 註冊表原子寫(僅回退路徑):臨時文件 + rename,崩潰不會截斷 workspace.json,並留 .bak。

插件明確不做的事:不刪 attachments 目錄(內容尋址的共享存儲,多個會話可能引用同一張圖),不做批量刪除、不做回收站/軟刪除,不改 @deepseek-ai/* 本體,不全盤掃描 sessions 根目錄。

適用場景與注意

適合已經在用 dsh web profile、長期積累會話日誌、需要真正清理而非歸檔的用戶。它依賴 DSH 的多個內部契約(sessionPersistence 的公開 root 字段與 list/stat/flush,encodeSegment / projectKey 目錄佈局,workspace/session-stop 與 api-session/removed 事件,workspaceRegistry 的 unarchiveSession / unpinSession / detachSession,sessionQuery 的 traceSession() 後代樹形狀,以及瀏覽器半身的 slot 名與 dsh-client-ui-primitives 的導出),這些都是在 0.2.0-rc.2 上覈對過的,未在其他上游版本驗證前不要假定兼容。升級 DSH 後如果刪除開始對存在的會話報 session/absent,或對話框組件缺失,多半是某個契約變了 —— 失敗方向是安全的(寧可拒刪不誤刪),對照新版源碼更新 index.js 頂部的路徑函數或組件用法即可。

幾個使用上的注意點:

  1. 插件以當前 dsh web 進程的權限運行,裝之前先讀 index.js 與 client.js,確認審計無異議。許可證是 MIT。
  2. 路徑變換函數是復刻的,DSH 升級改了它們就要同步更新,否則刪除會誤報 session/absent。
  3. 刪除不可逆,插件本身不給回收站,想留檔要在確認框裏先下載 ZIP。
  4. 子會話默認級聯刪除;確實只想刪自身可以傳 cascade: false(HTTP body),但默認是不留孤兒。
  5. 目錄證明是來源:SkillHub 目錄頁返回內容極少,沒有提供可覈對的功能清單或安裝命令,本文事實以 GitHub 倉庫 README 和 package.json 爲準。安裝命令來自 README 原文,指向的倉庫地址與插件名不一致(命令裏是 TonyZiYuan/dsh-session-delete),裝之前請自行確認倉庫歸屬。

結尾

dsh-session-delete 的價值不在於加一個按鈕,而在於替 DSH 補上「刪除」這一層,同時把級聯刪除、註冊表清理、不可逆操作的入口控制都做在設計裏。沒有儀式感的停刪刪淨,比多一個菜單項更值得看。

目錄頁:https://www.skillhub.cn/plugins/Neruviko/dsh-session-delete
GitHub:https://github.com/Neruviko/dsh-session-delete

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

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

小夜