dsh-workspace-plus 0.1.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.
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/cordis.patch.yml +16 -0
- package/icon.svg +5 -0
- package/lib/client.js +1412 -0
- package/lib/index.js +707 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sen70s
|
|
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,348 @@
|
|
|
1
|
+
# dsh-workspace-plus
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness Web GUI 的插件。一个功能,两个入口:
|
|
4
|
+
|
|
5
|
+
| 入口 | 说明 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| **工作区行上的目录按钮**(弹窗) | 一个工作区登记 N 个目录,每个目录带标签;标签在对话里可直接指代 |
|
|
8
|
+
| **设置 → `workspace+`** | 一个开关:是否对**默认工作区**启用本功能(**默认关闭**) |
|
|
9
|
+
|
|
10
|
+
功能 2 解决的问题:一个工作区(DSH 里 = 一个目录)装不下真实项目。前后端分在两个仓库、文档在第三个目录时,
|
|
11
|
+
每次都要在对话里手写绝对路径,而且目录名相似时 agent 容易改错文件。本插件让工作区带上
|
|
12
|
+
「标签 → 绝对路径」映射,并**在每个请求里把这张表注入 agent 上下文**,于是:
|
|
13
|
+
|
|
14
|
+
> 用户:`把 backend 的登录接口改成 JWT`
|
|
15
|
+
>
|
|
16
|
+
> agent 直接去 `D:\proj\backend`,不会因为旁边有个 `D:\proj\backend-legacy` 而改错。
|
|
17
|
+
|
|
18
|
+
> 本仓库只包含插件本身;没有改动 DSH 安装目录里的任何文件。state 文件由插件自己写在 `~/.dsh/workspaceplus/`。
|
|
19
|
+
|
|
20
|
+
## 目录结构
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
dsh-workspace-plus/
|
|
24
|
+
├── package.json # dsh.bundle(patch 层)+ dsh.client(浏览器半声明)
|
|
25
|
+
├── cordis.patch.yml # bundle 层:insert 一行 name: 'dsh-workspace-plus'
|
|
26
|
+
├── lib/
|
|
27
|
+
│ ├── index.js # 宿主半:store + 设置 + 上下文注入 + workspace_dirs 工具 + HTTP 路由
|
|
28
|
+
│ └── client.js # 浏览器半:设置页 + 目录弹窗 + 工作区行按钮(无需构建)
|
|
29
|
+
├── icon.svg # 插件管理页显示的图标
|
|
30
|
+
├── tools/verify.mjs # 离线自检(258 项断言,不需要 DSH 在跑)
|
|
31
|
+
└── README.md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`lib/` 下两个文件都是**手写源码,不需要任何构建步骤**。`lib/client.js` 按 DSH 客户端模块加载器要求的
|
|
35
|
+
`window.__ModuleLoader__.load({ id, factory })` 工厂式 CJS 形态书写。
|
|
36
|
+
|
|
37
|
+
## 设置:设置 → `workspace+`
|
|
38
|
+
|
|
39
|
+
一个开关:**对默认工作区启用**。
|
|
40
|
+
|
|
41
|
+
「默认工作区」是 DSH 首次启动时自动创建的那个(`workspaceRegistry` 里 `defaultWorkspaceId`
|
|
42
|
+
指向它,即使它的注册被删掉这个身份也会保留)。它属于产品而不属于本插件,所以:
|
|
43
|
+
|
|
44
|
+
- **默认关闭**:不在它的工作区行显示目录按钮,不把标签表注入它的会话上下文,`workspace_dirs`
|
|
45
|
+
工具对它直接返回一个指向上面的开关的错误。
|
|
46
|
+
- 其它工作区不受影响,始终启用。
|
|
47
|
+
- 打开开关后立即生效(无需重启、无需刷新):行按钮出现、上下文开始注入。关掉不会删除已登记的标签。
|
|
48
|
+
|
|
49
|
+
宿主怎么认出默认工作区:它只调用**公开的** `workspaceRegistry.initializeDefault()`,
|
|
50
|
+
并传一个「拒绝创建」的目录解析器 —— 已有默认工作区时该调用直接返回它(无副作用),
|
|
51
|
+
注册表非空时返回 `undefined`,只有在**真的会创建**工作区的那条路径上才会走到解析器、抛错、被捕获。
|
|
52
|
+
所以这个探测永远不会替你凭空创建一个工作区。
|
|
53
|
+
|
|
54
|
+
> 首次探测可能早于注册表服务初始化完成而失败。失败时状态保持「未决」并在下一次请求/工具调用时重试,
|
|
55
|
+
> 而不是把「没有默认工作区」永久缓存下来 —— 否则默认工作区会被静默启用。
|
|
56
|
+
|
|
57
|
+
## 功能是怎么工作的
|
|
58
|
+
|
|
59
|
+
### 数据
|
|
60
|
+
|
|
61
|
+
一份插件自己的状态文件,键是工作区目录(大小写无关):
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"version": 1,
|
|
66
|
+
"workspaces": {
|
|
67
|
+
"d:\\proj": {
|
|
68
|
+
"path": "D:\\proj",
|
|
69
|
+
"dirs": [
|
|
70
|
+
{ "label": "backend", "path": "D:\\proj\\backend", "note": "后端 API" },
|
|
71
|
+
{ "label": "docs", "path": "D:\\docs" }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
位置:`$DSH_HOME/workspaceplus/directories.json`(没设 `DSH_HOME` 时用 `~/.dsh/...`)。
|
|
79
|
+
写入是「临时文件 + rename」,中途崩溃不会留下半个文件;文件损坏或缺失时按空配置启动,不会拖垮 DSH 启动。
|
|
80
|
+
也可以直接手改这个文件,改完刷新页面即可。
|
|
81
|
+
|
|
82
|
+
### 三条注入路径
|
|
83
|
+
|
|
84
|
+
1. **上下文(自动,主要机制)** —— 宿主半注册了一个 runtime-context 贡献
|
|
85
|
+
`workspaceplus:directories`(order 130,紧跟在 sandbox/approval 那组之后):
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
ctx.systemPrompt.context({
|
|
89
|
+
name: 'workspaceplus:directories',
|
|
90
|
+
order: 130,
|
|
91
|
+
text: (context) => {
|
|
92
|
+
const cwd = context.agent?.session?.header?.cwd
|
|
93
|
+
// 该 cwd 没有登记目录时返回 '',空上下文不产生任何 token 开销
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
于是**每次请求**该工作区会话都能看到标签表,不需要工具调用。表里给的是绝对路径,并明确写了
|
|
99
|
+
「不要根据目录名相似去推测位置」「标签不区分大小写」「只在标签目录和工作区根目录内工作」等规则。
|
|
100
|
+
|
|
101
|
+
2. **工具(按需)** —— `workspace_dirs`,宿主半用 `ctx.tools.register()` 注册:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
workspace_dirs { action: list|add|remove|update, label?, path?, note?, workspace? }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`workspace` 省略时用当前会话的 cwd,所以用户在对话里说「加一个 infra 指向 D:\deploy」,
|
|
108
|
+
agent 直接就能落库。同时也用于确认映射(例如上下文被压缩掉之后)。
|
|
109
|
+
|
|
110
|
+
3. **HTTP 路由(给界面用)** —— `POST/GET /workspaceplus/api`,与页面同源,
|
|
111
|
+
沿用壳层的 `ctx.connection.admit(req)` 鉴权。
|
|
112
|
+
|
|
113
|
+
### 界面:一个弹窗,两个触发器
|
|
114
|
+
|
|
115
|
+
弹窗本身注册在 **`shell.overlay`** —— 和壳层自己的「重命名工作区 / 删除工作区」对话框同一个层
|
|
116
|
+
(那两个是 `workspace.session-rename`、`workspace.session-archive`),所以观感和层级天然一致。
|
|
117
|
+
|
|
118
|
+
**主触发器:工作区行上的目录按钮。** 这里要说清楚一件事:
|
|
119
|
+
|
|
120
|
+
> 工作区那一行的「...」菜单在官方实现里是**硬编码的 Rename/Delete**,没有插件槽位。
|
|
121
|
+
> `dsh-client-ui-workspace` 的源码注释原文是
|
|
122
|
+
> 「Workspace row menus are visual-only except Rename/Delete」,
|
|
123
|
+
> 代码里 `ProjectRowItem` 也只收到 `actions: { rename, delete }`。
|
|
124
|
+
|
|
125
|
+
所以「往那个菜单里加一项」无法用官方 API 做到。替代方案是把按钮**追加到该行的操作区**
|
|
126
|
+
(`rowActions`),定位不依赖易变的哈希类名,而用壳层自己发出的稳定锚点:
|
|
127
|
+
|
|
128
|
+
- `data-slot="sidebar.workspaces"` —— 渲染器给每个 slot 出口都加的锚(`SlotOutlet`)
|
|
129
|
+
- `data-row-key="workspace:<workspaceId>"` —— 工作区分组行自带,且
|
|
130
|
+
`buildGroup(workspace.workspaceId, workspace.workspaceId, …)` 保证这个 key **就是** workspace id,
|
|
131
|
+
与宿主 `workspaceRegistry` 的 `Workspace.id`(客户端 `workspaceView().workspaceId`)一致
|
|
132
|
+
|
|
133
|
+
按钮**追加在操作区的最后**(React 的 child 协调不需要在它前面插入),并用 `MutationObserver`
|
|
134
|
+
做增删对账 —— React 重渲染时并不知道这个节点的存在。点击时 `preventDefault + stopPropagation`,
|
|
135
|
+
所以不会触发行本身的展开/折叠。
|
|
136
|
+
|
|
137
|
+
外观上刻意对齐壳层自己的行内按钮(`Rows.module.css` 的 `.iconButton`):16×16、无边框、无底色、
|
|
138
|
+
`color: label-tertiary`,**hover 只变颜色不加背景**;图标直接复用官方的文件夹图形
|
|
139
|
+
(`IconFolderOpenArtwork` 的路径数据,静态内联,不引入运行时依赖),所以和旁边的「...」「+」是同一套视觉。
|
|
140
|
+
|
|
141
|
+
**兜底触发器:侧边栏 `workdirs` 条目。** 只有当工作区行按钮**装不上**时才注册(比如将来壳层改了行 DOM,
|
|
142
|
+
或这个工作区列表还是空的)。注册它会连带一个 `main` key:壳层的侧边栏面板按钮**无条件**调用
|
|
143
|
+
`layout.selectPanel(id)`,对一个没有注册 `main` key 的 id 会直接抛错,所以那个 key 必须存在,
|
|
144
|
+
由一个提示面板占着,同时图标的点击被 `stopPropagation` 截住、改为打开弹窗。健康环境下
|
|
145
|
+
侧边栏不会多出任何图标(有一个 1.2s 的 settle 窗口避免闪烁)。
|
|
146
|
+
|
|
147
|
+
弹窗**锁定触发它的那个工作区**:从哪个工作区行点的,就只编辑那个工作区,弹窗里没有选择器,
|
|
148
|
+
只显示工作区名和根目录。位于工作区根目录之外的目录会被标出来(见下面的沙箱一节)。
|
|
149
|
+
如果 HTTP 通道不可用,弹窗会明确说明原因(区分 401 / 404 / 传输不可达),而不是假装映射是空的 ——
|
|
150
|
+
此时仍然可以在对话里用 `workspace_dirs` 管理。
|
|
151
|
+
|
|
152
|
+
**添加目录时可以用系统的目录选择器**:路径输入框旁边的「浏览…」调用壳层自己的
|
|
153
|
+
`ctx.uiWorkspace.pickDirectory()`(就是「选择工作区」用的那一个),所以桌面版是原生对话框、
|
|
154
|
+
浏览器版是壳层的浏览面板。选中后自动填路径,**标签为空时**还会按目录名猜一个合法标签
|
|
155
|
+
(`D:\proj\my api` → `my-api`;纯中文目录名不猜,避免塞一个宿主会拒绝的标签)。取消选择不会改动任何字段。
|
|
156
|
+
`ui-workspace` 没有挂载时(合成里没有这一行)整个按钮自动置灰,插件其余部分照常工作。
|
|
157
|
+
|
|
158
|
+
> 唯一的例外是兜底入口:它是全局的、没有工作区上下文,所以那条路径下弹窗会退回显示选择器。
|
|
159
|
+
> 正常使用中不会看到它。
|
|
160
|
+
|
|
161
|
+
## ⚠ 沙箱:一个会话只有一个可写根
|
|
162
|
+
|
|
163
|
+
DSH 的沙箱策略把每个会话的可写范围限定在 `SessionHeader.cwd`(也就是工作区目录);
|
|
164
|
+
`SandboxExecutionPolicy` 不支持额外的可写根(这是 `dsh-sandbox-policy` 明确记录的限制)。
|
|
165
|
+
所以:
|
|
166
|
+
|
|
167
|
+
| 目录位置 | `danger-full-access` | `workspace-write` / `read-only` |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| 工作区内(含子目录) | 可写 | 可写 |
|
|
170
|
+
| 工作区之外 | 可写 | **只能读**,写入需要按工具返回的提示走一次 `sandbox_permissions` 升级(需要你批准) |
|
|
171
|
+
|
|
172
|
+
**推荐用法**:把工作区建在几个工程的**共同父目录**上,然后把这些工程登记为标签目录。
|
|
173
|
+
例如工作区 = `D:\proj`,标签 `frontend → D:\proj\frontend`、`backend → D:\proj\backend`。
|
|
174
|
+
这样全部落在可写根内,标签路由和沙箱都满意。
|
|
175
|
+
|
|
176
|
+
父目录不会被“浪费上下文”:cwd 本身只是一个路径字符串,成本和其他路径一样;真正的浪费来自 agent 在父目录里
|
|
177
|
+
无范围地 `**/*` 乱扫。注入的上下文已经明确禁止这件事(规则 3:只在标签目录和工作区根目录内工作、
|
|
178
|
+
不要扫描同级目录、不要用无范围模式“先找找看”),定位文件一律先按标签选目录。
|
|
179
|
+
|
|
180
|
+
### 为什么不能“在沙箱内加一个可写根”
|
|
181
|
+
|
|
182
|
+
这不是配置问题,是设计上没有这个口子,我核对过实现:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
// @deepseek-ai/dsh-sandbox/lib/roots.js
|
|
186
|
+
function writableRoots(policy) {
|
|
187
|
+
if (policy.mode !== 'workspace-write') return []
|
|
188
|
+
return [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`workspace-write` 的可写集合永远只有「会话 cwd + 平台临时目录」;`SandboxExecutionPolicy` 里也没有额外根的位置。
|
|
193
|
+
|
|
194
|
+
而且 **symlink / junction 也绕不过去**:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
// @deepseek-ai/dsh-fs-sandbox/lib/index.js
|
|
198
|
+
const fresh = await this.resolve(target.displayPath) // realpath 到最深的已存在祖先
|
|
199
|
+
for (const root of writableRoots(policy)) if (await isPathUnder(fresh.targetKey, root)) { contained = true; break }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
两边都做 canonical(realpath)后再比包含关系,所以「在工作区里放一个指向外部目录的软链接」
|
|
203
|
+
会被解析回外部真实路径,照样 `FS_SANDBOX_DENIED`。唯一合法的放宽方式是**逐次调用**的升级
|
|
204
|
+
(`sandbox_permissions` + `justification` → 批准),跨根写入即等于每一次都升到 `danger-full-access`。
|
|
205
|
+
|
|
206
|
+
### 工程确实跨盘符时
|
|
207
|
+
|
|
208
|
+
没有任何共同父目录,此时只有两条路:
|
|
209
|
+
|
|
210
|
+
1. 会话权限切到 `danger-full-access`;
|
|
211
|
+
2. 保持 `workspace-write`,接受每次越界写入弹一次批准(agent 会按工具提示发起升级)。
|
|
212
|
+
|
|
213
|
+
插件本身不做任何绕过沙箱的事,只是把限制如实告诉 agent(上下文里会标注哪些目录在根目录之外)
|
|
214
|
+
和界面(行尾标注「工作区根目录之外」)。
|
|
215
|
+
|
|
216
|
+
## 安装
|
|
217
|
+
|
|
218
|
+
从 npm 安装(推荐):
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
dsh plugin --profile desktop add dsh-workspace-plus
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
也可以直接从 GitHub 或本地 checkout 安装:
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
dsh plugin --profile desktop add github:Sen70s/dsh-workspace-plus
|
|
228
|
+
dsh plugin --profile desktop add <本地 checkout 目录>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`lib/` 是仓库里已提交的产物、包内没有 `prepare` 构建脚本,所以上面三种方式都不需要
|
|
232
|
+
pnpm 的 `allowBuilds` 构建授权,装到的就是可直接加载的代码。想锁定版本可以用
|
|
233
|
+
`github:Sen70s/dsh-workspace-plus#v0.1.1`。要求 DSH `>=0.1.7-rc.2`(桌面版当前就是
|
|
234
|
+
`@deepseek-ai/dsh-desktop 0.1.7-rc.2`;npm 上是 `next` 通道)。
|
|
235
|
+
|
|
236
|
+
> 0.1.0 曾以包名 `dsh-workspaceplus` 发布;自 0.1.1 起更名为 `dsh-workspace-plus`,旧包已废弃,
|
|
237
|
+
> 请勿再安装。
|
|
238
|
+
|
|
239
|
+
装完后**重启 `dsh`** 并刷新页面。悬停任一工作区那一行,操作区会多出一个目录图标,点它打开弹窗;
|
|
240
|
+
设置面板里会多出一个 `workspace+` 分区(默认工作区默认关闭,见下一节)。
|
|
241
|
+
|
|
242
|
+
卸载:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
dsh plugin --profile desktop remove dsh-workspace-plus
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## 使用
|
|
249
|
+
|
|
250
|
+
界面上:悬停工作区行 → 点目录图标 → 填「标签 / 绝对路径 / 说明」→ 添加。弹窗已经锁定在这个工作区上,
|
|
251
|
+
不需要(也不能)再选一次。默认工作区要先在 设置 → `workspace+` 里打开开关,它的行上才会出现那个图标;
|
|
252
|
+
此时也可以直接在设置页里管理,或让 agent 用 `workspace_dirs`。
|
|
253
|
+
|
|
254
|
+
对话里(工作区 = `D:\proj`):
|
|
255
|
+
|
|
256
|
+
- 「把 `D:\proj\backend` 加为 backend 标签,说明写后端 API」→ agent 调 `workspace_dirs add`
|
|
257
|
+
- 「现在有哪些标签?」→ `workspace_dirs list`
|
|
258
|
+
- 「把 backend 改指向 `D:\proj\backend-v2`」→ `workspace_dirs update`
|
|
259
|
+
- 「删掉 docs 标签」→ `workspace_dirs remove`
|
|
260
|
+
- 之后:「backend 加个健康检查接口」→ agent 直接用 `D:\proj\backend` 的绝对路径
|
|
261
|
+
|
|
262
|
+
## 自检
|
|
263
|
+
|
|
264
|
+
不需要安装、不需要 DSH 在跑:
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
node tools/verify.mjs
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
它会(当前 `283/283 checks passed`,另有 1 项按环境跳过):
|
|
271
|
+
|
|
272
|
+
- 校验 manifest、patch 层、`dsh.client.inject` 的取值;
|
|
273
|
+
- 用临时 `DSH_HOME` 运行宿主半:标签/路径校验、增删改、去重、大小写折叠、损坏文件容错、原子写;
|
|
274
|
+
- 用假的 Cordis ctx 验证三处注册,并**真实执行** `workspace_dirs` 的 list/add/remove;
|
|
275
|
+
- 用假的 `node:http` req/res 真实调用 HTTP 路由的 GET/POST/405/401 分支;
|
|
276
|
+
- 在 `node:vm` 里加载浏览器半,用**最小 DOM 替身**(只实现本插件用到的那几个选择器)真实驱动
|
|
277
|
+
工作区行按钮:挂载前不误报、按行注入、未分组行跳过、重复对账幂等、行移除后回收、
|
|
278
|
+
点击只开弹窗而不冒泡到行、dispose 清理干净;
|
|
279
|
+
- 校验弹窗的外部 store(打开/切换/关闭/退订)与各槽位注册,
|
|
280
|
+
校验行按钮 CSS 与壳层 `.iconButton` 的度量一致(16×16、无底色、hover 只变色),
|
|
281
|
+
校验**默认工作区闸门**(宿主快照标 disabled 后行按钮消失、开关打开后回来、探测失败会重试),
|
|
282
|
+
校验**目录选择器**(标签猜测始终满足宿主语法、取消不改变任何字段、失败被捕获并给出原因、
|
|
283
|
+
没有挂载 ui-workspace 时按钮自动置灰且不会调用),
|
|
284
|
+
并用最小 React + Button/Switch/Modal 桩**真实渲染**设置页(含拨动开关 → POST 一次 settings op →
|
|
285
|
+
共享快照更新)与弹窗/面板的各状态文案。
|
|
286
|
+
|
|
287
|
+
## 故障排查
|
|
288
|
+
|
|
289
|
+
### 工作区行上没有出现目录按钮
|
|
290
|
+
|
|
291
|
+
1. 先确认宿主半已加载(见下一条的判据)。
|
|
292
|
+
2. **这个工作区是默认工作区,且 设置 → `workspace+` 的开关还关着** —— 这是默认行为。
|
|
293
|
+
3. 悬停工作区行才会显示操作区,这是壳层的行为。
|
|
294
|
+
4. 兜底入口会在 1.2s 后出现在侧边栏(`workdirs`);如果它出现了,说明行锚点没匹配上 ——
|
|
295
|
+
多半是 DSH 升级改了行 DOM。此时功能仍然可用(走 `workspace_dirs` 工具或兜底入口),
|
|
296
|
+
需要的话按新的 `data-row-key` 更新 `WORKSPACE_ROW_SELECTOR`。
|
|
297
|
+
|
|
298
|
+
### 弹窗显示 `host responded 404`
|
|
299
|
+
|
|
300
|
+
**含义**:请求已经到达 DSH 宿主进程,但宿主里没有 `/workspaceplus/api` 这条路由 —— 宿主半还在跑旧代码。
|
|
301
|
+
|
|
302
|
+
浏览器半会随 `client.js` 的内容变化热更新,**宿主半不会**(`dsh-hmr` 的 `root` 为空,不监听这个插件目录)。
|
|
303
|
+
所以只刷新页面会出现「新界面 + 旧宿主」的组合。**重启 `dsh` 即可。**
|
|
304
|
+
|
|
305
|
+
重启成功的判据是启动终端里的这一行:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
[dsh-workspace-plus] host half loaded — workspace label routing is active (<state file path>)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
没有这行说明该插件行加载失败,去 **设置 → 插件** 看它的状态。另一个快速判据:
|
|
312
|
+
让 agent 调一次 `workspace_dirs`,或看工具列表里有没有这个工具。
|
|
313
|
+
|
|
314
|
+
### 桌面版和 Web UI 在这件事上一样吗
|
|
315
|
+
|
|
316
|
+
一样。官方桌面壳用自定义协议 `dsh-app://app/` 承载页面,它的分发逻辑是
|
|
317
|
+
(`resources/app.asar` 的 `lib/main.js`,`protocol.handle(SCHEME, …)`):
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
if (url.hostname === 'app') {
|
|
321
|
+
if (url.pathname === '/' || url.pathname === '/index.html' || url.pathname.startsWith('/assets/')
|
|
322
|
+
|| ['/favicon.svg', '/manifest.webmanifest'].includes(url.pathname)) return serveWebDocument(...)
|
|
323
|
+
return forwardWebRequest(request, hostUrl, hostCookie) // 其余全部带 cookie 转发给 Host HTTP 服务
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
也就是说**除了**首页、静态资源这几个路径由本地 dist 提供,其余请求一律原样转发给真正的 Host,
|
|
328
|
+
并附上桌面壳持有的宿主 cookie。所以插件注册的 `ctx.webServer` 路由(以及 `/api`、`/plugins/*`)
|
|
329
|
+
在桌面版里和浏览器里走的是同一条路,401/404 的语义也一致。
|
|
330
|
+
|
|
331
|
+
### 其他
|
|
332
|
+
|
|
333
|
+
- **弹窗 401**:当前文档没有完成 token 交换,拿 `dsh` 启动时打印的带 token 地址重开 GUI。
|
|
334
|
+
- **下拉框显示「没有可用工作区」**:宿主没有挂载 `workspaceRegistry`,或确实还没有工作区。
|
|
335
|
+
- **`workspace_dirs` 不在工具列表里**:宿主半没加载,同上。
|
|
336
|
+
|
|
337
|
+
## 继续扩展
|
|
338
|
+
|
|
339
|
+
- **让 shell 也能用标签**:可以在宿主半加一个 `ctx.shellEnv.register(...)` 贡献,
|
|
340
|
+
把 `DSH_WORKSPACE_DIR_<LABEL>` 传给每次命令执行。
|
|
341
|
+
- **把按钮插进「...」菜单**:只有等官方给工作区行开放槽位才值得做;现在唯一可行的做法是
|
|
342
|
+
连带替换掉官方的 Rename/Delete(用 `ctx.workspaces.rename/delete` 复刻),风险明显更高。
|
|
343
|
+
- **换成 TypeScript + tsdown**:`lib/` 就是产物目录,源文件放 `src/`、让 tsdown 输出
|
|
344
|
+
`lib/index.js` 与 `lib/client.js` 即可,`exports` 不用改。参见
|
|
345
|
+
[插件开发文档](https://deepseek-harness.github.io/deepseek-harness/develop/basic/) 与
|
|
346
|
+
[打包与安装](https://deepseek-harness.github.io/deepseek-harness/develop/basic/publish)。
|
|
347
|
+
- **成本提示**:`workspace_dirs` 的工具 schema 会出现在每个会话的每次请求里(约二百 token)。
|
|
348
|
+
不需要自然语言管理时,可以在 profile 里禁用整行,只用界面管理。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# dsh-workspace-plus bundle patch.
|
|
2
|
+
#
|
|
3
|
+
# This file is the package's `dsh.bundle.patch` layer: a top-level YAML array of
|
|
4
|
+
# loader patch entries, applied when a profile lists `dsh-workspace-plus` in
|
|
5
|
+
# `dsh.profile.bundles`. The `insert` list appends one loader row; `name` is the
|
|
6
|
+
# package name (never a relative source path), because the profile resolves it
|
|
7
|
+
# through Node module resolution from the profile directory.
|
|
8
|
+
#
|
|
9
|
+
# The row is a dual-face plugin: the Node half (exports "." -> lib/index.js)
|
|
10
|
+
# runs in the host process, and the browser half (exports "./client" ->
|
|
11
|
+
# lib/client.js) loads in the web GUI because package.json declares `dsh.client`.
|
|
12
|
+
# The browser half is what adds the directory button and the workspace+
|
|
13
|
+
# settings section.
|
|
14
|
+
- insert:
|
|
15
|
+
- id: workspaceplus
|
|
16
|
+
name: 'dsh-workspace-plus'
|
package/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<circle cx="12" cy="12" r="9" />
|
|
3
|
+
<path d="M3.6 9h16.8M3.6 15h16.8" />
|
|
4
|
+
<path d="M12 3c2.6 2.6 4 5.6 4 9s-1.4 6.4-4 9c-2.6-2.6-4-5.6-4-9s1.4-6.4 4-9Z" />
|
|
5
|
+
</svg>
|