DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Session Relocate — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-session-relocate

Session Relocate

跨工作区移动 DSH 对话:把某个 Session 的日志文件与工作区归属整体搬到另一个工作区(可自动注册新工作区)。带日志级崩溃恢复的安全事务,并且会先安全释放「已打开但空闲」的会话 —— 所以正在打开的对话也能直接搬,不需要重启 DSH。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-session-relocate@0.2.2
README兼容性版本

兼容性与来源证明

Session Relocate 以 dsh-session-relocate 发布,当前版本为 0.2.2。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/10/19

版本

0.2.2stable
2026/10/19
0.2.1stable
2026/10/19
0.2.0stable
2026/10/18
查看其余 1 个版本收起版本
0.1.0stable
2026/10/17

相关插件

正在加载相关插件…

最新版
0.2.2
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
241.7 kB
文件数
13
Surface
web
许可证
MIT
发布源
npm
GitHub
★ 0
周下载
0
最近提交
2026/10/19
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 productivity-workflow 分类下经过校验的插件。

Acp App@deepseek-ai/dsh-acp-appdsh ACP 配置文件包:基于 dsh-base 的仅限自动化的 JSON-RPC stdio 和进程生命周期管理Experimental Schedule Bundle@deepseek-ai/dsh-experimental-schedule-bundle为 Web 组合添加 Schedule、其任务页面和时间上下文的可选捆绑包Ab Ocrdsh-ab-ocr--- description: "外部 ocr 工具:逐页识别扫描的 PDF 或图像,仅在处理完最后一页后按顺序合并页面,并使用模型确定的标题级别保存为一个 Markdown 文档。" kind: "package-reference" ---Scriptor@linfengqaqtat/dsh-scriptorWebnovel Writer:DSH 长篇网文写作工作台

README

dsh-session-relocate

让 DSH 的对话能跨工作区搬家 —— 连正在回答的那个也能搬。

装完之后,你在侧边栏里就能把一个对话从「工作区 A」搬到「工作区 B」:它在 A 里消失、在 B 里出现, 而且接着继续聊 —— 它的文件沙箱根、工具工作目录、后续读写全部变成 B。


三步用起来

1. 装

# 从 npm 装:
dsh plugin --profile web add dsh-session-relocate

# 或者从源码装(把路径换成你 clone 下来的 checkout):
dsh plugin --profile web add <你 clone 下来的 dsh-session-relocate 目录>

2. 重启 dsh web

主机侧是 Node 模块,必须重启才会加载。

3. 确认它活了

  • 侧边栏里任意一个对话的 「…」菜单里会多出一项 移动到;
  • 或者直接问它一句:
curl http://127.0.0.1:3080/api/dsh-session-relocate/status

返回 {"ok":true, ...} 就是在线。


怎么用

方式一:菜单

  1. 鼠标移到侧边栏里某个对话上,点 …;
  2. 点 移动到 —— 会列出除「它当前所在的工作区」以外的所有工作区;
  3. 点目标工作区。列表立刻刷新,这个对话出现在目标工作区下;
  4. 打开它继续聊:它现在就运行在新目录里。

想搬到一个还不存在的工作区:点 其它目录…,输入一个绝对路径 —— 目录会被创建, 并自动注册成一个新工作区。

方式二:直接拖

把侧边栏里的对话拖到另一个工作区上,松手就搬,不用点菜单。

投放目标是整块工作区分组,不是那个 34px 的文件夹行:

  • 拖到目标工作区的文件夹行上 —— 可以;
  • 拖到它下面任意一条会话行上 —— 也可以;
  • 拖到分组里的空隙、或「展开其余 N 个会话」上 —— 一样可以。

松手前,整块目标分组会亮起蓝框。

多选也能拖。 在多选模式里勾好几条后,抓住其中任意一条拖到目标工作区,松手就是整批搬(逐条文件事务 + 最后一次性写归属),进度显示「正在移动 x/N…」;拖的如果是一条没被勾中的行,就只搬它自己。

