前言¶
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。