@zfdx123/dsh-session-cleaner 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +84 -103
  2. 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
- 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
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
- 卸载(可逆):`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。
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
- 两个入口共用**同一个确认框**,用的是 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。
23
+ ```sh
24
+ # 软链(源码改完 profile 立刻就是新的,只需重启 dsh)
25
+ dsh plugin --profile web add link:/absolute/path/to/dsh-atelier/packages/dsh-session-cleaner
48
26
 
49
- 组件库缺席或**缺少其中任一成员**时,插件整体退回自绘 SVG(确认框同时退回 `window.confirm`):
50
- `loadPrimitives` 的形态检查是**全有或全无**的,所以「只到了一半」的组件库不会让界面变成一半原生一半自绘。
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
- > 菜单是命令式 DOM,没有自己的 React 树,所以这一侧的确认框挂在一个独立的 React root 上
58
- > (`react-dom/client` 的 `createRoot`),用完即卸载;确认框本身与设置页是同一个组件。
35
+ 会话行右侧 ⋮ → **「删除会话」**,位置紧跟在**「归档会话」下面**。点击 → 确认框 → 该行立即消失。会话正在运行时该项置灰;已打开(有 agent 附着)的会话会被服务端拒绝,请先关闭它。
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
 
@@ -100,73 +76,78 @@ await fetch('/api-ext/session.delete', {
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
- `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 路由 |
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
+ 它不删任何东西,是「插件到底看见了什么」的安全查看方式。
94
+
95
+ ## 它做什么
96
+
97
+ - **删除四个表面。** 一次 `deleteSession` 依次做四件事:
98
+
99
+ 1. **live store**:如果该会话有内存条目就 detach 掉(`SessionStore.enter()` 返回的那个 disposer),并把该会话从**前端**的会话列表里摘掉。
100
+ > **踩过的坑:光 detach 不会让前端掉行。** 前端唯一的「会话没了」通知是 `session/disposed`,而 `SessionStore.detachEntered` 只在 `entry.announced === true` 时才发它——只有 agent 真正打开过的会话才会置位(`sessions.enter` 只由 agent loop 调用;`session.list` 只是读 live 条目、其余从持久化 `summarizeCold`,**不会**把冷会话 prepare/enter 进 store)。而本路由**拒绝**一切有 agent 附着的会话,所以它删掉的会话根本没有内存条目、任何事件都不会发。detach 那一步因此是**防御性**保留的:万一有「agent 已消失、条目还在」的残留,也不能让它变成一个指向已删文件的幽灵行。
101
+ > 真正让界面掉行的是客户端:删完由客户端自己调 `ctx.sessions.handleSessionRemoved(id)`(正是 `api-session/removed` 中继调用的那个方法),再 `refresh()` 跟 host 基线对账——host 基线来自磁盘上的会话日志,已经不含它了。
102
+ 2. **记账**:从全局归档集合移除,并从每个记账了它的 workspace 移除(host 会据此推 `archived` 帧给前端,两个设置页的归档集合因此同步)。
103
+ 3. **磁盘**:删除 `<sessions 根>/<project>/<sessionId>`(遍历全部 project 目录,只删名字**恰好等于** id 的目录)。
104
+ 4. **投影缓存**:删除 `session_projcache` 里该会话的行(文件搜索索引是派生的,会自行收敛);域没打开时该行保留,只写一条 warn 日志,删除本身仍然成功。
105
+
106
+ - **三个入口共用同一套语义。** 菜单、设置页、HTTP 都走同一个 `deleteSession`;两个界面入口还共用同一个确认框:用 DSH 自己的 UI 原语(`@deepseek-ai/dsh-client-ui-primitives` 的 `Modal` + `Button`)搭的标题 + 一句说明 + `[取消]` / `[删除]`,删除按钮是原生那种**红字描边**(`--dsw-alias-state-error-primary`)。
107
+ - **不使用** `window.confirm` / `window.alert`——浏览器原生弹窗在这里既不统一也很丑;
108
+ - 删除过程中框内显示「正在删除…」;**被拒绝(会话打开中)时框不关**,错误就显示在框里;
109
+ - 只有 `Esc`、点遮罩、点 `[取消]` 或删除成功才会关闭。
110
+ - **⋮ 菜单与图标。** 菜单是命令式 DOM、没有自己的 React 树,所以这一侧的确认框挂在一个独立的 React root 上(`react-dom/client` 的 `createRoot`),用完即卸载;确认框本身与设置页是同一个组件。三个图标同样用 DSH 自己的图标组件,不自绘:搜索框 `IconSearchOutline16`、组头折叠箭头 `IconChevronDownOutline14`、⋮ 菜单里的 `IconTrashOutline16`。(图标组件只收 `{size, className}`,转发不了 `style`/`aria-hidden`,所以定位、旋转、颜色挂在图标外面那层 box 上,两条路径共用同一个 box。)⋮ 菜单里那个图标由 React 渲染进一个临时容器、把 SVG 取出来后再把临时 root 卸掉——菜单项背后不留挂着的 root。
111
+ - **组件库缺席就整体降级。** 组件库不存在或**缺少其中任一成员**时,插件整体退回自绘 SVG(确认框同时退回 `window.confirm`):`loadPrimitives` 的形态检查是**全有或全无**的,所以「只到了一半」的组件库不会让界面变成一半原生一半自绘。
112
+ - **安全边界。** id 必须匹配 `^(session-)?<uuid>$`,否则直接 `bad-request`,不会进入任何路径拼接;**有 agent 附着的会话拒绝删除**(running 或 idle 都算「已打开」)——不会把别人正在用的会话从底下抽走;只删「位于 sessions 根之下、且目录名恰好等于该 id」的目录;两个路由都校验 HTTP 方法(POST)**和** JSON content-type(不符返回 415)。
113
+ - **sessions 根与宿主同源。** `sessionsRoot()` 复刻宿主的 `DSH_HOME` 处理:`$DSH_HOME` 为空或纯空白视为未设置 → `~/.dsh/sessions`;`~`、`~/`、`~\` 先展开再 `resolve`;相对路径按工作目录解析——所以插件和持久化后端永远指向同一棵树。
114
+ - **形态:bundle 而不是动态插件。** 同样的功能先用动态 Cordis 插件做过一版(未随本仓库发布)。bundle 形态在三点上更好:
115
+
116
+ | 维度 | 动态插件 | bundle(本包) |
117
+ | --- | --- | --- |
118
+ | 持久性 | 进程级,DSH 重启即失效 | 装进 profile,**重启仍在** |
119
+ | 跨平台 | 沙箱不给 Node API,只能 shell 出去(要分平台写引擎 + 处理引号) | 直接 `node:fs`,**无 shell、无平台分支** |
120
+ | 入口 | 只能挂设置页 | 设置页 **+ 侧边栏 ⋮ 菜单** + HTTP 路由 |
121
+
122
+ ## 前置要求
123
+
124
+ - DeepSeek Harness `^0.1.6-alpha.1`(`engines.dsh`),且是 **web profile**:宿主侧声明 `webServer`、`workspaceRegistry`、`sessions`、`agents`、`storageDomain`,客户端侧要 `slots`/`locale`/`sessions`/`uiWorkspace`
125
+ - Node `^22.19.0 || >=24.0.0`
126
+ - 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`)
127
+ - 会话树的位置由 `DSH_HOME` 决定;插件的 `sessionsRoot()` 与宿主解析规则一致
128
+
129
+ ## 已知限制
130
+
131
+ - **⋮ 菜单靠 DOM 增强。** DSH 没有给行菜单公开 Slot,所以只能在菜单打开时往里插一项。识别方式是**语义**的:同一处新增(或从它向上 6 层)同时含「归档会话」与「分叉会话/重命名」两个文案才认定为会话行菜单,插入点是包含两者的最小元素;行优先取**刚点击的 ⋮ 触发器**(`aria-label` 前缀或 `rowActions` 容器,3 秒新鲜度窗口),窗口外才退回 `closest('[role="treeitem"]')`——不靠矩形距离猜测。若 DSH 改了这些文案或结构,该项会静默不出现(设置页仍然可用),诊断路由会记录跳过原因。
132
+ - **会话 id 从 React fiber 里读。** 行的 DOM 上没有任何携带 id 的属性,所以从行元素的 `__reactFiber$*` / `__reactInternalInstance$*` 向上找 10 层,取 `memoizedProps.node.id`(或 `props.sessionId` / `props.node.sessionId`)。拿不到才退回标题反查(走 `/api/session.list` 的目录,标题**重复时跳过注入**,避免删错);目录取不到时只损失「运行中」标记,行仍可用。
133
+ - **会话打开中(running 或 idle agent)时拒绝删除**,不代用户关会话。
134
+ - **删除不可恢复**:文件、记账、live 条目、投影缓存行都会消失。
135
+ - **不广播 `session/disposed`。** 被删的冷会话本来就没有 live 条目,所以宿主不会发任何「会话没了」的事件——本页面由客户端自己把行摘掉;**其他已打开的页面/标签页要刷新才会同步**。
136
+ - **投影缓存域没打开时那一行会保留**(只写 warn 日志,删除仍返回成功):缓存是折叠快捷方式而不是权威,下次冷读会自行收敛。
137
+ - **宿主半改动要重启进程**;客户端半刷新页面即可,但 bundle 整体不做热加载。
141
138
 
142
139
  ## 开发
143
140
 
144
141
  ```sh
145
142
  node --test test/host.test.js # node:test 版(host)
146
143
  node test/run.js # host + client 全部用例,平铺断言(受限沙箱里也能跑)
144
+ npm test # 上面两条依次跑
147
145
  ```
148
146
 
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
147
+ `test/cases.js` 是 host 用例,只操作 `mkdtemp` 出来的 scratch 目录:`deleteSession` 的第三个参数 `{ root }` 就是为此留的测试缝,**不会碰到真实会话**。
148
+
149
+ `test/client-cases.js` 是客户端用例:把 `client.js` 挂在一个极简的 `window.__ModuleLoader__` 垫片上、配一个**照抄真 React 语义**的替身(`props.children`、函数组件即时展开、hook 槽位跨渲染存活并在 setState 时重渲染),再给 `Modal`/`Button` 提供保持契约的替身(`Modal` 关闭时渲染 `null`),于是设置页可以**无浏览器、无 DOM、无框架**地被完整驱动——点删除、在确认框里点取消/删除、点组头折叠、往搜索框里打字——并断言它渲染了什么、对注入的服务做了什么。⋮ 菜单那一路另配一个迷你 DOM(节点、属性、子节点,以及安装器真正查询的那几个选择器),于是菜单项也能被真正构建出来、点进去删一次。图标用例的组件库替身分三档(完整 / 缺图标 / 模块表缺失)并记录「哪个图标组件被调用」,同时区分「React 渲染出来的 svg」与「自绘的 svg」——所以「用原生图标」和「退回自绘」两条路径都有断言。
150
+
151
+ ## 许可
171
152
 
172
153
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zfdx123/dsh-session-cleaner",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",