dsh-session-delete:给 DSH Web 界面补上会话删除

前言

DSH 的会话日志就是磁盘上的一个目录,但 DSH 本身没有删除的能力:SessionPersistence 只有 create/open/flush/stat/list,WorkspaceRegistry 只有 archive/unarchive。归档只是软删除,日志目录、注册表条目和子会话(subagent)的日志都留在磁盘上。长期跑下来,会话越积越多,却没有从界面真正清掉的入口。

dsh-session-delete 就是解决这个问题的第三方插件,维护者 Neruviko,属于客户端类插件,目前在社区目录 SkillHub 上星标为 0。它给侧边栏会话行的「…」菜单加了「删除会话」,带确认对话框和备份引导。下面介绍它做了什么、怎么装、删除过程的安全设计,以及使用上的注意事项。

这是什么

一句话:在 DSH Web 界面里,把一个会话连同磁盘日志和它的子会话子树真正删掉,并清理工作区注册表。

它的目标运行环境是 DSH 0.2.0-rc.2 的 Web GUI(作者实测可用),Node 版本要求 ^22.19.0 || >=24.0.0,与 DSH 本体一致。插件包版本 0.2.2,MIT 许可,两个半身(Host 与 Browser)都是手写的,没有构建步骤。

核心功能

功能清单来自插件 README,已在 GitHub 与 SkillHub 目录页核对:

  1. 侧边栏任意会话行的「…」菜单里新增「删除会话」,位置在「归档会话」之后。
  2. 可以删除任意会话,包括正在运行或正打开着的会话;删除前会先派发停止事件,停掉它的运行中工作(轮次、子代理、任务与提醒都会被取消)。
  3. 删除父会话时连带删除整棵子会话子树。子会话在侧边栏不可见,无法单独删除,只删父会话会留下再也访问不到的孤儿日志。
  4. 确认对话框显示会话标题、创建时间、日志大小、子会话数量;有子会话时明示「将同时删除 N 个子会话及其后代」。
  5. 对话框内嵌「下载此会话的 ZIP 归档」链接,引导先备份。
  6. 必须勾选确认框才能点删除。
  7. 删除后侧边栏行立即消失,不需要刷新页面。

值得单独说明的是它不加隐藏扫描:会话目录不是枚举出来的,而是由 JSONL 后端的目录布局确定性算出来的:

<DSH_HOME>/sessions/<projectKey(cwd)>/<encodeSegment(sessionId)>/session.v4.jsonl.zstd

index.js 里逐字符复刻了两个路径变换函数,encodeSegment(把任意字符串编码成单个安全路径段)和 projectKey(把 cwd 变成项目目录名,分隔符折叠成 -,两端加 --,例如 /home/me/projects/demo → --home-me-projects-demo--)。子树则来自 DSH 自己的 traceSession(),或 sessionPersistence.list() 里 header 的 parentSession 链接,同样不碰磁盘目录枚举。

安装与启用

下面是 README 给出的安装命令,是手写 git+https 形式,不是 dsh plugin add github:owner/repo。装到 web profile(其他 profile 同理,把 web 换掉即可):

dsh plugin --profile web add git+https://github.com/TonyZiYuan/dsh-session-delete.git

dsh plugin add 转发给 pnpm,装完会自动把新装的 bundle 追加到 profile 的 dsh.profile.bundles(依据包里 dsh.bundle 字段),不需要手工改 package.json。

如果安装报错说某个依赖的构建脚本被拦截,pnpm 默认不运行已安装依赖的 prepare 脚本,而 git 依赖需要先构建。此时按提示把包加进 profile 的 pnpm-workspace.yaml(在 ~/.dsh/profiles/web/ 下):

packages:
  - .

allowBuilds:
  dsh-session-delete: true

然后重跑上面的安装命令。pnpm ≥ 11 把 onlyBuiltDependencies 等设置合并成了这个 allowBuilds 映射,详见 pnpm v10 → v11 迁移说明。

更新和卸载:

dsh plugin --profile web update dsh-session-delete
dsh plugin --profile web remove dsh-session-delete

也可以从本地目录安装,路径必须是绝对路径:

