dsh-session-reaper
给 DSH Web 侧栏的会话加上「删除」,并且删得可回退。
DSH 本身没有删除会话的能力:ui-workspace 的已知限制里写明「sessions can be archived
but never deleted」,持久化层也明确不留删除 API。归档只是把会话从列表里藏起来,
日志仍留在 ~/.dsh/sessions 下持续占盘。这个插件补上这段缺口。
行为
- 会话行「⋯」菜单里多一项 删除会话…(order 500,排在归档之后);
- 点它弹确认框,显示标题与 session id;
- 确认后会话日志整体移入回收站,不是就地删除;
- 侧栏底部有 回收站 入口,带数量角标;
- 回收站里可以逐条 恢复 或 彻底删除(需输入
DELETE二次确认)。
不做批量删除,不做自动过期清理——回收站只在你手动清理时变小。
安装
安装的本质只有两步:让 pnpm 把包拉进 profile 的 node_modules,再把包名加进 bundles。
难点只在于「用哪一个 pnpm」——DSH 桌面版自带一套 node/pnpm,但装在哪个盘因机器而异,
所以下面的命令不写死任何路径。
第 1 步:找到能用的 pnpm
先看系统里有没有:
node --version
pnpm --version # 或 npm --version
有
node和pnpm(或npm) → 直接用它们,跳到第 2 步。 DSH 自带的 node 是 24.x,系统 node 只要是 20 以上就没问题。没有 → 用 DSH 自带的那套,先把 DSH 安装目录记成
$DSH_HOME:$DSH_HOME = "D:\dsh" # ← 改成你自己的实际路径 & "$DSH_HOME\resources\runtime\primary-runtime\dependencies\node\bin\node.exe" ` "$DSH_HOME\resources\runtime\primary-runtime\dependencies\pnpm\bin\pnpm.cjs" ` --version能打印出版本号(形如
11.x)就说明这两个路径是对的。
第 2 步:装进 profile
# Windows
cd $env:USERPROFILE\.dsh\profiles\desktop
# Linux / macOS
cd ~/.dsh/profiles/desktop
然后二选一:
# 系统有 pnpm
pnpm add github:GGBound-1229/dsh-session-reaper
# 系统有 npm,没 pnpm
npm install github:GGBound-1229/dsh-session-reaper
# 两个都没有,用 DSH 自带的
& "$DSH_HOME\resources\runtime\primary-runtime\dependencies\node\bin\node.exe" `
"$DSH_HOME\resources\runtime\primary-runtime\dependencies\pnpm\bin\pnpm.cjs" `
add github:GGBound-1229/dsh-session-reaper
第 3 步:把包名加进 bundles
打开 ~/.dsh/profiles/desktop/package.json,在 dsh.profile.bundles 数组末尾加一行:
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-session-reaper"
]
这一步不能省。 只加 dependencies 不加 bundles,包在磁盘上但 DSH 不会加载它;
只加 bundles 不加 dependencies,DSH 找不到包、启动报错。
第 4 步:重启 DSH
完全退出再打开。重启后侧栏会话行的「⋯」菜单里应该多一项 删除会话…。
装不上时的排查顺序
pnpm add报权限错误 → 用管理员权限重开终端,或确认.dsh目录可写;pnpm add报找不到 git → 先装 git,GitHub 安装方式依赖它;- 装完
node_modules里有目录但菜单没变化 → 十有八九是漏了第 3 步的bundles; - 改了代码但不生效 → pnpm 的
file:依赖是复制不是链接,改完要重新装一次。
删除的执行顺序(不要改动)
workspaceRegistry.stopSessionActivity(id)—— 停掉这个会话上还在跑的工作;sessionPersistence.flush()—— 把待写数据落盘,避免移走目录后仍有写句柄;- 校验路径确实落在
sessionsRoot之内; - 整个会话目录
rename进回收站(跨卷时退化为复制+删除); workspace.detachSession(id)—— 走官方实体写路径解绑记账;workspaceRegistry.archiveSession(id)—— 计入归档集合,让客户端立刻隐藏这一行。
这个顺序是有原因的。WorkspaceRegistry.bootstrap() 在启动时会按 cwd 重新枚举持久化层里
所有 header,给工作区重新记账。所以日志目录必须先消失,否则下次启动这个会话会从磁盘
被重新扫回来、重新挂到工作区——看起来就是「删了又复活」。反过来(先解绑、再移目录)如果
中途失败,就会留下「记账没了但文件还在」的半死状态,下次启动一样会复活。
第 5 步失败时,第 4 步的移动会被回滚,不留下两边不一致的状态。
第 6 步解决的是纯客户端问题,不是可有可无的收尾,见下节。
为什么删除后要顺手归档
删完会话,服务端数据是对的:日志进了回收站、workspace.json 里也没有它了。
但界面上会冒出一个「未分组」分组,里面挂着刚删掉的会话,直到下次启动才消失。
原因在客户端。侧栏分组用的是 owningGroupKey():拿会话 id 去各 workspace 的
sessionIds 里找归属,找不到就返回空串,而空串正是「未分组」的 key。
与此同时,客户端的会话投影(list.ids)由另一条数据流维护,它不会因为我们移走了
日志就立刻知道这个会话没了。于是这个会话「可见但无归属」,正好落进
groupByWorkspace() 收集 stray 的那一支。
归档是唯一能立刻把会话从列表里藏起来、又不依赖投影重建的机制——
sessionVisible() 对默认过滤器直接返回 !archived.has(id)。
所以删除时一并归档,恢复时取消归档,彻底删除时把 id 从集合里摘掉免得无限增长。
归档只影响可见性,不动日志、不改工作区记账,当「界面上的临时开关」用是安全的。 归档失败也不致命:界面上会暂时多一个未分组条目,重启后自愈。
存储
- 会话日志:
~/.dsh/sessions/--<normalized-cwd>--/session-<uuid>/ - 回收站:
~/.dsh/session-reaper/entries/<ISO时间戳>__<sessionId>/—— 移过来的会话目录本身index.json—— 条目索引(标题、原路径、大小、删除时间)
恢复需要 index.json 里的 originalDir,所以要手工恢复请用它,别只搬目录。
端点
全部挂在 /dsh-session-reaper 下,每个都过 connection.requestRejection 信任围栏:
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /list |
列出回收站 |
| POST | /delete |
{sessionId} 移入回收站 |
| POST | /restore |
{entryId} 从回收站恢复 |
| POST | /purge |
{entryId} 彻底删除 |
安全边界
- 不给模型注册任何工具,模型无法自己调用这个插件;
- 只接受本机已认证客户端(
connection.requestRejection); - session id 必须匹配
^session-[0-9a-f-]{36}$; - 所有文件路径
path.resolve后必须落在sessionsRoot或回收站之内; - 删除路径里不存在对会话目录的递归删除,只有
rename;递归删除只出现在 「从回收站彻底删除」这一处,且需要输入DELETE; - 零第三方运行时依赖,只用
node:fs/node:path/node:os; apply()全程 try/catch,插件任何故障只记一行日志,DSH 照常启动。
已知坑(踩过,别再踩)
Cordis 的 ctx.inject 必须一个服务一个声明。 一次性声明多个时,只要其中一个服务
不存在,整个插件会被拒绝挂载,而且不报错。所以这里对
sessionPersistence / workspaceRegistry / webServer 各调一次 ctx.inject。
客户端插件只能 require('react') / require('react-dom')。 拿不到
@deepseek-ai/dsh-client-ui-primitives,也拿不到内置包注入的 hooks(比如菜单的
useMenuOpenState)。所以 UI 用原生元素自绘,菜单项与对话框之间用一个模块级 store 通信。
安装后要跑一次 pnpm install。 profile 里的 file: 依赖由 pnpm 复制进
node_modules,手工建 junction 不会被登记,DSH 的 bundle 解析器不认识这个包。
关闭
在 profile 的 cordis.patch.yml 里给这一行加上 enabled: false:
- id: dsh-session-reaper
name: dsh-session-reaper
config:
enabled: false
或者直接从 profile package.json 的 dsh.profile.bundles 里删掉本包名。
No comments yet. Be the first to write one.