@zfdx123/dsh-session-cleaner 1.0.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/LICENSE +21 -0
- package/README.md +172 -0
- package/client.js +1147 -0
- package/cordis.patch.yml +6 -0
- package/lib/index.js +414 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zfdx123
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @zfdx123/dsh-session-cleaner
|
|
2
|
+
|
|
3
|
+
给 DSH(DeepSeek Harness)补上**删除会话**的能力——从**运行中**的 web 运行时里删,不需要重启。
|
|
4
|
+
|
|
5
|
+
DSH 只有「归档」:`workspace.archiveSession` 把会话 id 加进一个注册表集合,**文件仍留在磁盘上**;不存在
|
|
6
|
+
`session.delete`。本插件补上这个缺口。
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
dsh plugin --profile web add file:E:\work\ai\dsh-session-cleaner\bundle
|
|
12
|
+
# 或者发布到 npm / git 之后:
|
|
13
|
+
# dsh plugin --profile web add @zfdx123/dsh-session-cleaner
|
|
14
|
+
# dsh plugin --profile web add github:<owner>/dsh-session-cleaner
|
|
15
|
+
|
|
16
|
+
# 然后重启 dsh web(bundle 不做热加载)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
卸载(可逆):`dsh plugin --profile web remove @zfdx123/dsh-session-cleaner` + 重启。
|
|
20
|
+
|
|
21
|
+
安装时 `dsh plugin` 会把带 `dsh.bundle.patch` 的依赖自动补进 `dsh.profile.bundles`。
|
|
22
|
+
|
|
23
|
+
> **改完代码要重启**:`dsh web` 在启动时装配 bundle,客户端那一半不做热加载,所以改完本目录后
|
|
24
|
+
> 重启 + 刷新页面即可生效。
|
|
25
|
+
> 本机当前是按**软链**装的(`link:E:\work\ai\dsh-session-cleaner\bundle` → profile 的
|
|
26
|
+
> `node_modules/@zfdx123/dsh-session-cleaner`),源码改完 profile 立刻就是新的,**不需要重新 add**。
|
|
27
|
+
> 若改用 `dsh plugin --profile web add file:…\bundle`,Windows 上落成的是**拷贝**,那才需要重新 add。
|
|
28
|
+
|
|
29
|
+
## 用法
|
|
30
|
+
|
|
31
|
+
### 删除确认框
|
|
32
|
+
|
|
33
|
+
两个入口共用**同一个确认框**,用的是 DSH 自己的 UI 原语
|
|
34
|
+
(`@deepseek-ai/dsh-client-ui-primitives` 的 `Modal` + `Button`),跟「删除工作区」那个确认框同款:
|
|
35
|
+
标题 + 一句说明 + `[取消]` / `[删除]`,删除按钮是原生那种**红字描边**(`--dsw-alias-state-error-primary`)。
|
|
36
|
+
|
|
37
|
+
- **不使用** `window.confirm` / `window.alert`——浏览器原生弹窗在这里既不统一也很丑;
|
|
38
|
+
- 删除过程中框内显示「正在删除…」;**被拒绝(会话打开中)时框不关**,错误就显示在框里;
|
|
39
|
+
- 只有 `Esc`、点遮罩、点 `[取消]` 或删除成功才会关闭。
|
|
40
|
+
|
|
41
|
+
### 图标
|
|
42
|
+
|
|
43
|
+
三个图标同样用 DSH 自己的图标组件,不自绘:搜索框 `IconSearchOutline16`、组头折叠箭头
|
|
44
|
+
`IconChevronDownOutline14`、⋮ 菜单里的 `IconTrashOutline16`。(图标组件只收 `{size, className}`,
|
|
45
|
+
转发不了 `style`/`aria-hidden`,所以定位、旋转、颜色挂在图标外面那层 box 上,两条路径共用同一个 box。)
|
|
46
|
+
⋮ 菜单是命令式 DOM、没有 React 树,组件库又只给组件不给标记,所以那个图标由 React 渲染进一个临时容器、
|
|
47
|
+
把 SVG 取出来后再把临时 root 卸掉——菜单项背后不留挂着的 root。
|
|
48
|
+
|
|
49
|
+
组件库缺席或**缺少其中任一成员**时,插件整体退回自绘 SVG(确认框同时退回 `window.confirm`):
|
|
50
|
+
`loadPrimitives` 的形态检查是**全有或全无**的,所以「只到了一半」的组件库不会让界面变成一半原生一半自绘。
|
|
51
|
+
|
|
52
|
+
### ⋮ 菜单(侧边栏会话行)
|
|
53
|
+
|
|
54
|
+
会话行右侧 ⋮ → **「删除会话」**,位置紧跟在**「归档会话」下面**。点击 → 确认框 → 该行立即消失。
|
|
55
|
+
会话正在运行时该项置灰;已打开(有 agent 附着)的会话会被服务端拒绝,请先关闭它。
|
|
56
|
+
|
|
57
|
+
> 菜单是命令式 DOM,没有自己的 React 树,所以这一侧的确认框挂在一个独立的 React root 上
|
|
58
|
+
> (`react-dom/client` 的 `createRoot`),用完即卸载;确认框本身与设置页是同一个组件。
|
|
59
|
+
|
|
60
|
+
### 设置页「会话清理」
|
|
61
|
+
|
|
62
|
+
列出**全部**会话:workspace 成员、未分组的游离会话、已归档会话、以及旧格式(裸 UUID)会话。
|
|
63
|
+
|
|
64
|
+
按 **workspace 分组**显示,不再混在一条时间线里:
|
|
65
|
+
|
|
66
|
+
- 组顺序与侧边栏一致(宿主给的 workspace 顺序),**「未分组」放最后**;组内按最近活动倒序,空组不渲染;
|
|
67
|
+
- 组头是 `名称 · N 个会话`,**点一下折叠/展开**(默认展开);
|
|
68
|
+
- **已归档 / 旧格式只是行内徽章**,会话仍留在它所属的 workspace 组里——「归档」是行的属性,不是一个分组;
|
|
69
|
+
- 每行可**删除**;已归档的行额外提供**取消归档**。
|
|
70
|
+
|
|
71
|
+
搜索框:命中**组名**则整组保留,否则按行标题过滤;搜索时命中的组**自动展开**(命中藏在折叠的组里等于没搜到),
|
|
72
|
+
组头计数改为 `命中/总数`。
|
|
73
|
+
|
|
74
|
+
### HTTP
|
|
75
|
+
|
|
76
|
+
```http
|
|
77
|
+
POST /api-ext/session.delete
|
|
78
|
+
Content-Type: application/json
|
|
79
|
+
|
|
80
|
+
{ "sessionId": "session-1bb8d361-ea6b-4b92-bab2-c858c92e8822" }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
同源页面里可直接调用:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
await fetch('/api-ext/session.delete', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'content-type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ sessionId: 'session-…' }),
|
|
90
|
+
}).then(r => r.json());
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
响应沿用 host 的 JSON 信封:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{ "ok": true, "value": { "liveBefore": true, "liveDetached": true,
|
|
97
|
+
"accounting": { "unarchived": true, "detached": ["…"] },
|
|
98
|
+
"files": { "root": "…", "removed": ["…"], "failed": [] },
|
|
99
|
+
"projection": { "ok": true, "deleted": true } } }
|
|
100
|
+
{ "ok": false, "error": { "code": "refused", "message": "…" } }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
状态码:`200` 成功;`400 bad-request`(id 非法 / JSON 坏);`405 method-not-allowed`(方法不是 POST);
|
|
104
|
+
`415 unsupported-media-type`(`content-type` 不是 `application/json`——两个路由都只收 JSON);
|
|
105
|
+
`409 refused`(会话被打开);`500 internal`。
|
|
106
|
+
|
|
107
|
+
## 删除做了什么(四步)
|
|
108
|
+
|
|
109
|
+
1. **live store**:如果该会话有内存条目就 detach 掉(`SessionStore.enter()` 返回的那个 disposer),
|
|
110
|
+
并把该会话从**前端**的会话列表里摘掉。
|
|
111
|
+
> **踩过的坑:光 detach 不会让前端掉行。** 前端唯一的「会话没了」通知是 `session/disposed`,
|
|
112
|
+
> 而 `SessionStore.detachEntered` 只在 `entry.announced === true` 时才发它——只有 agent 真正
|
|
113
|
+
> 打开过的会话才会置位(`sessions.enter` 只由 agent loop 调用;`session.list` 只是读 live 条目、
|
|
114
|
+
> 其余从持久化 `summarizeCold`,**不会**把冷会话 prepare/enter 进 store)。而本路由**拒绝**一切
|
|
115
|
+
> 有 agent 附着的会话,所以它删掉的会话根本没有内存条目、任何事件都不会发。detach 那一步因此是
|
|
116
|
+
> **防御性**保留的:万一有「agent 已消失、条目还在」的残留,也不能让它变成一个指向已删文件的幽灵行。
|
|
117
|
+
> 真正让界面掉行的是客户端:删完由客户端自己调 `ctx.sessions.handleSessionRemoved(id)`
|
|
118
|
+
> (正是 `api-session/removed` 中继调用的那个方法),再 `refresh()` 跟 host 基线对账——
|
|
119
|
+
> host 基线来自磁盘上的会话日志,已经不含它了。
|
|
120
|
+
2. **记账**:从全局归档集合移除,并从每个记账了它的 workspace 移除(host 会据此推 `archived` 帧给前端,
|
|
121
|
+
两个设置页的归档集合因此同步)。
|
|
122
|
+
3. **磁盘**:删除 `<sessions 根>/<project>/<sessionId>`(遍历全部 project 目录,只删名字**恰好等于** id 的目录)。
|
|
123
|
+
4. **投影缓存**:删除 `session_projcache` 里该会话的行(文件搜索索引是派生的,会自行收敛)。
|
|
124
|
+
|
|
125
|
+
## 安全
|
|
126
|
+
|
|
127
|
+
- id 必须匹配 `^(session-)?<uuid>$`,否则直接 `bad-request`,不会进入任何路径拼接;
|
|
128
|
+
- **有 agent 附着的会话拒绝删除**(running 或 idle 都算「已打开」)——不会把别人正在用的会话从底下抽走;
|
|
129
|
+
- 只删「位于 sessions 根之下、且目录名恰好等于该 id」的目录;
|
|
130
|
+
- 删除不可恢复:文件、记账、live 条目、投影缓存行都会消失。
|
|
131
|
+
|
|
132
|
+
## 为什么是 bundle 而不是动态插件
|
|
133
|
+
|
|
134
|
+
同样的功能先用动态 Cordis 插件做过一版(未随本仓库发布)。bundle 形态在三点上更好:
|
|
135
|
+
|
|
136
|
+
| 维度 | 动态插件 | bundle(本包) |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| 持久性 | 进程级,DSH 重启即失效 | 装进 profile,**重启仍在** |
|
|
139
|
+
| 跨平台 | 沙箱不给 Node API,只能 shell 出去(要分平台写引擎 + 处理引号) | 直接 `node:fs`,**无 shell、无平台分支** |
|
|
140
|
+
| 入口 | 只能挂设置页 | 设置页 **+ 侧边栏 ⋮ 菜单** + HTTP 路由 |
|
|
141
|
+
|
|
142
|
+
## 开发
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
node --test test/host.test.js # node:test 版(host)
|
|
146
|
+
node test/run.js # host + client 全部用例,平铺断言(受限沙箱里也能跑)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`test/cases.js` 是 host 用例,只操作 `mkdtemp` 出来的 scratch 目录:`deleteSession` 的第三个参数
|
|
150
|
+
`{ root }` 就是为此留的测试缝,**不会碰到真实会话**。
|
|
151
|
+
`test/client-cases.js` 是客户端用例:把 `client.js` 挂在一个极简的 `window.__ModuleLoader__` 垫片上、
|
|
152
|
+
配一个**照抄真 React 语义**的替身(`props.children`、函数组件即时展开、hook 槽位跨渲染存活并在 setState 时重渲染),
|
|
153
|
+
再给 `Modal`/`Button` 提供保持契约的替身(`Modal` 关闭时渲染 `null`),于是设置页可以**无浏览器、无 DOM、无框架**
|
|
154
|
+
地被完整驱动——点删除、在确认框里点取消/删除、点组头折叠、往搜索框里打字——并断言它渲染了什么、
|
|
155
|
+
对注入的服务做了什么。
|
|
156
|
+
⋮ 菜单那一路另配一个迷你 DOM(节点、属性、子节点,以及安装器真正查询的那几个选择器),于是菜单项也能被真正
|
|
157
|
+
构建出来、点进去删一次。图标用例的组件库替身分三档(完整 / 缺图标 / 模块表缺失)并记录「哪个图标组件被调用」,
|
|
158
|
+
同时区分「React 渲染出来的 svg」与「自绘的 svg」——所以「用原生图标」和「退回自绘」两条路径都有断言。
|
|
159
|
+
|
|
160
|
+
## 已知边界
|
|
161
|
+
|
|
162
|
+
- **⋮ 菜单靠 DOM 增强**:DSH 没有给行菜单公开 Slot,所以只能在菜单打开时往里插一项。识别方式是**语义**的
|
|
163
|
+
(同一处新增同时含「归档会话」与「分叉会话/重命名」两个文案才认定为会话行菜单),行通过刚点击的
|
|
164
|
+
⋮ 触发器定位——不靠时间窗或矩形距离猜测。若 DSH 改了这些文案,该项会静默不出现(设置页仍然可用)。
|
|
165
|
+
- **会话 id 从 React fiber 里读**:行的 DOM 上没有任何携带 id 的属性,所以从行元素的
|
|
166
|
+
`__reactFiber$*` 往上找,取 `memoizedProps.node.id`(或 `props.sessionId`)。拿不到才退回标题反查,
|
|
167
|
+
**标题重复时跳过注入**,避免删错。
|
|
168
|
+
- 会话打开中(idle agent)时拒绝删除,不代用户关会话。
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|