dsh plugin --profile web add link:/absolute/path/to/dsh-session-delete

link: 的相对路径是相对 profile 的 manifest 目录(即 ~/.dsh/profiles/web/)解析的,写成 ./dsh-session-delete 会指向 ~/.dsh/profiles/web/dsh-session-delete,装不上。

装完之后必须重启 dsh web,bundle 与浏览器半身只在启动时加载(patchReload: live 只能热更新 patch,加载不了新的浏览器代码)。用这条确认挂载成功:

dsh --profile web --dump-config   # 应能看到 `dsh-session-delete` 条目

典型用法

插件不带命令行入口,装好后操作全在 Web 界面里。一个会话的删除流程,按 README 描述的顺序:

  1. 停止运行中工作。派发 workspace/session-stop(与 DSH 归档时 stopActivity: true 是同一事件),各 provider 取消该会话的轮次、子代理、任务与提醒,随后短暂等待让取消落地。provider 抛错只记日志,不阻断删除。
  2. flush 屏障。调用 sessionPersistence.flush(),确保没有缓冲事件。失败不阻断,后续的白名单校验仍然守住字节。
  3. 定位目录。从 header 自带的 cwd 算出目录,不扫描 sessions 根目录。
  4. 枚举后代子树。用 Session Query 的 traceSession() 拿到任意深度的子会话树;没有 query 服务时回退为 sessionPersistence.list() 里 header 的 parentSession 链接。默认连带删除整棵子树。
  5. 整树预检。对父会话与每个后代逐个定位目录、检查是否逃出 sessions root、校验内容白名单。全部通过才继续;任一失败则整体中止,一个都不删。
  6. 递归删除。逐个 fs.rm(dir, { recursive: true, force: true })。操作系统句柄锁挡住时(Windows EPERM/EBUSY,POSIX EACCES/ETXTBSY)返回 session/in-use,提示关掉该会话后重试。
  7. 清理注册表。对每个已删 id 执行 workspaceRegistry 的 unarchiveSession / unpinSession / detachSession,同时更新内存缓存、持久化表与已连接浏览器的推流。没有 registry 组成时回退为原子改写 workspace.json(临时文件 + rename,留一份 .bak)。此步失败不阻断删除,仅在响应里标记 registryCleaned: false。
  8. 发布移除公告。对每个已删 id 发出 api-session/removed,DSH 既有的转发链路会把该行从所有已连接浏览器的侧边栏移除。不发这一步,行会一直留到下次刷新。
  9. 记入墓碑。被删 id(含整个子树)记入本 Host 代的内存墓碑集,供浏览器半身的「幽灵收割器」使用。

HTTP 层面有两个路由,写请求额外要求来源是回环地址:

方法 路径 作用
GET /api/session-delete/sessions 可删会话清单 + tombstones(本 Host 代已删 id,供收割器用)
POST /api/session-delete/delete 删除,body {sessionId, cascade?},cascade 默认 true

删除响应里有 descendants 摘要,便于确认级联结果:

{
  "ok": true,
  "deleted": true,
  "absent": false,
  "freedBytes": 8192,
  "registryCleaned": true,
  "descendants": { "requested": 3, "deleted": 3, "ids": ["child-1a", "child-1b", "grand-1a"] }
}

两条路由都套 ctx.connection.requestRejection() 信任栅栏(Host/Origin 伪造与未认证请求会被拒绝)。这是 README 里明确写的内容,可以直接核对。

有一个坑:刷新后幽灵会复活

这是 README 里一段比较关键的说明。聊过天的会话,其 Agent/Session 对象会常驻 Host 内存(空闲会话要能被继续对话,是 DSH 的既定设计)。删除销毁了磁盘日志,但没有公开 API 能销毁别的 fiber 持有的 live 会话对象;客户端列表基线合并 live 会话又是无条件的(dsh-session-query 的 listSessions)。于是刷新浏览器后,基线从内存重建,已删的行会复活:出现在「未分组」下(注册表成员已清理),点开仍有完整文本(历史读取 live 优先,不碰磁盘)。

