DSH 会话跨版本迁移:扫描旧版会话目录(v0 明文、.zstd 与 .zip 导出包),改写 header cwd 绑定目标工作区并落盘,打开会话时由持久化层自动完成版本升级;设置侧边栏(环境 / 导入 / 列表 / 转换)加 CLI(check / fix / import)。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:EIGHTfs/dsh-session-migrate
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
DSH 会话日志跨版本迁移——把低版本 generation(v0 等)投放到当前实例可识别的位置, 由 DSH 在打开该日志时沿迁移边自动还原到当前格式版本。
为什么需要它
DSH 会话日志格式是预发布格式,随 harness 版本演进(v0 → v1 → v2 → v3)。 官方不提供迁移命令,但持久化层在打开日志时会沿迁移边串行还原。 本插件负责「安全投放」,迁移本身交给 DSH——这样格式再演进也不会过期。
核心机制
转换一条会话需要两步,缺一不可:
| 步骤 | 动作 | 说明 |
|---|---|---|
| ① 投放 | 写入 sessions/--<cwd编码>--/<id>/session.jsonl.zstd |
改写 header.cwd,只重建首帧 |
| ② 触发 | WebSocket session/follow |
只有这步才会真的产出 vN 文件 |
实测对照(决定了为什么必须有 ②):
| 动作 | 是否触发迁移 |
|---|---|
session/list(HTTP) |
✗ |
session/page(HTTP 冷读,能读出内容) |
✗ |
session/follow(WebSocket 流式) |
✓ lock 与 vN 立刻落盘 |
功能
| 能力 | 入口 | 说明 |
|---|---|---|
| 旧会话扫描 | legacy.js |
扫描 session.old/,读出 id / 版本 / cwd / 帧结构 |
| 体检与迁移路径 | GET /check |
首帧契约、帧数、目标版本能力、v0→v1→v2→v3 规划 |
| 工作区探测 | GET /workspaces |
候选工作区列表 |
| 导入 | POST /import |
multipart 上传或 JSON {paths};支持 .jsonl / .jsonl.zstd / .zip |
| 转换 | POST /convert |
投放 + 触发迁移,可传 trigger:false 只投放 |
| 布局修复 | POST /fix-layout |
移出 sessions/ 下非法裸目录(否则工作区列表全空) |
| 转换去重 | POST /convert 内置 |
转换时按会话 id 全工作区查重:同 id 已存在于其它工作区则提示并移出旧份备份(可恢复),重新转换到本次目标工作区,杜绝「一个会话两次恢复到不同工作区」 |
| CLI | node cli.mjs |
list / check / fix / import,可脱离 DSH 独立运行 |
用法
CLI
node cli.mjs list <DSH_HOME> # 列会话与布局问题
node cli.mjs check <文件> [--home <DSH_HOME>] # 单文件体检(含目标版本与迁移路径)
node cli.mjs fix <DSH_HOME> # 移出 sessions/ 下非法裸目录
node cli.mjs import <源文件|源目录> <DSH_HOME> # 双路径导入(含 subagents)
node cli.mjs import <源> <DSH_HOME> --cwd <cwd> # 指定目标 cwd
HTTP
curl -s localhost:30801/api/session-migrate/state
curl -s 'localhost:30801/api/session-migrate/check?file=<会话文件绝对路径>'
curl -s -X POST localhost:30801/api/session-migrate/convert \
-H 'content-type: application/json' \
-d '{"ids":["session-xxx"],"cwd":"/path/工作区/测试","trigger":true}'
两个必须避开的坑
坑 1|备份放错位置 → 工作区列表全空
sessions/ 根下只允许 --<cwd编码>-- 形式的目录。放任何裸目录会让 DSH 报
uses the unsupported flat-file layout,表现为工作区列表为空。
所有备份一律放 <home>/../session-backups/(home 之外)。
坑 2|zstd 整体重压缩 → 会话读取失败
v0 布局是每行一个独立 zstd frame,且首帧必须正好是 header 那一行。
「解压→改→重压」会合并成 1 帧,DSH 报
corrupt Zstandard session log: first frame is not exactly one header line。
正确做法:只重建首帧,其余字节原样拼接。
目录结构
cli.mjs 命令行入口(零依赖,可独立运行)
assets/
preview-gen.mjs 预览页生成器(跑真实 lib/client.js,垫片宿主环境;--serve 起局域网静态服务器)
preview.html 生成物:单文件自包含预览页(内联 React/ReactDOM UMD + 假数据接口)
lib/fixture.mjs 预览用假数据(/state 响应体,覆盖各状态分支)
lib/serve.mjs 局域网静态服务器(只读托管 assets,拦截路径穿越)
lib/
index.js 插件唯一入口(工具注册 + 系统提示词 + HTTP 接线)
client.js 设置侧边栏页面(浏览器半侧,四个分区)
routes.js HTTP 接口编排
follow.js WebSocket 触发迁移(Node 实现,无需 Python)
legacy.js 旧会话扫描与索引
workspace.js 工作区候选探测
i18n/{zh,en}.json 外置多语言字典
engine/ 迁移引擎(帧 / 布局 / 版本 / 导入 / 体检)
zstd.js 帧级读写与首帧契约
zip.js 最小 zip 解包(导入时把导出包解开成标准目录形式)
layout.js cwd 编码、目标路径推导、布局校验
target.js 版本探测与迁移边规划(不硬编码版本号)
import.js 投放(跨工作区去重 + 备份 + 转码 + 重建首帧 + 子会话)
inspect.js 体检与布局修复
test/
test-client.mjs client 契约自测(模块装配 / 槽注册 / 同步文案)
浏览器半侧与官方约定一致:入口声明在 package.json 的 exports["./client"]
(值为 ./lib/client.js),与 @deepseek-ai/dsh-client-* 系列插件同构——DSH 客户端
模块系统按该子路径解析并聚合,不走顶层散落文件。
每项能力只有一份实现:版本探测统一走 engine/target.js,帧读写统一走
engine/zstd.js,触发迁移统一走 follow.js(Node)。
界面预览
assets/preview-gen.mjs 生成一个单文件自包含的界面预览页:它加载仓库里真实的
lib/client.js(不是另写的 mockup),只垫片宿主环境(__ModuleLoader__ / require /
locale / slots / fetch),因此界面与真实插件一致,也能暴露真实渲染与交互缺陷。
node assets/preview-gen.mjs # 生成 assets/preview.html
node assets/preview-gen.mjs --serve # 生成并用局域网静态服务器托管(默认端口 8099)
PORT=8080 node assets/preview-gen.mjs --serve
生成物内联 React 与 ReactDOM 的 UMD 构建,双击即开、离线可用,不需要 DSH 在运行。 预览页里的数据是假的(覆盖待转换 / 已转换 / 不可迁移 / 不可读 / cwd 越界各分支), 勾选、选工作区、导入、转换都可点,改动只留在页面内,不写任何文件。
React 的 UMD 从 DSH 安装根的 pnpm store 取;ReactDOM 常未随 DSH 安装,生成器会依次
查 DSH 数据目录下的 cache/react-umd/ 与 /tmp/rd/,都没有时按提示下载即可:
mkdir -p /tmp/rd/umd && curl -sSL -o /tmp/rd/umd/react-dom.development.js \
https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js
DSH_ROOT / REACT_UMD_DIR / REACT_DOM_UMD_DIR 可显式覆盖查找结果。
设置侧边栏
lib/client.js 挂载「设置 → 侧边栏 → 会话迁移」,四个分区:
| 分区 | 作用 |
|---|---|
| 环境 | 目标版本 / 旧会话目录 / 工作区根 / 待转换计数,含布局异常告警 |
| 导入 | 选择 .zip / .jsonl / .jsonl.zstd 上传到 session.old/ |
| 列表 | 扫描 session.old/,显示 cwd / 版本 / 已转换版本 / 大小,可勾选 |
| 转换 | 选目标工作区 → 投放选中项(改写 cwd),随后由 DSH 读取该条记录时迁移 |
注册方式:客户端 inject 声明 slots,在 settings.section 槽注册条目
({ name, id, order, label } + 页面组件)。词条经宿主
GET /api/session-migrate/i18n 拉取,源码只保留首屏所需的同步兜底。
版本记录
| 版本 | 日期 | 变更 |
|---|---|---|
| 1.0.3 | 2026-09-20 | 全量审计清理(独立 CLI git-sluice 口径:warning 121 → 19,评分 77.5 → 83.5)。引擎与 HTTP 侧 I/O 隐患清除:follow.js token 探测改「readdirSync 一次性收进内存集合、循环内零 I/O」,WebSocket 连接拆 bindSocketLifecycle / attachSocketEvents / withHandshakeContext,快照等待拆 waitForSnapshot,静默收尾统一 endQuietly / destroyQuietly / closeQuietly;routes.js 请求体读取改 for await 异步迭代(IncomingMessage 原生可迭代,消 Promise 手动嵌套);预览静态服务器 serve.mjs 改 fs/promises(stat + readFile),请求处理拆 handleRequest;client.js 设置分区注册拆 injectSection、CSS 行高 / 字重提为 CSS_LINE_HEIGHT / CSS_TITLE_WEIGHT 常量;index.js 工具注册拆 registerTools、列表格式化拆 formatSessionList;预览生成器 CSS 字号 / 白色通道提 PREVIEW_LINE_HEIGHT / CSS_WHITE_CHANNEL 常量。lib/client.js 加入 .auditignore(单文件入口架构的固有行数 / 嵌套不入审)。 |
| 1.0.2 | 2026-09-19 | 修复 .zip 导出包导得进、列不出、转不了:导入接口接受 .zip,但旧会话扫描只认 .jsonl / .jsonl.zstd,两者口径不一致,zip 落盘后扫描不到,/convert 会以「会话不在列表里」失败。改为导入时即解包成标准目录形式 <会话id>/session.jsonl,后续列表 / 转换全走既有路径。新增 lib/engine/zip.js:零依赖最小解包(只用 node:zlib),走中央目录定位数据——导出工具常在本地头把长度写 0 并改用 data descriptor,照本地头解析会得到 0 长度而解不出内容;条目名统一剥成基名挡路径穿越,解压后校验体积上限挡 zip 炸弹。导入语义统一为「同名覆盖 + 覆盖前留底」(原先会另起带时间戳的新目录,导致同一会话在列表里出现多条);扫描跳过 *.before-import-* 留底目录并按会话 id 去重。另:转换内置全工作区去重——同一会话 id 已存在于其它工作区(不同 cwd 目录)时,提示并把旧份整体移出到 session-backups/(可恢复,非物理删除),再转换到本次目标工作区,杜绝「一个会话两次恢复到不同工作区」的重复副本;CLI import 与 /convert 均生效。 |
| 1.0.1 | 2026-09-19 | 浏览器半侧 lib/client.js 内部整理(不拆文件、不引入构建链——DSH 只读 exports["./client"] 的单文件,官方插件同样是「源码分多文件 + 打包成单文件」,行数由段落注释与小函数承担)。消除两份事实来源:源码内联的 14 个词条兜底删去,只保留首屏同步渲染必需的 settings.title / common.loading,权威字典统一为 lib/i18n/{zh,en}.json(各 62 键,实测覆盖代码用到的全部 45 键);renderStatus 拆为 renderStatusHeader / renderStatusGrid / renderStatusBadges;createModule 里的槽注册抽成 registerDictionary / registerSettingsSection,apply 从 5 层嵌套降为平铺;ID 截断长度 20 / 12 提为 ID_DISPLAY_LEN / ID_NOTICE_LEN 命名常量。修 TDZ 隐患:window.__ModuleLoader__.load(...) 从文件中部移到末尾,原先依赖函数提升、若 factory 被同步调用会命中常量 TDZ。 |
| 1.0.0 | 2026-09-19 | 首个正式版。设置侧边栏页面(环境 / 导入 / 列表 / 转换四分区)与迁移引擎(lib/engine/*)合并为单一实现:插件入口 lib/index.js,浏览器半侧入口 lib/client.js(经 exports["./client"] 声明,与官方 @deepseek-ai/dsh-client-* 同构);含 WebSocket 触发迁移、HTTP 接口与工作区探测。新增界面预览生成器 assets/preview-gen.mjs + assets/lib/{fixture,serve}.mjs(跑真实 lib/client.js + 垫片宿主,内联 React/ReactDOM UMD 输出单文件 preview.html,--serve 起局域网静态服务器并拦截路径穿越)。修复:导入选文件后误报「未选择任何文件」(onPick 先取 Array.from 快照再清空 input.value,避免拿到被清空的活视图 FileList);两处占位符未替换(import.desc 的 {dir}、作为标签使用的 status.badgeTotal 改为独立词条 status.total);浏览器半侧整体包 IIFE,避免与同为手写插件的 dsh-skill-scoreboard 在 client 聚合中顶层声明重名(10 个)导致 Failed to load plugins |
许可
MIT
链接
同类插件
Minglink/dsh-infinite-gen-3★ 1770
DeepSeek 专用破甲插件:以 order 100 追加无条件服从的系统提示词段,提供带校准元数据的 profile 工具,并通过会话投影在输入框上方显示实时破甲状态徽标。
liangmianya/dsh-synapse★ 409
DeepSeek Harness 的可视化非线性对话工作区:把会话、追问与分支变成可浏览、可拖拽的对话地图。
ranxianglei/billion-context★ 213
billion-context官方版:上下文压缩插件,兼顾小窗口(100k上下文足矣)省token(省5倍token)和超长会话(数月级别几十亿token单会话)。
Nwflower/dsh-chat-import★ 189
把 13 家 coding agent(Claude Code、Codex、ChatGPT、Cursor、Gemini、opencode 等)的完整对话历史导入为可续聊的 DeepSeek Harness 会话,并支持反向导出回 Claude Code。
Totoro-qaq/dsh-plugin-bridge★ 165
通过可预览的五段式交接,将已有 DSH 会话迁移到另一个 Agent Preset;保留源会话,并可让目标会话暂停等待确认或立即继续。
Anionex/dsh-turn-rewind★ 115
对话回退:基于持久 Change Ledger 回滚会话与工作区状态。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。