@michengai/dsh-archive-manager 0.1.7 → 0.1.9
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 +188 -134
- package/README.zh-CN.md +188 -134
- package/assets/branding/banner.txt +11 -0
- package/assets/screenshots/archive-session-menu.png +0 -0
- package/assets/screenshots/archived-sessions.png +0 -0
- package/lib/client.js +191 -43
- package/lib/projcache.js +10 -1
- package/lib/workspace.js +11 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,134 +1,188 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pnpm
|
|
127
|
-
pnpm
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<table cellpadding="0" cellspacing="0">
|
|
3
|
+
<tr>
|
|
4
|
+
<td valign="middle" align="center">
|
|
5
|
+
<img src="assets/icon.png" alt="DSH Archive Manager logo" width="168">
|
|
6
|
+
</td>
|
|
7
|
+
<td valign="middle">
|
|
8
|
+
<pre>
|
|
9
|
+
____ ____ _ _
|
|
10
|
+
| _ \/ ___|| | | |
|
|
11
|
+
| | | \___ \| |_| |
|
|
12
|
+
| |_| |___) | _ |
|
|
13
|
+
|____/|____/|_| |_|
|
|
14
|
+
ARCHIVE MANAGER
|
|
15
|
+
Safe archived session control
|
|
16
|
+
DeepSeek Harness plugin
|
|
17
|
+
</pre>
|
|
18
|
+
</td>
|
|
19
|
+
</tr>
|
|
20
|
+
</table>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
# DSH Archive Manager
|
|
26
|
+
|
|
27
|
+
**Safely manage archived sessions in DeepSeek Harness**
|
|
28
|
+
|
|
29
|
+
[简体中文](README.zh-CN.md) · [Apache-2.0](LICENSE)
|
|
30
|
+
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://www.npmjs.com/package/@michengai/dsh-archive-manager)
|
|
33
|
+
[](https://github.com/MichengAI/dsh-archive-manager)
|
|
34
|
+
[](https://nodejs.org/)
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
> DSH Archive Manager is a community-maintained DeepSeek Harness (DSH) plugin, not an official DeepSeek AI product.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- Archive a session from the sidebar session menu.
|
|
42
|
+
- Search archived chats and filter them by workspace in **Settings → Archived**.
|
|
43
|
+
- Restore a session to its original workspace with **Unarchive**.
|
|
44
|
+
- Permanently delete a confirmed session, its workspace association, archive marker, and projection cache.
|
|
45
|
+
- Delete all archived chats after confirmation, including child agents.
|
|
46
|
+
- Remove unloaded deleted sessions from connected sidebars immediately.
|
|
47
|
+
- Paste one sentence into DSH, Codex, or WorkBuddy and let that agent install the plugin locally.
|
|
48
|
+
|
|
49
|
+
## Screenshots
|
|
50
|
+
|
|
51
|
+
Open the sidebar session menu and choose **Archive session**:
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
Search, filter by project, unarchive, or permanently delete chats in **Settings → Archived**:
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## Prerequisites
|
|
60
|
+
|
|
61
|
+
- A working DeepSeek Harness Web installation with `dsh` available in PowerShell.
|
|
62
|
+
- Examples use the `web` profile; replace it with the target profile.
|
|
63
|
+
- Source installation and development require Node.js 22+ and pnpm. npm installation does not require running `pnpm install` separately.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
`dsh plugin add` forwards to `pnpm add` in the profile directory. Without a version and official registry, a local mirror or minimum-release-age policy can leave you on an older build.
|
|
68
|
+
|
|
69
|
+
### Ask another agent to install it
|
|
70
|
+
|
|
71
|
+
This plugin runs inside DeepSeek Harness Web. Copy one of the sentences below into DSH, Codex, or WorkBuddy and let that agent install it into your local `web` profile.
|
|
72
|
+
|
|
73
|
+
From npm:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
Install the latest DSH plugin @michengai/dsh-archive-manager into my local web profile using the official npm registry: dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/. Then run dsh --profile web --dump-config, confirm archive-manager is mounted, and remind me to restart DSH Web and hard-refresh the browser.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
From source:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
Install the DSH plugin from source at https://github.com/MichengAI/dsh-archive-manager: clone it, run pnpm install --frozen-lockfile and pnpm build, then run dsh plugin --profile web add . from that directory. Do not copy lib by itself. Then run dsh --profile web --dump-config, confirm archive-manager is mounted, and remind me to restart DSH Web and hard-refresh the browser.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Product | How to use it |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| DSH | Send one of the sentences above to the current session. |
|
|
88
|
+
| Codex | Send one of the sentences above to Codex and let it install locally. |
|
|
89
|
+
| WorkBuddy | Send one of the sentences above to WorkBuddy; for a source install you can also paste `https://github.com/MichengAI/dsh-archive-manager`. |
|
|
90
|
+
|
|
91
|
+
Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Archived**.
|
|
92
|
+
|
|
93
|
+
You can also run the same npm command yourself:
|
|
94
|
+
|
|
95
|
+
```powershell
|
|
96
|
+
dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If `dsh` is not on PATH, replace the leading `dsh` with `npx --yes @deepseek-ai/dsh`.
|
|
100
|
+
|
|
101
|
+
### Install the latest package from the official npm registry
|
|
102
|
+
|
|
103
|
+
Run this from any PowerShell directory:
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
107
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
108
|
+
dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
|
|
109
|
+
dsh --profile web --dump-config
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To pin a release, replace `@latest` with a version such as `@0.1.8`.
|
|
113
|
+
|
|
114
|
+
The configuration output should contain `workspace-archive-manager` and `ui-workspace-archive-manager`. Restart DSH Web and hard-refresh the browser. Do not copy client files manually: the Settings page and archive menu need the mounted plugin.
|
|
115
|
+
|
|
116
|
+
### Install from source
|
|
117
|
+
|
|
118
|
+
Use this for debugging or unpublished changes. The cloned directory becomes the plugin source path:
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
122
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
123
|
+
Set-Location D:\Repository\deepseek-harness-plugin
|
|
124
|
+
git clone https://github.com/MichengAI/dsh-archive-manager.git
|
|
125
|
+
Set-Location .\dsh-archive-manager
|
|
126
|
+
pnpm install --frozen-lockfile
|
|
127
|
+
pnpm build
|
|
128
|
+
dsh plugin --profile web add .
|
|
129
|
+
dsh --profile web --dump-config
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Restart DSH Web and hard-refresh the browser. `dsh plugin ... add .` reads the package metadata and `cordis.patch.yml`; do not install by copying `lib` directly.
|
|
133
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
1. Open the sidebar session menu and choose **Archive session**.
|
|
137
|
+
2. Open **Settings → Archived** to inspect sessions by workspace.
|
|
138
|
+
3. Use the search box or project filter to narrow the list.
|
|
139
|
+
4. Select **Unarchive** to restore a session, or use the delete icon to remove it permanently.
|
|
140
|
+
5. Confirm deletion. **It cannot be undone.**
|
|
141
|
+
|
|
142
|
+
If the entry is missing after installation or upgrade, restart DSH Web and hard-refresh the browser. It is located directly after **Connectors** in Settings.
|
|
143
|
+
|
|
144
|
+
## Data handling limits
|
|
145
|
+
|
|
146
|
+
- Deletion always requires confirmation.
|
|
147
|
+
- It removes the session directory, workspace records, archive set, and projection cache.
|
|
148
|
+
- A live session finishes writing before cleanup to prevent data truncation.
|
|
149
|
+
- The plugin replaces DSH’s default workspace and projection services. Install through the DSH profile instead of manually composing the patch.
|
|
150
|
+
|
|
151
|
+
## Secondary development
|
|
152
|
+
|
|
153
|
+
This repository has no `src` directory. `lib` is directly maintained runtime source, which is its current layout rather than the recommended layout for new plugins. New plugins should prefer `src` built to `lib`.
|
|
154
|
+
|
|
155
|
+
- [lib\index.js](lib/index.js): host service entry point.
|
|
156
|
+
- [lib\workspace.js](lib/workspace.js): archived-session and workspace service.
|
|
157
|
+
- [lib\projcache.js](lib/projcache.js): session projection cache.
|
|
158
|
+
- [lib\client.js](lib/client.js): Settings page and archive UI.
|
|
159
|
+
- `test\*.test.mjs`: host, client, Remote, and styling coverage.
|
|
160
|
+
|
|
161
|
+
After changing the runtime source, validate, test, and install from the local directory:
|
|
162
|
+
|
|
163
|
+
```powershell
|
|
164
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
165
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
166
|
+
pnpm build
|
|
167
|
+
pnpm test
|
|
168
|
+
pnpm pack:check
|
|
169
|
+
dsh plugin --profile web add .
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`pnpm build` validates package integrity; it does not compile `lib` into another directory.
|
|
173
|
+
|
|
174
|
+
## Validation
|
|
175
|
+
|
|
176
|
+
```powershell
|
|
177
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
178
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
179
|
+
pnpm build
|
|
180
|
+
pnpm test
|
|
181
|
+
pnpm pack:check
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`prepublishOnly` runs the build check and tests before publishing.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
Licensed under [Apache License 2.0](LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -1,134 +1,188 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pnpm
|
|
127
|
-
pnpm
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<table cellpadding="0" cellspacing="0">
|
|
3
|
+
<tr>
|
|
4
|
+
<td valign="middle" align="center">
|
|
5
|
+
<img src="assets/icon.png" alt="DSH Archive Manager 标识" width="168">
|
|
6
|
+
</td>
|
|
7
|
+
<td valign="middle">
|
|
8
|
+
<pre>
|
|
9
|
+
____ ____ _ _
|
|
10
|
+
| _ \/ ___|| | | |
|
|
11
|
+
| | | \___ \| |_| |
|
|
12
|
+
| |_| |___) | _ |
|
|
13
|
+
|____/|____/|_| |_|
|
|
14
|
+
ARCHIVE MANAGER
|
|
15
|
+
Safe archived session control
|
|
16
|
+
DeepSeek Harness plugin
|
|
17
|
+
</pre>
|
|
18
|
+
</td>
|
|
19
|
+
</tr>
|
|
20
|
+
</table>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
# DSH Archive Manager
|
|
26
|
+
|
|
27
|
+
**在 DeepSeek Harness 中安全管理已归档会话**
|
|
28
|
+
|
|
29
|
+
[English](README.md) · [Apache-2.0](LICENSE)
|
|
30
|
+
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://www.npmjs.com/package/@michengai/dsh-archive-manager)
|
|
33
|
+
[](https://github.com/MichengAI/dsh-archive-manager)
|
|
34
|
+
[](https://nodejs.org/)
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
> DSH Archive Manager 是社区维护的 DeepSeek Harness(DSH)插件,并非 DeepSeek AI 官方产品。
|
|
38
|
+
|
|
39
|
+
## 功能概览
|
|
40
|
+
|
|
41
|
+
- 在侧栏会话菜单中选择「归档会话」。
|
|
42
|
+
- 在「设置 → 已归档」按工作区查看归档会话,并支持搜索和按项目筛选。
|
|
43
|
+
- 安全取消归档,将会话恢复到原工作区位置。
|
|
44
|
+
- 经确认后永久删除会话、工作区归属、归档标记和投影缓存。
|
|
45
|
+
- 经确认后删除全部已归档聊天,包含子代理。
|
|
46
|
+
- 已删除的未加载归档会话会立即从已连接客户端的侧栏移除。
|
|
47
|
+
- 可把一句话复制到 DSH、Codex 或 WorkBuddy,让对方代装到本机 DSH。
|
|
48
|
+
|
|
49
|
+
## 界面预览
|
|
50
|
+
|
|
51
|
+
在侧栏会话菜单中选择「归档会话」:
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
在「设置 → 已归档」中搜索、按项目筛选、取消归档或永久删除:
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## 前置条件
|
|
60
|
+
|
|
61
|
+
- 已可正常运行 DeepSeek Harness Web,且可在 PowerShell 中使用 `dsh`。
|
|
62
|
+
- 以下示例使用 `web` profile;请替换为实际目标 profile。
|
|
63
|
+
- 从源码安装或二次开发需要 Node.js 22+ 与 pnpm;仅从 npm 安装无需另外执行 `pnpm install`。
|
|
64
|
+
|
|
65
|
+
## 安装
|
|
66
|
+
|
|
67
|
+
`dsh plugin add` 会转发到 profile 目录里的 `pnpm add`。不写版本、不指定官方源时,本机镜像和最短发布间隔可能让你停在旧版。
|
|
68
|
+
|
|
69
|
+
### 交给其他 Agent 一句话安装
|
|
70
|
+
|
|
71
|
+
本插件运行在 DeepSeek Harness Web 里。把下面其中一句复制到 DSH、Codex 或 WorkBuddy,让它代你安装到本机 `web` profile。
|
|
72
|
+
|
|
73
|
+
从 npm 安装:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
请把 DSH 插件 @michengai/dsh-archive-manager 最新版装进本机 web profile,使用官方 npm 源执行:dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/。装完执行 dsh --profile web --dump-config,确认已挂载 archive-manager,并提醒我重启 DSH Web 后硬刷新浏览器。
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
从源码安装:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
请从源码安装 DSH 插件 https://github.com/MichengAI/dsh-archive-manager:克隆到本机后执行 pnpm install --frozen-lockfile 和 pnpm build,再用 dsh plugin --profile web add . 把当前目录装进 web profile。不要只复制 lib。装完执行 dsh --profile web --dump-config,确认已挂载 archive-manager,并提醒我重启 DSH Web 后硬刷新浏览器。
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| 产品 | 怎么用 |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| DSH | 把上面其中一句发给当前会话。 |
|
|
88
|
+
| Codex | 把上面其中一句发给 Codex,让它在本机执行安装。 |
|
|
89
|
+
| WorkBuddy | 把上面其中一句发给 WorkBuddy;源码安装也可同时粘贴仓库地址 `https://github.com/MichengAI/dsh-archive-manager`。 |
|
|
90
|
+
|
|
91
|
+
Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 → 已归档」。
|
|
92
|
+
|
|
93
|
+
也可以自己执行同一条 npm 命令:
|
|
94
|
+
|
|
95
|
+
```powershell
|
|
96
|
+
dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
未把 `dsh` 装进 PATH 时,把开头的 `dsh` 换成 `npx --yes @deepseek-ai/dsh`。
|
|
100
|
+
|
|
101
|
+
### 从官方 npm 安装最新版
|
|
102
|
+
|
|
103
|
+
在任意 PowerShell 目录执行:
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
107
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
108
|
+
dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
|
|
109
|
+
dsh --profile web --dump-config
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
需要钉死某一版时,把 `@latest` 换成具体版本,例如 `@0.1.8`。
|
|
113
|
+
|
|
114
|
+
配置输出中应包含 `workspace-archive-manager` 与 `ui-workspace-archive-manager`。安装后重启 DSH Web 并在浏览器硬刷新;请勿手工复制客户端文件,否则设置页和归档菜单不会被挂载。
|
|
115
|
+
|
|
116
|
+
### 从源码安装
|
|
117
|
+
|
|
118
|
+
适用于调试或使用未发布改动。克隆后的目录会直接作为插件安装路径:
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
122
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
123
|
+
Set-Location D:\Repository\deepseek-harness-plugin
|
|
124
|
+
git clone https://github.com/MichengAI/dsh-archive-manager.git
|
|
125
|
+
Set-Location .\dsh-archive-manager
|
|
126
|
+
pnpm install --frozen-lockfile
|
|
127
|
+
pnpm build
|
|
128
|
+
dsh plugin --profile web add .
|
|
129
|
+
dsh --profile web --dump-config
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
完成后重启 DSH Web 并硬刷新浏览器。`dsh plugin ... add .` 会读取当前目录的包信息和 `cordis.patch.yml`;不要改为直接复制 `lib` 目录。
|
|
133
|
+
|
|
134
|
+
## 使用
|
|
135
|
+
|
|
136
|
+
1. 在侧栏右键或打开会话菜单,选择「归档会话」。
|
|
137
|
+
2. 打开「设置 → 已归档」,按工作区查看归档会话。
|
|
138
|
+
3. 用搜索框或项目筛选收窄列表。
|
|
139
|
+
4. 点击「取消归档」恢复会话;点击删除图标永久移除会话。
|
|
140
|
+
5. 删除前确认提示。**删除无法撤销。**
|
|
141
|
+
|
|
142
|
+
安装或升级后找不到入口时,重启 DSH Web 并硬刷新浏览器;入口位于「设置」中,连接器之后。
|
|
143
|
+
|
|
144
|
+
## 数据处理边界
|
|
145
|
+
|
|
146
|
+
- 删除操作始终需要确认。
|
|
147
|
+
- 删除会移除会话目录、工作区记录、归档集合和投影缓存。
|
|
148
|
+
- 正在写入的会话会在完成写入后清理,避免截断数据。
|
|
149
|
+
- 本插件替换 DSH 默认的工作区和会话投影服务;请仅通过 DSH profile 安装,避免手工拼接补丁配置。
|
|
150
|
+
|
|
151
|
+
## 二次开发
|
|
152
|
+
|
|
153
|
+
当前仓库未提供 `src` 源目录,`lib` 是直接维护的运行源码;这是当前仓库的实现方式,不是新插件的推荐布局。新插件建议使用 `src` 开发并构建到 `lib`:
|
|
154
|
+
|
|
155
|
+
- [lib\index.js](lib/index.js):客户端插件 Host 服务入口。
|
|
156
|
+
- [lib\workspace.js](lib/workspace.js):归档会话和工作区服务实现。
|
|
157
|
+
- [lib\projcache.js](lib/projcache.js):会话投影缓存实现。
|
|
158
|
+
- [lib\client.js](lib/client.js):设置页和归档会话界面。
|
|
159
|
+
- `test\*.test.mjs`:Host、客户端、Remote 和样式边界测试。
|
|
160
|
+
|
|
161
|
+
修改后运行检查、测试并用本地目录重新安装:
|
|
162
|
+
|
|
163
|
+
```powershell
|
|
164
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
165
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
166
|
+
pnpm build
|
|
167
|
+
pnpm test
|
|
168
|
+
pnpm pack:check
|
|
169
|
+
dsh plugin --profile web add .
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`pnpm build` 负责发布包完整性检查,不会将 `lib` 重新编译为其他目录。
|
|
173
|
+
|
|
174
|
+
## 验证
|
|
175
|
+
|
|
176
|
+
```powershell
|
|
177
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
178
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
179
|
+
pnpm build
|
|
180
|
+
pnpm test
|
|
181
|
+
pnpm pack:check
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`prepublishOnly` 会在发布前执行构建检查与测试。
|
|
185
|
+
|
|
186
|
+
## 许可证
|
|
187
|
+
|
|
188
|
+
本项目采用 [Apache License 2.0](LICENSE)。
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -229,8 +229,7 @@ window.__ModuleLoader__.load({
|
|
|
229
229
|
return ordered;
|
|
230
230
|
}
|
|
231
231
|
/**
|
|
232
|
-
*
|
|
233
|
-
* 每棵树都是先子后父,避免级联删除后再删子会话时被当成未知。
|
|
232
|
+
* 仅供 __test 使用:展开已归档 id 及其仍在目录中的 subagent 子会话,先子后父。运行时删除只提交根会话或归档集合。
|
|
234
233
|
*/
|
|
235
234
|
function collectArchivedDeleteAllIds(archivedSessionIds, byId) {
|
|
236
235
|
const catalog = byId ?? {};
|
|
@@ -276,6 +275,15 @@ window.__ModuleLoader__.load({
|
|
|
276
275
|
const detail = reason instanceof Error ? reason.message : String(reason);
|
|
277
276
|
return t("archives.unarchiveFailed", { detail });
|
|
278
277
|
}
|
|
278
|
+
function formatArchiveError(reason, t) {
|
|
279
|
+
if (isUnknownSessionError(reason)) return t("archives.archiveUnknown");
|
|
280
|
+
const detail = reason instanceof Error ? reason.message : String(reason);
|
|
281
|
+
return t("archives.archiveFailed", { detail });
|
|
282
|
+
}
|
|
283
|
+
function formatForkError(reason, t) {
|
|
284
|
+
const detail = reason instanceof Error ? reason.message : String(reason);
|
|
285
|
+
return t("archives.forkFailed", { detail });
|
|
286
|
+
}
|
|
279
287
|
/**
|
|
280
288
|
* A blank session is the selected Workspace's provisional New Session row;
|
|
281
289
|
* its canonical title never enters search (blank rows are query-excluded)
|
|
@@ -1661,7 +1669,11 @@ window.__ModuleLoader__.load({
|
|
|
1661
1669
|
now,
|
|
1662
1670
|
onOpen: open,
|
|
1663
1671
|
onRename: onSessionRename,
|
|
1664
|
-
onFork:
|
|
1672
|
+
onFork: (sessionId) => {
|
|
1673
|
+
Promise.resolve(forkSession(sessionId)).catch((reason) => {
|
|
1674
|
+
showArchivedToast(formatForkError(reason, t));
|
|
1675
|
+
});
|
|
1676
|
+
},
|
|
1665
1677
|
onArchive: onSessionArchive,
|
|
1666
1678
|
onUnarchive: onSessionUnarchive,
|
|
1667
1679
|
onDeleteSession: onSessionDelete,
|
|
@@ -1797,7 +1809,11 @@ window.__ModuleLoader__.load({
|
|
|
1797
1809
|
now,
|
|
1798
1810
|
onOpen: open,
|
|
1799
1811
|
onRename: onSessionRename,
|
|
1800
|
-
onFork:
|
|
1812
|
+
onFork: (sessionId) => {
|
|
1813
|
+
Promise.resolve(forkSession(sessionId)).catch((reason) => {
|
|
1814
|
+
showArchivedToast(formatForkError(reason, t));
|
|
1815
|
+
});
|
|
1816
|
+
},
|
|
1801
1817
|
onArchive: onSessionArchive,
|
|
1802
1818
|
onUnarchive: onSessionUnarchive,
|
|
1803
1819
|
onDeleteSession: onSessionDelete,
|
|
@@ -1918,14 +1934,17 @@ window.__ModuleLoader__.load({
|
|
|
1918
1934
|
const archivedSet = (0, react.useMemo)(() => new Set(archivedSessionIds), [archivedSessionIds]);
|
|
1919
1935
|
const [archivedToast, setArchivedToast] = (0, react.useState)(null);
|
|
1920
1936
|
const archivedToastSeq = (0, react.useRef)(0);
|
|
1937
|
+
const showArchivedToast = (text) => {
|
|
1938
|
+
archivedToastSeq.current += 1;
|
|
1939
|
+
setArchivedToast({
|
|
1940
|
+
text,
|
|
1941
|
+
seq: archivedToastSeq.current
|
|
1942
|
+
});
|
|
1943
|
+
};
|
|
1921
1944
|
/** Open a session, unless it is archived (show the archived hint instead). */
|
|
1922
1945
|
const guardedOpen = (sessionId) => {
|
|
1923
1946
|
if (archivedSet.has(sessionId)) {
|
|
1924
|
-
|
|
1925
|
-
setArchivedToast({
|
|
1926
|
-
text: t("archived.notOpenable"),
|
|
1927
|
-
seq: archivedToastSeq.current
|
|
1928
|
-
});
|
|
1947
|
+
showArchivedToast(t("archived.notOpenable"));
|
|
1929
1948
|
return;
|
|
1930
1949
|
}
|
|
1931
1950
|
open(sessionId);
|
|
@@ -2091,7 +2110,7 @@ window.__ModuleLoader__.load({
|
|
|
2091
2110
|
};
|
|
2092
2111
|
const onSessionArchive = (sessionId) => {
|
|
2093
2112
|
archiveSession(sessionId).catch((reason) => {
|
|
2094
|
-
|
|
2113
|
+
showArchivedToast(formatArchiveError(reason, t));
|
|
2095
2114
|
});
|
|
2096
2115
|
};
|
|
2097
2116
|
const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
|
|
@@ -2124,7 +2143,7 @@ window.__ModuleLoader__.load({
|
|
|
2124
2143
|
};
|
|
2125
2144
|
const onSessionUnarchive = (sessionId) => {
|
|
2126
2145
|
unarchiveSession(sessionId).catch((reason) => {
|
|
2127
|
-
|
|
2146
|
+
showArchivedToast(formatUnarchiveError(reason, t));
|
|
2128
2147
|
});
|
|
2129
2148
|
};
|
|
2130
2149
|
const [deleteSessionTarget, setDeleteSessionTarget] = (0, react.useState)(null);
|
|
@@ -2148,17 +2167,8 @@ window.__ModuleLoader__.load({
|
|
|
2148
2167
|
setDeletingSession(true);
|
|
2149
2168
|
setDeleteSessionError(null);
|
|
2150
2169
|
const rootId = deleteSessionTarget.sessionId;
|
|
2151
|
-
const ids = collectSessionAndDescendantIds(rootId, sessionSnapshot.byId);
|
|
2152
2170
|
(async () => {
|
|
2153
|
-
|
|
2154
|
-
try {
|
|
2155
|
-
await deleteSession(id);
|
|
2156
|
-
} catch (reason) {
|
|
2157
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
2158
|
-
if (id !== rootId && isUnknownSessionError(reason)) continue;
|
|
2159
|
-
throw reason;
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2171
|
+
await deleteSession(rootId);
|
|
2162
2172
|
})().then(() => {
|
|
2163
2173
|
setDeletingSession(false);
|
|
2164
2174
|
setDeleteSessionTarget(null);
|
|
@@ -2545,7 +2555,7 @@ window.__ModuleLoader__.load({
|
|
|
2545
2555
|
}
|
|
2546
2556
|
//#endregion
|
|
2547
2557
|
//#region dsh-archive-manager: settings section
|
|
2548
|
-
const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 32px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.dsham_settings h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.2px;line-height:28px}.dsham_settingsIntro{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;color:var(--dsw-alias-state-error-primary);background:transparent;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:16px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{min-width:
|
|
2558
|
+
const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 32px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.dsham_settings h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.2px;line-height:28px}.dsham_settingsIntro{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;color:var(--dsw-alias-state-error-primary);background:transparent;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:16px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{position:relative;min-width:168px;flex:none}.dsham_selectTrigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;line-height:20px;text-align:left}.dsham_selectTrigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsham_selectTrigger[aria-expanded='true']{border-color:var(--dsw-alias-state-success-primary)}.dsham_selectValue{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsham_selectCaret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}.dsham_selectMenu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;min-width:100%;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}.dsham_selectOption{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dsham_selectOption:hover,.dsham_selectOption[data-active='true']{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectOption[aria-selected='true']{color:var(--dsw-alias-state-success-primary)}.dsham_settingsGroup{margin:0 0 20px}.dsham_settingsGroupHeading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.dsham_settingsGroupTitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.dsham_settingsGroupTitle svg{flex:none;color:var(--dsw-alias-label-secondary)}.dsham_settingsCount{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsList{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsRow{display:flex;align-items:center;gap:12px;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsham_settingsRow:last-child{border-bottom:0}.dsham_settingsContent{min-width:0;flex:1}.dsham_settingsTitle{overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsham_settingsMeta{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.dsham_settingsActions{display:flex;align-items:center;gap:8px}.dsham_settingsAction{min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsAction:hover{filter:brightness(1.12)}.dsham_settingsDelete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsEmpty{padding:28px 8px;color:var(--dsw-alias-label-secondary);text-align:center}.dsham_settingsError{margin-top:10px;color:var(--dsw-alias-state-error-primary);font-size:12px}@media(max-width:720px){.dsham_settings{width:100%;margin:28px auto 48px;padding:0 16px}.dsham_settingsHeader{margin-bottom:28px}.dsham_settingsToolbar{flex-wrap:wrap;margin-bottom:28px}.dsham_settingsSearch{flex-basis:100%}.dsham_settingsFilter{flex:1;min-width:0}.dsham_settingsGroup{margin-bottom:32px}.dsham_settingsRow{padding:10px 12px}.dsham_settingsActions{gap:4px}}";
|
|
2549
2559
|
const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
|
|
2550
2560
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
|
|
2551
2561
|
const tag = document.createElement("style");
|
|
@@ -2554,6 +2564,133 @@ window.__ModuleLoader__.load({
|
|
|
2554
2564
|
tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE;
|
|
2555
2565
|
document.head.appendChild(tag);
|
|
2556
2566
|
}
|
|
2567
|
+
/** 设置页项目筛选:自定义菜单,避免原生 select 弹出系统浅色下拉。 */
|
|
2568
|
+
function ArchiveProjectSelect({ id, value, options, onChange, "aria-label": ariaLabel }) {
|
|
2569
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2570
|
+
const selectedIndex = Math.max(0, options.findIndex((option) => option.value === value));
|
|
2571
|
+
const [active, setActive] = (0, react.useState)(selectedIndex);
|
|
2572
|
+
const rootRef = (0, react.useRef)(null);
|
|
2573
|
+
const triggerRef = (0, react.useRef)(null);
|
|
2574
|
+
const listRef = (0, react.useRef)(null);
|
|
2575
|
+
const wasOpen = (0, react.useRef)(false);
|
|
2576
|
+
const selected = options[selectedIndex];
|
|
2577
|
+
(0, react.useEffect)(() => {
|
|
2578
|
+
if (!open) return;
|
|
2579
|
+
setActive(selectedIndex);
|
|
2580
|
+
const onPointerDown = (event) => {
|
|
2581
|
+
const target = event.target;
|
|
2582
|
+
if (target instanceof Node && rootRef.current?.contains(target) === true) return;
|
|
2583
|
+
setOpen(false);
|
|
2584
|
+
};
|
|
2585
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
2586
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
2587
|
+
}, [open, selectedIndex]);
|
|
2588
|
+
(0, react.useEffect)(() => {
|
|
2589
|
+
if (open) {
|
|
2590
|
+
listRef.current?.focus();
|
|
2591
|
+
wasOpen.current = true;
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (wasOpen.current) {
|
|
2595
|
+
triggerRef.current?.focus();
|
|
2596
|
+
wasOpen.current = false;
|
|
2597
|
+
}
|
|
2598
|
+
}, [open]);
|
|
2599
|
+
(0, react.useEffect)(() => {
|
|
2600
|
+
if (!open) return;
|
|
2601
|
+
document.getElementById(id + "-opt-" + String(active))?.scrollIntoView({ block: "nearest" });
|
|
2602
|
+
}, [active, open, id]);
|
|
2603
|
+
const choose = (next) => {
|
|
2604
|
+
onChange(next);
|
|
2605
|
+
setOpen(false);
|
|
2606
|
+
};
|
|
2607
|
+
const move = (next) => {
|
|
2608
|
+
if (options.length === 0) return;
|
|
2609
|
+
setActive(Math.min(options.length - 1, Math.max(0, next)));
|
|
2610
|
+
};
|
|
2611
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2612
|
+
className: "dsham_settingsFilter",
|
|
2613
|
+
ref: rootRef,
|
|
2614
|
+
children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
2615
|
+
id,
|
|
2616
|
+
ref: triggerRef,
|
|
2617
|
+
type: "button",
|
|
2618
|
+
className: "dsham_selectTrigger",
|
|
2619
|
+
"aria-label": ariaLabel,
|
|
2620
|
+
"aria-haspopup": "listbox",
|
|
2621
|
+
"aria-expanded": open,
|
|
2622
|
+
"aria-controls": id + "-list",
|
|
2623
|
+
onClick: () => setOpen((current) => !current),
|
|
2624
|
+
onKeyDown: (event) => {
|
|
2625
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
|
|
2626
|
+
event.preventDefault();
|
|
2627
|
+
setOpen(true);
|
|
2628
|
+
}
|
|
2629
|
+
},
|
|
2630
|
+
children: [(0, react_jsx_runtime.jsx)("span", { className: "dsham_selectValue", children: selected === void 0 ? "" : selected.label }), (0, react_jsx_runtime.jsx)("svg", {
|
|
2631
|
+
className: "dsham_selectCaret",
|
|
2632
|
+
viewBox: "0 0 12 12",
|
|
2633
|
+
"aria-hidden": true,
|
|
2634
|
+
focusable: false,
|
|
2635
|
+
children: (0, react_jsx_runtime.jsx)("path", {
|
|
2636
|
+
d: "M2.5 4.5L6 8l3.5-3.5",
|
|
2637
|
+
fill: "none",
|
|
2638
|
+
stroke: "currentColor",
|
|
2639
|
+
strokeWidth: "1.5",
|
|
2640
|
+
strokeLinecap: "round",
|
|
2641
|
+
strokeLinejoin: "round"
|
|
2642
|
+
})
|
|
2643
|
+
})]
|
|
2644
|
+
}), open ? (0, react_jsx_runtime.jsx)("div", {
|
|
2645
|
+
id: id + "-list",
|
|
2646
|
+
ref: listRef,
|
|
2647
|
+
className: "dsham_selectMenu",
|
|
2648
|
+
role: "listbox",
|
|
2649
|
+
tabIndex: 0,
|
|
2650
|
+
"aria-activedescendant": id + "-opt-" + String(active),
|
|
2651
|
+
onKeyDown: (event) => {
|
|
2652
|
+
if (event.key === "ArrowDown") {
|
|
2653
|
+
event.preventDefault();
|
|
2654
|
+
move(active + 1);
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
if (event.key === "ArrowUp") {
|
|
2658
|
+
event.preventDefault();
|
|
2659
|
+
move(active - 1);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (event.key === "Home") {
|
|
2663
|
+
event.preventDefault();
|
|
2664
|
+
move(0);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
if (event.key === "End") {
|
|
2668
|
+
event.preventDefault();
|
|
2669
|
+
move(options.length - 1);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2673
|
+
event.preventDefault();
|
|
2674
|
+
const option = options[active];
|
|
2675
|
+
if (option !== void 0) choose(option.value);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
if (event.key === "Escape" || event.key === "Tab") setOpen(false);
|
|
2679
|
+
},
|
|
2680
|
+
children: options.map((option, index) => (0, react_jsx_runtime.jsx)("button", {
|
|
2681
|
+
id: id + "-opt-" + String(index),
|
|
2682
|
+
type: "button",
|
|
2683
|
+
role: "option",
|
|
2684
|
+
className: "dsham_selectOption",
|
|
2685
|
+
"aria-selected": option.value === value,
|
|
2686
|
+
"data-active": index === active,
|
|
2687
|
+
onMouseEnter: () => setActive(index),
|
|
2688
|
+
onClick: () => choose(option.value),
|
|
2689
|
+
children: option.label
|
|
2690
|
+
}, option.value === "all" ? "all" : option.value))
|
|
2691
|
+
}) : null]
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2557
2694
|
/** 管理设置页中的归档会话,数据直接订阅 DSH 的会话与工作区投影。 */
|
|
2558
2695
|
function ArchivedSessionsSection({ sessionStore, workspaceStore, unarchiveSession, deleteSession, t }) {
|
|
2559
2696
|
const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
|
|
@@ -2564,7 +2701,7 @@ window.__ModuleLoader__.load({
|
|
|
2564
2701
|
const [query, setQuery] = (0, react.useState)("");
|
|
2565
2702
|
const [project, setProject] = (0, react.useState)("all");
|
|
2566
2703
|
const groups = (0, react.useMemo)(() => {
|
|
2567
|
-
// 列表仍隐藏 subagent
|
|
2704
|
+
// 列表仍隐藏 subagent;删除全部只提交已归档 id,子代理由服务端级联。
|
|
2568
2705
|
const archived = new Set(workspaceState.archivedSessionIds);
|
|
2569
2706
|
const byWorkspace = workspaceState.items.map((workspace) => ({
|
|
2570
2707
|
title: workspace.title,
|
|
@@ -2589,8 +2726,16 @@ window.__ModuleLoader__.load({
|
|
|
2589
2726
|
sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
|
|
2590
2727
|
})).filter((group) => group.sessions.length > 0);
|
|
2591
2728
|
}, [groups, project, query, t]);
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2729
|
+
const deleteAllSessionIds = (0, react.useMemo)(() => {
|
|
2730
|
+
const ids = [];
|
|
2731
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2732
|
+
for (const id of workspaceState.archivedSessionIds ?? []) {
|
|
2733
|
+
if (typeof id !== "string" || id.length === 0 || seen.has(id)) continue;
|
|
2734
|
+
seen.add(id);
|
|
2735
|
+
ids.push(id);
|
|
2736
|
+
}
|
|
2737
|
+
return ids;
|
|
2738
|
+
}, [workspaceState.archivedSessionIds]);
|
|
2594
2739
|
const onUnarchive = (sessionId) => {
|
|
2595
2740
|
setError(null);
|
|
2596
2741
|
unarchiveSession(sessionId).catch((reason) => {
|
|
@@ -2605,12 +2750,11 @@ window.__ModuleLoader__.load({
|
|
|
2605
2750
|
setBusy(true);
|
|
2606
2751
|
setError(null);
|
|
2607
2752
|
try {
|
|
2608
|
-
for (const id of deleteTarget.all ? deleteAllSessionIds :
|
|
2753
|
+
for (const id of deleteTarget.all ? deleteAllSessionIds : [deleteTarget.id]) {
|
|
2609
2754
|
try {
|
|
2610
2755
|
await deleteSession(id);
|
|
2611
2756
|
} catch (reason) {
|
|
2612
|
-
|
|
2613
|
-
if ((deleteTarget.all || id !== deleteTarget.id) && isUnknownSessionError(reason)) continue;
|
|
2757
|
+
if (deleteTarget.all && isUnknownSessionError(reason)) continue;
|
|
2614
2758
|
throw reason;
|
|
2615
2759
|
}
|
|
2616
2760
|
}
|
|
@@ -2624,7 +2768,7 @@ window.__ModuleLoader__.load({
|
|
|
2624
2768
|
return (0, react_jsx_runtime.jsxs)("section", {
|
|
2625
2769
|
className: "dsham_settings",
|
|
2626
2770
|
"aria-label": t("archives.title"),
|
|
2627
|
-
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)(
|
|
2771
|
+
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-project-filter", value: project, options: [{ value: "all", label: t("archives.allProjects") }, ...groups.map((group) => ({ value: group.title, label: group.title }))], onChange: setProject, "aria-label": t("archives.projectFilter") })] }), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => (0, react_jsx_runtime.jsxs)("section", {
|
|
2628
2772
|
className: "dsham_settingsGroup",
|
|
2629
2773
|
children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupHeading", children: [(0, react_jsx_runtime.jsxs)("h3", { className: "dsham_settingsGroupTitle", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}), group.title] }), (0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: group.sessions.length }) })] }), (0, react_jsx_runtime.jsx)("div", {
|
|
2630
2774
|
className: "dsham_settingsList",
|
|
@@ -2696,12 +2840,15 @@ window.__ModuleLoader__.load({
|
|
|
2696
2840
|
"archives.timestamp": "{date},{time}",
|
|
2697
2841
|
"archives.deleteAll": "全部删除",
|
|
2698
2842
|
"archives.deleteAllTitle": "删除全部已归档聊天",
|
|
2699
|
-
"archives.deleteAllDesc": "将永久删除全部 {n}
|
|
2843
|
+
"archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理(含正在运行的)和记录,此操作不可恢复。",
|
|
2700
2844
|
"archives.deleteAllPending": "正在删除已归档聊天…",
|
|
2701
2845
|
"archives.unarchiveUnknown": "会话已不存在,无法取消归档。",
|
|
2702
|
-
"archives.unarchiveFailed": "取消归档失败:{detail}",
|
|
2846
|
+
"archives.unarchiveFailed": "取消归档失败:{detail}",
|
|
2847
|
+
"archives.archiveUnknown": "会话已不存在,无法归档。",
|
|
2848
|
+
"archives.archiveFailed": "归档失败:{detail}",
|
|
2849
|
+
"archives.forkFailed": "分叉会话失败:{detail}",
|
|
2703
2850
|
"deleteSession.title": "删除会话",
|
|
2704
|
-
"deleteSession.desc": "将永久删除会话“{name}
|
|
2851
|
+
"deleteSession.desc": "将永久删除会话“{name}”及其子代理(含正在运行的)和全部记录(对话内容、统计、缓存),此操作不可恢复。",
|
|
2705
2852
|
"deleteSession.pending": "正在删除会话…",
|
|
2706
2853
|
"deleteSession.unknown": "会话已不存在或已被删除。",
|
|
2707
2854
|
"deleteSession.failed": "删除会话失败:{detail}",
|
|
@@ -2785,12 +2932,15 @@ window.__ModuleLoader__.load({
|
|
|
2785
2932
|
"archives.timestamp": "{date}, {time}",
|
|
2786
2933
|
"archives.deleteAll": "Delete all",
|
|
2787
2934
|
"archives.deleteAllTitle": "Delete all archived chats",
|
|
2788
|
-
"archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents, and their records. This cannot be undone.",
|
|
2935
|
+
"archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents (including any that are still running), and their records. This cannot be undone.",
|
|
2789
2936
|
"archives.deleteAllPending": "Deleting archived chats…",
|
|
2790
2937
|
"archives.unarchiveUnknown": "This session no longer exists, so it cannot be unarchived.",
|
|
2791
|
-
"archives.unarchiveFailed": "Could not unarchive the session: {detail}",
|
|
2938
|
+
"archives.unarchiveFailed": "Could not unarchive the session: {detail}",
|
|
2939
|
+
"archives.archiveUnknown": "This session no longer exists, so it cannot be archived.",
|
|
2940
|
+
"archives.archiveFailed": "Could not archive the session: {detail}",
|
|
2941
|
+
"archives.forkFailed": "Could not fork the session: {detail}",
|
|
2792
2942
|
"deleteSession.title": "Delete session",
|
|
2793
|
-
"deleteSession.desc": "This permanently deletes session “{name}”, its child agents, and all of its records (conversation, stats, cache). This cannot be undone.",
|
|
2943
|
+
"deleteSession.desc": "This permanently deletes session “{name}”, its child agents (including any that are still running), and all of its records (conversation, stats, cache). This cannot be undone.",
|
|
2794
2944
|
"deleteSession.pending": "Deleting session…",
|
|
2795
2945
|
"deleteSession.unknown": "This session no longer exists or was already deleted.",
|
|
2796
2946
|
"deleteSession.failed": "Could not delete the session: {detail}",
|
|
@@ -2919,14 +3069,12 @@ window.__ModuleLoader__.load({
|
|
|
2919
3069
|
const result = await session.rename(title);
|
|
2920
3070
|
if (!result.ok) throw new Error(result.error.message);
|
|
2921
3071
|
},
|
|
2922
|
-
forkSession: (sessionId) => {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
}).catch(() => {});
|
|
2929
|
-
},
|
|
3072
|
+
forkSession: (sessionId) => ctx.sessions.fork({
|
|
3073
|
+
sessionId,
|
|
3074
|
+
increaseTitle: true
|
|
3075
|
+
}).then((childId) => {
|
|
3076
|
+
ctx.sessions.open(childId);
|
|
3077
|
+
}),
|
|
2930
3078
|
renameWorkspace: async (workspaceId, title) => {
|
|
2931
3079
|
await ctx.workspaces.rename(workspaceId, title);
|
|
2932
3080
|
},
|
package/lib/projcache.js
CHANGED
|
@@ -23,6 +23,8 @@ import { SessionProjectionCache } from "@deepseek-ai/dsh-session-projection-cach
|
|
|
23
23
|
var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
24
24
|
/** 已永久删除的会话不会再允许投影缓存写入。 */
|
|
25
25
|
deletedSessionIds = /* @__PURE__ */ new Set();
|
|
26
|
+
deletedSessionOrder = [];
|
|
27
|
+
deletedSessionTombstoneLimit = 4096;
|
|
26
28
|
/** Tail of in-flight checkpoint writes (settled promises only). */
|
|
27
29
|
writeTail = Promise.resolve();
|
|
28
30
|
constructor(ctx, config) {
|
|
@@ -57,7 +59,14 @@ var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
|
57
59
|
* @returns resolution after the row is gone.
|
|
58
60
|
*/
|
|
59
61
|
async delete(id) {
|
|
60
|
-
this.deletedSessionIds.
|
|
62
|
+
if (!this.deletedSessionIds.has(id)) {
|
|
63
|
+
this.deletedSessionIds.add(id);
|
|
64
|
+
this.deletedSessionOrder.push(id);
|
|
65
|
+
}
|
|
66
|
+
while (this.deletedSessionOrder.length > this.deletedSessionTombstoneLimit) {
|
|
67
|
+
const oldest = this.deletedSessionOrder.shift();
|
|
68
|
+
if (oldest !== void 0 && oldest !== id) this.deletedSessionIds.delete(oldest);
|
|
69
|
+
}
|
|
61
70
|
await this.requireTable().delete(id);
|
|
62
71
|
}
|
|
63
72
|
};
|
package/lib/workspace.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
WorkspaceRegistry
|
|
5
|
-
WorkspaceUnknownSessionError
|
|
4
|
+
WorkspaceRegistry
|
|
6
5
|
} from "@deepseek-ai/dsh-workspace";
|
|
7
6
|
import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
|
|
8
7
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
@@ -38,7 +37,7 @@ function markRemoteMethod(instance, method) {
|
|
|
38
37
|
Remote(method)(void 0, context);
|
|
39
38
|
}
|
|
40
39
|
function unknownSessionMessage(sessionId) {
|
|
41
|
-
return `
|
|
40
|
+
return `unknown session "${sessionId}" (UNKNOWN_SESSION)`;
|
|
42
41
|
}
|
|
43
42
|
var ArchiveUnknownSessionError = class extends Error {
|
|
44
43
|
sessionId;
|
|
@@ -160,7 +159,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
160
159
|
*/
|
|
161
160
|
async unarchiveSession(sessionId) {
|
|
162
161
|
return this.enqueueOperation(async () => {
|
|
163
|
-
if (!await this.sessionKnown(sessionId)) throw new
|
|
162
|
+
if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
|
|
164
163
|
const state = this.requireState();
|
|
165
164
|
if (!state.archivedSessionIds.includes(sessionId)) return { archivedSessionIds: [...state.archivedSessionIds] };
|
|
166
165
|
const next = {
|
|
@@ -223,6 +222,11 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
223
222
|
* 从父类内存索引中遗忘已删除会话,并阻止后续 indexHeaders 把它加回。
|
|
224
223
|
* 实时会话以同 id 重新出现时(自定义 id 复用)会撤掉墓碑。
|
|
225
224
|
*/
|
|
225
|
+
clearTombstone(sessionId) {
|
|
226
|
+
this.deletedSessionIds.delete(sessionId);
|
|
227
|
+
const idx = this.deletedSessionOrder.indexOf(sessionId);
|
|
228
|
+
if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
|
|
229
|
+
}
|
|
226
230
|
forgetIndexedSession(sessionId) {
|
|
227
231
|
if (!this.deletedSessionIds.has(sessionId)) {
|
|
228
232
|
this.deletedSessionIds.add(sessionId);
|
|
@@ -242,7 +246,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
242
246
|
*/
|
|
243
247
|
async sessionKnown(id) {
|
|
244
248
|
if (this.ctx.get("sessions")?.get(id) !== void 0) {
|
|
245
|
-
this.
|
|
249
|
+
this.clearTombstone(id);
|
|
246
250
|
return true;
|
|
247
251
|
}
|
|
248
252
|
if (this.deletedSessionIds.has(id)) return false;
|
|
@@ -326,8 +330,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
326
330
|
}
|
|
327
331
|
for (const childId of descendants) {
|
|
328
332
|
try {
|
|
333
|
+
if (!await this.sessionKnown(childId)) continue;
|
|
329
334
|
await this.deleteSessionCore(childId);
|
|
330
335
|
} catch (error) {
|
|
336
|
+
if (error instanceof ArchiveUnknownSessionError) continue;
|
|
331
337
|
this.ctx.logger.warn(`archive-manager: cascade delete of subagent session "${childId}" (child of "${sessionId}") failed: ${String(error)}`);
|
|
332
338
|
}
|
|
333
339
|
}
|