插件用两层配合来对冲:

  • Host 侧:每次成功删除把 id 记入内存墓碑集,GET /sessions 响应带 tombstones 数组。墓碑与幽灵同生共死,Host 重启后两者都不存在。
  • Browser 侧:订阅客户端会话列表快照,任何 id 若「在墓碑里却出现在列表中」,自动再发一次删除,走 absent 幂等路径,触发注册表清理与重公告,行随即掉落。只收割墓碑内的 id,新开的 blank 会话不受影响。

效果是刷新后幽灵即使闪现,也会在亚秒级被收掉。README 也明说,毫秒级的闪现窗口无法在插件层根除,需要上游在基线合并处支持「已删除会话排除」。手动兜底方式是对幽灵行再点一次「删除会话」,对话框会显示「日志已不在磁盘」状态,确认即可清除。

安全设计

README 列出的安全设计直接转述如下:

  • 目录必须在 sessions root 之内,且 basename 必须等于 encodeSegment(sessionId),父会话与每个后代都各自校验。
  • sessionId 必须匹配 /^[A-Za-z0-9._-]{1,128}$/。
  • 内容白名单:用户放在会话目录里的文件不属于本插件该销毁的对象,宁可拒绝。
  • 级联也是全有或全无:整棵子树先全部预检,任一成员不通过则一个都不删。
  • 先停后删:删除前派发 workspace/session-stop,避免在活跃写入者下面撕裂日志;真正被句柄挡住时操作系统自己会拒绝,映射为 session/in-use。
  • 循环血缘:枚举后代时带 seen 集合,畸形血缘不会无限递归。
  • 注册表原子写(仅回退路径):临时文件 + rename,崩溃不会截断 workspace.json,并留 .bak。

插件明确不做的事:不删 attachments 目录(内容寻址的共享存储,多个会话可能引用同一张图),不做批量删除、不做回收站/软删除,不改 @deepseek-ai/* 本体,不全盘扫描 sessions 根目录。

适用场景与注意

适合已经在用 dsh web profile、长期积累会话日志、需要真正清理而非归档的用户。它依赖 DSH 的多个内部契约(sessionPersistence 的公开 root 字段与 list/stat/flush,encodeSegment / projectKey 目录布局,workspace/session-stop 与 api-session/removed 事件,workspaceRegistry 的 unarchiveSession / unpinSession / detachSession,sessionQuery 的 traceSession() 后代树形状,以及浏览器半身的 slot 名与 dsh-client-ui-primitives 的导出),这些都是在 0.2.0-rc.2 上核对过的,未在其他上游版本验证前不要假定兼容。升级 DSH 后如果删除开始对存在的会话报 session/absent,或对话框组件缺失,多半是某个契约变了 —— 失败方向是安全的(宁可拒删不误删),对照新版源码更新 index.js 顶部的路径函数或组件用法即可。

几个使用上的注意点:

  1. 插件以当前 dsh web 进程的权限运行,装之前先读 index.js 与 client.js,确认审计无异议。许可证是 MIT。
  2. 路径变换函数是复刻的,DSH 升级改了它们就要同步更新,否则删除会误报 session/absent。
  3. 删除不可逆,插件本身不给回收站,想留档要在确认框里先下载 ZIP。
  4. 子会话默认级联删除;确实只想删自身可以传 cascade: false(HTTP body),但默认是不留孤儿。
  5. 目录证明是来源:SkillHub 目录页返回内容极少,没有提供可核对的功能清单或安装命令,本文事实以 GitHub 仓库 README 和 package.json 为准。安装命令来自 README 原文,指向的仓库地址与插件名不一致(命令里是 TonyZiYuan/dsh-session-delete),装之前请自行确认仓库归属。

结尾

dsh-session-delete 的价值不在于加一个按钮,而在于替 DSH 补上「删除」这一层,同时把级联删除、注册表清理、不可逆操作的入口控制都做在设计里。没有仪式感的停删删净,比多一个菜单项更值得看。

目录页:https://www.skillhub.cn/plugins/Neruviko/dsh-session-delete
GitHub:https://github.com/Neruviko/dsh-session-delete

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