@zfdx123/dsh-session-cleaner 1.0.0 → 1.0.2
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/README.md +89 -104
- package/client.js +2 -2
- package/lib/index.js +24 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,61 +1,38 @@
|
|
|
1
1
|
# @zfdx123/dsh-session-cleaner
|
|
2
2
|
|
|
3
|
-
给 DSH(DeepSeek Harness)补上**删除会话**的能力——从**运行中**的 web 运行时里删,不需要重启。
|
|
4
|
-
|
|
5
|
-
DSH 只有「归档」:`workspace.archiveSession` 把会话 id 加进一个注册表集合,**文件仍留在磁盘上**;不存在
|
|
6
|
-
`session.delete`。本插件补上这个缺口。
|
|
3
|
+
给 DSH(DeepSeek Harness)补上**删除会话**的能力——从**运行中**的 web 运行时里删,不需要重启。DSH 只有「归档」:`workspace.archiveSession` 把会话 id 加进一个注册表集合,**文件仍留在磁盘上**;不存在 `session.delete`。本插件补上这个缺口:一次删除会清掉**四个表面**——live store 条目、工作区记账(归档集合与各 workspace 的会话槽)、磁盘产物目录、投影缓存行——并从三个入口暴露出来(侧边栏会话行的 ⋮ 菜单、设置页「会话清理」、一个 HTTP 路由),删除确认框复用 DSH 自己的 UI 原语,与「删除工作区」那个框同款。当前版本 1.0.0,面向 DSH `^0.1.6-alpha.1`。
|
|
7
4
|
|
|
8
5
|
## 安装
|
|
9
6
|
|
|
10
7
|
```sh
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
8
|
+
# 从 npm 安装单个包
|
|
9
|
+
dsh plugin --profile web add @zfdx123/dsh-session-cleaner
|
|
10
|
+
|
|
11
|
+
# 一次装齐整套(MCP 管理器、技能管理器、记忆、CodeGraph、钩子排序、会话清理、Superpowers)
|
|
12
|
+
dsh plugin --profile web add @zfdx123/dsh-atelier
|
|
15
13
|
|
|
16
14
|
# 然后重启 dsh web(bundle 不做热加载)
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
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。
|
|
17
|
+
安装时 `dsh plugin` 会把带 `dsh.bundle.patch` 的依赖自动补进 `dsh.profile.bundles`。卸载是可逆的:`dsh plugin --profile web remove @zfdx123/dsh-session-cleaner` + 重启。
|
|
28
18
|
|
|
29
|
-
|
|
19
|
+
**宿主那一半改完要重启**:`dsh web` 在启动时装配 bundle,客户端那一半不做热加载,所以改完代码后重启进程 + 刷新页面才会生效。
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
本地开发有两种装法,区别只在落盘形态:
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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。
|
|
23
|
+
```sh
|
|
24
|
+
# 软链(源码改完 profile 立刻就是新的,只需重启 dsh)
|
|
25
|
+
dsh plugin --profile web add link:/absolute/path/to/dsh-atelier/packages/dsh-session-cleaner
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
# 拷贝(Windows 上 file: 落成的是拷贝,改完要重新 add)
|
|
28
|
+
dsh plugin --profile web add file:/absolute/path/to/dsh-atelier/packages/dsh-session-cleaner
|
|
29
|
+
```
|
|
51
30
|
|
|
52
|
-
|
|
31
|
+
## 快速上手
|
|
53
32
|
|
|
54
|
-
|
|
55
|
-
会话正在运行时该项置灰;已打开(有 agent 附着)的会话会被服务端拒绝,请先关闭它。
|
|
33
|
+
### 侧边栏 ⋮ 菜单
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
> (`react-dom/client` 的 `createRoot`),用完即卸载;确认框本身与设置页是同一个组件。
|
|
35
|
+
会话行右侧 ⋮ → **「删除会话」**,位置紧跟在**「归档会话」下面**。点击 → 确认框 → 该行立即消失。**只有正在运行(`running`)的会话会被拒绝**——等它跑完再删;闲置(`idle`)的会话照删不误,也就是「我在界面里已经离开的会话」都能删。
|
|
59
36
|
|
|
60
37
|
### 设置页「会话清理」
|
|
61
38
|
|
|
@@ -68,8 +45,7 @@ dsh plugin --profile web add file:E:\work\ai\dsh-session-cleaner\bundle
|
|
|
68
45
|
- **已归档 / 旧格式只是行内徽章**,会话仍留在它所属的 workspace 组里——「归档」是行的属性,不是一个分组;
|
|
69
46
|
- 每行可**删除**;已归档的行额外提供**取消归档**。
|
|
70
47
|
|
|
71
|
-
|
|
72
|
-
组头计数改为 `命中/总数`。
|
|
48
|
+
搜索框:命中**组名**则整组保留,否则按行标题过滤;搜索时命中的组**自动展开**(命中藏在折叠的组里等于没搜到),组头计数改为 `命中/总数`。
|
|
73
49
|
|
|
74
50
|
### HTTP
|
|
75
51
|
|
|
@@ -93,80 +69,89 @@ await fetch('/api-ext/session.delete', {
|
|
|
93
69
|
响应沿用 host 的 JSON 信封:
|
|
94
70
|
|
|
95
71
|
```json
|
|
96
|
-
{ "ok": true, "value": { "liveBefore": true, "liveDetached": true,
|
|
72
|
+
{ "ok": true, "value": { "agentStatus": "idle", "liveBefore": true, "liveDetached": true,
|
|
97
73
|
"accounting": { "unarchived": true, "detached": ["…"] },
|
|
98
74
|
"files": { "root": "…", "removed": ["…"], "failed": [] },
|
|
99
75
|
"projection": { "ok": true, "deleted": true } } }
|
|
100
76
|
{ "ok": false, "error": { "code": "refused", "message": "…" } }
|
|
101
77
|
```
|
|
102
78
|
|
|
103
|
-
状态码:`200` 成功;`400 bad-request`(id 非法 / JSON 坏);`405 method-not-allowed`(方法不是 POST
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
79
|
+
状态码:`200` 成功;`400 bad-request`(id 非法 / JSON 坏);`405 method-not-allowed`(方法不是 POST);`415 unsupported-media-type`(`content-type` 不是 `application/json`——两个路由都只收 JSON);`409 refused`(会话正在运行);`500 internal`。
|
|
80
|
+
|
|
81
|
+
### 排查:诊断路由
|
|
82
|
+
|
|
83
|
+
菜单项是 DOM 增强(见「已知限制」),所以客户端会把每一步观测 POST 到 `POST /api-ext/session.cleaner.diag`(`{ "report": { "event": "…", "detail": … } }`),同一个路由把这份**内存环形缓冲**(上限 100 条,不落盘)读回来;带一个 `inspect` 字段时还返回该会话的**只读**删除面报告——id 是否合法、sessions 根、会被删的产物目录、有没有 live 条目、agent 状态、是否已归档、属于哪些 workspace:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
await fetch('/api-ext/session.cleaner.diag', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'content-type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ inspect: 'session-…' }),
|
|
90
|
+
}).then(r => r.json());
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
报告里还会给出 `agentStatus`——就是决定「删还是拒」的那一个字段(`null` 表示本进程里没有附着 agent,`idle` 可删,`running` 会被拒)。
|
|
94
|
+
|
|
95
|
+
它不删任何东西,是「插件到底看见了什么」的安全查看方式。
|
|
96
|
+
|
|
97
|
+
## 它做什么
|
|
98
|
+
|
|
99
|
+
- **删除四个表面。** 一次 `deleteSession` 依次做四件事:
|
|
100
|
+
|
|
101
|
+
1. **live store**:如果该会话有内存条目就 detach 掉(`SessionStore.enter()` 返回的那个 disposer),并把该会话从**前端**的会话列表里摘掉。
|
|
102
|
+
> **为什么会话会出现「有 agent 附着」这件事。** DSH 把一个会话装进内存的唯一入口是 agent:`sessions.enter` 只由 agent loop 调用,`session.list` 只是读 live 条目、其余从持久化 `summarizeCold`,**不会**把冷会话 prepare/enter 进 store。而 agent 一旦建立就**没有退出路径**——唯一的清理是 agent-loop 工厂自己那条生命周期 disposer(`dsh-agent-loop` `lib/index.js` 的 `dispose()`,`detachAgent?.(); detachSession?.()`),它只在 owner fiber 卸载时才跑。所以「在界面里点开过、现在没在跑」的会话,在**这次的 dsh web 进程里**永远带着一个 `status: 'idle'` 的 agent。早期版本因此把「有 agent」当成「被打开」一律拒绝,结果是这类会话**在重启前根本删不掉**(用户看到的是「已关闭的会话点了删除没反应」)——这是 bug,不是设计。
|
|
103
|
+
> **现在只拦真正在跑的。** `agent.status === 'running'`(以及读不出状态这种无法归类的情况)才 `409 refused`;`idle` 照删:先 `detach` 掉 store 条目(`detachEntered` 在 `entry.announced === true` 时会发 `session/disposed`),再删文件。删完之后**磁盘上已不存在可追加的会话**,残留的 idle agent 对象停在记录表里(见「已知限制」)。
|
|
104
|
+
> **踩过的坑:光 detach 不够时靠谁掉行。** 前端唯一的「会话没了」通知是 `session/disposed`,它只在 `entry.announced === true` 时才发。真正让界面掉行的是**客户端**:删完由客户端自己调 `ctx.sessions.handleSessionRemoved(id)`(正是 `api-session/removed` 中继调用的那个方法),再 `refresh()` 跟 host 基线对账——host 基线来自磁盘上的会话日志,已经不含它了。
|
|
105
|
+
2. **记账**:从全局归档集合移除,并从每个记账了它的 workspace 移除(host 会据此推 `archived` 帧给前端,两个设置页的归档集合因此同步)。
|
|
106
|
+
3. **磁盘**:删除 `<sessions 根>/<project>/<sessionId>`(遍历全部 project 目录,只删名字**恰好等于** id 的目录)。
|
|
107
|
+
4. **投影缓存**:删除 `session_projcache` 里该会话的行(文件搜索索引是派生的,会自行收敛);域没打开时该行保留,只写一条 warn 日志,删除本身仍然成功。
|
|
108
|
+
|
|
109
|
+
- **三个入口共用同一套语义。** 菜单、设置页、HTTP 都走同一个 `deleteSession`;两个界面入口还共用同一个确认框:用 DSH 自己的 UI 原语(`@deepseek-ai/dsh-client-ui-primitives` 的 `Modal` + `Button`)搭的标题 + 一句说明 + `[取消]` / `[删除]`,删除按钮是原生那种**红字描边**(`--dsw-alias-state-error-primary`)。
|
|
110
|
+
- **不使用** `window.confirm` / `window.alert`——浏览器原生弹窗在这里既不统一也很丑;
|
|
111
|
+
- 删除过程中框内显示「正在删除…」;**被拒绝(会话打开中)时框不关**,错误就显示在框里;
|
|
112
|
+
- 只有 `Esc`、点遮罩、点 `[取消]` 或删除成功才会关闭。
|
|
113
|
+
- **⋮ 菜单与图标。** 菜单是命令式 DOM、没有自己的 React 树,所以这一侧的确认框挂在一个独立的 React root 上(`react-dom/client` 的 `createRoot`),用完即卸载;确认框本身与设置页是同一个组件。三个图标同样用 DSH 自己的图标组件,不自绘:搜索框 `IconSearchOutline16`、组头折叠箭头 `IconChevronDownOutline14`、⋮ 菜单里的 `IconTrashOutline16`。(图标组件只收 `{size, className}`,转发不了 `style`/`aria-hidden`,所以定位、旋转、颜色挂在图标外面那层 box 上,两条路径共用同一个 box。)⋮ 菜单里那个图标由 React 渲染进一个临时容器、把 SVG 取出来后再把临时 root 卸掉——菜单项背后不留挂着的 root。
|
|
114
|
+
- **组件库缺席就整体降级。** 组件库不存在或**缺少其中任一成员**时,插件整体退回自绘 SVG(确认框同时退回 `window.confirm`):`loadPrimitives` 的形态检查是**全有或全无**的,所以「只到了一半」的组件库不会让界面变成一半原生一半自绘。
|
|
115
|
+
- **安全边界。** id 必须匹配 `^(session-)?<uuid>$`,否则直接 `bad-request`,不会进入任何路径拼接;**正在运行的会话拒绝删除**(`agent.status === 'running'`,以及状态读不出来这种无法归类的情况)——不会把正在跑的活儿从底下抽走;只删「位于 sessions 根之下、且目录名恰好等于该 id」的目录;两个路由都校验 HTTP 方法(POST)**和** JSON content-type(不符返回 415)。
|
|
116
|
+
- **sessions 根与宿主同源。** `sessionsRoot()` 复刻宿主的 `DSH_HOME` 处理:`$DSH_HOME` 为空或纯空白视为未设置 → `~/.dsh/sessions`;`~`、`~/`、`~\` 先展开再 `resolve`;相对路径按工作目录解析——所以插件和持久化后端永远指向同一棵树。
|
|
117
|
+
- **形态:bundle 而不是动态插件。** 同样的功能先用动态 Cordis 插件做过一版(未随本仓库发布)。bundle 形态在三点上更好:
|
|
118
|
+
|
|
119
|
+
| 维度 | 动态插件 | bundle(本包) |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| 持久性 | 进程级,DSH 重启即失效 | 装进 profile,**重启仍在** |
|
|
122
|
+
| 跨平台 | 沙箱不给 Node API,只能 shell 出去(要分平台写引擎 + 处理引号) | 直接 `node:fs`,**无 shell、无平台分支** |
|
|
123
|
+
| 入口 | 只能挂设置页 | 设置页 **+ 侧边栏 ⋮ 菜单** + HTTP 路由 |
|
|
124
|
+
|
|
125
|
+
## 前置要求
|
|
126
|
+
|
|
127
|
+
- DeepSeek Harness `^0.1.6-alpha.1`(`engines.dsh`),且是 **web profile**:宿主侧声明 `webServer`、`workspaceRegistry`、`sessions`、`agents`、`storageDomain`,客户端侧要 `slots`/`locale`/`sessions`/`uiWorkspace`
|
|
128
|
+
- Node `^22.19.0 || >=24.0.0`
|
|
129
|
+
- peer `@deepseek-ai/cordis ^4.0.2`;`@deepseek-ai/dsh-agent`、`@deepseek-ai/dsh-host-webserver`、`@deepseek-ai/dsh-session`、`@deepseek-ai/dsh-workspace` 为可选 peer(都声明为 `^0.1.6-alpha.1`)
|
|
130
|
+
- 会话树的位置由 `DSH_HOME` 决定;插件的 `sessionsRoot()` 与宿主解析规则一致
|
|
131
|
+
|
|
132
|
+
## 已知限制
|
|
133
|
+
|
|
134
|
+
- **⋮ 菜单靠 DOM 增强。** DSH 没有给行菜单公开 Slot,所以只能在菜单打开时往里插一项。识别方式是**语义**的:同一处新增(或从它向上 6 层)同时含「归档会话」与「分叉会话/重命名」两个文案才认定为会话行菜单,插入点是包含两者的最小元素;行优先取**刚点击的 ⋮ 触发器**(`aria-label` 前缀或 `rowActions` 容器,3 秒新鲜度窗口),窗口外才退回 `closest('[role="treeitem"]')`——不靠矩形距离猜测。若 DSH 改了这些文案或结构,该项会静默不出现(设置页仍然可用),诊断路由会记录跳过原因。
|
|
135
|
+
- **会话 id 从 React fiber 里读。** 行的 DOM 上没有任何携带 id 的属性,所以从行元素的 `__reactFiber$*` / `__reactInternalInstance$*` 向上找 10 层,取 `memoizedProps.node.id`(或 `props.sessionId` / `props.node.sessionId`)。拿不到才退回标题反查(走 `/api/session.list` 的目录,标题**重复时跳过注入**,避免删错);目录取不到时只损失「运行中」标记,行仍可用。
|
|
136
|
+
- **正在运行(`running`)的会话拒绝删除**,不代用户中止。
|
|
137
|
+
- **删除后会在记录表里留下一个「孤儿」agent 对象(idle)。** 宿主没有对外公开的「卸载单个 agent」入口:能正常收尾的那条路(关持久化句柄、发 `agent/disposed`、把条目移出 `ctx.agents`)是 agent-loop 工厂私有的生命周期 disposer,只挂在 owner fiber 的 teardown 链上;从外面只能拿到 `scope` 或 store 条目,单独 `detach` 会留下一个「agent 还在、store 条目没了」的悬空对象(这正是早期版本要防的幽灵行)。所以本插件**不动它**:删除后该 agent 停在 `idle` 且其会话已从 store 摘除、磁盘已删,不会再跑任何一轮(会话重开也走不到它:`session.list` 只从持久化读列表,日志已不存在)。残留对象随进程结束消失,想立刻清空就重启 `dsh web`。
|
|
138
|
+
- **删除不可恢复**:文件、记账、live 条目、投影缓存行都会消失。
|
|
139
|
+
- **不广播 `session/disposed`(冷会话场景)。** 被删的会话若没有 live 条目,宿主不会发任何「会话没了」的事件——本页面由客户端自己把行摘掉;**其他已打开的页面/标签页要刷新才会同步**。
|
|
140
|
+
- **投影缓存域没打开时那一行会保留**(只写 warn 日志,删除仍返回成功):缓存是折叠快捷方式而不是权威,下次冷读会自行收敛。
|
|
141
|
+
- **宿主半改动要重启进程**;客户端半刷新页面即可,但 bundle 整体不做热加载。
|
|
141
142
|
|
|
142
143
|
## 开发
|
|
143
144
|
|
|
144
145
|
```sh
|
|
145
146
|
node --test test/host.test.js # node:test 版(host)
|
|
146
147
|
node test/run.js # host + client 全部用例,平铺断言(受限沙箱里也能跑)
|
|
148
|
+
npm test # 上面两条依次跑
|
|
147
149
|
```
|
|
148
150
|
|
|
149
|
-
`test/cases.js` 是 host 用例,只操作 `mkdtemp` 出来的 scratch 目录:`deleteSession` 的第三个参数
|
|
150
|
-
|
|
151
|
-
`test/client-cases.js` 是客户端用例:把 `client.js` 挂在一个极简的 `window.__ModuleLoader__`
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
151
|
+
`test/cases.js` 是 host 用例,只操作 `mkdtemp` 出来的 scratch 目录:`deleteSession` 的第三个参数 `{ root }` 就是为此留的测试缝,**不会碰到真实会话**。
|
|
152
|
+
|
|
153
|
+
`test/client-cases.js` 是客户端用例:把 `client.js` 挂在一个极简的 `window.__ModuleLoader__` 垫片上、配一个**照抄真 React 语义**的替身(`props.children`、函数组件即时展开、hook 槽位跨渲染存活并在 setState 时重渲染),再给 `Modal`/`Button` 提供保持契约的替身(`Modal` 关闭时渲染 `null`),于是设置页可以**无浏览器、无 DOM、无框架**地被完整驱动——点删除、在确认框里点取消/删除、点组头折叠、往搜索框里打字——并断言它渲染了什么、对注入的服务做了什么。⋮ 菜单那一路另配一个迷你 DOM(节点、属性、子节点,以及安装器真正查询的那几个选择器),于是菜单项也能被真正构建出来、点进去删一次。图标用例的组件库替身分三档(完整 / 缺图标 / 模块表缺失)并记录「哪个图标组件被调用」,同时区分「React 渲染出来的 svg」与「自绘的 svg」——所以「用原生图标」和「退回自绘」两条路径都有断言。
|
|
154
|
+
|
|
155
|
+
## 许可
|
|
171
156
|
|
|
172
157
|
MIT
|
package/client.js
CHANGED
|
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
del: '删除',
|
|
48
48
|
delNamed: '删除会话 {title}',
|
|
49
49
|
confirm: '确定要永久删除会话「{title}」吗?此操作不可恢复。',
|
|
50
|
-
refused: '
|
|
50
|
+
refused: '该会话正在运行,请等它跑完(或先中止)再删除。',
|
|
51
51
|
failed: '删除失败:{message}',
|
|
52
52
|
unarchive: '取消归档',
|
|
53
53
|
cancel: '取消',
|
|
@@ -76,7 +76,7 @@ window.__ModuleLoader__.load({
|
|
|
76
76
|
del: 'Delete',
|
|
77
77
|
delNamed: 'Delete session {title}',
|
|
78
78
|
confirm: 'Permanently delete session “{title}”? This cannot be undone.',
|
|
79
|
-
refused: 'This session is
|
|
79
|
+
refused: 'This session is running; wait for it to finish (or stop it) before deleting.',
|
|
80
80
|
failed: 'Delete failed: {message}',
|
|
81
81
|
unarchive: 'Unarchive',
|
|
82
82
|
cancel: 'Cancel',
|
package/lib/index.js
CHANGED
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
// session ever has an entry: the agent loop is the sole `sessions.enter`
|
|
12
12
|
// caller, and `session.list` reads live entries while summarizing every
|
|
13
13
|
// other session from persistence — it does not prepare cold sessions into
|
|
14
|
-
// the store.
|
|
15
|
-
//
|
|
16
|
-
// outlived its agent cannot survive as a row over deleted files;
|
|
14
|
+
// the store. An IDLE agent is detached and deleted; a RUNNING one is
|
|
15
|
+
// refused, because interrupting work in flight is not this plugin's to do;
|
|
17
16
|
// 2. drops the id from the global archive set and from every workspace
|
|
18
17
|
// record that accounts for it;
|
|
19
18
|
// 3. deletes the on-disk artifact directory `<root>/<project>/<sessionId>`;
|
|
20
19
|
// 4. deletes the session's `session_projcache` row (the file search index is
|
|
21
20
|
// derived and prunes itself).
|
|
22
21
|
//
|
|
23
|
-
// The sidebar row itself is dropped by the CLIENT half
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
22
|
+
// The sidebar row itself is dropped by the CLIENT half, which calls the store's
|
|
23
|
+
// own `handleSessionRemoved` and then reconciles against the host baseline —
|
|
24
|
+
// `session/list` reads the deleted session's log from persistence, so the
|
|
25
|
+
// refreshed baseline no longer carries the row. (`session/disposed` fires too
|
|
26
|
+
// whenever the detached entry was announced, but the client's own removal is
|
|
27
|
+
// what makes the row leave without waiting for that event.)
|
|
27
28
|
//
|
|
28
29
|
// Everything is `node:fs` — no shell, no quoting, no platform branch.
|
|
29
30
|
//
|
|
@@ -297,17 +298,25 @@ export async function deleteSession(ctx, sessionId, options = {}) {
|
|
|
297
298
|
error.code = 'bad-request'
|
|
298
299
|
throw error
|
|
299
300
|
}
|
|
300
|
-
// An
|
|
301
|
-
//
|
|
302
|
-
//
|
|
301
|
+
// An agent at work is not this plugin's to interrupt: a `running` agent is
|
|
302
|
+
// mid-turn, and a status this build does not know could be anything, so both
|
|
303
|
+
// are refused and the user is told to wait.
|
|
303
304
|
const agent = ctx.agents?.get?.(sessionId)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
305
|
+
const agentStatus = agent === undefined ? null : typeof agent.status === 'string' ? agent.status : 'attached'
|
|
306
|
+
if (agentStatus !== null && agentStatus !== 'idle') {
|
|
307
|
+
const reason = agentStatus === 'running' ? 'is running' : `is in use (agent status: ${agentStatus})`
|
|
308
|
+
const error = new Error(`session "${sessionId}" ${reason}; wait for it to finish before deleting`)
|
|
307
309
|
error.code = 'refused'
|
|
308
310
|
throw error
|
|
309
311
|
}
|
|
310
|
-
|
|
312
|
+
// An IDLE agent is what every session the UI has ever shown keeps in the
|
|
313
|
+
// registry — DSH never tears one down when the user leaves a session, so it
|
|
314
|
+
// would otherwise stay undeletable for the life of the process. Nothing is
|
|
315
|
+
// being computed, so the delete proceeds; the live entry goes through the
|
|
316
|
+
// store's own disposer (which also emits `session/disposed` when the entry was
|
|
317
|
+
// announced), and the deleted session's files can no longer be appended to.
|
|
318
|
+
// The registry's agent object outlives this deletion — see the README's
|
|
319
|
+
// "已知限制" — which is why `agentStatus` is reported back (see below).
|
|
311
320
|
const liveBefore = ctx.sessions?.get?.(sessionId) !== undefined
|
|
312
321
|
const liveDetached = detachLiveEntry(ctx.sessions, sessionId)
|
|
313
322
|
const accounting = await detachAccounting(ctx, sessionId)
|
|
@@ -321,6 +330,7 @@ export async function deleteSession(ctx, sessionId, options = {}) {
|
|
|
321
330
|
|
|
322
331
|
return {
|
|
323
332
|
sessionId,
|
|
333
|
+
agentStatus, // the attached agent this delete went through, or null when none
|
|
324
334
|
liveBefore, // true when a live store entry existed (only an agent session has one)
|
|
325
335
|
liveDetached,
|
|
326
336
|
accounting,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zfdx123/dsh-session-cleaner",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "会话清理:在运行中的 web 运行时里彻底删除 DSH 会话——实时 store 条目、工作区记录、磁盘产物与投影缓存行一并清掉,并在会话行菜单里加一个删除入口。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/zfdx123/dsh-atelier/tree/main/packages/dsh-session-cleaner#readme",
|