组内上下拖动仍然是官方的排序功能,插件一点不抢。 判断依据是"指针落在哪个分组里": 在别的分组里才接管并搬走;在它自己所在的那个分组里就完全让路。

批量搬(多选)

侧边栏某个对话的「…」菜单里点 多选,每行左侧就会出现勾选圈;点整行即勾选(不会打开对话),底部出现操作条 已选 N 个 / 取消 / 移动。选好目标工作区后逐条搬,进度实时显示「正在移动 x/N…」,失败的那条原地不动、保留选中方便重试。

  • 装了 dsh-delete-session 的话,它的「多选」底栏会多出一个 移动 按钮 —— 两个插件共用同一套选择模式,不会出现两个「多选」入口。
  • 多选状态下直接拖也行:抓一条选中的行拖到目标工作区,整批一起搬(见上一节)。
  • 目标列表会直接标出「将移动 M 个 / 跳过 K 个」;全部已在目标工作区的项会置灰,点了也不会发请求。
  • 批量不是把 N 次单搬简单循环:文件事务逐条做(进度、失败隔离),工作区归属最后一次性写入。按实测的单条固定开销推算,3 条会话的总耗时从约 0.96 秒降到约 0.6 秒,条数越多差距越大。
  • 中途崩了也不怕:每条会话的迁移日志还在,下次启动(或下一次桥接调用)会按文件状态幂等补完归属。

连同子对话(默认开)

子智能体的会话(origin: subagent,侧边栏里是隐藏的)默认会跟着父对话一起搬:父搬到哪,它的子、孙会话也搬到哪里,cwd 和工作区归属一起改。每条子会话独立走事务,失败按条隔离,不会拖累父对话的回执。

想只搬父对话:Plugins → dsh-session-relocate → 配置 里把「连同子对话一起移动」关掉(存在浏览器本地,默认开)。关掉后子会话留在旧工作区 —— 旧工作区先别删,子会话日志还在里面。

正在回答的对话也能搬

包括正在回答、正跑子代理、正跑后台任务的那个。 插件会先拿到它的写所有权,再动文件:

  • 打开着但空闲:先把它从 DSH 内存里安全释放,再搬;
  • 正在跑活:就地接管 —— 换掉写句柄的 header 并在写链内搬文件,不打断这一轮回答。

一句诚实的话:正在跑的那一轮会继续跑完,但它手里的工具工作目录仍然是旧目录 (会话在内存里的 header 是冻结的,改不了);文件和归属都已经搬走,这个对话下次打开时才真正 在新目录里跑。想要"正在跑活就拒绝"的保守行为,把 moveLive 设为 false。

如果你搬的正是屏幕上打开的那个对话

页面需要跳转/刷新一次再打开它(客户端手里那份是旧的)。搬别的对话则完全无感。


出问题怎么办

症状多半是怎么办
「…」菜单里没有 移动到装完没重启,或插件没加载重启 dsh web;再 curl .../status 看是不是 200
拖动一点反应都没有DSH 改了侧边栏的 DOM 约定,或 dragMove 被关看 dragMove(设回 true);菜单那条路不受影响,先用它
搬完打开是空的 / 还在旧工作区搬的正是当前打开的那个对话刷新页面,再打开它
提示"归属会在下次启动自动补齐"这个 DSH 版本的注册表没有可用的刷新入口重启一次 DSH,归属会自动补上
提示"这个对话正在跑"moveLive 被设成 false等它跑完,或把 moveLive 设回 true
提示"打开着的会话搬不了"releaseLive 被设成 false先切到别的对话,或把 releaseLive 设回 true
搬完这一轮的工具还在旧目录设计如此(内存 header 冻结)不用管;下次打开就在新目录了

它是怎么做到的

为什么 DSH 自己搬不了

DSH 发行版里没有任何跨工作区移动会话的能力(moveSession / relocateSession / changeCwd 零命中), 因为「对话属于哪个工作区」被编码在几个互相约束的地方:

