dsh-session-mover:把侧栏会话拖到另一个项目分组

前言

用 DSH 时,一段对话的所属项目有时候需要在事后调整:这段工作是在项目 A 里开始的,后来发现它其实属于项目 B;或者想把某段对话从「脚手架」挪到正式项目下。官方 API 只支持同一 workspace 内的排序(insertSessionBefore 要求会话已记账),而会话的 JSONL header 是只读的,改不了 cwd。

dsh-session-mover 解决的就是这件事。它是 railgun20001 维护的一个客户端插件,把侧栏里按项目分组显示的会话拖到另一个项目标题上即可改归属。它明确不是文件迁移——会话的 JSONL 日志、附件始终留在原项目目录,插件只改 workspace 归属账本,并把读取侧投影到新项目。

下面介绍它的能力、安装方式和使用边界。

定位与核心机制

插件把「会话属于哪个项目」记在两处:每个 workspace 记录里的 sessionIds(持久化账本),以及注册表内存索引 sessionPaths。它以账本为权威:

  1. 移动时先让内存索引指向目标目录,再改写两边 workspace 的记录;
  2. 启动时把账本里的归属重新灌回索引,让移动跨重启保持;
  3. 每次写官方账本前再修一次索引(写前修复),避免被官方剪枝剔除。

安装与启用

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)。

典型用法

  1. 侧栏按项目分组显示会话(默认视图);
  2. 按住任意一段对话,往另一个项目的标题上拖;
  3. 目标项目亮起描边,松手即完成移动。

拖动落点有几种差异:拖到目标项目的某个会话行上会插到那个会话前面;拖到项目标题上则排到最前。拖到侧栏列表上下边缘时列表自动滚动,靠近边缘滚得越快,长列表跨分组不用先手动翻页。拖回原项目或拖到同一分组内,仍是官方的手动排序,插件不干预。

移动成功后:

  • 对话立即出现在目标项目下,原项目不再列出它;
  • 若该对话已经加载,工作目录一并切到目标项目目录,后续工具调用在新目录执行;
  • 若尚未加载,归属已生效但工作目录保持原样,控制台日志会说明;
  • 从移动过的会话分叉出来的新对话也归到目标项目,跟随父会话对齐账本、索引与工作目录。

插件不显示提示条,移动结果写进浏览器控制台(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 目录」这条不变式,因此官方有两处写路径会踩到移动过的会话:

  1. 写前修复:官方 WorkspaceEntity.mutate() 按内存索引 sessionPaths 剪枝,不在该目录的会话会被从账本里删掉并落盘。护栏在写之前按移动日志修好索引,避免改名、排序、删除、新建会话这类无关操作静默剔除受管会话。
  2. 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
羽毛球分组比赛记分
小程序二维码

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

小夜