安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add "https://github.com/SunshineR04/dsh-session-manager/releases/download/v0.3.0/dsh-session-manager-0.3.0.tgz"
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
README
DSH 插件:彻底删除会话(永久物理删除,回收磁盘),配套一个跨工作区的 已归档会话汇总列表,以及会话三点菜单里的红色「彻底删除」项。
官方已经做了什么,本插件补什么
DeepSeek Harness 从不删除会话文件。 官方的删除工作区文案写得很直白: *「这只会把「{name}」从工作区列表移除。**文件夹和会话日志都会保留。*它的 会话会出现在未分组下。」 于是一个长期使用的安装会为每段对话永久保留一个 会话目录,没有任何回收磁盘的入口。
归档/恢复这半边已经不再是缺口。 从 dsh 0.1.7 起,官方自带三点菜单里的 归档与取消归档、侧栏的显示已归档 / 仅显示已归档视图筛选、撤销 toast, 以及一道准入闸门(归档会话及其子代理谱系会被阻止继续执行模型步骤)。请直接 用官方那套 —— 本插件不再试图替代它们。
| 能力 | 官方(dsh 0.1.7) | 本插件 |
|---|---|---|
| 归档 / 取消归档 | ✅ 三点菜单,停止工作进行中任务的确认,撤销 toast | ✅ |
| 找到已归档会话 | ✅ 侧栏显示已归档筛选 | ✅ 所有工作区汇成一张列表 |
| 删除会话文件 | ❌ 从不 | ✅ 红色彻底删除 |
| 批量操作 | ❌ | ✅ 全选 + 一次确认批量删除 |
每次删除都是直接物理删除,没有备份层。
功能
1. 设置页:设置 → 会话管理
- 列出全部已归档会话:标题、所属工作区、项目目录、更新时间、运行状态
- 每个会话提供 恢复(回到归档前在工作区中的位置)和红色 彻底删除
- 全选 + 批量删除:勾选若干行(或表头全选框)后一次确认删除全部选中 会话。流程串行且容错——单个失败会完整列出原因而其余照常删除;正在运行 任务的会话会被跳过(确认框会先告知数量);每次批量给出统一结果:全部成功、 部分失败或全部因运行中被拒绝
- 删除前弹出破坏性确认框——每次删除都是直接物理删除,没有备份层
- 数据来自官方的 session / workspace 客户端 store,实时响应,无需手动刷新
2. 会话三点菜单:红色的「彻底删除」
左侧会话列表中,鼠标悬停会话行 → 右侧 ⋯ 菜单,在原有 重命名 / 分叉会话 / 归档会话 下方新增红色「彻底删除」项,点击后同样弹出确认框。
该行是官方插槽
sidebar.workspaces.session.menu.item里的一个普通条目 (idsession-manager-delete,order 500),用与内置 置顶 / 重命名 / 分叉 / 归档 行相同的MenuItemButton原语渲染 —— 危险红、分隔线、键盘行为都直接 来自官方菜单本身,而不是对它的样式模仿。该插槽自 dsh 0.1.7-alpha.1 起提供。在此之前,这一行只能靠观测 DOM + React fiber 树反查会话来注入,于是官方菜单任何标记变化都可能让它静默消失 —— 0.1.7-rc.2 给每个菜单项加上快捷键提示,就正好如此。
3. Agent 工具(模型可直接调用)
| 工具 | 说明 |
|---|---|
session_list_archived |
列出已归档会话(JSON) |
session_restore_archived |
按 id 恢复(可逆操作,无需确认) |
session_delete_permanently |
按 id 彻底删除已归档会话;必须 confirm: true;拒绝未知 id、未归档 id 与正在运行的会话 |
安装
需要 dsh 0.1.7 这条线(含 rc 版本)。它的两个硬依赖都自 0.1.7 起提供:
尺寸中性的产品图标,以及三点菜单那一行所注册的
sidebar.workspaces.session.menu.item 插槽。在更旧的版本上,三点菜单不会挂载
这一行,删除只剩下设置页一条路径。
git clone https://github.com/SunshineR04/dsh-session-manager.git
dsh plugin --profile <name> add "file:<克隆目录的绝对路径>"
dsh plugin add 会通过 pnpm 安装并自动把包加入 dsh.profile.bundles;
本包的 cordis.patch.yml(bundle layer)自动挂载插件行,无需手改配置。
刷新页面后若插件仍未出现,再重启 dsh 桌面端(运行中的 profile 会在刷新时重建
大部分 patch,但新加入的 bundle 不保证会被拾取)。
⚠ 不要用裸 npm 包名安装本插件。 npm 上的
dsh-session-manager是另一个 无关项目(hkkz9522/dsh-session-manager, 已发布到 0.5.x,维护者不同),它同样会注册一个会话菜单行——所以装错也会"看起来能用"。 请按上面的方式从本仓库安装。
也可以在 profile 的 cordis.patch.yml 手工挂载(不推荐,bundle 通道更省事):
- insert:
- id: session-manager
name: dsh-session-manager
删除语义(重要)
⚠ 一个
DSH_HOME只应跑一个 host。 待删队列是单个读-改-写文件,而串行化它的操作锁是 进程内的:两个 dsh 实例共用一个 home 时,双方可能读到同一份快照、后写者赢,静默丢掉对方的标记。 该标记的墓碑从此没有任何东西能扫除,而文件已经不在——这一行既无法恢复、也无法再删除。scripts/twohost-race-probe.mjs用真实 manager 复现了它(输出RESULT: LOST 1 marker(s): …)。 忠实修法是改队列格式(按 id 的原子标记文件)或引入真正的跨进程锁,不是加重试循环。
「彻底删除」按经过设计的顺序执行:
- 仅已打开的会话:在只读存在性检查之后、任何变更之前先写入持久 的待删标记(崩溃安全——半途崩溃时下次启动总有标记可扫);从该队列 文件读回的 id 会先通过格式校验才允许触达文件系统。
- 登记清理(持久且广播):从所属工作区的
sessionIds摘除 (detachSession),再从全局归档集移除。摘除时并不只信任工作区sessionIds的过滤视图——注册表的 canonical-cwd 头索引一旦失准, getter 会把该 id「隐身」,导致 detach 被跳过、已删会话以「未分组」 身份复活(0.1.4 及之前的真实 bug),因此同时对照工作区的原始记录兜底。 - 删除会话文件:
~/.dsh/sessions/<编码项目目录>/<session-id>/(session.jsonl.zstd日志本体)。目录解析走三级兜底:注册表头 + persistencelocate→ persistence 头清单 → sessions 根目录原始扫描 (目录名与已校验的会话 id 精确相等),头接缝失准不再会静默漏删。 - 删除元数据缓存:
~/.dsh/storages/session_projcache/sessions/<id>.json及其.bak-*检查点。 - 广播官方
api-session/removed事件:所有已连接客户端的会话列表 立即移除该会话。(宿主自身只在 live 会话 dispose 时才发这个事件, 冷删除永远等不到。)
搜索索引(SQLite)会随源文件消失自动对账,无需处理;附件存储是内容寻址的, 保留不误伤其它会话,也不做删除。
- 每次删除都是直接物理删除——没有备份层,确认框请看清会话标题。
- 删除已打开的会话同样立即生效:登记、文件、元数据当场清除(后续
flush 不会复活任何东西——append 按路径打开日志,从不重建已删目录)。
由于 dsh 没有公开的「关闭会话」API,内存中的副本会存续到所属界面作用域
消失为止;此时该 id 以墓碑形式保留在归档集中,重启 dsh 后自动完成
收尾清理。有运行任务的会话会被拒绝;
allowDeleteRunning只跳过这道拒绝 ——强制删除同样打墓碑并入队收尾。- 删除当下:客户端不会再为这个 id 回拉会话列表(删除成功即加入本次
会话的待删集合;
api-session/removed到达后的去抖回拉同样会跳过,且 触发时与触发后各校验一次——删除响应与事件走不同通道,事件常常先到), 因此刚删掉的会话在任何视图里都不会立刻回到列表,包括 「全部对话(显示已归档)」。 - 唯一残留形态:重新加载页面、或另一个客户端,会从宿主列表重新学到 这个 id——因为 dsh 仍在列出内存副本。该 id 是归档项,默认视图(隐藏 已归档)始终隐藏它;只有在「显示已归档 / 仅显示已归档」视图下,它会以 归档样式出现在「未分组」。banner 里的待删队列就是它,重启后彻底消失。
- 修复钩子(0.4.1 起默认开启):每次读取删除队列时,宿主会为仍存活
的排队 id 重播官方
api-session/removed,所有已连接客户端随即把它从自己 的列表里移除——于是归档视图也能保持干净;代价是每次列表回拉后可能有一瞬间 的闪现(客户端按「一次残留一次修复」去抖,不会轮询)。客户端在宿主回应 ping 后就读取队列,读取失败会自动重试,因此一次瞬时失败不会让这个修复 在该页面整段失效。其他每一次显式回拉(「刷新」按钮、删除后的校验、取消删除) 都会在回拉之后重新检查残留,因此被回拉带回来的行同样会被修掉;而整批删除中 只要有一个是打开中的会话,就跳过整批回拉(这些 id 已由api-session/removed从 store 中摘除)。设为reannouncePendingRemovals: false可退回「仅墓碑」行为:此时重新加载页面 后,显示归档行的侧边栏会重新看到它。
- 删除当下:客户端不会再为这个 id 回拉会话列表(删除成功即加入本次
会话的待删集合;
- 待清理横幅:设置页顶部只列出仍可操作的条目。文件仍在盘上的
条目(例如删除中途崩溃的残留)提供「取消删除」,取消时先读队列、先摘标记
再清墓碑——反过来会留下「墓碑已清、标记仍在」,那是数据丢失:下一次启动会
读到这个残留标记,把用户刚刚取消掉的会话删掉。注册表拒绝解除归档时会把标记
写回,因此取消仍可安全重试;队列读不出来时什么都不会被改动;而不在队列里
的 id 会被以
session/not-pending拒绝(取消是唯一一个「把会话放回去」的操作, 不能去恢复一个没人排队的会话)。文件已删净的条目(打开 会话的正常删除)已无任何可执行动作,因此不再逐行占据横幅,而是折叠为 一行「已删除 · 重启后自动清理」汇总(附展开按钮可查看具体 id)—— 这也不仅是 UI 隐藏:通过 RPC/工具取消它同样会被宿主以session/data-gone拒绝(解除墓碑只会让无文件的内存残留以「未分组」复活)。 - 队列文件读不出来时,宁可不做也不猜:待删队列是删除收尾的唯一凭据,
每个写入者都会把「刚读到的那份快照」写回文件——所以把一次读取失败当成
「队列为空」会在下一次写入时抹掉全部标记,而它们的墓碑会永远留在归档集里
(文件已删、无法恢复、也无法清除)。因此:文件不存在=正常的空队列;
读取失败或 JSON 损坏(含非原子回退写造成的撕裂文件)=拒绝执行,与删除队列
相关的操作(列表、恢复、取消、打开会话的删除、启动收尾)统一返回
session-manager/internal并在宿主日志留痕,队列文件保持原样不动; 修复该文件后一切恢复正常。冷会话删除仍会继续执行(队列损坏时它是唯一 还能用的删除路径),但会在返回结果里明确写出「本次删除不具备崩溃安全」—— 没有标记,中途崩溃就没有任何东西能在下次启动时收尾。 - 冷会话删除在同一次调用内收尾,且不再有崩溃窗口:它同样先写待删标记、同样
在操作期间保留归档墓碑,文件删净后再一起清掉。文件阶段失败(Windows
EPERM/EBUSY、杀毒/索引器/外部句柄占用)时条目会同时保持墓碑与队列, 而不是报告「已删除」却让侧边栏把它当活会话重新渲染出来,下次启动的收尾流程会 重试。在此之前,冷会话删除不写标记,因此「归档集写入」到「rm」之间一旦崩溃, 该会话既没归档也没删除:下次启动会把它当未分组会话重新列出,而本插件再也 看不到、恢复不了、重试不了它。 - 恢复操作只从归档集移除 id —— 归档本身保留工作区
sessionIds槽位, 因此恢复后会话回到归档前的原位置。处于删除队列中的 id 会被以session/pending拒绝(文件仍在盘上时可先「取消删除」)——恢复它只会 暴露一个无文件的空壳。
配置
| 字段 | 默认 | 说明 |
|---|---|---|
sessionListLimit |
500 |
单次列表返回上限 |
allowDeleteRunning |
false |
强制删除正在运行任务的会话(仅跳过拒绝——强制删除同样打墓碑并入队收尾;危险) |
toolDeleteRequiresConfirm |
true |
Agent 删除工具强制 confirm: true |
menuDeleteAvailable |
true |
是否在三点菜单挂红色删除项 |
reannouncePendingRemovals |
true |
读取删除队列时,为仍存活的排队 id 重播官方 api-session/removed,让「显示已归档」视图也不再渲染残留墓碑(设为 false 退回「仅墓碑」行为,见「删除语义」) |
开发
pnpm install
pnpm test # 语法检查 + 宿主单测 + 客户端渲染冒烟测试
开发环境需要 Node ≥ 22.22.2——渲染测试在 jsdom 里挂载 React,而 jsdom 30
声明 ^22.22.2 || ^24.15.0 || >=26。插件本身在 Node ≥ 20 即可运行
(engines.node),只有测试套件需要更新的运行时。
渲染测试(test/client.render.test.mjs)用 React + jsdom 真实挂载设置页
组件,能抓住宿主单测覆盖不到的 UI 崩溃(如 hooks 顺序 / 变量提升错误)。
接入验证请走下方「浏览器 E2E」:scripts/e2e-seed.mjs 建一个带标记的一次性
DSH_HOME,scripts/e2e-check.mjs 在真实 UI 里做只读断言。
浏览器端到端验证(可选)
启动一个一次性 dsh web 实例,跑在隔离的 DSH_HOME 上(绝不碰真实数据),用
puppeteer-core + 本机 Chrome 驱动界面:
cp scripts/e2e-seed.local.example.json scripts/e2e-seed.local.json
# ^ 填入你自己的会话/工作区数据(已 gitignore,不会提交)
node scripts/e2e-seed.mjs <e2e-home> ~/.dsh # 1. 播种隔离测试 HOME
# 2. 在该 HOME 下建 profile 装本插件,再启动测试 web 实例。
# ⚠ 桌面版 `dsh` 启动器把 DSH_HOME 写死为真实 home,因此前缀
# DSH_HOME=<e2e-home> 并不能隔离(已实测:profile 会落到真实 home)。
# 改为用一个能指向该 home 的 Node CLI —— dsh npm 包里的那个入口:
# ⚠ 不要再使用本文件早期版本推荐的 `--expose-internals "<app.asar>/lib/desktop-cli.js"`
# 形式:在某些桌面构建上该 asar 路径解析不了,CLI 会以 MODULE_NOT_FOUND 退出
# (2026-09-30 针对已装桌面端实测)。
# DSH=$(npm root -g)/@deepseek-ai/dsh/lib/bin.js
# DSH_HOME=<e2e-home> node "$DSH" --profile sm-test --from-default-profile web --dump-config
# ^ 从内置 web 模板建 profile 后直接退出(不启动)
# DSH_HOME=<e2e-home> node "$DSH" plugin --profile sm-test add <tarball>
# DSH_HOME=<e2e-home> node "$DSH" --profile sm-test --no-open --port 43123
# ^ 启动;打印的那一行里带 token URL
node scripts/e2e-check.mjs <打印出的带 token 的 URL> # 3. 只读检查:三点菜单/设置页
node scripts/e2e-mutations.mjs <URL> <e2e-home> # 4. 闭环:恢复→归档→彻底删除
node scripts/e2e-residue.mjs <URL> --home <e2e-home> # 5. 残留验收:两个视图都不该有该行
node scripts/e2e-contrast.mjs <URL> <e2e-home> # 6. 危险红字在明暗两套主题下的 WCAG AA 对比度
⚠ 第 6 步要求该 home 的归档集非空(seed 会造一个已归档会话),并且真的测到那一行: 设置页头部的批量删除/刷新按钮在归档集为空时也会渲染,所以「测到了东西」不算验收, 没测到归档行仍然 exit 2。它是只读的:不归档、不删除任何会话。
⚠ 第 4、5 步会真的删除会话,因此有两道互相独立、缺一不可的守卫
(scripts/e2e-guard.mjs):
--home(或第二个参数)必须是一个本仓库播种过的 home:标记文件必须是为这个路径 写的。真实~/.dsh会按名字、按前缀(它内部的任何路径同样拒绝)被拒,大小写变体、 8.3 短名、junction/符号链接、\\?\路径也绕不过去(比较会解析真实路径,Windows 上 忽略大小写)。只有名字像标记的目录、从别的 home 复制来的标记、以及 0.4.8 之前 没记录 home 的标记同样被拒——请重新播种。- URL 背后的实例必须正在使用这个 home。URL 本身证明不了这一点:脚本会在第一次点击前
询问页面能看到哪些会话(插件自己的 list 接口 + 页面上渲染出的
session:…行), 只要有任何一个 id 不在播种 home 里就拒绝;页面什么都答不出来时按失败处理(fail closed)。 没有这道检查时,把一个破坏性脚本指向你自己正在运行的 dsh、再传一个合法的--home, 可以通过全部检查并删掉你的真实会话。
两道守卫都有无需浏览器、无需 dsh 的单元测试(test/e2e-guard.test.mjs);seed 自身也会
先校验完整 spec、拒绝 源 == 目标,并在任何删除动作之前写下标记。
其余脚本是诊断工具,不是验收测试:e2e-bug2.mjs 与 e2e-live.mjs 复现已修
的现场 bug(两者都会真删会话,都有守卫),e2e-probe.mjs 是一次性的 DOM 侦察。
哪些脚本真的会失败很关键——一份永远以 0 退出的过程记录,无论读起来多让人放心,
都不是测试:
| 脚本 | 会失败吗 |
|---|---|
e2e-residue.mjs |
会(抛错 → exit 1) |
e2e-dialog-style.mjs |
会(8 项样式检查,任一不过就抛) |
e2e-realclick.mjs |
会——它的 bug-1 hover/点击断言是真的 exit 1 路径,不再只是日志 |
e2e-contrast.mjs |
会(危险红字在明暗两套主题下须达 WCAG AA) |
e2e-guard.mjs |
会(拒绝未播种的 home) |
e2e-check.mjs、e2e-mutations.mjs、e2e-bug2.mjs、e2e-live.mjs、e2e-probe.mjs |
不会——它们打印过程记录。真崩溃(缺 Chrome、超时)仍会非 0 退出,但断言失败在退出码里看不见,所以要看输出 |
Chrome 不在默认路径时设置 CHROME_PATH。
如果你想手写调用本插件的路由(这些脚本都走真实 UI,所以都没体现这一点):路由挂在共享的
/api 前缀下,并且要求 connection 插件那套信封 ——
POST /api/session-manager/<endpoint>,content-type: application/json,body 为
{ type: 'client-request', rpcId, method: 'session-manager/<endpoint>', payload }。
其中 method 必填且必须与端点一致;否则会得到 405 / 415 / 400 或
bad-request: invalid client-request message。
参考项目
本插件在开发前调研了以下 GitHub 项目(其中部分可直接在本地
~/.dsh/profiles/desktop/node_modules 中对照源码):
- omdsh-dev/DSH-better-sidebar ——
dsh 社区插件标杆:bundle patch、client 注入、settings.section 注册、
/sidebar/api宿主路由,都是本插件结构的直接参照。 - ysr666/dsh-vision-router —— 同类「设置页 + 宿主 RPC + 手写无打包 client」插件,client 模块骨架完全照此。
- deepseek-ai/deepseek-harness ——
dsh 本体:
dsh-api-workspace-controller(archiveSession / follow 流)、dsh-api-session-controller、dsh-session-persistence-jsonl(目录编码、 zstd 日志)、dsh-workspace(归档集语义与恢复定位)等包是本文档所有行为 结论的依据。 - koishijs/koishi —— dsh 的 Cordis 插件
运行时上游,
ctx.effect/ctx.inject/ patch 体系的理解来源。 - tmux-plugins/tmux-resurrect —— 概念参照:终端会话的保存/恢复/清理产品形态。
- opencode-ai/opencode —— 概念参照: 编码代理的会话持久化与恢复入口设计。
License
MIT
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。