事实说明
目录名由 cwd 推导会话存在 ~/.dsh/sessions/<由 cwd 编码的项目目录>/<会话目录>/session.vN.jsonl.zstd,项目目录名是 cwd 的编码(分隔符折叠成 -,非 ASCII 变 ~XXXX)。
cwd 住在日志第一行每个 generation 文件的第一行就是 header,含 cwd;header 的 cwd 与物理位置不一致会被判定成 corrupt session log。
归属 = 注册表 + 校验工作区记录在 ~/.dsh/storages/workspace.json 的 sessionIds 里,但 attachSession 还会校验 header 的 cwd 必须等于工作区 path。
不能出现「两份」findLog(id) 一旦发现同一个 id 出现在两个项目目录里就直接抛错 —— 这个会话会永久打不开。

所以「移动」必须同时做三件事,而且中间态必须不伤源数据: 改 header 的 cwd → 把会话目录搬到新项目目录 → 改注册表归属。

移动是一个可恢复事务,不是一次 rename

  1. 校验:会话存在、目标工作区存在、源 ≠ 目标、日志读得出来;正在跑活时确认能就地接管;
  2. 如果这个对话正打开着:空闲的走 release、正在跑的走 handoff(见下两节);
  3. 写迁移日志(~/.dsh/session-move/<opId>.json);
  4. 把整个会话目录复制到暂存区 <sessionsRoot>/_session-move-staging/<opId>/<会话目录>/。 暂存区比 DSH 认的会话目录深一层,所以后端的扫描器永远看不到它;
  5. 在暂存区里改每个 generation 的 header cwd,并逐字节自校验:除第一行外, 明文必须与源文件完全一致、行数也必须一致 —— 不满足就整体放弃;
  6. rename 到目标目录(同一存储根下,是原子操作);
  7. 删源目录;
  8. 刷新注册表的 header 索引 → attachSession 到目标 → 从源 detachSession;
  9. 失效该会话的投影缓存条目,删掉迁移日志。

关键取舍:改写发生在暂存区,源目录在改名之前一个字节都没被动过。所以任何一步失败都能干净回滚; 即使进程在改名后被杀死,也只会留下「目标已完整 + 源还在」这一种中间态,由启动时的恢复流程 (recoverPendingMoves)幂等推完。

A.「先释放再搬」—— 打开着但空闲的对话

会话日志是单写者的:写所有权由 agent-loop 的写句柄持有(它调 sessionPersistence.open(id, 'write')),只在 DSH 进程退出时才释放。不释放就搬文件, 那个句柄会继续往旧路径写、并在旧位置重建一个空壳目录 —— 这就是生态里其它迁移插件 要么直接拒绝、要么排队等重启的原因。

释放的缝是存在的(SessionStore 每个条目自带 detach()),同一个 DSH 版本里 dsh-delete-session 已经在生产里用它。本插件的顺序:

  1. sessions.flush(session) —— 持久化屏障,把缓冲的事件落盘;
  2. sessions.store.get(id).detach() —— 从 live store 摘掉,派发 session/disposed;
  3. 持久层收到 session/disposed 会 close() 那个写句柄,从而释放 session.lock 租约 (它是 fire-and-forget 的异步动作);
  4. 探针确认:反复尝试 sessionPersistence.open(id, 'write') —— 能拿到就证明已经没有活跃写者, 立刻 close() 归还;
  5. 探针通过后才开始文件事务。

任何一步失败都会整体放弃且不碰任何文件(release-failed)。想恢复旧行为就把 releaseLive 设为 false:打开中的会话直接拒绝。

第 8 步里「刷新 header 索引」是必需的:注册表的 header 缓存是启动时建的,不刷新的话它会拿旧 cwd 去比目标路径、然后自己把 attach 拒掉。这个入口是包私有方法,所以做了 feature-detect —— 探测不到时磁盘操作照样完成,归属会在下次启动时自动补齐(菜单会告诉你)。

B.「就地接管」—— 正在跑的对话也能搬

