Files
session-delete/README.md
T
2026-09-30 17:26:44 +08:00

106 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 会话彻底删除
DeepSeek Harness(DSH)插件:给会话加上**真正不可逆的删除**,并清理已删除会话遗留的临时文件。
DSH 原本只能归档会话——归档只是把会话从列表里移开,日志仍然留在磁盘上(DSH 官方的 JSONL 会话存储后端明确写着「没有任何东西会删除会话文件,日志会一直堆在 root 下直到被外部移除」,会话存储契约里也没有删除 API)。这个插件补上的正是这一块:两处删除入口,以及插件页里的一次性孤儿文件清理。
## 功能
### 1. 删除会话
两个入口,行为一致,都会先弹出确认弹窗(复刻系统 Modal 的样式与键盘行为:Esc 关闭、Tab 焦点圈定、打开时焦点进入、关闭后焦点归位):
- 会话行右侧 **⋯** 菜单 → **删除会话**
- 会话行悬停时的 🗑 按钮(在官方的「归档」「置顶」之前,是悬停区的第一个按钮)
确认后一次性处理:
| 对象 | 处理方式 |
| --- | --- |
| 会话日志目录 | 整个删除,仅保留 `session.lock` |
| 子智能体会话 | 按血缘递归收集,连同日志与溢出文件一并删除 |
| 工具输出溢出文件(spill) | 删除该会话(含子会话)对应的 spill 目录 |
| 工作区登记项 | 解除绑定、取消置顶、取消归档 |
| 客户端列表 | 广播 `api-session/removed`,列表行立即消失 |
有意**不**处理:
- **附件对象**(`$DSH_HOME/attachments/v1/objects`):按内容 sha256 寻址,同一份内容可能被多个会话共享,而 DSH 没有引用计数,跟着一个会话删除会破坏别的会话;
- **投影缓存**(`$DSH_HOME/storages/session_projcache`):由日志派生的缓存,读取时带生命周期身份校验,永远不会串到别的会话。
### 2. 清理临时文件
**设置 → 插件 → 会话彻底删除**(打开插件页就能看到,不必再点进「包含的组件」里那一行),清理 spill 后端留下的**孤儿**目录。判定偏保守:
1. 目录名必须精确等于 `session-<sha256(会话 id) 前 12 位>`——这是 spill 后端唯一会生成的形状;
2. 该名字不属于任何已知会话(`sessionQuery.listSessions()` 已经把存量会话与内存中正在运行的会话合并成同一份语料,所以「已知」只需要一个来源);
3. 只在 spill 的 root 内操作(后端当前 root + 系统临时目录下的 `dsh-spill*`);
4. 符号链接一律跳过、不跟随;单项失败只记警告。
结果直接显示在该页(清理了几个目录、几个文件、释放多少空间),可反复点击。
这块清理占的是 `plugins.bundle.config` 槽位,key 就是本包的**包名**(`@dsh-plugin/session-delete`)。它渲染在插件页的**描述与「包含的组件」之间**,所以打开插件页即可看到,不用再点一次。
没有用行级槽位 `plugins.row.config`(key 形如 `<包名>#<行 id>`):它只会在「包含的组件」那一行上多长出一个「配置」入口,把本插件唯一的控件藏到二级页面里。本插件页顶部是 DSH 自己渲染的插件标题与描述,所以这块配置不再重复标题,直接讲清理这件事本身。
### 关于「包含的组件」
插件页下方的「包含的组件 / 组件」是 **DSH 插件页固定渲染**的:`ui-plugin-manager` 无条件列出这个 bundle 在 `cordis.patch.yml` 里声明的**每一行**(行 id、运行状态、单独开关),没有"跳过"的口子。任何 bundle 都至少要声明一行才能挂上宿主半,所以这一节必然出现——本插件就是这一行。它也不是纯冗余:它标出运行状态、允许单独关掉这个组件。插件侧没法隐藏它,但插件的功能不再挂在它下面——清理入口已经在页面本身上了。
## 行为与边界
- **仍在运行的会话**无法被插件从内存里移除:它会先被归档并停止,日志与溢出文件在**下次启动 DSH 时**删除(界面会明确提示);在此之前取消归档就会取消这次删除。
- 删除**不可恢复**,但**不是安全擦除**:按普通文件删除处理,底层介质上可能有残留。
- 工作区里的代码文件不受影响。
- 溢出文件被删除后,fork 出来的会话日志里若仍保留指向它的路径文本,那些路径会失效。
- 行内按钮的悬停气泡由按钮自身定位。指针停在会话行上时,行自己的悬停预览卡可能和这个气泡同时出现——插件不会去动宿主的行 DOM 来避免这一点(那是插件不该依赖的自有渲染结构)。若观感不能接受,可把气泡改为只在键盘 focus 时显示:删掉 `DeleteSessionRowButton` 里的 `onMouseEnter: show` / `onMouseLeave: hide` 两行即可。
## 安装
DSH 的 **插件 → 添加插件** 里有两个输入,含义不同:
- **上面那个输入框**:装什么(包名 / Git 地址 / 本地 tgz 的绝对路径);
- **下面的「安装源」**:只决定**按包名安装时**去哪个注册表查。填 Git 地址或 tgz 时它不参与拉取,保持默认(官方源)即可,不必切到「自定义地址」。
### 1. Git 仓库地址(推荐)
在上面的输入框里填:
```text
https://gitea.iwake.top/dsh-plugin/session-delete.git#v2.0.2
```
`#` 后面可以跟标签或提交,用来锁定版本;不写则取默认分支。仓库是公开的,不需要凭据,也不需要改安装源。
想跟随 2.x 的最新版本,可以把后缀写成 `#semver:^2.0.2`——pnpm 会按仓库里的标签挑最高的 2.x,以后发了新版本,移除旧版本再装一次就能拿到。
### 2. 本地 tgz
把仓库打包成 tgz(例如在仓库目录执行 `npm pack`,得到 `dsh-plugin-session-delete-2.0.2.tgz`),把它在运行 DSH 那台机器上的绝对路径填进上面的输入框。
### 升级
DSH 暂不支持插件自动更新,升级就是用新标签重装一次(建议先移除旧版本)。安装或升级后:**宿主半变更需要重启 DSH**(完全退出再打开,仅关闭窗口不一定退出进程);只改浏览器半时刷新页面即可。
如果你是在本地开发这个插件(profile 里装的是 `link:` 到工作目录),**光刷新页面看不到改动**:profile 的 `node_modules` 里可能是一份与源码同 inode 的硬链接副本,需要移除再重装一次插件,让 profile 重新取一份。
## 兼容性
基于 **DeepSeek Harness 0.2.0-rc.2** 编写。DSH 仍在演进,其它版本与平台组合未逐一验证。
- **浏览器半**不 import 任何 Harness 客户端包(上游规范如此要求:这些包随时可能变化,渲染抛错会让整个插槽条目变空白),而是复刻 `dsh-client-ui-primitives` 的 markup、CSS 声明、图标 path 数据与键盘行为,只保留 `--dsw-*` 主题 token 引用。它**不读写宿主的 DOM**,也不占用官方槽位:行内按钮用自己的 id 与官方「归档」「置顶」并列,不是顶替。上游若改动这些内部结构,界面细节可能失配(例如悬停气泡不再出现),但不会导致插件加载失败。
- **宿主半**只依赖稳定的服务契约:`sessionQuery`(`traceSession` 取血缘、`listSessions` 取语料)、`sessionPersistence`(含其 JSONL 后端的 `locate()` 诊断钩子,用于定位会话目录)、`workspaceRegistry`、`connection.fetch`(注册受认证的 exact 路由),以及 spill 后端的目录规则。
- **跨半边通信**走 `connection.fetch` 注册的 `POST /api/plugin/session-delete/delete` 与 `.../orphans` 两条路由,浏览器半用文档相对路径请求(支持挂载前缀)。这是静态客户端包与宿主半通信的正规通道;`host.call` 属于动态客户端运行器,不适用于包形式的插件。
## 贡献
提交信息用「emoji + Conventional Commits + 中文描述」,规则、类型表和示例见 [CONTRIBUTING.md](CONTRIBUTING.md)。想省事可以让 git 带上模板:
```bash
git config commit.template .gitmessage
```
## License
[MIT](LICENSE)