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