写句柄的写入路径是每次从它手上的 header 现算的,而这个 header 是可写槽位(被冻结的是 那个对象,不是这个属性)。所以顺序是:

  1. 从后端的 tracker.writers 取到这个会话的 live 写句柄(取不到就整体放弃, 绝不静默搬 —— 那会留下「文件已搬、句柄还指着旧路径」的坏状态);
  2. await handle.drainLive() + await handle.flush() —— 把缓冲事件落盘。 drainLive 自己会往写链上排队,所以必须在进入临界区之前调用,否则死锁;
  3. await handle.enqueueChain(...) —— 进入该句柄唯一的串行点。此后所有 append / flush 都排在我们后面;
  4. 在临界区里:释放内核写锁并置空 → handle.header = { ...header, cwd: 目标 } → 跑文件事务(暂存 / 改写 / 原子改名 / 删源)。失败就把 header 与锁状态一起回滚;
  5. 归属绕开 attachSession:live 会话在内存里的 header 是 deep-frozen 的旧 cwd, 注册表的 cwd 校验必然失败。改为直接走注册表自己的写路径(entity.mutate → table.update),并先把路径索引指到新 cwd(mutate 内部会用它过滤 sessionIds, 顺序反了会把刚加进去的 id 自己滤掉)。

写锁的形态值得单独记一句:Windows 上是由路径派生的命名内核信号量,根本没有锁文件; POSIX 上才是会话目录里的 session.lock(flock)。两种情况下锁的名字/对象都跟路径绑定, 所以「先释放、让下一次写入在新路径重取」是必需且正确的。


配置

cordis.patch.yml 里这一行的 config(profile 层可覆盖):

