跳过主要内容
Z

dsh-session-trash

zhaoqianguo/dsh-session-trash

给 DSH 加上可恢复的会话回收站:右键删除、恢复、彻底删除、按保留天数自动到期清理、批量恢复与批量彻底删除、回收站/归档/活动会话的容量统计、接管归档会话(取消归档或移入回收站),并附带 10 个 agent 工具。回收站条目默认保留 30 天,到期后永久删除;agent 工具也可以彻底删除条目或清空回收站。

安装

dsh plugin --profile web add github:zhaoqianguo/dsh-session-trash

README

会话回收站 · Session Trash for DeepSeek Harness

Adds a real trash bin to DeepSeek Harness. DSH's built-in archive only records a session id in a set — the log file never leaves the disk. This plugin turns deletion into a recoverable flow: right-click a session → Delete session moves it into ~/.dsh/session-trash, where it can be restored, purged permanently, or expired automatically after a configurable retention period. It also takes over archived sessions (list, unarchive, move to the bin) and reports capacity for bin / archived / active sessions. 10 agent tools included.

Install: dsh plugin --profile web add github:<owner>/dsh-session-trash, or install a local checkout by pasting its absolute path into Plugins → + → Install third-party plugin. Zero runtime dependencies; no build step.


中文说明

给 DSH 补上真正的删除:DSH 自带的「归档」只是把会话 id 记进一份集合,日志文件永远留在磁盘上。这个插件把它变成可管理、可回收、可彻底清除的流程,并顺带接管归档会话的管理和磁盘占用统计。

它做了什么

能力入口
删除会话(移入回收站)侧栏会话行右键 → 删除会话,一键完成(可恢复,所以不再二次确认),底部弹出提示「已移入回收站」
打开面板(回收站 / 归档两个页签)侧栏面板栏 → 回收站 图标;想直接落在归档页签,则右键菜单 → 查看归档会话
容量统计面板顶部三块 chip:回收站 / 归档 / 活动会话各自的条数 + 占用
批量操作回收站页:全选 / 恢复选中 / 彻底删除选中 / 清理已过期;归档页:取消归档选中 / 移入回收站选中
接管归档归档页列出仍存在于磁盘上的归档会话,可直接取消归档(恢复回侧栏)或移入回收站(之后才按保留天数清理)
恢复 / 彻底删除 / 清空 / 改保留天数回收站页的每行按钮 + 底部 footer
不可撤销操作的确认彻底删除单个 / 批量、清空回收站都会弹一个确认框(标题 + 影响范围 + 取消/确认),不再用「再点一次」
过期自动清理启动后 10 秒扫一次,之后每小时扫一次
agent 自助操作10 个工具,见下表

agent 工具

工具作用
session_trash_list列出回收站条目,附带总占用与归档统计
session_trash_delete把会话移入回收站
session_trash_restore从回收站恢复
session_trash_purge彻底删除单条
session_trash_purge_many彻底删除一批(session_ids 数组)
session_trash_empty清空回收站
session_trash_settings读/写保留天数(0 = 永不自动删除)
session_trash_archive_list列出归档会话及其占用
session_trash_archive_delete把归档会话移入回收站
session_trash_archive_restore取消归档

它怎么工作(原理)

DSH 的会话在磁盘上是:

~/.dsh/sessions/--<编码后的工作区路径>--/session-<uuid>/session.v4.jsonl.zstd

「归档」的状态不在文件里,而在 ~/.dsh/storages/workspace.json 的 global.archivedSessionIds 数组里,由 ctx.workspaceRegistry 服务维护。所以删除分两步:

  1. ctx.workspaceRegistry.archiveSession(id) —— 侧栏里那一行立刻消失;归档门(ArchivedSessionGate)也会拒绝再唤醒该会话的 agent,避免它继续往被移走的文件里写。
  2. 把整个会话目录 rename 到 ~/.dsh/session-trash/items/<id>/session,同目录写 meta.json(标题、原工作区、删除时间、过期时间、占用字节)。

恢复 = 目录搬回原位置 + unarchiveSession;彻底删除 = rm -rf 并把 id 从归档集合里摘掉。任何一步失败都会回滚归档状态。

归档页签只做「读」和两个动作(取消归档 / 移入回收站):归档本身不会被自动删除,只有移入回收站之后才受保留天数约束 —— 这样「接管归档」不会变成「偷偷清理归档」。

磁盘布局:

~/.dsh/session-trash/settings.json           { "retentionDays": 30 }
~/.dsh/session-trash/items/<sessionId>/meta.json
~/.dsh/session-trash/items/<sessionId>/session/   ← 原来的会话目录

保留天数存在 settings.json 而不是插件配置里,因为面板上要能直接改;0 表示永不自动删除。

HTTP 路由

