dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Permanently delete DSH conversations from the web GUI session row menu: batch selection, running-turn refusal, and automatic switch away from a deleted chat
彻底删除对话 —— 给 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>/,含全部格式代际的日志),并清掉投影缓存里对应的那条记录。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 → shipped app.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 标记的会话也是独立目录,可以选;删父会话不会连带删子会话。