把 DSH 會話日誌按項目分開存放:@local/session-project-logs

前言

DSH 的會話日誌默認全部堆在 ~/.dsh/sessions/ 下,按項目鍵分成 --D-Gitlab--/ 這類子目錄。項目少時還能忍受,項目多了之後,這個根目錄會越來越大,而且它跟具體項目的目錄沒有綁定關係——換機器、清理磁盤、給每個項目單獨歸檔都很不方便。想把某個目錄挪進項目裏,直接做 symlink 或 junction 又會被官方後端看不見(README 裏給了實測原因)。@local/session-project-logs 就是解決這件事的插件。

它由 yyqinfo 維護,託管在 https://github.com/yyqinfo/dsh-session-project-logs,在社區插件目錄頁的分類是「記憶」。它做的事一句話概括:讓 DSH 的新會話把日誌寫到項目自己的 .dsh/sessions/,同時歷史會話仍留在原處照常使用。

下面按「爲什麼不能直接挪目錄」「插件怎麼分流」「怎麼安裝」「怎麼遷移和校驗」「要注意什麼」展開。

爲什麼不能直接把目錄「挪成鏈接」

最直覺的做法是把 ~/.dsh/sessions/<項目鍵> 換成指向項目目錄的目錄鏈接(junction/symlink)。README 明確說這條路實測走不通:官方後端枚舉項目目錄用的是 readdir(..., { withFileTypes: true }).filter(e => e.isDirectory()),而 Windows junction / POSIX symlink 的 Dirent.isDirectory() 是 false、isSymbolicLink() 是 true,於是被鏈接的項目目錄對後端完全不可見——會話列表、恢復、stat 全部看不到。所以插件不碰文件系統鏈接來承載存儲。

插件的做法:保留原後端,再按項目掛一個

後端只有一個 root,但可以再掛一個。插件保留原有後端(繼續服務中心根目錄裏的歷史會話),爲每個項目額外掛一個同一版本、同一實現的官方後端,root = <項目>/.dsh/sessions,然後把持久化服務的入口按項目分流:

方法 行爲
create() 會話的 cwd 屬於合格項目 → 交給該項目的後端;否則交給原後端(中心根目錄)
open() / stat() 按會話 id 找到所屬後端(索引 + 兜底掃描)
list() 合併中心根目錄與所有已掛載項目根目錄的結果
flush() 扇出到所有後端

存儲格式完全沒變:每個項目後端就是官方後端,寫出來的仍是標準的 session.v4.jsonl.zstd,所以離線遷移只需要移動目錄,不需要轉碼。

插件通過 process.argv[1](運行的 @deepseek-ai/dsh 包內)解析出 @deepseek-ai/dsh-session-persistence-jsonl 與 @deepseek-ai/dsh-session-persistence,用同一個模塊實例掛載後端並複用錯誤類。啓動時會校驗 SessionPersistence 的類身份與每個掛載出來的後端實例,任一處不符就整體放棄並記日誌,存儲行爲保持原樣——不會「半開半閉」地改寫用戶日誌。

安裝與啓用

本機試用可以直接指向源碼目錄,用 link:,改代碼立即生效:

plugin_manager install_bundle(target = <本目錄絕對路徑>)

裝好後 cordis.patch.yml 會向 profile 注入一行插件(id session-project-logs)。README 給出的推薦方式是命令行一條命令,倉庫地址爲 https://github.com/yyqinfo/dsh-session-project-logs:

# 從 Release 裝(包已打好,最快)
dsh plugin --profile web add "https://github.com/yyqinfo/dsh-session-project-logs/releases/download/v0.0.2/local-session-project-logs-0.0.2.tgz"

# 或直接從倉庫裝(pnpm 會 clone 後打包)
dsh plugin --profile web add "git+https://github.com/yyqinfo/dsh-session-project-logs.git"

裝完重啓 DSH 生效。卸載:

dsh plugin --profile web remove @local/session-project-logs

幾點約束來自 README:

  • --profile 是必填項,web 就是 dsh web 用的那個檔案,dsh tui 用 tui。
  • Desktop 用戶把檔案換成 desktop,並且要先完全退出 Desktop 再執行。
  • 裝之前最好先停掉 DSH:運行中的進程會佔住它啓動時加載的包。

不想敲命令行時,也可以在任意 DSH 會話裏讓 agent 調 plugin_manager,走運行中的 Host 插件管理器,能熱加載(不需要重啓):

plugin_manager install_bundle(target = "https://github.com/yyqinfo/dsh-session-project-logs/releases/download/v0.0.2/local-session-project-logs-0.0.2.tgz")

典型用法:遷移歷史會話

插件自帶的命令行工具裝在 profile 裏。遷移歷史會話是離線操作,必須先停 DSH,原因是後端把 root 固定在進程裏,每次追加都按 root + cwd + id 重新算路徑,會話目錄被搬走後,運行中的寫句柄下次追加會指向不存在的目錄,日誌寫不進去(README 記錄了這條實測教訓)。

node bin/session-project-logs.mjs status                  # 看每個項目的中心/項目內會話數
node bin/session-project-logs.mjs migrate --dry-run --all  # 預覽全量
node bin/session-project-logs.mjs migrate --all            # 真正搬遷(先停 DSH)
node bin/session-project-logs.mjs restore --all            # 反悔:搬回中心根目錄

migrate 的活動保護:會跳過最近 --quiet(默認 10)分鐘內有寫入的會話,並打印 written N min ago, looks live — left in place。確實需要強制時用 --force,但正在運行的會話會因此寫不進日誌。目標範圍:不帶參數時是插件登記過的項目($DSH_HOME/logs/session-project-logs.state.json),--all 則從中心根目錄的會話頭反查全部項目。項目目錄已不存在、或落在用戶主目錄/臨時目錄/盤符根的項目不會搬。

