dsh-plugin-session-delete
彻底删除对话 —— 给 DSH Web GUI 的会话右键菜单加一个「删除对话…」入口,把选中的会话从磁盘上真正删掉。
DSH 原生只有「归档」(archive):归档只是把会话从侧栏隐藏,日志文件仍然留在
~/.dsh/sessions/下。官方在@deepseek-ai/dsh-session-persistence-jsonl的已知限制里写得很明白:"Nothing deletes session files — logs accumulate underrootuntil removed externally; the seam has no deletion API." 这个插件就是那个 external remover。
它做什么
- 右键菜单入口:侧栏每个会话行的「…」菜单底部多一项「删除对话…」(排在官方「归档会话」之后)。
- 多选 + 二次确认:弹框列出全部会话(含已归档、子会话),可以多选;必须手动输入
DELETE才能确认,删之前清清楚楚告诉你后果不可撤销。 - 真正落盘删除:宿主侧删掉该会话自己的目录(
<root>/--<project>--/<id>/,含全部格式代际的日志),并清掉投影缓存里对应的那条记录。 - 拦住正在跑的会话:有活动 Agent(正在产出一个回合)的会话会被拒绝(
blocked),不会去动一个还有写入者在缓冲的日志。只是"打开着"不算 —— 那正是右键的那一行,空闲时不持有写入者。 - 删完自动切走:如果删掉的正是当前打开的那个对话,主面板会自动回到空白新会话页。
- 列表立即同步:删除后宿主发出
api-session/removed,所有已连接的页面无需刷新即可移除该行。
安装
# 从仓库根目录(<harness>)执行;desktop profile 由桌面端管理,用桌面端命令行
dsh plugin --profile desktop add link:<仓库路径>/session-delete
用 link:,不要用 file:。 两者差别是致命的:
| 写法 | 结果 | 改完代码之后 |
|---|---|---|
file:<路径> |
pnpm 建真实副本 | 必须重新 add 才生效,否则跑的还是旧字节 |
link:<路径> |
Windows 上建目录联接 | 立即生效(配合 HMR 甚至不用重启) |
卸载即还原:
dsh plugin --profile desktop remove dsh-plugin-session-delete
安全设计
删除是不可逆的,所以宿主侧不信任客户端送来的任何路径,只信任两件东西:持久化后端的 stat() 和日志自己的 header。
| 风险 | 处理 |
|---|---|
| 客户端伪造路径 / 目录穿越 | 客户端只能送会话 id。路径由 root + header 的 cwd + header 的 id 重新推导,id 走与后端逐字节相同的 ~XXXX 转义,且必须严格位于 root 之内。 |
| 任意字符串被当作删除成功 | stat() 找不到对应日志的 id 一律返回失败,绝不报告成"已删除"。 |
| 删到还有写入者的会话 | 有活动 Agent(正在产出回合)的 id 在探测前、删除前各拦一次,返回 blocked。只是打开着、空闲的会话不拦——它不持有写入者,且删完会清掉主面板。 |
| 跨站页面 / DNS rebinding 打到本机端口 | 路由只接受 Host 等于实际监听地址、且 Origin(若存在)同源的请求;否则 403。 |
| 删错目录 | 只删整个会话目录,且删前先 stat 证明它是目录;删完不动任何兄弟目录。 |
路由只有两条,都挂在同样的校验之后:
GET /session-delete/candidates → { ok, sessions: [{ sessionId, title, cwd, origin, createdAt, sizeBytes, archived, running }] }
POST /session-delete/delete → { ok, deleted[], blocked[{sessionId, reason}], failed[{sessionId, message}] }
body: { "sessionIds": ["<id>", …] } (1..500)
结构
session-delete/
├── package.json # dsh.bundle.patch + dsh.client 声明
├── cordis.patch.yml # 往 profile 里 insert 一行宿主插件
├── lib/index.js # 宿主半:两条 HTTP 路由 + 删除例程(手写 ESM,无需构建)
├── lib/client.js # 浏览器半:lazy-CJS bundle(手写,无需构建)
└── tests/ # 两个可执行规格
lib/ 是提交进仓库的产物,没有构建步骤:宿主半是普通 ESM,浏览器半是 window.__ModuleLoader__.load({ id, factory }) 形式的 lazy-CJS,require() 只取平台基线里的 react 与 @deepseek-ai/dsh-client-ui-primitives(不打包自己的副本)。改完源码直接生效,HMR 会重挂。
测试
node tests/host.spec.mjs # 宿主半:在临时目录里种一份真实会话数据的副本,跑真 HTTP
node tests/client.spec.mjs # 浏览器半:假 module loader + 极简 hook dispatcher,跑真组件
两个都是自包含的:
host.spec.mjs从$DSH_HOME/sessions(或DSH_SESSIONS_DIR)只读复制样本到系统临时目录,全程不碰真实会话;覆盖候选列表、请求加固、blocked语义(跑着的拦、开着的放行)、永久删除、列表同步、目录穿越与"日志 header 指名根外路径"的 containment 用例、以及卸载时路由注销。client.spec.mjs需要拿到@deepseek-ai/dsh-client-ui-primitives的导出面,按此顺序找:DSH_PRIMITIVES_PATH→ 已安装的 profile → shippedapp.asar(后者优先用运行时自己的 asar 感知fs,普通 Node 下退回本仓库自带的 asar 索引读取器)。用桌面端自带的运行时跑最省事:# Windows $env:ELECTRON_RUN_AS_NODE=1; & '<安装目录>\DeepSeek Harness.exe' tests\client.spec.mjs覆盖入口契约、中英文案 key 对齐、确认门槛、删除往返、以及被拒绝后弹框保持打开。
已知限制
- 删除不可撤销:没有回收站。
lib/index.js的删除是rm(dir, { recursive: true, force: true })。 - 只删会话日志目录:附件(
~/.dsh/attachments/)与工作区记录不做级联清理,会话消失后它们不再被引用。 - 子会话可单独删除:列表里带
subagent标记的会话也是独立目录,可以选;删父会话不会连带删子会话。 - 删掉打开着的空闲会话依赖两层保护:客户端会清掉主面板;写入者自己的文件句柄会让删除在 Windows 上直接失败(另一个句柄没有删除共享就 unlink 不掉),而不是删一半。所以真遇到"会话开着且写入者尚未释放",结果是明确报错,不是静默损坏。
- 运行中判定是进程内视角:另一个 DSH 进程正在写的会话,本进程看不到它的 Agent,因此判定为可删。后端有单写者锁,这种情形属于"操作者明确要删"。
No comments yet. Be the first to write one.