客户端只打一个端点 POST /api/session-trash,body 形如 { "action": "...", ... },错误也返回 200 + { ok: false, code, error }。跨源保护见下面的「同源策略」一节。支持的 action:

list / panel / delete / restore / restore-many / purge / purge-many / empty / settings / purge-expired / archive-list / archive-restore / archive-delete。

panel 一次返回面板所需的全部内容(回收站条目 + 归档条目 + 三项统计 + 设置 + 根目录),避免打开面板时打三四次请求。

文件

文件作用
index.jsHost 侧 Cordis 插件:注册 /api/session-trash 路由、10 个 agent 工具、定时清理
trash-store.js纯 node:fs 的回收站实现(无 DSH 依赖,可单测)
sessions-scan.js只读磁盘的扫描层:读 workspace.json 的归档/工作区映射、扫 sessions/ 目录、从 projcache 兜底读标题
tool-schema.js自带的最小 defineTool:把工具参数 DSL 编译成 JSON Schema(不 import 任何 DSH 内部包,原因见下)
client.jsWeb 侧客户端 bundle:两个右键菜单项(查看归档会话 / 删除会话)+ 回收站/归档双页签面板
cordis.patch.yml补丁层:把插件插进 profile 的插件树
test/trash-store.test.mjs回收站存储层单测(含 id 越界防护)
test/host-route.test.mjsHost 侧路由/回滚/统计/批量/归档/工具契约的集成测试(真实 http 请求)
test/client-smoke.test.mjs客户端 bundle 的加载、槽位注册、页签切换冒烟测试

安装

从插件市场 / GitHub 装(发布后)

dsh plugin --profile web add github:<owner>/dsh-session-trash

发布到插件市场后,市场卡片上的安装按钮等价于上面这条命令。

从本地目录装(开发时)

  1. DSH 侧栏 → 插件 → 右上角 + → 安装第三方插件。

  2. 规格填本目录的绝对路径(插件管理器接受本地目录;它会把该目录 pnpm add 成 link: 依赖,并因为这个包声明了 dsh.bundle.patch 自动把它加进 profile 的插件栈):

    /Users/zhaolanbaobao/Desktop/code/my-agent/session-trash
    
  3. 装完 重启 DSH。Host 侧代码(路由、工具、定时器)不会热替换;客户端 bundle 也在重启后才重新加载。

  4. 重启后:侧栏面板栏多一个回收站图标;会话行右键菜单多出查看归档会话、删除会话。回收站本身从面板图标进入,右键菜单里不再放一个重复的「打开回收站」。

为什么插件不 import DSH 内部包

第一版 index.js 里写了 import { defineTool } from '@deepseek-ai/dsh-tools',装到 DSH 里会报:

failed to import: Error [ERR_MODULE_NOT_FOUND]: Cannot find package
'@deepseek-ai/dsh-tools' imported from .../session-trash/index.js

原因:该包只存在于 DSH 应用包内部(app.asar/node_modules),不在 profile 的 ~/.dsh/profiles/<profile>/node_modules 里;本地目录插件被 link 进 profile 后,Node 从插件真实路径向上找 node_modules,永远找不到它。第三方插件要么自带依赖,要么不依赖 DSH 内部包。

本插件的选择是零依赖:tool-schema.js 复刻了官方 defineTool 中本插件用到的那部分(参数 DSL → JSON Schema + 严格参数校验),其余全部只用 node: 内置模块。ctx.tools.register() 需要的也只是 { name, description, parameters, output: { schema, render }, execute } 这个普通对象。副作用是:插件目录里没有 node_modules 也能跑,自测不需要 stub 任何 DSH 包。

卸载 / 数据

  • 卸载:插件页移除本插件 → 重启 DSH。插件不会删回收站数据。
  • 彻底清盘:退出 DSH 后 rm -rf ~/.dsh/session-trash(回收站条目 + 设置一起没)。
  • 改本地代码:改完重启 DSH 即可(link: 依赖指向原目录,不用重装;本插件没有构建步骤)。

自测(不用装插件也能跑)

cd session-trash && npm test

