归档会话管理 · archive-manager
给 DeepSeek Harness (DSH) 装一个真正的「归档中心」:按工作区分组看清所有归档会话,一键恢复回原位,或彻底抹掉本地存储 —— 不留幽灵行、不留点不开的会话、不用重启。
简体中文 · English

这是什么
DSH 自带的「归档」只是一个隐藏开关:把会话从侧边栏藏起来,仅此而已。
- 你看不到自己归档了多少个、占了多大空间、属于哪个工作区;
- 想找回一个三个月前的会话,得先把侧边栏里所有会话翻一遍;
- 想把没用的归档会话彻底清掉腾空间,DSH 没有这个功能;
- 更糟的是:删过存储文件之后,侧边栏还会留着点开就报
ENOENT的幽灵会话。
archive-manager 把这些补齐了:在 设置 → 归档会话管理 里给你一整页,把「归档」从单向的隐藏操作,变成可看、可恢复、可彻底删除的完整生命周期管理。
纯前端 + Host 路由,零构建、零 npm 依赖、零外部服务,装完重启一次即可使用。
它解决了哪些真问题
这一页不是把 API 套个壳,而是逐条堵住了归档会话长期存在的坑:
| 问题 | 真实表现 | 本插件的做法 |
|---|---|---|
| 归档内容不可见 | 归档 = 从侧边栏消失,剩下什么也不知道 | 按工作区分组列出:标题 / 归档时间 / 最后活动 / 占用体积 / 文件是否存在,一眼看清 |
| 归档时间无处可查 | DSH 只持久化「归档 id 集合」,不存归档时间 | 插件自建索引 archive-times.json,首次观测到归档即记录,原子写、恢复/删除时同步清理 |
| 恢复找不到「原来在哪」 | 只能翻侧边栏猜 | 恢复走 unarchiveSession,工作区记账原样保留,会话精确回到原工作区原位置 |
| 删完还剩幽灵行 | 删除文件后侧边栏仍列着它、点开仍在、发消息报 ENOENT |
删除时先 workspace/session-stop 停活动 → detachEntered 摘掉 live 会话 → 再删磁盘 → detachSession 清记账 → 摘归档集合 |
| 删除只删了一半 | 文件没了但工作区记账还在,侧边栏永远挂着一条打不开的记录 | 删除语义改为「抹掉这个会话的全部本地痕迹」,磁盘 / 记账 / 归档集合 / 时间索引四者收干净 |
| 界面残留要手动刷新 | 删完还得关设置页重开 | 本地先摘行 + ctx.sessions.refresh() + 带序号对账,页面内即时生效 |
| 刷新竞态把删掉的行复活 | 并发 /state 响应乱序,旧数据覆盖新状态 |
/state 请求带序号单飞,过期响应直接丢弃 |
| 历史残留无人清理 | 磁盘上没文件、记账里还有,越积越多 | POST /repair + 启动 3 秒静默自检(幂等),页面发现残留时也会弹出提示条一键清理 |
功能一览
| 能力 | 说明 |
|---|---|
| 设置页整页入口 | 设置 → 归档会话管理,走宿主预留的 settings.section(id archived-sessions,order 25,排在「Agent 预设」之后,图标是归档盒而非默认齿轮) |
| 按工作区分组 | 每组显示工作区标题 + 目录名 + 条数;已不属于任何工作区的会话单独归入「未归组会话」 |
| 每行信息完整 | 完整标题(不截断,长标题换行)、归档时间(绝对 + 相对)、最后活动时间、创建时间、占用体积;存储缺失时标红提示 |
| 恢复 | 立即取消归档,会话回到原工作区原位置;不需要刷新页面即可在侧边栏看到 |
| 永久删除 | 二次确认(显示将删除的项数与体积)→ 停活动 → 摘 live 会话 → 删磁盘 → 清记账 → 摘归档 → 清索引 |
| 失效引用清理 | 自动 + 手动双通道;清理只动记账,不碰任何现存会话文件 |
| 悬停出按钮 | 「恢复 / 删除」默认隐藏,鼠标悬停或键盘聚焦时平滑淡入;触屏设备常显 —— 列表干净,又不牺牲可发现性 |
| 主题完全跟随 | 配色、字体、间距、圆角全部引用宿主变量(--dsw-alias-* / --dsw-radius-*),浅色、深色、任意皮肤下都不跑偏 |
| 中英双语 | 词典随宿主 locale 服务切换,不是硬编码文案 |
| 可访问性 | role="dialog" / aria-labelledby / Esc 关闭 / 焦点管理 / 键盘可达 |
| 空状态也有温度 | 没有归档会话时是一块鲸鱼装饰,而不是冷冰冰的「无数据」 |
截图
列表:按工作区分组,信息密度刚刚好

