Saltar al contenido principal
R

dsh-session-mover

railgun20001/dsh-session-mover

拖动会话到另一个项目分组,修改其所属项目(只改归属账本与读取投影,不搬会话文件)

Instalar

dsh plugin --profile web add github:railgun20001/dsh-session-mover

README

dsh-session-mover

把侧栏里的对话直接拖到另一个「项目」分组上,就能修改这段对话的所属项目。

这是「改归属」,不是「搬文件」。 对话的 JSONL 日志、header、附件始终留在原项目目录里; 插件改的是 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)。

用法

  1. 侧栏按项目分组显示会话(默认视图);
  2. 按住任意一段对话,往另一个项目的标题上拖;
  3. 目标项目会亮起描边,松手即完成移动;
  4. 拖到目标项目的某个会话行上时,会插到那个会话前面;拖到项目标题上则排到最前;
  5. 拖到侧栏列表的上下边缘时列表会自动滚动,越贴近边缘滚得越快,长列表里跨很多分组也不用先手动翻页;
  6. 拖回原项目、或拖到同一分组内,仍然是官方的手动排序,插件不干预。

移动成功后:

  • 对话立刻出现在目标项目下,原项目不再列出它;
  • 若该对话已经加载(有存活 Agent 或已在内存中),工作目录会一并切到目标项目的目录,后续工具调用都在新目录里执行;
  • 若对话尚未加载,归属已经生效,但工作目录保持原样(控制台日志会说明这一点);
  • 侧边文件树与「在文件夹中打开」也跟到目标项目的目录:会话摘要里的 cwd 会按归属账本改写,宿主侧的文件接口同时把该目录认作工作区根。
    • 已经打开着的文件树页签会保留它打开时的根目录(页签存储按页签缓存根路径),关闭再打开一次即可看到新目录。
  • 从移动过的会话分叉出来的新对话也归到目标项目:分叉产生的子会话会跟随父会话的项目对齐归属、侧栏索引与工作目录,不会掉回老项目(见分叉跟随父会话)。
  • 插件不显示提示条:移动结果写进浏览器控制台(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

要回到原项目,直接把会话拖回去即可。插件不提供额外的「还原」命令——归属就是唯一的持久事实,拖回原项目后它和普通会话完全一样。

判断「卸载/禁用后会发生什么」

这是本插件最重要的使用约定,请在卸载或禁用前把移动过的会话拖回原项目:

  • 归属账本被改过,而「会话属于哪个项目」的成员过滤仍按 header.cwd(原目录)判断。 插件在场时会把这层差异补回去;插件缺席时(卸载、禁用、加载失败、升级不兼容), 移动过的会话会从所有项目里消失,落到「未分组」。
  • 这不是数据丢失:重新启用插件(启动重放)或把会话再拖回任意项目即可恢复。
  • 非 web/webServer 的 profile(例如纯 CLI)不加载本插件,那里看到的永远是「原项目归属」。

写入路径的两道护栏

插件不满足官方「header.cwd 必须等于 workspace 目录」这条不变式,所以官方有两处写路径 会踩到移动过的会话,插件各加了一层护栏(lib/guards.js):

  1. 写前修复:官方 WorkspaceEntity.mutate() 每次写都会按内存索引 sessionPaths 剪枝 (不在该目录的会话会被从账本里删掉并落盘)。护栏在写之前按移动日志把索引修好, 避免「改名/排序/删除/新建会话」这类无关操作把受管会话静默剔除。
  2. attach 接管:官方 attachSession() 要求 header.cwd 等于目标项目目录,否则抛错。 分叉出来的子会话 header 写的是父会话原始 cwd,forkWorkspace() 又按账本把它挂到 父会话所在项目——校验必然失败。护栏在失败后按账本接管:只有能证明「这个会话确实属于本项目」 (账本指向它,或它是本项目某个会话的分叉)时才绕过校验,其余情况原样抛错。

两道护栏都只处理移动日志里记着的会话 + 本次正在移动的那一个,普通会话的官方剪枝语义不变; 插件卸载时补丁会被还原。

分叉跟随父会话

DSH 的「分叉会话」是「复制一段事件前缀 + 新建会话」,子会话 header 里写的是父会话 header 里的原始 cwd,而不是父会话当前的工作目录。对移动过的会话(账本在项目 B、header 还停在项目 A)就会出现:子会话带着 A 的目录,宿主的成员索引(candidateMatches)不认它,侧栏于是把它过滤出项目分组,Agent 也还在 A 里干活。

插件在 session/created 之后(等官方 attachSession 走完)对齐三件事:账本归属 → 内存索引 → 工作目录;并在会话被打开(agent/created)时再对齐一次工作目录,把更早分叉出来、当时没机会对齐的子会话也治好。判定只看 header 里的稳定事实(isSeeded / parentSession / origin),父会话未分组、目标项目被删、Agent 还没起来时都只是跳过,绝不影响分叉本身。

子会话同样会写进移动日志(kind: "fork"),所以写前修复与自检都覆盖它。

启动自检

宿主每次启动都会打印一行自检摘要:

[dsh-session-mover] 自检:受管会话 3 个(目标目录缺失 0,原目录已不存在 1)
  • 目标目录缺失:目标项目的文件夹被删/改名,文件树与工具调用会报 ENOENT;
  • 原目录已不存在:会话文件可能已经随旧文件夹消失,需要尽快确认备份。

两类情况都会逐条 warn,便于在用户发现「会话打不开」之前先定位。

实现说明

DSH 把「会话属于哪个项目」记在两处:

  • 每个 workspace 记录里的 sessionIds(持久化的有序归属账本);
  • 注册表内存索引 sessionPaths(sessionId → 规范化 cwd),用于过滤掉已经不在该目录的成员。

官方 API 只支持同一 workspace 内的排序(insertSessionBefore 要求会话已记账),会话的 JSONL header 又是只读的(按 cwd 分目录、已提交的 header 永不重写)。所以本插件把归属账本当权威:

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

但「归属」还有第三个读者:任何按会话 cwd 推导目录的界面。侧边文件树的根、会话头部的「打开文件夹」目标、聊天里相对路径的解析,用的都是会话摘要里的 cwd——它来自永不重写的 header,不会因为账本变了而变。所以插件再补两块(都不改写会话文件的内容与位置):

  • 宿主侧:用 typert 的 effect-scoped 覆盖点接管 workspaceFileScope 查找参数(lib/view.js),让文件系统接口按账本而不是 header 解析工作区根。这是 DSH 明确支持的装配扩展点(业务包注册声明与默认策略,Host 组合可覆盖解析行为),插件卸载即撤除;账本里没有的会话原样交回默认策略。
  • 客户端侧:宿主多开一个只读的 /api/ownership(会话 → 目录),客户端半在会话列表快照的写入处把 cwd 覆盖成账本目录。createSnapshotStore 的 set 是唯一写入口,改写一次即覆盖首屏、重连修复、增删改全部来源;移动完成后立刻重放一次快照,不必等页面刷新。

唯一写入会话日志的地方是「工作目录切换」:会话已加载时插件调用官方的 workingDirectory.set(), 它会追加一条 working-directory/change 事件(与用户自己 cd 完全一样,模型也会收到一条目录变更通知); 会话未加载时跳过,不写任何东西。

兼容性

插件对 WorkspaceRegistry 的内部结构做运行时探测(lib/core.js 的 resolveRegistryParts)。结构不符时「移动」会失败并给出说明,不会写坏任何数据;侧栏本身永远不受影响。

读取侧的接管同样全部按「失败就降级」处理:typert 缺失、workspaceFileScope 没被注册、或已被别的组合配置过,插件都只在日志里说明原因,文件树退回按会话 header 解析目录——与没有读取侧覆盖时完全一致,不影响移动。

护栏同理:实体原型上拿不到 mutate / attachSession 时只记日志,官方行为保持原样。

工作目录同步也按「宿主有没有这个服务」决定:旧宿主(如 0.2.0-rc.2)没有 workingDirectory 服务时,插件只改归属、不写 working-directory/change 事件—— 那个版本不注册该事件类型,写进去会让日志变成「未知事件」,之后 fork / 读取都会拒绝解析。

已知边界

  • 不是文件迁移:会话 JSONL、附件、崩溃恢复目录仍在原项目目录。删除/改名旧项目文件夹、 按目录备份、换机迁移都会漏掉已「移动」的会话。
  • 读取侧只覆盖两处:workspaceFileScope 与客户端会话列表快照。ACP/IDE 会话列表、CLI、 第三方插件、导出与备份仍按 header.cwd(原目录)看。
  • 历史消息里的旧绝对路径:在新项目的 workspaceRoot 下会变成越界(workspace-file/outside-workspace)。
  • 目标目录消失时:官方 workingDirectory.ensure() 会「恢复到 header 原始目录」,把工作目录拉回旧项目; 自检会告警,但不会自动修复。
  • 与「真迁移」类插件互斥:如 dsh-workspace-mover 会搬文件并改写 header,两者同装必然互相打架。

测试

node test/core-test.mjs     # 归属账本:移动、幂等、锚点、启动重放、归属投影
node test/scroll-test.mjs   # 边缘自动滚动:触发带与速度曲线
node test/view-test.mjs     # 读取侧视图:账本优先、默认回退、各类降级
node test/client-test.mjs   # 客户端半:快照 cwd 改写、行/快照复用、新会话重取
node test/fork-test.mjs     # 分叉跟随:账本/索引/工作目录对齐与全部降级分支
node test/guard-test.mjs    # 护栏:写前修复、attach 接管、日志存取
node test/managed-test.mjs  # 自检与日志清理:清单、陈旧条目跳过/清理
node test/workdir-test.mjs  # 工作目录:Agent/会话/旧宿主三条路径

真机验收矩阵见 docs/acceptance.md。

Plugins relacionados