键默认含义
bridgePrefix/api/dsh-session-relocate主机桥接前缀。改动后客户端自动跟随(通过引导全局下发)。
journalsDir<dsh home>/session-move迁移日志目录。
sessionsRoot后端的 root,退化为 <dsh home>/sessions会话存储根。一般不用设。
home$DSH_HOME 或 ~/.dshDSH home。
moveLivetrue正在跑活的对话是否走「就地接管」。设 false 则正在跑活的会被拒绝。
dragMovetrue侧边栏拖动搬移(整块工作区分组都是投放目标)。依赖官方的 DOM 行属性与分组结构;DSH 改了约定时可一键关掉(菜单那条路不受影响)。
releaseLivetrue打开着但空闲的对话,搬之前是否自动释放它。设 false 则打开中的直接拒绝。
releaseRetries / releaseRetryDelayMs24 / 150释放后探测写租约的重试次数与间隔。前 5 次几乎即时(0/10/25/50/100 ms),之后按 150 ms 退避;回执里会带上 releaseAttempts。
verifyauto改写后的自校验强度。auto:压缩后 ≤ fullVerifyLimit 的日志全文解码,更大的只做帧级比对 + 解头解尾(回执标 verification: 'sampled')。full:一律全文解码。
fullVerifyLimit2097152(2 MB)verify: auto 下的分界点。
selftestfalse打开后多挂几个 __selftest/* 路由,用来在真实进程里造/清一个测试会话、施加写入压力、验证接管。

桥接 API

方法路径说明
POST<prefix>/describe{ sessionId? } → 工作区列表;带 sessionId 时额外给出 currentWorkspaceId、live、willHandoff(正在跑,会就地接管)、willRelease(空闲,会先释放)、activity、canMove、reason。
POST<prefix>/move{ sessionId, targetWorkspaceId } 或 { sessionId, targetPath } → 迁移回执(applied: 'live' | 'pending'、released、releaseAttempts、handedOff、verification: 'full' | 'sampled')。带 deferRegistry: true 时只做文件事务、保留迁移日志。
POST<prefix>/move-commit{ sessionIds } → 把一批 defer 过的迁移的归属一次性写进注册表(每个目标/源工作区各一次 mutate),返回逐条结果。
GET<prefix>/status版本/配置(含 moveLive/releaseLive/verify/bulkDefer)/待收尾的迁移列表;调用时会顺带尝试恢复。
POST<prefix>/__selftest/create-live仅 selftest: true 时存在。造一个「已打开 + 持有写租约」的会话({ workspacePath, pressure? })。
POST<prefix>/__selftest/state仅自测时存在。写入压力的采样时间戳 + 每次接管的窗口 + 写句柄现状。
POST<prefix>/__selftest/ensure-lease仅自测时存在。强制写句柄重取内核锁,用来证明它把路径算到了新目录。
POST<prefix>/__selftest/drop仅自测时存在。释放 + 摘归属 + 删目录 +(可选)注销工作区。

失败一律是 { ok: false, code, message },code 形如 session-live、session-active、 already-there、corrupt-log。

测试与验证

node --test

覆盖:路径编码与真实 ~/.dsh/sessions 目录名逐一比对、对真实会话日志做 header 外科手术的逐字节校验、 完整迁移 / 回滚 / 崩溃恢复、桥接端到端(假 cordis ctx)、客户端状态机(假 module loader + react 替身)、 以及拖动的分组归属规则。全部用合成数据,不碰你的真实会话。

三条真机验证(都不需要额外装包):

脚本验证什么
scripts/e2e.mjs live在临时 DSH_HOME 里造一个持有真实写租约 + 持续写入压力的会话(等价于"正在回答"),边跑边搬:断言 header 换成目标、内核锁释放、没有任何一次写入插进接管窗口、搬完还活着、旧目录绝不被重建。
scripts/drag-verify.mjsCDP 驱动 headless Chrome 打开真实 DSH 页面:客户端半真的执行、引导全局带上 drag: true、合成拖放事件后会话真的落到了目标工作区。
scripts/drag-live-check.mjs对着你正在跑的那个 DSH 做只读检查:分组契约在真实 DOM 上成立、拖到别组会话行被接管并高亮、拖回自己分组完全让路。它只合成事件、绝不投放,所以永远不会搬动任何会话。

已知限制

  • 拖动搬移依赖官方的 DOM 约定:行属性(data-row-key="session:<id>" / "workspace:<workspaceId>") 加上分组结构(官方 renderGroup 把每个工作区渲染成一个「恰好只含一个工作区行」的容器)。 它读的是官方渲染出来的东西,不是未公开的拖拽载荷;DSH 若改掉这些,拖动会分级退化: 分组读不到 → 退化成「只认文件夹行」;连行属性也没有 → 完全不介入。 菜单那条路始终不受影响,dragMove: false 可以一键全关。
  • 嵌套工作区(工作区套工作区)时,父分组里若渲染了子分组,父分组的会话行不再算投放目标 —— 这是保守取舍:宁可少接管,也不猜错工作区。
  • 正在跑活的那一轮不会换工作目录:会话在内存里的 header 是 deep-frozen 的,改不了; 文件与归属都搬走了,但那一轮的工具仍然用旧目录,下次打开才真正在新目录里跑。
  • 正在跑活的接管依赖私有内部结构(tracker.writers、写句柄的 header 槽位、enqueueChain、 以及注册表的 entity.mutate)。DSH 换 rc 版本时这些都可能变,所以每一处都 feature-detect: 探测不到就整体放弃(handoff-failed),绝不半途而废。
  • 多进程共用一个 DSH home 时:接管期间会短暂释放内核写锁,理论上另一个进程可能抢进来。 单进程部署(默认)没有这个问题。
  • 搬「当前屏幕上打开的那个」之后,页面要跳转/刷新一次再打开它(客户端手里那份是旧的)。
  • 只移动这一个会话。由它 fork 出来的子会话是自包含的(继承事件已物理写入子日志),留在原处不会坏; 子代理子会话同样留在原处。
  • 一个会话同时只能属于一个工作区,这是 DSH 的模型,不是插件的选择。
  • 主机侧假设会话存储根下可以放一个 _session-move-staging 暂存目录(对 DSH 不可见,用完即清)。