better-dsh-session-deletetool 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +299 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/README.zh-CN.md +171 -0
- package/client.js +1466 -0
- package/cordis.patch.yml +4 -0
- package/host.js +1431 -0
- package/icon.svg +6 -0
- package/package.json +62 -0
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# better-dsh-session-deletetool
|
|
2
|
+
|
|
3
|
+
**真正删除 DeepSeek Harness 里的会话 —— DSH 自带的能力只有归档。**
|
|
4
|
+
|
|
5
|
+
[English](README.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 为什么需要它
|
|
10
|
+
|
|
11
|
+
DSH 只能**归档**会话:归档把那一行从侧边栏隐藏,工件一样不动,日志、工程记录、投影缓存都留在磁盘上,随时可以恢复。持久化接口本身根本没有删除能力,`@deepseek-ai/dsh-session-persistence-jsonl` 写得很直白:
|
|
12
|
+
|
|
13
|
+
> **Nothing deletes session files** —— 日志在 `root` 下不断堆积,直到被外部删除;这一层没有删除接口。
|
|
14
|
+
|
|
15
|
+
于是用得越久,会话只增不减。这个插件补上缺失的那一步:在会话的 `⋯` 菜单里加一项**删除会话**,真的把数据抹掉;它弹出的确认框也是唯一能看清一个会话全家结构的地方。
|
|
16
|
+
|
|
17
|
+
## 删了什么
|
|
18
|
+
|
|
19
|
+
对选中的会话,宿主半会:
|
|
20
|
+
|
|
21
|
+
1. **删掉会话工件目录** —— 当前日志、保留的每一代历史格式、以及目录里的会话本地文件(路径由 `ctx.sessionPersistence.locate(header)` 给出);
|
|
22
|
+
2. **删掉你勾选的子会话** —— 子会话是各自独立的 session,各有各的日志。DSH 把每个子会话的父 id 记在它自己的 header 里(`SessionHeader.parentSession`),而且它创建的每种关系都写这个字段:子智能体由运行时写入并带 `origin: 'subagent'`,fork 出来的对话写入并带 `isSeeded`,Agent Teams 的名册也靠同一个字段解析。插件按这条血缘广度优先遍历,再与持久化的 `subagentCatalog` 投影(`ctx.subagents.listDescendants`)取并集,因此连 header 已经读不出来的子会话也能被点名。删除**从最深的往下**,集合在动手之前先收集完,一次最多删 200 个,而且只删勾上的那些;
|
|
23
|
+
3. **摘掉工程记录** —— 每个 id 从每个 Workspace 记录的 `sessionIds` 里移除(`Workspace.detachSession`),并从注册表全局的归档集合、置顶集合里移除;
|
|
24
|
+
4. **删掉投影缓存** —— `session_projcache` 域里的记录和磁盘上对应的 `<id>.json`;
|
|
25
|
+
5. **关掉它拥有的终端** —— `ctx.terminals` 不属于任何一条准入族,服务要等 Agent 被释放才回收,而那时日志早就没了;
|
|
26
|
+
6. **通知页面** —— 每个被删的 id 各发一次 `api-session/removed`,这是官方会话控制器销毁会话时发的同一个事件,侧边栏的行会立刻消失。
|
|
27
|
+
|
|
28
|
+
### 看清家族,再选择删什么
|
|
29
|
+
|
|
30
|
+
侧边栏里,子智能体和派生对话都是各自独立的一行,看不出是谁生的,DSH 本身也没有任何地方画这条关系。这个确认框是唯一画出来的地方:整个家族画成**一棵树**,包括派生对话自己带出来的下一层。
|
|
31
|
+
|
|
32
|
+
- 第 1 行是**本对话**,它的勾选框管整个家族,名字跟侧边栏显示的一致;
|
|
33
|
+
- 第 2 行是一个**可折叠的「子智能体(n)」分支**,装本对话自己生的子智能体;
|
|
34
|
+
- 第 3 行是一个**可折叠的「派生对话(n)」分支**,分支里每个派生对话自带它自己的「子智能体(n)」子分支,所以**派生对话的派生对话画在父辈块里面**(缩进多一格),而不是跟它并排;
|
|
35
|
+
- 子智能体自己派生出来的对话,同理画在那个子智能体的块里面;
|
|
36
|
+
- 每行都有勾选框,标出名字和 id 尾号,还开着的或还有工作没跑完的会带状态标记;名字跟侧边栏同一套规则:日志里有名字用名字,没有就用它项目目录的末段,再没有才用 id,所以不会显示成"未命名";
|
|
37
|
+
- 层级只靠**缩进**表达:一级缩进一格、二级两格,父行后面紧跟它自己画出来的子行,所以第 3 层那行一眼能看出是挂在上面第 2 层那行下面,而不是"反正更深一点"。
|
|
38
|
+
|
|
39
|
+
每行的勾选框管**它自己那棵子树**:下面全都勾上时它是勾上的,只勾了一部分时显示成半选,按一下整棵子树一起勾或一起取消;分组标题的勾选框管这个分组列出的所有行。删除集合只存这一份 id,不再有"选中的根 + 排除的子行"两套状态,所以底部的计数不会和画面对不上。
|
|
40
|
+
|
|
41
|
+
读不到的血缘会写在列表上方(比如某个分支的目录打不开),而不是静默少几行——少行和"这个会话确实没有子会话"原来长得一样。
|
|
42
|
+
|
|
43
|
+
确认时只提交勾上的 id,按钮上写着即将删掉几个会话。没勾的子会话会作为独立的会话根留下来。选择会在宿主侧拿它自己走出来的血缘做校验,请求只能点到这个家族里的会话。fork 出来的对话本身也是独立会话,所以它是"带勾选框列出来",而不是被静默带走。
|
|
44
|
+
|
|
45
|
+
### 批量删除
|
|
46
|
+
|
|
47
|
+
工作区标题行、搜索图标**左边**多了一个垃圾桶图标(画的是 `deleting icon.svg` 那个图),点开是一个总览所有会话的弹窗:
|
|
48
|
+
|
|
49
|
+
- **按工作区分区。** 分区顺序跟注册表里工作区的顺序一致,标题是工作区名,右边写这个区里有几个会话;没有被任何工作区认领的会话归到最后一个**未归类**区。
|
|
50
|
+
- **每个会话画成和单会话确认框同一棵树**:会话行下面是它自己的「子智能体(n)」和「派生对话(n)」两个可折叠分组,派生对话再把自己那一层接在它自己的块里面,层级只靠缩进表达。
|
|
51
|
+
- **会话名跟侧边栏一致**:日志里有名字就用名字,没有就用它项目目录的末段(比如 `Project_lzc`),再没有才用 id。没改过名的会话因此显示成目录名,而不是"未命名"。
|
|
52
|
+
- **勾选按子树走,子会话可以单独删。** 勾上母会话,它下面勾中的子会话和派生对话跟着一起删;只勾某个子智能体、不勾它的母会话时,那个子智能体自己作为一个删除根提交,母会话保持不动。底部按钮始终写着这次会删掉几个会话。
|
|
53
|
+
|
|
54
|
+
弹窗里同样有「停止并删除」那个开关,默认打开,和单会话确认框一致。确认后每个被勾的会话根各自走一次上面那条删除流程;某个根失败只在弹窗里列出来,其余照删。
|
|
55
|
+
|
|
56
|
+
### 什么会拦,什么不会
|
|
57
|
+
|
|
58
|
+
**只有"还有工作在跑"会拦,"还开着"不会。** 门槛用的是 DSH 自己归档时的那条 `workspace/session-activity` 瀑布:Agent 注册表报进行中的回合,后台任务注册表报后台任务,子智能体运行时报运行中的子会话,定时提醒报生效中的任务。这个询问是**对删除集合里的每一个会话各问一次**,因为这条准入是按被问的会话作答的:只问目标的话,子会话正在跑的回合或后台任务根本不会出现。确认框会把还在跑的东西写在对应行上、也写在汇总里,并提供**停止并删除**——先对每个忙碌的会话派发一次 `workspace/session-stop`,和 `archiveSession(id, { stopActivity: true })` 完全一致。
|
|
59
|
+
|
|
60
|
+
宿主内存里还持有这个会话(`ctx.sessions` / `ctx.agents`)时照样删。本机实测(Windows):文件句柄还开着时 `rm` 依然成功,目录项立刻消失,之后的写入落进已删除的文件而不是把它复活。早先的版本会拒绝这种会话并提示"先切换到别的会话",结果把没显示在窗口里的会话也一起误伤了,那条规则已经去掉。
|
|
61
|
+
|
|
62
|
+
### 已知边界
|
|
63
|
+
|
|
64
|
+
- **附件二进制不删。** 上传的图片和文件存在 `~/.dsh/attachments/v1/objects/<hash>`,是内容寻址的去重仓库、多个会话共享,服务本身没有引用计数,所以这些 blob 会留下。
|
|
65
|
+
- **搜索索引会自己收拾。** `dsh-session-query-sqlite` 是派生索引,每次检索前都会跟持久化对账,被删掉的源从下一次检索起就查不到了。
|
|
66
|
+
- **会话仍被占用时可能留下一条死缓存。** 删除时宿主还持有它的话,它被销毁的那一刻投影缓存可能再写回一条 checkpoint。那条记录永远读不到(没有日志就没有会话),只是几十 KB 的死文件。
|
|
67
|
+
- **改插件代码要重启 DSH。** 如果 profile 的 HMR 不监视模块文件,替换已安装包里的代码需要重启;客户端那一半刷新页面即可。
|
|
68
|
+
|
|
69
|
+
## 安装
|
|
70
|
+
|
|
71
|
+
下面几条都走 DSH 自带的插件管理,它接受一个安装 spec(`@deepseek-ai/dsh-plugin-manager`):registry 包名、git 主机简写、仓库 URL、tarball,或者本地绝对路径。按你的网络情况挑一条。
|
|
72
|
+
|
|
73
|
+
### 1. 直接从 GitHub 装(需要能访问 github.com)
|
|
74
|
+
|
|
75
|
+
spec:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
github:Lzcdebear/better-dsh-session-deletetool
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
等价写法:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
https://github.com/Lzcdebear/better-dsh-session-deletetool
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
把 spec 交给 DSH:
|
|
88
|
+
|
|
89
|
+
- **在应用里**:设置 → 插件 → 安装入口,粘贴 spec。(插件管理界面和 `plugin_manager` 工具接受的是同一个字符串。)
|
|
90
|
+
- **在会话里**:让 Agent 装 —— 工具调用是 `plugin_manager`,`action: "install_bundle"`,`target: "github:Lzcdebear/better-dsh-session-deletetool"`。
|
|
91
|
+
|
|
92
|
+
可以用 `#` 钉住某个 ref:`github:Lzcdebear/better-dsh-session-deletetool#v0.1.0`。
|
|
93
|
+
|
|
94
|
+
连接检查失败时 DSH 会给出一个有界的日志路径。如果这台机器根本连不上 github.com,走第 2 或第 3 条,或者先给 git 配上代理(`git config --global http.proxy http://127.0.0.1:7890`)。
|
|
95
|
+
|
|
96
|
+
### 2. 从本地副本装(离线可用)
|
|
97
|
+
|
|
98
|
+
把仓库下载下来(ZIP 或 `git clone`),解压到任意位置,然后把**绝对目录**交给插件管理:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
plugin_manager { action: "install_bundle", target: "D:\\plugins\\better-dsh-session-deletetool" }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
DSH 会记一条 `link:` 依赖并重载 profile。开发这个插件用的就是这条路,网络受限时也走它。
|
|
105
|
+
|
|
106
|
+
### 3. 从发布 tarball 装
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
https://github.com/Lzcdebear/better-dsh-session-deletetool/archive/refs/heads/main.tar.gz
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
安装入口同第 1 条;git 不可用但 HTTPS 通的时候好用。
|
|
113
|
+
|
|
114
|
+
### 装完之后
|
|
115
|
+
|
|
116
|
+
宿主半随 profile 一起加载;客户端半刷新页面后出现。会话的 `⋯` 菜单里会多一项**删除会话 / Delete conversation**。
|
|
117
|
+
|
|
118
|
+
想卸载就用同一个管理入口:`plugin_manager { action: "remove_bundle", target: "better-dsh-session-deletetool" }`(bundle 键就是包名)。
|
|
119
|
+
|
|
120
|
+
## 用法
|
|
121
|
+
|
|
122
|
+
1. 在任意会话行上点 `⋯`,选**删除会话**。
|
|
123
|
+
2. 确认框先向宿主问一次真实状态,写清楚:会删掉什么、它是不是还被 Harness 占用、还剩什么在工作。
|
|
124
|
+
3. 接着把整个家族画成一棵树让你挑:本对话一行,下面按「子智能体(n)」「派生对话(n)」两个可折叠分组展开,派生对话把自己那一层接在自己的块里面;每行都有勾选框(带标题、id 尾号和状态标记),分组标题的勾选框管这一块。不想删的把勾去掉。
|
|
125
|
+
4. 确认。按钮上写着即将删掉几个会话;如果还有在跑的工作,按钮会变成**停止并删除**,按下去先停掉那些工作。
|
|
126
|
+
5. 相关的行立刻从侧边栏消失,数据也从磁盘上没了。
|
|
127
|
+
|
|
128
|
+
批量的走法:点工作区标题行里、搜索图标**左边**那个垃圾桶图标,在按工作区分好区的列表里勾会话(勾母会话连带子会话,子会话可以单独勾),然后确认。
|
|
129
|
+
|
|
130
|
+
## HTTP 接口
|
|
131
|
+
|
|
132
|
+
客户端半通过 `ctx.webServer` 上四条同源 `exact` 路由访问宿主。纯 JS、无构建步骤的插件没法声明带类型的 `ctx.remote` 命名空间(那需要 Typert 生成产物),所以这里用的是社区插件 `dshmarket` 同款通道。
|
|
133
|
+
|
|
134
|
+
| 路由 | 方法 | 作用 |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `/better-dsh-session-deletetool/inspect?sessionId=…` | GET | 返回 `stored` / `open` / `agent` / `running` / `activity` / `artifactDirectory` / `warnings`(没读到的血缘线索,避免"少几行"被当成"这个会话没有子会话"),以及 `descendants` = `{ count, subagents, derived, truncated, maxDeletable, items[] }`,其中每项带 `id`、`kind`、`depth`、`parentId`、`title`、`open`、`agent`、`running` 和它自己的 `activity` |
|
|
137
|
+
| `/better-dsh-session-deletetool/delete` | POST | body `{ sessionId, stop?, descendants? }`——`descendants` 不传表示整个家族,传空数组表示只删它自己;返回 `removed`、`descendants`(每条带 `kind`)、`kept`、`stoppedActivity`、`terminalsKilled`、`warnings`、`runtime`、`activity`。点到的名字不在家族里会在动手之前就以 `400 unknown-descendant` 拒绝 |
|
|
138
|
+
| `/better-dsh-session-deletetool/catalog` | GET | 返回 `{ ok, workspaces: [{ key, workspaceId, title, path, sessions[] }], totals }`;每个会话行带 `id`、`kind`(`root` / `subagent` / `derived`)、`depth`、`parentId`、`hasChildren`、`family`、`subagents`、`derived`、`title`、`cwd`、`open`、`agent`、`running`、`activity`。分区顺序就是注册表里工作区的顺序,`workspaceId` 为 `null` 的那个分区是"未归类" |
|
|
139
|
+
| `/better-dsh-session-deletetool/delete-batch` | POST | body `{ roots: [{ sessionId, descendants? }], stop? }`,每个根各自照单会话那条规则走一遍;返回 `{ ok, roots, removed[], failed[] }`。某个根失败只记一条 `failed`,不会中止其余。一次最多 200 个根 |
|
|
140
|
+
|
|
141
|
+
四条路由各自带同源校验:`Host` 必须是环回、`sec-fetch-site` 不能是 `cross-site`、`Origin` 存在时必须与 `Host` 同源。别的网站页面打不进来。
|
|
142
|
+
|
|
143
|
+
## 构成
|
|
144
|
+
|
|
145
|
+
| 文件 | 作用 |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `host.js` | 宿主半:四条路由、工件/工程记录/缓存清理、子会话子树、按工作区分区的会话总览 |
|
|
148
|
+
| `client.js` | 客户端半:`sidebar.workspaces.session.menu.item` 菜单项(order 500)+ 批量图标 + 两个 `shell.overlay` 对话框 |
|
|
149
|
+
| `cordis.patch.yml` | 把宿主行插进 profile 层栈 |
|
|
150
|
+
| `test/host.test.mjs` | 路由测试,跑在真实临时目录上 |
|
|
151
|
+
| `icon.svg` | 插件图标(批量按钮画的是 `deleting icon.svg` 那个图) |
|
|
152
|
+
|
|
153
|
+
样式只用宿主主题 token(`--dsw-alias-*`),浅色深色都跟随;界面文案通过客户端 locale 服务本地化(英文 / 简体中文)。
|
|
154
|
+
|
|
155
|
+
批量入口为什么不注册一个新插槽:`sidebar.workspaces` 是 single 槽,第二个注册者会**顶掉**官方那套会话浏览器,而不是并排。所以客户端半注册在 `sidebar.footer.action`(本身不渲染任何东西),再把自己的按钮用 portal 挂进浏览器自己的搜索格子里,并用一个 `MutationObserver` 在 React 重渲染表头时补回来。这一步除了"打开弹窗"没有任何权限,真正干活的是宿主那两条路由。哪天 harness 改掉了这个格子的 CSS 类名,按钮会不出现,别的地方不受影响。
|
|
156
|
+
|
|
157
|
+
## 开发
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
node --test test/host.test.mjs
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
测试直接驱动真实的 `apply()` 注册,配假的宿主服务和真实的临时目录,覆盖:删除本体、有活时的拦截与停止、子会话子树、状态查询,以及各种拒绝路径(坏 id、坏 body、错方法、跨站来源)。
|
|
164
|
+
|
|
165
|
+
## 许可
|
|
166
|
+
|
|
167
|
+
[MIT](LICENSE)
|
|
168
|
+
|
|
169
|
+
## 作者
|
|
170
|
+
|
|
171
|
+
- Bilibili:<https://space.bilibili.com/220996778>
|