dsh-tool-switch 0.0.0-stage → 0.2.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/INSTALL.md ADDED
@@ -0,0 +1,321 @@
1
+ # 安装指南
2
+
3
+ > 适用:**DSH Desktop 0.13.x**(内置 DeepSeek Harness `0.2.2-alpha.x`)与同版本的 `dsh` CLI。
4
+ > 插件本身**零依赖、无需构建**:`index.js` 是 Host 半侧(Node ESM),`lib/client.js` 是手写的
5
+ > 客户端 bundle(`window.__ModuleLoader__.load({ id, factory })` 格式,与官方插件产物同构)。
6
+
7
+ 安装有两条路,选一条就好:
8
+
9
+ | 方式 | 适合谁 | 特点 |
10
+ |---|---|---|
11
+ | **A. 作为「组合包」安装**(推荐) | 一般用户 | 插件页点几下就装上;升级 DSH 后仍在;可随时停用/卸载 |
12
+ | **B. 在补丁层里手工挂载** | 想直接对着源码改、或不想让包管理器动 profile | 立刻热生效,改完就见效;但 profile 重装/清理时可能丢 |
13
+
14
+ > 两条路**不要同时用**——同一份插件被两个 Loader 入口解析到,客户端会报
15
+ > `package dsh-tool-switch resolves from multiple active Loader sources` 并拒绝组图。
16
+
17
+ ---
18
+
19
+ ## 0. 先认清三个位置
20
+
21
+ | 名字 | 是什么 | 怎么找 |
22
+ |---|---|---|
23
+ | **插件目录** `<PLUGIN_DIR>` | 这个仓库放在哪 | 你自己放的地方,例如 `C:\Users\<你>\Documents\Codex\dsh-tool-switch` |
24
+ | **Profile 目录** `<PROFILE_DIR>` | 当前正在跑的那套插件组合 | 见下表;有 `cordis.patch.yml`、`package.json`、`node_modules` |
25
+ | **Harness 数据目录** `<HARNESS_DIR>` | 会话、日志、`cordis.patch.yml` 所在 | 见下表;日志在 `<HARNESS_DIR>\logs\harness.log` |
26
+
27
+ | 系统 | `<HARNESS_DIR>` | `<PROFILE_DIR>`(默认 profile 名 `web`) |
28
+ |---|---|---|
29
+ | Windows | `%APPDATA%\dsh-desktop\harness` | `<HARNESS_DIR>\profiles\web` |
30
+ | macOS | `~/Library/Application Support/dsh-desktop/harness` | `<HARNESS_DIR>/profiles/web` |
31
+ | Linux | `~/.config/dsh-desktop/harness` | `<HARNESS_DIR>/profiles/web` |
32
+
33
+ **怎么确认自己找对了**:`<PROFILE_DIR>\package.json` 里有 `"dsh": { "profile": { "bundles": [...] } }`,
34
+ `<PROFILE_DIR>\cordis.patch.yml` 里有你装过的其它插件(`dsh-ops`、`dsh-market` 之类)。
35
+
36
+ 想在命令行里确认(Desktop 拉起 Harness 时会注入这些变量,普通终端里没有):
37
+
38
+ ```powershell
39
+ # PowerShell(Windows 预设)
40
+ $env:DSH_PROFILE_DIR # -> <PROFILE_DIR>
41
+ $env:DSH_PROFILE # -> web
42
+ ```
43
+
44
+ ```bash
45
+ # bash(macOS / Linux)
46
+ echo "$DSH_PROFILE_DIR" "$DSH_PROFILE"
47
+ # 或者:dsh --profile web --dump-config | head -40
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 方式 A:作为组合包安装(推荐)
53
+
54
+ 这个仓库根目录带着 `package.json` 的 `dsh.bundle.patch` 与 `cordis.patch.yml`,
55
+ 所以它本身就是一个可安装的**组合包**:装上去之后,它自带的补丁层会把 Host 入口接进插件树,
56
+ 不需要你手改任何 profile 配置。
57
+
58
+ ### A-1 用界面装(最省事)
59
+
60
+ 1. 左侧栏点 **插件**(不是「设置 → 插件」,那里是只读列表)。
61
+ 2. 点 **安装**,把 **插件目录的绝对路径** 贴进去,例如
62
+ `C:\Users\<你>\Documents\Codex\dsh-tool-switch`。
63
+ 也可以粘这个仓库的地址:`https://github.com/suqing114/dsh-tool-switch`。
64
+ 3. 确认安装 → 等日志跑完(本插件没有依赖,通常一两秒)。
65
+ 4. 安装完它会自动被选中/启用。**刷新页面(Ctrl+R)**,输入框下方出现 `⚙ 工具` 胶囊即成功。
66
+
67
+ > 装的是本地目录时,profile 记录的是 `link:<绝对路径>`:以后你在这个仓库里改代码,
68
+ > 效果直接跟着变(Host 侧改完仍需重启,客户端 bundle 会热替换)。
69
+
70
+ ### A-2 用 agent / CLI 装
71
+
72
+ 在 DSH 里让 agent 装(Full access 下不会弹审批):
73
+
74
+ ```
75
+ 用 plugin_manager 的 install_bundle,target 指向 C:\Users\<你>\Documents\Codex\dsh-tool-switch
76
+ ```
77
+
78
+ 或命令行:
79
+
80
+ ```powershell
81
+ dsh plugin install "C:\Users\<你>\Documents\Codex\dsh-tool-switch"
82
+ dsh plugin list # 确认 dsh-tool-switch 在列表里且已启用
83
+ ```
84
+
85
+ ### A-3 卸载
86
+
87
+ - 界面:**插件** 页找到 `dsh-tool-switch` → 取消选择 / 移除。
88
+ - CLI:`dsh plugin remove dsh-tool-switch`。
89
+ - 移除后刷新页面,按钮消失;`<HARNESS_DIR>\dsh-tool-switch\state.json`(或
90
+ `~/.dsh-tool-switch/state.json`)可以留着,也可以删掉。
91
+
92
+ ---
93
+
94
+ ## 方式 B:在补丁层里手工挂载
95
+
96
+ 适合「我就在改这份源码」的场景。两种写法二选一。
97
+
98
+ ### B-1 直接指向目录(file URL,本机现在用的就是这种)
99
+
100
+ 编辑 `<HARNESS_DIR>\cordis.patch.yml`,在**末尾**追加(注意 YAML 顶层是数组,前面的条目不要动):
101
+
102
+ ```yaml
103
+ - insert:
104
+ - id: dsh-tool-switch
105
+ name: "file:///C:/Users/<你>/Documents/Codex/dsh-tool-switch/index.js?v=1"
106
+ config:
107
+ stateDir: C:\Users\<你>\AppData\Roaming\dsh-desktop\harness\dsh-tool-switch
108
+ ```
109
+
110
+ - Windows 路径写成 `file:///C:/...`(正斜杠,三个斜杠);macOS/Linux 例如
111
+ `"file:///Users/<你>/code/dsh-tool-switch/index.js?v=1"`。
112
+ - `?v=1` 是 **ESM 模块缓存版本后缀**:Host 侧代码改过之后,把这个数字 +1(或重启 DSH)
113
+ 才会加载新代码;客户端 `lib/client.js` 不用管,它按 mtime 自动热替换。
114
+ - `config` 可以整段省略(默认状态目录是 `~/.dsh-tool-switch`)。
115
+ - **热生效**:保存文件后 Loader 会自己重挂,不用重启;页面刷新一次即可看到按钮。
116
+ - **卸载**:删掉 `- insert:` 这整块(连同它下面的两行),保存即可热卸载;
117
+ 插件卸载时会自动撤销它装过的所有工具遮罩。
118
+
119
+ ### B-2 拷进 profile 的 node_modules,用包名挂
120
+
121
+ ```powershell
122
+ # Windows:把仓库复制进 profile 的 node_modules(不要用符号链接,pnpm 清理时容易断)
123
+ Copy-Item -Recurse "C:\Users\<你>\Documents\Codex\dsh-tool-switch" `
124
+ "$env:APPDATA\dsh-desktop\harness\profiles\web\node_modules\dsh-tool-switch"
125
+ ```
126
+
127
+ ```yaml
128
+ - insert:
129
+ - id: dsh-tool-switch
130
+ name: "dsh-tool-switch"
131
+ ```
132
+
133
+ > 注意:这种方式下次 `pnpm install` / 插件页装别的包时,可能把这份拷贝清掉。
134
+ > 要长期用请走**方式 A**。
135
+
136
+ ---
137
+
138
+ ## 方式 C:用包管理器装(Git / tarball / 注册表)
139
+
140
+ 先弄清一件事:`pnpm add` 只把包放进 `node_modules`,DSH 还要知道「这个组合包要参与组合」。
141
+ 所以是**两步**——先装包,再让 profile 认它(插件页做的就是这两步)。
142
+
143
+ ### C-1 从 GitHub 直接装
144
+
145
+ ```powershell
146
+ cd <PROFILE_DIR>
147
+ pnpm add github:suqing114/dsh-tool-switch#v0.2.0 # 建议锁 tag,别跟 main 漂移
148
+ ```
149
+
150
+ 然后二选一让 profile 认它:
151
+
152
+ - 把 `"dsh-tool-switch"` 加进 `<PROFILE_DIR>\package.json` 的 `dsh.profile.bundles` 数组
153
+ (它自带补丁层,会自动插入 Host 入口);
154
+ - 或者在 `<HARNESS_DIR>\cordis.patch.yml` 里 insert 一行 `name: 'dsh-tool-switch'`(用包名)。
155
+
156
+ > 实测(2026-11-13,pnpm 10.34.5):空目录里 `pnpm add github:suqing114/dsh-tool-switch` →
157
+ > `+ dsh-tool-switch 0.2.0`;装出来的 `index.js`、`lib/client.js` 都能过 `node --check`,
158
+ > `dsh.bundle.patch` / `dsh.client` 声明完整。
159
+
160
+ ### C-2 从本地 tarball 装
161
+
162
+ ```powershell
163
+ cd <PLUGIN_DIR>
164
+ npm pack # 产出 dsh-tool-switch-0.2.0.tgz,28 kB,只含发布文件
165
+ cd <PROFILE_DIR>
166
+ pnpm add "file:C:\Users\<你>\Documents\Codex\dsh-tool-switch\dsh-tool-switch-0.2.0.tgz"
167
+ ```
168
+
169
+ ### C-3 从 npm 注册表装
170
+
171
+ #### 现状(2026-11)
172
+
173
+ `npmjs.com` 上账号 `suyuqing` 开了 **通行密钥(passkey)2FA**。对这种情况 npm 的规则是:
174
+ CLI 发布必须带一次性密码 **或者** 带 bypass-2FA 的令牌——而 passkey 产生不了 6 位数字,
175
+ 所以「交互式 2FA 发布」这条在 CLI 上走不通(`npm publish` 只会打印一个被脱敏的
176
+ `https://www.npmjs.com/auth/cli/***` 然后 `EOTP` 退出,即使给它真 TTY 也一样)。
177
+
178
+ 因此**首次发布**要用一次**短命凭据**,之后全部改用 OIDC,长期不留任何 token。
179
+
180
+ #### ① 首次发布(只做一次,需要凭据)
181
+
182
+ 在 npmjs.com → Access Tokens 生成一个 **Granular Access Token**:
183
+
184
+ - ✅ **Bypass two-factor authentication**
185
+ - Permissions:**Read and write**
186
+ - **Expiration:7 days 或更短**(做完就删)
187
+
188
+ 然后在本目录用它发一次(token 只进临时文件,别写进仓库、别进 CI):
189
+
190
+ ```powershell
191
+ $tmp = "$env:TEMP\npmrc-publish"; "//registry.npmjs.org/:_authToken=`${NODE_AUTH_TOKEN}" | Set-Content $tmp -Encoding ascii
192
+ $env:NODE_AUTH_TOKEN = "npm_xxx" # 刚生成的短命 token
193
+ npm publish --userconfig $tmp # publishConfig 已是 public
194
+ Remove-Item $tmp # 立刻删掉临时文件
195
+ # 发布成功后,回 npmjs 把这个 token 吊销
196
+ ```
197
+
198
+ #### ② 之后永久改成 OIDC(不用 token、不用 2FA)
199
+
200
+ npmjs.com → 这个包 → **Settings → Trusted publishing**(GitHub Actions):
201
+
202
+ | 字段 | 值 |
203
+ |---|---|
204
+ | Organization or user | `suqing114` |
205
+ | Repository | `dsh-tool-switch` |
206
+ | Workflow filename | `publish.yml` |
207
+ | Allowed actions | `npm publish` |
208
+
209
+ 再把 **Publishing access** 改成 **“Require two-factor authentication and disallow tokens”**。
210
+ 这样连 bypass token 都不允许用,只剩仓库里的 `.github/workflows/publish.yml` 能发(它走 OIDC +
211
+ provenance,`permissions: id-token: write`,全程没有任何密钥)。
212
+
213
+ 之后发版就是:
214
+
215
+ ```powershell
216
+ # 改完 package.json 的 version
217
+ git tag v0.2.1; git push --tags # 工作流自动发布 + 生成 provenance
218
+ ```
219
+
220
+ #### ③ 装
221
+
222
+ ```powershell
223
+ pnpm add dsh-tool-switch # 或在插件页里直接装 dsh-tool-switch
224
+ ```
225
+
226
+ > 首次发布前可以用 `npm publish --dry-run` 看清单:本仓库当前是
227
+ > `package size 29.4 kB / unpacked 84 kB / total files 10`。
228
+
229
+ ### C-4 本地开发:link 进去
230
+
231
+ ```powershell
232
+ cd <PROFILE_DIR>
233
+ pnpm add "link:C:\Users\<你>\Documents\Codex\dsh-tool-switch"
234
+ ```
235
+
236
+ 之后你改仓库里的代码,profile 里的那份立刻跟着变(Host 侧仍需重启或换 `?v=`)。
237
+
238
+ > ⚠️ 已经用方式 A / B 装过的,**不要再用 pnpm 装一遍**:同一份插件被两个 Loader
239
+ > 来源解析到,客户端会报 `resolves from multiple active Loader sources` 并拒绝组图。
240
+ > 换方式之前先把旧的那条摘掉。
241
+
242
+ ## 装完怎么验(三条,从底层往上)
243
+
244
+ ```powershell
245
+ # 1) Host 路由活着:200 + JSON;不带 session 应 400;随便一个路径应 404
246
+ curl.exe "http://127.0.0.1:43129/tool-switch/state?session=test"
247
+ curl.exe "http://127.0.0.1:43129/tool-switch/state"
248
+ ```
249
+
250
+ ```powershell
251
+ # 2) 客户端启动图里有这一行(bundle 也能取到)
252
+ cd <PLUGIN_DIR>
253
+ node test\probe-client-graph.mjs
254
+ # 期望:entries 里出现 {"id":"dsh-tool-switch","url":"plugins/??dsh-tool-switch/client.js&rev=…"}
255
+ # 并且 bundle … 含我的代码: true
256
+ ```
257
+
258
+ 3. 页面里:输入框卡片**正下方那一行**、靠右、紧挨 `56%` 上下文档位左边 → `⚙ 工具` 胶囊。
259
+ 点开是分组面板(默认全部收起),每类后面有 `全关 / 全开`。
260
+
261
+ 端口不是 43129 的话(`dsh web --port` 或用别的方式起的),把地址换成实际端口;
262
+ `test\paths.mjs` 里所有路径/端口都能用环境变量覆盖:
263
+
264
+ ```powershell
265
+ $env:DSH_URL = "http://127.0.0.1:38999"
266
+ $env:DSH_HARNESS = "D:\elsewhere\harness"
267
+ node test\probe-route.mjs
268
+ ```
269
+
270
+ ---
271
+
272
+ ## headless / QQ 这类没有输入框的入口
273
+
274
+ 按钮是给页面用的。没有页面(甚至没有 `webServer`)的时候,用另外两条通道——它们和按钮写同一份状态:
275
+
276
+ 1. **直接跟模型说**:在会话里说「把 rea 那类工具关掉 / 打开」,模型会调用 `tool_switch`
277
+ (`action: off|on|list|reset`,`target` 认分组名、`mcp__rea__` 前缀、精确工具名或 `all`)。
278
+ 这条**不需要 webServer**,只要有 agent 能跑工具就行,QQ 那种转发式入口最合适。
279
+ 2. **斜杠命令**(需要该入口有命令适配器,例如 CLI 或网页 composer):
280
+
281
+ ```
282
+ /tools 看分组与当前开关
283
+ /tools off rea 关掉整组
284
+ /tools on mcp__rea__ 按前缀打开
285
+ /tools all off|all on 全关 / 全开
286
+ /tools reset 全部恢复
287
+ ```
288
+
289
+ 3. **HTTP**(有 `webServer` 时,脚本/外部程序用):
290
+
291
+ ```bash
292
+ curl "http://127.0.0.1:43129/tool-switch/state?session=session-<uuid>"
293
+ curl -X POST "http://127.0.0.1:43129/tool-switch/state" \
294
+ -H 'content-type: application/json' \
295
+ -d '{"session":"session-<uuid>","disabled":["mcp__rea__open_binary"]}'
296
+ ```
297
+
298
+ > `webServer` 只有 `dsh-web-app` 这个组合包会挂(桌面版、`dsh web` 自带;纯 CLI 组合没有),
299
+ > 所以脚本方案在没有页面的部署里要先确认这条路由存在(404 = 没挂,改用前两条)。
300
+ > `tool_switch` 自己不会被关掉,`all off` 之后它也还在,所以永远留着一个人能改回来的入口。
301
+
302
+ ## 常见问题
303
+
304
+ | 现象 | 原因 / 处理 |
305
+ |---|---|
306
+ | `curl …/tool-switch/state` 返回 **404** | 补丁没生效:确认 `- insert:` 块在 `cordis.patch.yml` 末尾、`id` 没拼错、文件保存了;看 `<HARNESS_DIR>\logs\harness.log` 里有没有 `patch: entry … not found` 之类的行;方式 B 下重新触发一次热加载(随便改一下该文件)即可 |
307
+ | 路由 200,但页面**没有按钮** | 客户端半侧没加载:先 Ctrl+R 硬刷;再跑第 2 条验证。若启动图里没有这一行,说明 `package.json` 的 `dsh.client` 没被读到——方式 B-1 改 `package.json` 后需要重启 DSH |
308
+ | 日志里 `resolves from multiple active Loader sources` | 同一份插件被挂了两遍(比如方式 A 和 B 同时开)。留一条,删掉另一条 |
309
+ | 改了 `index.js`,行为没变 | Host 侧代码只在条目重启时重新导入:方式 B-1 把 `?v=` 数字 +1,或重启 DSH |
310
+ | 改了 `lib/client.js`,样式没变 | 客户端 bundle 按 mtime 热替换,通常几秒内生效;没生效就 Ctrl+R |
311
+ | 面板里有些工具标 `· 会话固定`、勾选框是灰的 | 那是 agent 作用域内注册的工具,框架规定 `tools.restrict` 管不到它们(例如某些预设注册的 `ops_discover_tools`) |
312
+ | 关掉工具后模型“看不见”了,但我想验证真的挡住了 | 关掉某工具后让它调用一次,会得到 `UNKNOWN_TOOL`;`test\probe-live-restrict.mjs` 是同一路径的自动版(不会真的隐藏任何工具) |
313
+ | 想一次性恢复默认 | 删掉状态文件(`<HARNESS_DIR>\dsh-tool-switch\state.json` 或 `~/.dsh-tool-switch/state.json`),或在面板里点 `全开` |
314
+
315
+ ## 安全边界
316
+
317
+ - 路由只监听 loopback(Harness 的 `webServer` 本来就这么绑),同源访问;
318
+ `/api` 那套鉴权不覆盖它,因此**本机**任何进程都能读写这个状态——它只影响“本会话模型能用哪些工具”,
319
+ 不涉及凭证、文件内容或执行权限。
320
+ - 插件不联网、不读环境变量里的密钥;唯一的磁盘写入是它自己的 `state.json`。
321
+ - 状态按 sessionId 存,换会话互不影响;插件被卸载/停用时所有遮罩会被撤销,工具集合回到全开。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Su Yuqing (suqing114)
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 CHANGED
@@ -1,3 +1,172 @@
1
- # Temporary Holding Version
1
+ # dsh-tool-switch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 会话级「工具开关」:在输入框下面加一个**工具**按钮,点开按分组勾选这个会话里模型可以用的工具。
4
+ 关掉的工具不会出现在系统提示词的工具清单里,模型也调不到(调用会得到 `UNKNOWN_TOOL`),改完下一条消息就生效,随时可以再改。
5
+
6
+ ```
7
+ ┌──────────────────────────────────────────────┐
8
+ │ 本会话可用工具 12/330 × │
9
+ │ [ 搜索工具… ] [全开][全关]│
10
+ │ ▾ 会话与计划 12/12 │
11
+ │ ▾ 终端与执行 4/4 │
12
+ │ ☑ pwsh Run a PowerShell command. │
13
+ │ ☐ job_list List background jobs. │
14
+ │ ▾ rea · MCP 205/207 │
15
+ │ ▸ 图像与音视频 5/5 (点组名=整组开关)│
16
+ │ 改动从下一条消息开始生效;被关闭的工具模型既看不到也调用不了。│
17
+ └──────────────────────────────────────────────┘
18
+ ```
19
+
20
+ ## 它做了什么
21
+
22
+ - **按钮位置**:`conversation.composer.dock`——输入框(那个带花边的输入卡片)**正下方那一行**,
23
+ 靠右、紧挨着上下文档位圆环(`56%` 那个)左边,是个描边胶囊:`⚙ 工具`。
24
+ 关过工具后会变成强调色 + 一个小数字徽章(`工具 3` = 关了 3 个),悬停有提示。
25
+ - **默认全收起**:面板一打开(以及切换会话后)所有分类都是收起的,只列 `▸ 分类名 已开/总数 [全关][全开]`,
26
+ 想看某一组再点 `▸` 展开。工具栏上有 `展开` / `收起` 一次铺开或收拢所有分类;搜索时命中项会自动展开。
27
+ - **一键整组**:每个分组标题行右侧有 `全关` / `全开` 两个小按钮——`全关` 一次关掉这一组下所有可关闭的
28
+ 工具(锁定的那些自动跳过),`全开` 一次恢复。当前已经是目标状态的那个按钮会置灰,不会点了没反应还让人怀疑人生。
29
+ 点组名本身也还是整组开关(老入口保留)。
30
+ - **分组浏览**:内置能力按 会话与计划 / 终端与执行 / 文件与检索 / 网络检索 / 子代理与编排 /
31
+ 文档与演示 / 图像与音视频 / MCP 资源 / 社交与发布 / 工具链与维护 分类;MCP 工具按服务器
32
+ (`mcp__<server>__*`)单独成组;认不出来的按名字前缀兜底。支持搜索、折叠(搜索时自动展开)。
33
+ - **按会话记忆**:选择存在 `state.json` 里,按 sessionId 记「关掉了哪些」,重启后仍然生效。
34
+ - **可放进对话中途改**:直接改就行,下一次模型请求开始用新的集合。
35
+ - 面板用 `Esc` 或点面板外任意处关闭;`Esc` 在捕获阶段被面板吃掉并阻止冒泡,所以**不会**顺手把正在跑的回合停掉。
36
+ - 折叠状态是「一次性」的:关掉面板或换会话就回到全收起,不会记住你上次展开了哪组(刻意的,省得下次点开一堆)。
37
+
38
+ ## 原理(为什么它真的能挡住工具)
39
+
40
+ Host 侧用的是框架自带的按 agent 遮罩:
41
+
42
+ ```js
43
+ // 只有通过 agent 作用域上下文调用,遮罩才只作用于这个 agent
44
+ agent.ctx.tools.restrict({ allow: [...这个会话允许的工具名] })
45
+ ```
46
+
47
+ - 遮罩会同时作用于「提示词里的工具清单」「PTC 绑定」和「执行时的工具集合」,所以不是把 UI 藏起来,
48
+ 而是模型真的看不到、也调不到。
49
+ - 插件记的是**关闭集合**而不是开启集合:`allow = 该 agent 可限制的工具 − 关闭集合`。这样后来才连上的
50
+ MCP 工具默认就是可用的,不会被悄悄藏掉。
51
+ - 监听了 `agent/created` / `agent/disposed` / `tools/change`:新建会话、工具集合变化(MCP 晚了注册、
52
+ 插件热插拔)都会自动重算;只有 allow 集合真的变了才重装遮罩,避免无谓地让提示词前缀失效。
53
+ - 插件卸载时会撤掉自己装的所有遮罩(插件不在,就不该继续改会话能力)。
54
+
55
+ 浏览器半侧是一个手写的客户端 bundle(`lib/client.js`,与官方插件产物同构的
56
+ `window.__ModuleLoader__.load({ id, factory })` 形式),注册进 `conversation.composer.dock`
57
+ 这个 list slot,通过同源 `fetch` 和 Host 通信:
58
+
59
+ | 方法 | 路径 | 说明 |
60
+ |---|---|---|
61
+ | `GET` | `/tool-switch/state?session=<id>` | 工具目录 + 当前选择(分组、描述、是否可关闭) |
62
+ | `POST` | `/tool-switch/state` | body `{ session, disabled: [工具名…] }`,空数组=全部启用 |
63
+
64
+ 路由挂在框架的 `webServer` 上,同源、只监听回环;`/api` 那套鉴权不覆盖它,所以页面里 `fetch` 直接用。
65
+
66
+ ## 三个入口,同一份状态
67
+
68
+ 按钮只是壳。真正改状态的路径有三条,写的是同一份 `state.json`、走同一个重装逻辑:
69
+
70
+ | 入口 | 谁用 | 依赖 |
71
+ |---|---|---|
72
+ | 输入框下方的 `⚙ 工具` 按钮 | 桌面版 / 网页版 | 需要页面(客户端 bundle) |
73
+ | `/tools` 斜杠命令 | 有命令适配器的交互式入口(CLI、网页 composer) | 需要 `ctx.commands`,不需要 webServer、不进模型上下文 |
74
+ | `tool_switch` 工具 | **任何入口,包括 QQ / headless** | 只需要模型能调工具 |
75
+
76
+ ```text
77
+ /tools 看分组和当前开关
78
+ /tools off rea 关掉 rea 这一组(也认 终端与执行 / mcp:rea / mcp__rea__* / 精确工具名 / all)
79
+ /tools on mcp__rea__ 按前缀整片打开
80
+ /tools all off | all on 全关 / 全开(动词在前后都认)
81
+ /tools reset 全部恢复
82
+ ```
83
+
84
+ 模型那边同理:用户说「把 XX 类工具关掉」时它会调用 `tool_switch`(`action: off|on|list|reset` + `target`),
85
+ QQ 这种没有 UI、也没有 webServer 的部署就靠这条。`tool_switch` 自己**永远不会被关掉**——否则唯一的
86
+ headless 入口就没了,所以即使 `all off` 它也留在 allow 集合里。
87
+
88
+ ## 安装 / 卸载
89
+
90
+ **完整步骤看 [INSTALL.md](INSTALL.md)**(四种装法、三条验证命令、常见问题、跨平台路径都在那儿)。速览:
91
+
92
+ 1. 把仓库放到任意目录,例如 `C:\Users\<你>\Documents\Codex\dsh-tool-switch`。
93
+ 2. 挑一种挂上去:
94
+ - **A(推荐)当作组合包安装**:DSH 左侧栏 **插件 → 安装**,填这个目录的绝对路径
95
+ (或本仓库地址),确认后再刷新页面。仓库自带的 `cordis.patch.yml` 会负责接好 Host 入口。
96
+ - **C 包管理器直装**(就是「别的插件怎么装我就怎么装」):
97
+
98
+ ```powershell
99
+ cd <PROFILE_DIR>
100
+ pnpm add github:suqing114/dsh-tool-switch#v0.2.0 # 或 file:<本地 tgz> / link:<本地目录>
101
+ # 再把 "dsh-tool-switch" 加进本文件 dsh.profile.bundles(或用包名写进 cordis.patch.yml)
102
+ ```
103
+
104
+ `pnpm add` 只负责放包,profile 还得知道这个组合包要参与组合——两步都要做。
105
+ - **B 手工挂进补丁层**:在 `<HARNESS_DIR>\cordis.patch.yml` 末尾追加
106
+ (Windows 下 `<HARNESS_DIR>` = `%APPDATA%\dsh-desktop\harness`):
107
+
108
+ ```yaml
109
+ - insert:
110
+ - id: dsh-tool-switch
111
+ name: "file:///C:/Users/<你>/Documents/Codex/dsh-tool-switch/index.js?v=1"
112
+ config:
113
+ stateDir: C:\Users\<你>\AppData\Roaming\dsh-desktop\harness\dsh-tool-switch
114
+ ```
115
+
116
+ `?v=1` 只是 ESM 模块缓存的版本后缀:Host 侧代码改过之后把它 +1(或重启 DSH)才会重新加载。
117
+ **卸载**:删掉这整块即可(loader 会热卸载,插件装过的遮罩会一起撤销)。
118
+ 3. 刷新页面(Ctrl+R),输入框下方出现 `⚙ 工具` 胶囊。
119
+
120
+ > A、B **不要同时用**:同一份插件被两个 Loader 入口解析到,客户端会报
121
+ > `resolves from multiple active Loader sources` 并拒绝组图。
122
+
123
+ 状态文件:`<HARNESS_DIR>\dsh-tool-switch\state.json`(走 A 且没配 `stateDir` 时是 `~/.dsh-tool-switch/state.json`),
124
+ 格式 `{ version, sessions: { "<sessionId>": { disabled: [...] } } }`;删掉它等于把所有会话恢复成「全部启用」。
125
+
126
+ ### 测试脚本的路径
127
+
128
+ `test\paths.mjs` 集中解析路径与端口,都可用环境变量覆盖,仓库里没有写死任何个人目录:
129
+
130
+ | 变量 | 默认值 | 用途 |
131
+ |---|---|---|
132
+ | `DSH_APP` | `C:\Program Files\DSH Desktop\resources\app\package.json` | 借 app 自带的 React 做渲染测试 / 读前端 chunk |
133
+ | `DSH_HARNESS` | `%APPDATA%\dsh-desktop\harness` | 补丁层、profile node_modules、日志 |
134
+ | `DSH_URL` | `http://127.0.0.1:43129` | 探针打哪个 web GUI |
135
+ | `SESSION` | 无 | 活会话探针要探测的 `session-<uuid>` |
136
+
137
+ ## 找不到按钮时按这个顺序查
138
+
139
+ 1. **它很小**:12px 字、在输入框正下方那行的最右端(`⚙ 工具`,描边胶囊,左边是 `缓存命中 …`、
140
+ 右边是 `56%` 上下文档位)。
141
+ 2. **宿主半侧活着吗**:`curl http://127.0.0.1:43129/tool-switch/state?session=x` 应该返回
142
+ `{"ok":true,...}`;`/tool-switch/state`(不带 session)返回 400;别的路径 404。
143
+ 若 404,说明补丁条目没生效(看 `harness\logs\harness.log`),把插件停掉再启用一次即可。
144
+ 3. **客户端半侧加载了吗**:`node test\probe-client-graph.mjs`——启动图里应有
145
+ `{"id":"dsh-tool-switch","url":"plugins/??dsh-tool-switch/client.js&rev=…"}` 且 bundle 里能找到
146
+ 我们的代码。改 `lib/client.js` 后 `rev` 会变,页面会自动热替换;改 `index.js`(Host 侧)需要
147
+ 换 `?v=` 或重启。
148
+ 4. **页面太旧**:Ctrl+R 硬刷新一次。
149
+
150
+ ## 已知边界
151
+
152
+ - **agent 作用域里注册的工具关不掉**:框架规定「查看作用域自己的注册不受 restrict 影响」。这类工具在面板里
153
+ 标成 `· 会话固定` 且勾选框禁用(例如 agent preset 注册的 `ops_discover_tools`)。
154
+ - **只有按会话,没有全局默认**:新会话默认全部启用;想要「默认关掉某些」需要再加一层设置命名空间。
155
+ - **遮罩按 allow 列表生效**:如果你先关了工具,之后又有同名工具被重新注册,`tools/change` 会重算并把它算进
156
+ 「默认启用」。
157
+ - **面板没有写 i18n 字典**:文案是中文硬编码(内联在 bundle 里),没有走 `ctx.locale`。
158
+
159
+ ## 自测
160
+
161
+ ```bat
162
+ node test\test-host.mjs :: Host:分组/持久化/restrict 重装/HTTP 路由/卸载清理(10 项)
163
+ node test\test-client.mjs :: 客户端:bundle 协议、slot 注册形状、默认收起、整组一键、真实 React 渲染(6 项)
164
+ node test\verify-config.mjs :: 本机补丁层 + 本仓库的组合包声明(bundle patch / locale / icon)
165
+ node test\verify-client-inject.mjs :: dsh.client.inject 点名的包确实是启动图里的行
166
+ node test\probe-route.mjs :: 打真实实例,看 400 / 200 / 404
167
+ node test\probe-client-graph.mjs :: 启动图里有没有这一行、bundle 能否取到
168
+ node test\probe-live.mjs :: $env:SESSION=… 后看某个会话的目录/agent 视图
169
+ ```
170
+
171
+ `test\test-host.mjs` 用最小 ctx 桩跑通主路径;`test\test-client.mjs` 借 app 自带的 React 做静态渲染。
172
+ 路径与端口都由 [test/paths.mjs](test/paths.mjs) 解析,可用 `DSH_APP` / `DSH_HARNESS` / `DSH_URL` / `SESSION` 覆盖。
@@ -0,0 +1,9 @@
1
+ # dsh-tool-switch 作为组合包挂载时,由这一层把 Host 半侧的入口接进插件树。
2
+ #
3
+ # name 用包名(不是文件路径):组合包安装后,profile 的 node_modules 里能解析到这个包。
4
+ # 想固定状态目录或改路由,在 config 里追加即可,例如:
5
+ # config:
6
+ # stateDir: C:\Users\<你>\.dsh-tool-switch
7
+ - insert:
8
+ - id: dsh-tool-switch
9
+ name: 'dsh-tool-switch'
package/icon.svg ADDED
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Tool switch">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#4d6bfe"/>
5
+ <stop offset="1" stop-color="#8a4dfe"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="2" y="2" width="60" height="60" rx="16" fill="url(#bg)"/>
9
+ <!-- 三条滑轨 + 旋钮:三个工具开关 -->
10
+ <g stroke="#fff" stroke-width="3" stroke-linecap="round" fill="#fff">
11
+ <line x1="16" y1="21" x2="48" y2="21" opacity=".55"/>
12
+ <circle cx="40" cy="21" r="5"/>
13
+ <line x1="16" y1="32" x2="48" y2="32" opacity=".55"/>
14
+ <circle cx="24" cy="32" r="5"/>
15
+ <line x1="16" y1="43" x2="48" y2="43" opacity=".55"/>
16
+ <circle cx="34" cy="43" r="5"/>
17
+ </g>
18
+ </svg>