25 个用例,分三层:

  • 存储层:移入 / 元数据 / 重复与缺失会话 / 恢复 / 恢复冲突 / 彻底删除 / 清空 / 过期扫描 / 设置持久化与钳制 / 非法 id(../../evil)不许越界。
  • Host 集成:把 index.js 复制进一个没有 node_modules 的临时目录,真实起 http server 打 /api/session-trash,验证 panel 统计、批量恢复/彻底删除、归档列表/取消归档/移入回收站、删除失败回滚归档、跨源 403、不带 Origin 的 POST / loopback 别名 / 自定义 scheme 放行、text/plain 415、10 个工具的契约(同时也验证了零依赖:任一 @deepseek-ai/* 裸 import 都会让这组用例直接失败)。
  • 客户端:伪造 window.__ModuleLoader__ + React stub + 假 document,验证 bundle 能加载、4 处槽位注册的 id/order/label 正确、菜单删除一次点击即调用 Host 并弹出成功/失败 toast、面板里不再出现「再点一次」文案。

发布到插件市场

把本插件收录进 awesome-dsh-plugin 目录(awesome-dsh-plugin.com/index.json 与 dshmarket.com/index.json 是同一份目录的两个镜像)的完整步骤、条目 YAML 模板和评审要求,见 PUBLISHING.md;可直接提交的条目文件在 market-entry.yml。

安装到 DSH 之后,随便删一个会话,然后对 agent 说「列出回收站」—— 它会调用 session_trash_list,能列出就说明 Host 侧通了。侧栏同时应该出现「回收站」图标。

已知边界

  • 恢复后侧栏可能要刷新:归档状态是实时推送的(删除立刻消失),但恢复后会话行要等客户端重新拉一次会话列表;面板会提示「刷新页面(⌘R / Ctrl+R)」。
  • 正在运行的会话删不掉:archiveSession 会拒绝有活动工作的会话,插件把错误翻译成「这个会话还有正在运行的工作,先停止它再删除」。
  • 统计是实时扫盘的:每次打开面板 / 刷新都会 stat 一遍所有会话目录算占用,会话上万时会慢一点(每轮只 stat 不解压,通常几十毫秒级)。
  • 彻底删除会把 id 从 archivedSessionIds 里清掉(unarchiveSession 对不存在的会话是空操作,DSH 官方明确允许这种状态),所以 workspace.json 不会留幽灵条目。
  • 删掉当前正在看的会话:会话文件会真的被移走,但主界面仍停留在那个会话上(内容已经加载进内存)。切到别的会话或刷新即可;数据在回收站里,可以恢复。
  • 回收站条目的标题在删除那一刻由侧栏标题快照写入 meta.json;没给标题时回退到 projcache 里的标题。之后标题生成器再改名不会同步过来。
  • 只清理 sessions 目录下的会话日志。~/.dsh/storages/session_projcache/sessions/<id>.json(标题/折叠缓存)与 ~/.dsh/attachments/ 下被引用的附件不会被回收:前者是惰性读取、身份不匹配就作废的小文件,后者是内容寻址、可能被多个会话共享的 blob,删了有风险。
  • 一键删除没有二次确认,因为它只把会话移进回收站,30 天内都能恢复;真正不可撤销的彻底删除才有确认框。
  • 右键菜单里点删除后菜单会关掉,所以反馈走底部浮层提示(toast);失败时 toast 变红并显示 Host 返回的原因(例如「这个会话还有正在运行的工作」)。

同源策略(为什么不再严格比对 Origin)

早期版本要求 Origin 与 Host 完全一致,结果真机上删除全部 403「只接受同源请求」:桌面壳的页面可能以自定义 scheme 或 Origin: null 发出 fetch,也可能是同一个 loopback 服务换了写法(localhost vs 127.0.0.1)。现在的规则只拒绝「恶意网页能真正造成的东西」:

  1. 没带 Origin → 放行(浏览器同源 GET/HEAD 本来就不带,桌面壳的 POST 也可能不带);
  2. Origin: null → 只要 sec-fetch-site 不是 cross-site 就放行;
  3. 非 http(s) scheme(如 app://)→ 放行,只有桌面壳能产出这种 Origin;
  4. http(s) origin → host 相同、或双方都是 loopback(127.0.0.1 / localhost / ::1 / 0.0.0.0)才放行;sec-fetch-site: cross-site 直接拒绝;
  5. 真正的 CSRF 防线是 content-type:POST 必须是 application/json(否则 415)。跨站表单或 no-cors 请求只能发 CORS 安全列表里的类型,改成 JSON 就必然触发预检,而这条路由从不回应预检。

被拒绝时会往 DSH 日志里打一行 [session-trash] rejected a cross-origin request,带上 origin/host/sec-fetch-site,方便排查壳子的差异。

彻底删除会不会弄坏 DSH

不会。会话日志是 DSH 的唯一真相,删掉一个会话目录等价于「这个会话从未存在过」。DSH 只会在下面几处见到它的残影,且全部是惰性、可容忍的:

  1. global.archivedSessionIds(删除时写入)——彻底删除时已同步清除;
  2. 工作区记录的 sessionIds 槽位——DSH 的 sessionIds getter 用 candidateMatches 按「会话是否还在」过滤,读写都不报错;
  3. session_projcache 折叠缓存——按 id 存、打开时校验格式与血统(identity),孤儿记录永远匹配不上;
  4. 进程内存里的全文索引(session-search 用 :memory: SQLite)——重启前可能仍能搜到该标题,点开只会提示会话不存在。

相关插件