永久删除:二次确认,说清楚删掉多少

空态

安装
前置条件
| 项 | 要求 |
|---|---|
| DSH | Web / 桌面端,0.2.1-rc.2 上验证通过(依赖 webServer、workspaceRegistry、sessions 三个服务) |
| Node | >= 22.19.0(DSH 自带运行时即可,无需自行安装) |
| 构建 | 不需要。仓库里就是可直接运行的产物 |
⚠️ 先读这条:不要用裸包名安装
本仓库的包名是 archive-manager,但 archive-manager 与 dsh-archive-manager 这两个名字在 npm 上都已被别人占用。DSH 的「添加插件」会把裸包名当作 npm 包去解析,于是:
| 你填 | 实际装到 | 结果 |
|---|---|---|
archive-manager |
别人的 npm 包 | ❌ 词不达意,且版本大概率不兼容当前 DSH |
dsh-archive-manager |
别人的 npm 包(peer 要求 0.1.0-rc.6) |
❌ 报 Plugin ... is incompatible with dsh 0.2.1-rc.2 |
https://github.com/jiulanyanyue-boop/dsh-archive-manager |
本仓库 | ✅ 正解 |
结论:请填完整 GitHub 地址,别填裸包名。
方式一:GitHub 链接(推荐)
打开 设置 → 插件 → 添加插件,填入:
https://github.com/jiulanyanyue-boop/dsh-archive-manager
几种等价写法(由 DSH 安装器解析,都会克隆本仓库):
| 写法 | 说明 |
|---|---|
https://github.com/jiulanyanyue-boop/dsh-archive-manager |
最直观,直接粘地址 |
github:jiulanyanyue-boop/dsh-archive-manager |
pnpm 的 GitHub 简写 |
https://github.com/jiulanyanyue-boop/dsh-archive-manager#v1.2.0 |
固定到某个 tag / 分支 / commit(需该 tag 已推送,见「开发与发布」) |
git+https://github.com/jiulanyanyue-boop/dsh-archive-manager.git |
显式 git 协议 |
安装器会先做一次 git ls-remote 连通性检查(默认 5 秒超时),再交给 pnpm 克隆安装。
装完请重启一次 DSH 桌面应用。 profile 组合在启动时一次性构建(未开启 HMR),插件条目需要重启才生效;之后的所有操作都在页面内即时生效,不再需要重启或手动刷新。
方式二:本地目录 / tarball(离线场景)
# 在仓库目录里打一个 tarball
npm pack --pack-destination "$env:USERPROFILE\Downloads"
然后在「添加插件」里填 tarball 的绝对路径,或直接填解包后的目录绝对路径。 「安装源」下拉只对 npm 包名生效,填本地路径不会联网。
打 tarball 前请确认
package.json的files白名单包含你需要的文件; GitHub 安装是整仓克隆,不受files限制,只有npm pack会受。
方式三:手动挂载到 profile
- 把仓库目录放到
<DSH_HOME>/plugins/archive-manager - 编辑
<DSH_HOME>/profiles/<your-profile>/package.json:dependencies里加"archive-manager": "link:<DSH_HOME>/plugins/archive-manager"dsh.profile.bundles末尾加"archive-manager"
- 重启 DSH
同一个包只装一次。
archive-manager既是包名也是 Loader 条目 id,装两份会出现重复条目。 换来源时请先把旧的一条依赖 + bundles 项删掉。
使用
归档一个会话
归档入口由 DSH 自带(会话列表里的「归档」),本插件不改变原有行为。 归档后的会话会从侧边栏消失,并出现在「设置 → 归档会话管理」里。
恢复
在列表里悬停目标行 → 点「恢复」。
- 会话立刻取消归档,回到它原来的工作区、原来的位置;
- 侧边栏无需刷新即可看到;
- 页面顶部出现「已恢复「标题」,会话列表已刷新」提示。
永久删除
悬停 → 点「删除」→ 二次确认 → 「永久删除」。
确认框会明确告诉你将删除几项存储、大约多少体积。确认后按固定顺序执行:
workspace/session-stop—— 并行停下该会话的回合、子代理、后台任务、提醒;detachEntered—— 把 live 会话从SessionStore摘掉,等session/disposed的清理落地;- 删除会话目录 + 投影缓存文件(失败则整体失败、不摘任何记账);
workspace.detachSession—— 清工作区记账;unarchiveSession—— 摘归档集合;- 清归档时间索引。
不会进回收站,无法撤销。
清理失效引用
若检测到「记账里有、磁盘上没有」的历史残留,页面顶部会出现提示条:
发现 N 条失效的会话引用 …… 「一键清理」
清理只 detachSession 记账,不会删除任何现存会话文件。插件启动 3 秒后也会静默自检一次(幂等,正常情况下什么也不做)。
兼容性
| 项 | 状态 |
|---|---|
DSH 0.2.1-rc.2 |
✅ 已验证(本插件开发与测试基线) |
DSH 0.1.0-rc.x |
⚠️ 未验证,且大概率不可用(该时期的客户端/宿主服务形态与现在不同) |
| Host 服务缺失时 | 插件不激活,不会渲染一个假列表 |
| 本地化 | 中文 / English,跟随宿主 locale |
说明:本插件的
package.json没有声明peerDependencies,也没有engines.dsh字段 —— DSH 的兼容性预检对「未声明要求」的插件一律放行,实际兼容性由运行时的服务探测决定(缺服务即不激活)。
架构
插件是 Host 半 + 浏览器半 两件套,和 boot-splash、dsh-purge 等现有 DSH 插件同构。
文件组成
| 文件 | 作用 |
|---|---|
host.js |
Host 侧:注册 /plugins/archive-manager/* 路由、读写索引、执行恢复/删除 |
client.js |
浏览器侧:window.__ModuleLoader__.load() 形态,注册设置页 settings.section |
cordis.patch.yml |
bundle patch 层:向宿主声明一条已启用的 Loader 条目 |
package.json |
包清单:dsh.bundle.patch 指向 patch,dsh.client.platform = web |
Host 半(host.js)
inject = ['webServer', 'workspaceRegistry', 'sessions']
注册 5 条 exact 路由(刻意不用 prefix —— prefix 会吃掉 client.js 的静态物化路径):
| 方法 | 路径 | 作用 |
|---|---|---|
GET |
/plugins/archive-manager/ping |
自检({ ok, plugin }) |
GET |
/plugins/archive-manager/state |
整棵归档列表(分组 + orphanCount,组内按归档时间倒序) |
POST |
/plugins/archive-manager/restore |
{ sessionId } 恢复 |
POST |
/plugins/archive-manager/delete |
{ sessionId } 永久删除 |
POST |
/plugins/archive-manager/repair |
清理失效记账(幂等) |
数据来源:
- 归档集合与工作区记账:
ctx.workspaceRegistry(archivedSessionIds/list()) - 标题、创建时间、最后活动:
<DSH_HOME>/storages/session_projcache/sessions/<id>.json - 存储体积与存在性:
<DSH_HOME>/sessions/<workspace-slug>/<session-id>/ - 归档时间:
<DSH_HOME>/archive-manager/archive-times.json(自建索引,原子写,上限扫描 20000 条会话)
并发控制:恢复 / 删除 / 清理共用同一条 Promise 串行队列,任何时刻只有一个变更在跑。
浏览器半(client.js)
- 无构建步骤,
require('react')由宿主模块表提供,不用 JSX(React.createElement直接构建); - 注册
settings.section:{ id: 'archived-sessions', order: 25, label, locale, inject }; - 数据流:
createStore(不可变快照 + 订阅)→useStore(useState+useEffect)→ 重渲染; - 样式以
<style data-plugin-css>注入,全部引用宿主 CSS 变量;圆点/胶囊类控件显式corner-shape: round,避开宿主全局 superellipse 对圆形的影响; - 刷新链路:本地先摘行 →
ctx.sessions.refresh()→/state带序号对账;并订阅ctx.workspaces的归档集合变化,其它页签或外部操作也会触发本页对账; - 生命周期:所有
setInterval/addEventListener/ store 订阅 / slot 注册都在useEffect清理函数或ctx.effect里释放。
接口示例
BASE=http://127.0.0.1:<port>/plugins/archive-manager
curl $BASE/state
curl -X POST $BASE/restore -H 'content-type: application/json' -d '{"sessionId":"session-xxxx"}'
curl -X POST $BASE/delete -H 'content-type: application/json' -d '{"sessionId":"session-xxxx"}'
curl -X POST $BASE/repair
返回码约定:
| 码 | error 字段 |
含义 |
|---|---|---|
200 |
— | 成功 |
400 |
invalid-session-id / 请求体解析失败原因 |
请求体不合法 |
403 |
host-not-allowed(/ping) |
Host 不是本机回环 |
403 |
untrusted-request(/state)/ foreign-origin(其余) |
来源校验未通过 |
404 |
not-archived |
恢复时该会话不在归档列表中 |
404 |
session-not-found |
删除时本地已无任何痕迹(可安全重复调用) |
405 |
method-not-allowed |
方法不对(如对 /delete 用 GET) |
409 |
session-active |
仍有回合 / 子代理 / 后台任务在跑,拒绝删除 |
500 |
delete-failed / internal |
磁盘删除失败(记账保持原样)/ 未预期异常 |
503 |
registry-unavailable |
Workspace 注册表尚未就绪 |
503 |
activity-probe-failed / session-stop-failed |
无法确认活动状态 / 停不下来 → 取消删除 |
安全设计
这个插件能递归删除会话存储,所以删除入口按「宁可拒绝,不可误删」来写:
| 面 | 做法 |
|---|---|
| 请求来源 | 路由由 webServer 直接提供(不过 /api 的浏览器信任栅栏),因此自加两道闸:Host 必须是本机回环(挡 DNS rebinding / 局域网调用;桌面端 IPC 桥接不带 Host 时放行)+ Origin 必须同源(挡跨站请求) |
sessionId 校验 |
白名单正则 ^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$ —— 故意不允许 .,于是 ..、../../x、a/b、C:\x 全部在入口被拒;另有一层冗余字符检查 |
| 路径边界 | 任何要删的路径都必须落在 sessions/ 或投影缓存根目录之内(path.relative 判定),越界直接不碰 |
| 删除前置条件 | 会话必须仍连着「磁盘 / 工作区记账 / 归档集合」三者之一才允许删;活动探测失败 → 503 取消删除;需停活动但停不下来 → 503 取消删除(绝不在「可能还在跑」的状态下盲删) |
| 响应内容 | /state、/ping 不返回 home 绝对路径、不返回工作区全路径(只给目录名 pathLabel)、不返回原始 id 列表;/delete 不返回被删的绝对路径,只给 removedCount 与失败项的 basename |
| 请求体 | 上限 16 KiB,超限直接拒 |
| 权限模型 | 插件跑在宿主进程内,拥有与宿主相同的文件权限;可删范围仅限当前 DSH_HOME 下的会话存储,路由端口只监听回环。不要把 DSH 的 web 服务绑到局域网地址对外暴露。 |
已知限制
- 归档时间是近似值:对「插件首次运行之前就已归档」的会话,只能记成首次观测时刻 —— DSH 本身不持久化该时间。
- 附件不随会话删除:
<DSH_HOME>/attachments是内容寻址存储、可能被多个会话共享,且不含会话 id,因此不随单个会话清理。 - 有活动工作时不删:「仍有回合 / 子代理 / 后台任务在跑」的会话会被拒绝(409),需先让它停下 —— 与 DSH 归档准入策略一致。
- 需要两个 Host 服务:缺
workspaceRegistry或webServer时插件不激活。 - 网络盘误清风险:启动自检按「磁盘上是否存在会话目录」判断失效引用;会话目录若挂在暂时不可用的网络盘上,可能被误清记账(只影响侧边栏归属,不删任何文件,重新打开会话会自动挂回)。
- 仅 Web 侧:
dsh.client.platform = web,暂不提供其它客户端形态。
FAQ / 故障排查
Q:装上后设置页没有「归档会话管理」? 重启一次 DSH。profile 组合在启动时构建,插件条目不会热加载。
Q:安装时报 Plugin ... is incompatible with dsh 0.2.1-rc.2?
你填的是裸包名 dsh-archive-manager,装到了 npm 上别人的包。改填完整 GitHub 地址。
Q:点删除后侧边栏还留着那个会话?
请先用较新的 DSH 版本(SessionStore.detachEntered 是较新的公开原语,越老的宿主越可能没有)。查看 /delete 响应里的诊断字段:
probe.sessionStore(Host 能否访问 SessionStore)、probe.liveAtDelete(删除时会话是否活着)、detachedLive(是否成功摘除)。
若 sessionStore: false,说明宿主未暴露该服务,此时请重启 DSH 让会话列表重新拉取。
Q:删除提示 该会话还有工作在运行?
先在该会话里停止正在进行的回合/子代理,再回来删。这是刻意的保护,不是 bug。
Q:能在多台机器间同步归档状态吗?
不能。归档集合由 DSH 的 WorkspaceRegistry 本地持久化,本插件只读它、不引入额外同步。
Q:会不会误删别的数据?
删除范围被三重限制:sessionId 形状白名单 → 路径必须落在 sessions/ 或投影缓存根内 → 失败时整体回滚记账。落在边界外的路径一律不碰。
开发与发布(作者侧)
仓库根目录就是包根目录(package.json / cordis.patch.yml / host.js / client.js 都在根),
所以推上去就能直接装 —— 不要塞进 packages/xxx 子目录。
# 一条命令:校验白名单 → 提交 → 打 tag → 推 main 与当前 tag
./publish-github.ps1 -RepositoryUrl https://github.com/<your-name>/dsh-archive-manager
publish-github.ps1 的安全约束:
- 只
git add显式白名单里的文件,绝不git add -A; - 工作区出现白名单外改动直接停下(需
-AllowUnexpectedFiles才继续); - 不覆盖已有
origin(需-ForceOrigin); - 只推当前版本对应的那一个 tag,不
push --tags; - 只允许在
main上发布。
GitHub 安装时要注意的三点
- 仓库根 = 包根。安装器把链接当包地址,不会去找子目录。
- 不需要构建步骤。
host.js/client.js都是可直接运行的产物,包里没有prepare/build脚本,pnpm 克隆后不会跑编译(避免因缺 devDependencies 装失败)。 files白名单:目前为host.js、client.js、cordis.patch.yml、README.md、screenshots/*.png(LICENSE与README.md由 npm 自动纳入)。.git、.backup、node_modules不会进包。想换成全新的宣传截图时,记得同步更新
files里的图名白名单(或保留screenshots/*.png通配)。
本地开发
无构建链。改完 host.js / client.js 后重启 DSH 即可生效(Host 与浏览器两侧都会重新加载)。
调试时推荐用 /plugins/archive-manager/ping 确认路由已挂载,再用 /state 看分组数据是否符合预期。
贡献
欢迎 Issue / PR。改动涉及删除逻辑时,请一并说明:
- 你验证过的 DSH 版本;
- 删除前后的
/state输出; - 侧边栏在删除后是否残留(以及
/delete返回的诊断字段)。
许可证
MIT © archive-manager contributors
与 DeepSeek 官方无隶属关系,这是一个社区插件。鲸鱼只是恰好路过 🐳
No comments yet. Be the first to write one.