校驗與修復會話日誌

DSH 0.2.2 起後端對會話日誌加了硬約束,0.2.1 之前能讀的日誌會被拒。README 列出了常見症狀與對應的觸發原因:

症狀 觸發原因
complete frame contains a torn JSONL record 某個 zstd 幀沒有落在 JSON 記錄邊界上
invalid committed event at line N: … seq gap (expected X, got Y) 序號斷檔,或序號在文件裏倒退/重複
failed to project session …: invalid persisted inbox splice at session seq N 投影不接受日誌裏的收件箱拼接

插件提供了 verify 命令:

node bin/session-project-logs.mjs verify          # 掃描登記項目 + 中心根
node bin/session-project-logs.mjs verify --all    # 再按中心根的會話頭反查項目
node bin/session-project-logs.mjs verify --json

它會檢查序號斷檔、序號在文件內不遞增、幀內殘行、收件箱拼接是否可重放,並報告同目錄有沒有同步衝突副本。修復鏈路配合宿主自帶的工具:

node $DSH_HOME\tools\scan-session-logs.mjs --all
node $DSH_HOME\tools\merge-session-log.mjs "<會話目錄>\session.v4.jsonl.zstd" --fix-inbox --apply
node $DSH_HOME\tools\check-session-projections.mjs --all

merge-session-log 先備份 .bak-<時間>、寫後自校驗,通過才替換原文件;有衝突副本時按 seq 取寫入時間更晚的行合併,序號重複或倒退時去重重寫,--fix-inbox 把投影會拒絕的收件箱拼接降級爲空操作,沒有副本又有斷檔時用 --renumber 順延序號。

與路徑類插件的兼容視圖

有些插件不向宿主詢問會話目錄,而是自己按 ~/.dsh/sessions/<projectKey(cwd)>/<sessionId> 現推路徑(README 舉的例子有 @xrn1997/dsh-session-delete)。項目內存儲會讓它們報「會話目錄不存在」。插件默認維護一個兼容視圖:把 <root>/<projectKey> 做成指向 <項目>/<dirName>/<projectKey> 的目錄鏈接。效果是這類插件的內部操作作用於項目裏的真實目錄,刪除和恢復能真正生效;同時因爲鏈接的 Dirent.isDirectory() 爲 false,宿主後端不會重複列出或雙寫。插件停用時會自動撤掉這些鏈接,不想要可以通過配置 centralView: false 關閉。

這裏有兩個明確警告:

  • ~/.dsh/sessions 不再「空着」,裏面會出現指向項目的鏈接。不要遞歸刪除或移動這個目錄,rm -rf 或資源管理器刪除會順着鏈接刪進項目目錄。要清理只刪非鏈接的子項。
  • 跨卷限制:dsh-session-delete 用 rename 搬目錄,$DSH_HOME 與項目不在同一卷時會 EXDEV 失敗。這類項目要保持中心存儲並 restore,或等上游加複製後刪除的回退。

同步到另一臺機器

兼容視圖是 junction / 符號鏈接,只在本機有意義,同步工具對它的處理各不相同:

工具 默認行爲 規避
robocopy 跟進去,把目標複製成真目錄 加 /XJ(或 /SJ)排除 junction
rsync -a 複製鏈接本身 不要加 -L / --copy-links
Syncthing / OneDrive / 網盤 多數會跟隨或忽略 用忽略規則排除 sessions/<key>

README 給出的正確做法是「同步前撤掉、落地後重建」:

node bin/session-project-logs.mjs views strip             # 撤掉鏈接,項目裏的真實日誌不動
# ……執行同步……
node bin/session-project-logs.mjs views repair

如果已經被同步成真目錄,在目標機器上先 views status 逐個報告 link / 真目錄 / 缺失,再 views repair 刪掉副本換回鏈接。views repair 只在安全時替換:副本里若多出會話、或某會話日誌比項目裏的大(說明有人在副本上寫過),會報 blocked 並保持原樣,處理辦法是先 migrate <project> 再 views repair。目標機器如果不裝按路徑推算目錄的插件,最省事的做法就是配成 centralView: false。

適用場景與注意

適合管理多個長期項目、希望會話日誌跟着項目目錄走、且需要把日誌與項目一起歸檔或遷移的開發者。插件保留原有後端並讓新會話寫入 <項目>/.dsh/sessions/<項目鍵>/<會話 id>/,歷史會話仍在 ~/.dsh/sessions 照常列出、恢復、續寫;項目目錄消失時後端仍會掛載,寫入時目錄會重新創建;插件停用時立刻還原官方實現,已寫入項目目錄的會話仍在項目裏可讀。

需要注意:插件以當前 DSH 進程的權限運行,直接讀寫項目目錄和 ~/.dsh/sessions,安裝前應查看源碼與許可證(README 中 package.json 顯示 license 爲 MIT)。安裝命令務必取自官方 README,不要憑插件名自行拼接。package.json 裏的包名是 @local/session-project-logs,與倉庫名 dsh-session-project-logs 不一致,安裝和卸載時用前者。

結尾

核心價值是把新會話日誌分流到各自項目的 .dsh/sessions/,既有歷史會話不動、存儲格式不變,又用官方後端實例保證兼容。插件目錄頁:https://www.skillhub.cn/plugins/yyqinfo/dsh-session-project-logs,源碼:https://github.com/yyqinfo/dsh-session-project-logs。

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

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

小夜