jr-create/dsh-session-vault ↗★ 0
dsh-session-vault
Browse, export, import and carry DeepSeek Harness (DSH) sessions as portable, version-normalised archives. DSH 会话保管库:浏览、导出、导入与跨机器搬运会话。 适合需要对DSH会话进行归档、清理、导入导出及跨设备迁移的用户。
같은 패키지 이름의 다른 저장소
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:jr-create/dsh-session-vault使用
图形界面
设置面板分四个标签页:
- 导出 —— 搜索、勾选、
导出所选并下载。归档写入/dsh-session-vault/exports/并直接下载到浏览器。 - 导入 —— 点击或拖入
.dshsession;面板会先列出归档内容。可填新的工作区目录、选择冲突策略、勾选是否自动建目录,再预览或开始导入。 - 归档 —— 已导出的归档列表,支持下载 / 导入 / 删除。
- 清理 —— 三类垃圾各一个范围标签:可清理(未挂载且未归档)、已归档的未挂载(默认折叠,因为归档常常已是唯一的副本)、空壳会话(从未使用)(创建过但一句对话都没有)。计数始终可见,点标签切换。先
预览,再勾选「我明白」才能启用删除,最后还会再弹一次确认。空列表时也会把折叠起来的范围和数量一并说出来,不会因为"藏起来了"就看不到出路。
会话标题是怎么来的
面板里的标题按三档解析,每行还会标出来源是哪一档:
- 投影缓存(
titleSource: 'cache')—— 宿主已经算好的标题,最便宜。 - 会话自己的第一条人类消息(
titleSource: 'first-prompt')—— 缓存只覆盖本进程投影过的会话,所以"缓存没命中"是常态而不是异常:换了进程、换了机器、或者会话本来就没被投影过,缓存里都没有。这时才去打开日志,取第一条user/message且source.kind === 'user'的事件(注入的上下文也是user/message,但kind是plugin,不能拿来当标题),截断到 5 个词 / 40 个 UTF-8 字节,并先剥掉转义序列、控制字符和方向标记——第一条 prompt 是不可信文本,原样显示可能改写终端标题或让这一行看起来是别的意思。这一档只扫前 32 个事件、一次列表最多扫 128 个会话、最多并发打开 4 个日志,剩下的宁可不显示也不让面板等几百次解码。 - 都没有(
titleSource: null)—— 显示「(无标题)」。
事件数(eventCount)在列表里恒为 null:sessionPersistence.list() 只给 sizeBytes,而数事件要解压整份日志,列表页不该做这件事。所以面板这一列是省略而不是显示一个错的数字——真实事件数只在日志本来就被打开的地方才有(导出结果、归档检查)。
「已存储的会话」≠「侧栏里的会话」
这是最容易让人误会的地方,所以面板顶部有一个筛选器把这些数直接报出来:
| 筛选 | 含义 |
|---|---|
| 全部 | sessionPersistence 里所有会话日志 |
| 侧栏可见 | 属于某个工作区,侧栏里看得到 |
| 未挂载工作区 | 日志还在磁盘上,但不属于任何工作区(与「清理」标签页的口径一致:两个视图都不认领,才叫未挂载) |
| 子代理 | origin: 'subagent' 的子会话,侧栏本来就不显示 |
为什么会有「未挂载」的? 两种常见的:
- 删除了工作区。 DSH 删除工作区是只注销工作区、明确保留每一个会话日志(这是它的文档行为)。工作区没了,它的会话就从侧栏消失,但日志仍在磁盘上——于是本插件还列得出来。看起来就像"删了还在"。
- 子代理会话。 它们有日志、没有工作区归属,侧栏也不显示。
本插件不会因为它们不在侧栏就把它们藏起来——那些日志恰恰是最值得抢救的东西(比如工作区被误删)。它做的是把差异标出来:每行带 未挂载工作区 / 子代理 徽章,顶部报四个计数,并且可以按范围筛选、按范围导出。
要真正移除它们,得删掉会话本身(删工作区不够)。本插件现在提供这个操作——清理 标签页,或 session_delete 工具——但它是整个插件里唯一绕过服务直接动文件的动作,因为 SessionPersistence 根本没有删除接口。
这是对早期版本的反转:这里原本写的是"本插件故意不提供删除"。反转的理由是,那些日志会永远留在磁盘上、没有任何出路,而"彻底没有出路"并不比"有出口但加围栏"更安全——尤其当其中一个会话已经大到 1 MB 且 DSH 自己都读不出来时。围栏是:
- 只接受没有工作区认领的会话;已归档的还要额外显式 opt-in(
includeArchived),因为归档的存在就是为了能恢复,默认把它变成"可删除"等于把那条退路也删掉。工作区归属同时看注册表的校验视图和workspace.json的持久账本,取并集——注册表会把读不出头部的会话从sessionIds里过滤掉,只看它就可能误判;反过来,注册表的视图要等它启动时建好 canonical-cwd 索引才有内容,所以标签也用账本补上,并标出是哪一个视图认领的(workspaceClaim: 'registry' | 'ledger')。 - 空壳会话是另一类,需要另一个 opt-in(
includeEmpty):它是已挂载的,只是日志里没有任何对话。这比删除孤儿更宽——一个工作区名下的会话本来绝不可删——所以它的门槛也更高:只有在宿主读过日志并确认里面没有一条对话(conversation === false)时才会放行。日志读不出来导致的"不知道"永远不算放行,那种情况仍然按"挂载中"拒绝。 - 正在运行的会话拒绝删除。 在打开着的写句柄下面删日志是损坏,不是回收。查询活动会话时如果存储服务自己抛错,也按"可能活着"拒绝。
- 目录名由 id 经 DSH 自己的段编码器重新推出(
./..、分隔符、盘符、NUL 全部转义),再证明结果严格位于 sessions 根之内,所以任何构造出来的 id 都指不到别处。 - 真正删除必须显式
confirm: true;界面另外要求勾选"我明白删除不可撤销"并再弹一次确认。三道都过才动手。
删除会一并清掉投影缓存条目和(如果可达)dsh-spill 的溢出文件。不会生成归档——想留就先导出。
删除后会重新读一次会话列表,实测哪些 id 仍在被当前进程列出(survivors)。正常情况下是空的:sessionPersistence.list() 会重新扫描磁盘,所以删掉的会话立刻从列表消失,不需要重启。只有服务把列表缓存住时才会报出来,并明确告诉你重启 dsh 即可清除。
模型工具
Agent 可以直接调用:
session_list 列出所有已存储会话(orphansOnly 只列可删除的;unmountedOnly 列所有没人认领的,含已归档)
session_export 导出到 .dshsession
session_archive_inspect 查看归档内容(只读)
session_import 从归档导入
session_delete 永久删除孤儿 / 空壳会话(先 dryRun,再 confirm: true;
includeArchived 删已归档的,includeEmpty 删从未使用的)
HTTP 接口
浏览器端走 /api/dsh-session-vault/*:
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /status | 插件版本、目录、宿主服务可用性 |
| GET | /sessions | 会话列表(每项带 mounted / orphaned / workspaceClaim / titleSource) |
| GET | /orphans | 可清理的会话及可回收字节数;?includeArchived=1 带上已归档的,?includeEmpty=1 带上从未使用的;unmountedCount / archivedCount / emptyCount 三个计数不受筛选影响,永远报告真实总量 |
| GET | /archives | 已导出的归档列表 |
| POST | /export | { ids, fileName? } → 生成归档 |
| GET | /download?file= | 下载归档 |
| POST | /upload?name= | 上传归档(原始字节) |
| POST | /inspect | { file } → 归档内容 |
| POST | /import | { file, workspacePath?, mode?, dryRun? } |
| POST | /purge | { ids, dryRun?, confirm?, includeArchived?, includeEmpty? } → 删除孤儿或空壳会话 |
| POST | /delete | { file, confirm: true } |