前言¶
用 DSH 时,一段对话的所属项目有时候需要在事后调整:这段工作是在项目 A 里开始的,后来发现它其实属于项目 B;或者想把某段对话从「脚手架」挪到正式项目下。官方 API 只支持同一 workspace 内的排序(insertSessionBefore 要求会话已记账),而会话的 JSONL header 是只读的,改不了 cwd。
dsh-session-mover 解决的就是这件事。它是 railgun20001 维护的一个客户端插件,把侧栏里按项目分组显示的会话拖到另一个项目标题上即可改归属。它明确不是文件迁移——会话的 JSONL 日志、附件始终留在原项目目录,插件只改 workspace 归属账本,并把读取侧投影到新项目。
下面介绍它的能力、安装方式和使用边界。
定位与核心机制¶
插件把「会话属于哪个项目」记在两处:每个 workspace 记录里的 sessionIds(持久化账本),以及注册表内存索引 sessionPaths。它以账本为权威:
- 移动时先让内存索引指向目标目录,再改写两边 workspace 的记录;
- 启动时把账本里的归属重新灌回索引,让移动跨重启保持;
- 每次写官方账本前再修一次索引(写前修复),避免被官方剪枝剔除。
安装与启用¶
npm 包名、仓库名与插件内部 id 都是 dsh-session-mover。
从 npm 安装:
dsh plugin --profile web add dsh-session-mover
从源码安装:
git clone https://github.com/railgun20001/dsh-session-mover.git
dsh plugin --profile web add "link:C:/work/dsh-session-mover"
也可以在 DSH 的「插件」页点「添加插件」选择本地目录。宿主半(index.js)随 profile 热加载,客户端半(client.js)需要刷新一次页面(Ctrl+R)。
典型用法¶
- 侧栏按项目分组显示会话(默认视图);
- 按住任意一段对话,往另一个项目的标题上拖;
- 目标项目亮起描边,松手即完成移动。
拖动落点有几种差异:拖到目标项目的某个会话行上会插到那个会话前面;拖到项目标题上则排到最前。拖到侧栏列表上下边缘时列表自动滚动,靠近边缘滚得越快,长列表跨分组不用先手动翻页。拖回原项目或拖到同一分组内,仍是官方的手动排序,插件不干预。
移动成功后:
- 对话立即出现在目标项目下,原项目不再列出它;
- 若该对话已经加载,工作目录一并切到目标项目目录,后续工具调用在新目录执行;
- 若尚未加载,归属已生效但工作目录保持原样,控制台日志会说明;
- 从移动过的会话分叉出来的新对话也归到目标项目,跟随父会话对齐账本、索引与工作目录。
插件不显示提示条,移动结果写进浏览器控制台(console.info / console.warn)。
受管会话与自检¶
被移动过、header.cwd 还停在原目录的会话称为受管会话,插件逐条记在移动日志里:
$DSH_HOME/dsh-session-mover/journal-v1.json
DSH_HOME 未设置时是 ~/.dsh。日志只存「会话 id → 账本目录 / 原目录 / 时间 / 来源」,不含对话内容。查看受管清单:
__DSH_SESSION_MOVER__.managed()
// 或:GET /dsh-session-mover/api/managed
宿主每次启动会打印一行自检摘要:
[dsh-session-mover] 自检:受管会话 3 个(目标目录缺失 0,原目录已不存在 1)
目标目录缺失 意味着文件树与工具调用会报 ENOENT;原目录已不存在 意味着会话文件可能已随旧文件夹消失。两类情况都会逐条 warn。
卸载与禁用前必须拖回¶
这是本插件最重要的使用约定:归属账本被改过,而「会话属于哪个项目」的成员过滤仍按 header.cwd 判断。插件在场时会补回这层差异,插件缺席时(卸载、禁用、加载失败、升级不兼容),移动过的会话会从所有项目里消失,落到「未分组」。这不是数据丢失:重新启用插件或把会话再拖回任意项目即可恢复。非 web/webServer 的 profile(例如纯 CLI)不加载本插件,看到的始终是原项目归属。
要回到原项目,直接把会话拖回去即可,插件不提供额外的「还原」命令。
写入路径的两道护栏¶
插件不满足官方「header.cwd 必须等于 workspace 目录」这条不变式,因此官方有两处写路径会踩到移动过的会话:
- 写前修复:官方
WorkspaceEntity.mutate()按内存索引sessionPaths剪枝,不在该目录的会话会被从账本里删掉并落盘。护栏在写之前按移动日志修好索引,避免改名、排序、删除、新建会话这类无关操作静默剔除受管会话。 - attach 接管:官方
attachSession()要求header.cwd等于目标项目目录,否则抛错。护栏在失败后按账本接管,只有能证明会话确实属于本项目(账本指向它,或它是本项目某个会话的分叉)时才绕过校验,其余情况原样抛错。
读取侧的两条接缝¶
归属还有第三个读者:任何按会话 cwd 推导目录的界面。插件不改写会话文件,而是补两块接缝:
- 宿主侧:用 typert 的 effect-scoped 覆盖点接管
workspaceFileScope查找参数,让文件系统接口按账本而不是 header 解析工作区根。 - 客户端侧:宿主多开一个只读的
/api/ownership,客户端半在会话列表快照的写入处把cwd覆盖成账本目录,移动完成后立即重放快照,不必等页面刷新。
唯一写入会话日志的地方是「工作目录切换」:会话已加载时插件调用官方的 workingDirectory.set(),追加一条 working-directory/change 事件;未加载时跳过,不写任何东西。
已知边界¶
- 不是文件迁移:会话 JSONL、附件、崩溃恢复目录仍在原项目目录。删除或改名旧项目文件夹、按目录备份、换机迁移都会漏掉已「移动」的会话。
- 读取侧只覆盖两处:
workspaceFileScope与客户端会话列表快照。ACP/IDE 会话列表、CLI、第三方插件、导出与备份仍按header.cwd看。 - 历史消息里的旧绝对路径:在新项目的 workspaceRoot 下会变成越界(
workspace-file/outside-workspace)。 - 目标目录消失时:官方
workingDirectory.ensure()会把工作目录拉回旧项目,自检只告警,不自动修复。 - 与「真迁移」类插件互斥:如
dsh-workspace-mover会搬文件并改写 header,两者同装必然互相打架。
适用场景¶
适合把会话按项目分组管理、且愿意接受「归属与文件位置分离」这层模型的 DSH 用户。运行时以当前 dsh 进程权限执行,安装前应检查插件源码与许可证(MIT)。插件对 WorkspaceEntity 内部结构做运行时探测,结构不符时移动会失败并给出说明,不写坏数据;读取侧与护栏全部按「失败就降级」处理。
源码内附测试矩阵(test/core-test.mjs、scroll-test.mjs、view-test.mjs、client-test.mjs、fork-test.mjs、guard-test.mjs、managed-test.mjs、workdir-test.mjs),真机验收矩阵见 docs/acceptance.md。
小结¶
对于「会话开错了项目分组」这个场景,dsh-session-mover 以最小改动解决了归属问题:拖动即改账本、不搬文件、可拖回还原。代价是会话文件仍在原目录,且卸载或禁用前必须先把移动过的会话拖回。
- 目录页:https://www.skillhub.cn/plugins/railgun20001/dsh-session-mover
- GitHub:https://github.com/railgun20001/dsh-session-mover