dsh-plugin-archive-shelf
给 DeepSeek Harness Web GUI 用的归档架。
DSH 的会话可以归档,但产品没有回程票:侧栏菜单只能把它藏起来,没有取消归档入口, 也没有办法删除会话日志。这个插件补上这一页:列出所有已归档会话,并给出缺的那几个动作: 还原、排队删除(可取消)、以及在有宿主支持时的即时释放。
English | 中文
你会得到什么
设置 → 归档架(在「Agent 预设」下面那一栏):
- 每条已归档会话的标题、工作区、目录、归档时间与磁盘占用;
- 还原 —— 把它从归档集合里移除,会话立刻回到侧栏原来的位置(归档从不改动它的工作区槽位);
还原时插件会做两件产品自己不会做的事:重新拉一次浏览器会话列表(被释放过的会话已被宿主
的
session/disposed从列表里移除),并把新的归档集合对齐给产品的工作区状态。两者都做完, 侧栏才会立刻显示它。唯一的例外:还原的是最后一条归档会话时没有对齐锚点,那时提示会明说 「刷新页面即可」; - 彻底删除 —— 二次确认后删除会话日志目录、投影缓存与工作区记账。不可恢复;
- 排队删除 / 取消排队 —— 给删不掉的会话(运行中或已载入)排一个队,
下次启动
dsh时自动删除;随时可以取消,取消后什么都不会发生; - 释放 —— 仅当你的 DSH 带
agents.release(id)能力时出现(见下), 点一下把会话的运行实例卸载掉,于是不用重启就能立刻删除它; - 每行状态徽标:运行中(agent 正在跑一轮)/ 已载入(常驻在宿主进程内存里)/ 已排队(下次启动时删除);
- 清理 —— 清掉那些磁盘上已经没有对应会话的失效归档记录。
排队删除意味着什么
排队不会立刻删任何东西,它只是记下你的意图:
- 队列存在 harness home 下的
.archive-shelf/pending.json(会话根目录的上一级),重启后仍在; - 兑现只发生在下次启动
dsh之后(启动后几秒的清扫);读取或刷新归档架永远不会删任何东西, 所以"排队"和"删除"是两件分开的事。清扫时若会话仍在常驻/运行中,条目会留到再下一次启动; - 排队中的行显示已排队徽标与取消排队按钮;取消是幂等的,点错一次不会有任何后果;
- 如果你在排队后又还原了这个会话,队列条目会被丢弃,绝不会删掉一个你已恢复的会话。
关于「释放」按钮
会话一旦在宿主进程里被载入,就再也没有公开的卸载入口 —— 这是 DSH 当前的形态
(agents 服务没有按 id 释放的方法,创建者拿到的那份 dispose 句柄被丢弃了)。
所以插件的做法是能力探测:
- 你的 DSH 若提供
agents.release(id),列表接口会报canRelease: true, 行上出现释放按钮:释放 → 该会话不再常驻 → 立刻可删,全程不用重启; - 没有这个能力时按钮不会出现(而不是点了报错),队列删除照常可用。
这份宿主侧能力目前不在上游 DSH 里。需要它就得给自己的 DSH 打补丁
(源码装改 packages/core/agent,npm 装用 pnpm patch),或者等它被上游接受。
让「释放」按钮出现(可选:给宿主打补丁)
补丁随仓库提供:patches/host-release.patch(只动 packages/core/agent —— 保留 agent
handle 的 dispose 能力,并加一个按 id 释放的 release(id),外加一个 161 行的测试)。
cd /path/to/deepseek-harness
git apply /path/to/dsh-plugin-archive-shelf/patches/host-release.patch
pnpm exec tsc -b packages/core/agent && pnpm --filter @deepseek-ai/dsh-agent exec tsdown
# 然后重启 dsh:归档架里常驻的行就会多出「释放」按钮
- 升级 DSH 之后补丁可能被冲掉,或被
git pull拒绝。 冲突时先git checkout -- packages/core/agent再git apply一次即可,改完必须重新构建并重启。 - npm 安装的 DSH 不能直接套用这份补丁:那里拿到的是编译后的
lib/,需要用pnpm patch @deepseek-ai/dsh-agent做等价改动(改动语义相同,落点不同)。 - 想让所有人默认用上,正确做法是把它提到上游 PR —— 补丁本身就是为这个准备的。
环境要求
- 带
webprofile 的 DeepSeek Harness。开发与验证版本:0.1.5-rc.2。 - Node
^22.19 || >=24(与 harness 自身要求一致)。
安装
一条命令 —— 装依赖与挂载一次完成,重复执行也安全:
dsh plugin --profile web add github:JIUYUE-SEP/dsh-plugin-archive-shelf
然后重启 dsh(Ctrl+C 后重新 dsh web)并刷新页面。静态客户端插件随页面的
boot graph 下发,单纯刷新并不保证拿到新插件(浏览器缓存、扫描时机都会搅局),
重启则一定生效。
这一步为什么够了:本包在自己的 package.json 里声明了 dsh.bundle.patch,并自带
cordis.patch.yml 层;dsh plugin add 在 pnpm 装完后会自动把这个层加进 profile 的
dsh.profile.bundles,所以不需要你手动编辑 ~/.dsh/profiles/web/cordis.patch.yml。
# 手动追加到 ~/.dsh/profiles/web/cordis.patch.yml,然后重启
- insert:
- id: archive-shelf
name: dsh-plugin-archive-shelf
本地安装
git clone https://github.com/JIUYUE-SEP/dsh-plugin-archive-shelf.git
dsh plugin --profile web add ./dsh-plugin-archive-shelf # 同样会自动挂载;重启生效
卸载
dsh plugin --profile web remove dsh-plugin-archive-shelf # 同时从 dsh.profile.bundles 里摘掉
若你当初是照上面那个折叠块手动加的 - insert:,再把它一起删掉即可。
DSH 升级后的自检
这份插件补的是产品明确没有的能力(归档是单向的、没有会话删除入口、没有卸载会话的 API),
所以它必然要碰一些没有被承诺的东西:workspace registry 的私有 setState、会话日志的磁盘布局、
以及宿主补丁提供的 agents.release(id)。升级 DSH 之后,花一分钟走一遍:
cd /path/to/dsh-plugin-archive-shelf && npm test # 两套件:客户端契约/渲染 + 宿主行为(会打印各项数)
- 重启后打开 设置 → 归档架:列表能出、徽标(运行中 / 已载入 / 已排队)正确、没有报错条;
- 挑一条不重要的归档会话点还原 —— 它应回到侧栏原位(验证
setState还在); - 挑一条不要的会话彻底删除 —— 目录消失,且侧栏不留打不开的残行;
- 看「已载入」的行上有没有释放按钮。没有就是宿主补丁被升级冲掉了,
按上一节重新
git apply+ 重建 + 重启即可(此时功能会自动降级为排队删除)。
失败应该是响亮的:拿不到私有 API、路径对不上、读不到会话列表快照时,插件一律拒绝并写出原因,
不会猜着删。真报错的话,把归档架里那句话连同 npm test 的输出一起提 issue。
实现方式
没有构建步骤:两半都是纯 JavaScript,直接随包发布。
lib/index.js(宿主半) —— 一个 Cordis 插件,注入workspaceRegistry、sessionPersistence、webServer;注册一条同源 JSON 路由(POST /archive-shelf/api), 实现list/unarchive/delete/forget/queue/unqueue/release七个操作。 删除直接用node:fs—— 宿主插件就是普通 Node 代码,不需要任何沙箱提权。 队列是本插件自己的状态,写在<harness home>/.archive-shelf/pending.json, 写入走"临时文件 + rename",避免半截文件。lib/client.js(浏览器半) —— 直接写成客户端模块加载器的工厂形态 (window.__ModuleLoader__.load({ id, factory }))。原因是 harness 自己的客户端打包预设 位于其仓库内部、并未发布。它只注册一个settings.section, 于是这一页出现在设置卡片的左侧 tab 栏里,产品源码一行都不用改。
路由是自证的:POST + content-type: application/json + 自定义头
x-archive-shelf-client: 1 组合起来对浏览器是非简单请求,跨站页面会先撞上
CORS 预检(本服务不回答),再叠加一层 Origin/Host 同源校验。
这防的是跨站请求,不防同机其它进程 —— 那些进程本来就能直接读写会话文件。
已知限制
- 取消归档走的是内部 API。 harness 没有公开的 unarchive 操作,插件通过 workspace
registry 自己的
setState改写归档集合。将来 harness 改了内部结构,插件会明确报错 而不是静默失败。 - 常驻或运行中的会话不能直接删。 宿主进程已载入的会话把日志握在内存里;删掉磁盘文件会留下 "活着的会话 + 没有落盘记录"(宿主每次追加都按路径重开日志文件,删了目录要么报错、要么重建出 一份残档)。这类行有两个出口:排队删除(可取消,下次启动时兑现), 或在你打了宿主补丁的机器上释放后立刻删除。
- 排队删除要等下一次启动。 队列的目标就是跨进程:队列里的会话在启动清扫或下次打开归档架时 被真正删除。若某次清扫时它又常驻了(比如你刚打开过它),它会继续留在队列里等下一轮。
- 「释放」需要宿主侧支持。 见上文;没有这个能力的 DSH 上按钮不会出现,功能自动降级为排队。
- 不删附件。 被删除会话引用过的二进制附件仍留在 harness home 里。
- 删除不可恢复,没有回收站。
- 产品允许归档一个正在运行的会话(侧栏的归档动作没有任何拦截),这会让它在隐形状态下 继续消耗 token。归档架里的运行中徽标就是用来发现这种情况的。
开发
npm test # 零依赖;两个套件:契约/渲染 + 宿主行为
test/plugin.test.mjs把宿主半当 ESM 模块导入、按客户端加载器的方式求值浏览器半, 并用一个极简 React 替身渲染出行,断言徽标、按钮可见性与禁用理由、失败文案, 以及每个按钮真正发出的那个请求;test/host.test.mjs用假 Cordis 上下文 + 真 loopback HTTP 服务驱动真的apply(), 在临时目录里真删真写:路径逃逸、超大请求体、运行中/常驻拒绝、队列的排队/取消/跨重启兑现、 释放能力的三种结果、以及降级服务组合。每一项都是可运行的回归断言,npm test打印准确项数。
改代码前值得知道的两条不变量:
- 浏览器 bundle 的模块 id 必须等于包名(
registration.id === manifest.name)—— loader 按这个名字查工厂;两者漂移时测试会失败。 - 宿主半不解析任何裸模块名,只 import
node:内置模块,其它能力全靠注入的服务。 这正是它能待在 harness 仓库之外却仍能加载的原因。
许可证
MIT —— 见 LICENSE。
No comments yet. Be the first to write one.