@huanlin/dsh-plugin-tools-manager 0.2.0
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 +17 -0
- package/README.md +109 -0
- package/cordis.patch.yml +9 -0
- package/lib/client.js +490 -0
- package/lib/config.js +26 -0
- package/lib/gateway.js +180 -0
- package/lib/index.js +49 -0
- package/lib/policy.js +81 -0
- package/lib/registry.js +314 -0
- package/lib/settings.js +79 -0
- package/package.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 huanlin
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published
|
|
8
|
+
by the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# dsh-tools-manager
|
|
2
|
+
|
|
3
|
+
DSH 插件:以 tree 形式列出「插件 → 工具」,并支持全局启停单个工具。
|
|
4
|
+
|
|
5
|
+
- **Tree 列出工具**:按来源插件分组,展示每个插件注册的全部工具(name / description)。
|
|
6
|
+
- **全局启停**:对任意工具一键禁用/启用,跨会话持久化,立即生效(模型不可见 + 执行被拒,两层一致)。
|
|
7
|
+
- **UI 入口**:设置页独立「工具管理」Tab(`settings.section` slot,与「MCP」Tab 同级)。
|
|
8
|
+
|
|
9
|
+
## 架构
|
|
10
|
+
|
|
11
|
+
### 工具来源归属(tree 数据基础)
|
|
12
|
+
|
|
13
|
+
- **归因算法**(零源码修改):
|
|
14
|
+
1. 启动时快照 `ctx.tools.schemas()` 作基线,归到 `(baseline)` 分组。
|
|
15
|
+
2. 监听 Cordis `internal/plugin`(`fiber.name` = 插件名)→ 维护 `pendingPlugins` 栈。
|
|
16
|
+
3. 监听 `tools/change`(register/unregister/restriction 均触发,unfiltered)→ 对 `ctx.tools.schemas()` 做 diff:
|
|
17
|
+
- 新增工具 → 归因到栈顶(最近加载)插件;
|
|
18
|
+
- 移除工具 → 从归属表删除。
|
|
19
|
+
4. 插件卸载(`internal/status` DISPOSED)→ 其工具随 effect 自动注销,diff 自然移除。
|
|
20
|
+
|
|
21
|
+
### 全局启停(两层一致,缺一不可)
|
|
22
|
+
|
|
23
|
+
| 层 | 缝 | 实现 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 隐藏(模型不可见) | `system-prompt/assemble`(waterfall) | 过滤 `assembly.tools` 中 disabled 工具,返回变换后的 assembly |
|
|
26
|
+
| 拒执行(防绕过) | 全局 `ctx.tools.guard()`(plain ctx) | disabled 工具返回 reason(如 `tool "X" is disabled by tools-manager: "X"`) |
|
|
27
|
+
|
|
28
|
+
- 同一个 `disabled: Set<string>` 驱动两层,保证 prompt 与执行语义一致。
|
|
29
|
+
- 覆盖场景:模型直调(guard 兜底)、run_code SDK 子调用(guard 兜底)、隐藏后模型不再发起。
|
|
30
|
+
- 不依赖 `ctx.tools.restrict()`(其强制 scoped ctx,不能做全局过滤)。
|
|
31
|
+
|
|
32
|
+
### 持久化
|
|
33
|
+
|
|
34
|
+
- `ctx.settings.register('tools-manager', Config)` → 存 `$DSH_HOME/settings.yaml`,跨会话生效。
|
|
35
|
+
- `Config`:`{ disabled: string[] }`(Schemastery schema,默认 `[]`)。
|
|
36
|
+
- 设置变更即时应用(guard/assemble 读同一份 `disabled` 状态,无需重启)。
|
|
37
|
+
|
|
38
|
+
### Host↔Client 通道
|
|
39
|
+
|
|
40
|
+
- 宿主 HTTP 路由:`ctx.webServer.register({ kind: 'prefix', path: '/tools-manager/api', handler })`。
|
|
41
|
+
- 接口(JSON envelope):
|
|
42
|
+
- `POST /tools-manager/api/list` → `{ ok, value: { plugins: [{ name, tools: [{ name, description, disabled }] }] } }`
|
|
43
|
+
- `POST /tools-manager/api/set` body `{ toolName, disabled }` → `{ ok, value: { plugins: [...] } }`
|
|
44
|
+
- 错误:`{ ok: false, error: { code, message } }`
|
|
45
|
+
|
|
46
|
+
## 开发
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pnpm install # 安装依赖(link: 指向 ~/.dsh/source/current/)
|
|
50
|
+
pnpm run typecheck # tsc --noEmit
|
|
51
|
+
pnpm test # vitest run
|
|
52
|
+
pnpm run build # tsc (host) + build-client.mjs (client bundle) + tsc (types)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 目录结构
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
src/
|
|
59
|
+
├── index.ts # Host 入口:name, inject, apply(registry + policy + gateway)
|
|
60
|
+
├── config.ts # Config schema (schemastery), ResolvedConfig, resolveConfig
|
|
61
|
+
├── registry.ts # ToolRegistry: 归因 diff(internal/plugin + tools/change)
|
|
62
|
+
├── policy.ts # installPolicy: system-prompt/assemble 过滤 + tools.guard
|
|
63
|
+
├── settings.ts # installToolsManagerSettings: 注册 namespace,返回 bridge
|
|
64
|
+
├── gateway.ts # registerHttpGateway: /tools-manager/api 前缀路由(list/set)
|
|
65
|
+
└── client/
|
|
66
|
+
├── index.ts # Client 入口:settings.section slot 注册独立 Tab
|
|
67
|
+
├── ToolsManagerPanel.tsx # 面板组件(tree + 开关,inline styles 对齐 mcp-manager)
|
|
68
|
+
└── locales.ts # i18n (zh + en)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 运行
|
|
72
|
+
|
|
73
|
+
### 本地安装
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
dsh plugin --profile web add "link:D:/Projects/deepseek-harness/dsh-tools-manager"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 配置
|
|
80
|
+
|
|
81
|
+
默认配置(`cordis.patch.yml`):
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
disabled: [] # 预置禁用的工具名列表(默认空)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
运行时通过设置页「插件配置」分区的卡片修改,持久化到 `$DSH_HOME/settings.yaml`。
|
|
88
|
+
|
|
89
|
+
## 检查
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pnpm run typecheck && pnpm test && pnpm run build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
验证 `lib/` 产物:
|
|
96
|
+
- `lib/index.js` — host bundle(ESM)
|
|
97
|
+
- `lib/client.js` — client bundle(CJS,`window.__ModuleLoader__.load` 包裹)
|
|
98
|
+
- `lib/types/` — TypeScript 声明文件
|
|
99
|
+
- `cordis.patch.yml` — bundle 配置层
|
|
100
|
+
|
|
101
|
+
## 已知边界
|
|
102
|
+
|
|
103
|
+
| 风险 | 影响 | 缓解 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| 归因偏移(插件运行期动态注册工具) | tree 中工具挂错插件 | 记录 Known Limitations;`list` 接口对未归属工具归到 `(unknown)` 分组 |
|
|
106
|
+
| `system-prompt/assemble` 多监听时序 | 其他插件也可能变换 tools | 本项目过滤只删 disabled,`next()` 委托其余;序靠后仍正确 |
|
|
107
|
+
| guard 与 assemble 双层不同步 | 隐藏与拒执行不一致 | 单一 `disabled` 状态源;单测断言两缝读同一状态 |
|
|
108
|
+
| settings 服务缺失 | 启停不持久 | 降级为内存态(仅本次运行生效),`set` 返回明确错误 |
|
|
109
|
+
| `run_code` 模式 | 模型经 SDK 子调用禁用工具 | guard 全局兜底拒绝;文档说明"禁用=执行不可达" |
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# dsh-tools-manager bundle layer: inserts the plugin row.
|
|
2
|
+
# Loaded by `dsh plugin --profile <name> add link:./dsh-tools-manager`.
|
|
3
|
+
# The `name` is the package entry; Loader resolves it from profile node_modules.
|
|
4
|
+
# Defaults: no tools disabled.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: tools-manager
|
|
7
|
+
name: '@huanlin/dsh-plugin-tools-manager'
|
|
8
|
+
config:
|
|
9
|
+
disabled: []
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@huanlin/dsh-plugin-tools-manager", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
"use strict";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/client/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
apply: () => apply,
|
|
26
|
+
inject: () => inject
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/client/ToolsManagerPanel.tsx
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
33
|
+
|
|
34
|
+
// src/client/prefixTree.ts
|
|
35
|
+
function splitToolName(name) {
|
|
36
|
+
const parts = name.split("__");
|
|
37
|
+
if (parts.length > 1) {
|
|
38
|
+
const result = [];
|
|
39
|
+
for (let i = 0; i < parts.length; i++) {
|
|
40
|
+
if (i === 0) {
|
|
41
|
+
if (parts[i] !== "") result.push(parts[i]);
|
|
42
|
+
} else if (i === parts.length - 1) {
|
|
43
|
+
if (parts[i] !== "") result.push(parts[i]);
|
|
44
|
+
} else {
|
|
45
|
+
const sub2 = parts[i].split("_").filter((s) => s !== "");
|
|
46
|
+
result.push(...sub2);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
const sub = name.split("_").filter((s) => s !== "");
|
|
52
|
+
return sub.length > 0 ? sub : [name];
|
|
53
|
+
}
|
|
54
|
+
function buildPrefixTree(tools) {
|
|
55
|
+
const root = { children: /* @__PURE__ */ new Map() };
|
|
56
|
+
for (const tool of tools) {
|
|
57
|
+
const segments = splitToolName(tool.name);
|
|
58
|
+
let current = root;
|
|
59
|
+
for (let i = 0; i < segments.length; i++) {
|
|
60
|
+
const segment = segments[i];
|
|
61
|
+
const isLast = i === segments.length - 1;
|
|
62
|
+
if (isLast) {
|
|
63
|
+
current.children.set(segment, {
|
|
64
|
+
kind: "leaf",
|
|
65
|
+
name: tool.name,
|
|
66
|
+
description: tool.description,
|
|
67
|
+
disabled: tool.disabled
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
let child = current.children.get(segment);
|
|
71
|
+
if (child === void 0 || child.kind === "leaf") {
|
|
72
|
+
const branch = { children: /* @__PURE__ */ new Map() };
|
|
73
|
+
child = { kind: "branch", prefix: segments.slice(0, i + 1).join("_"), label: segment, branch };
|
|
74
|
+
current.children.set(segment, child);
|
|
75
|
+
}
|
|
76
|
+
if (child.kind === "branch") {
|
|
77
|
+
current = child.branch;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return convertBranchToNodes(root);
|
|
83
|
+
}
|
|
84
|
+
function convertBranchToNodes(branch) {
|
|
85
|
+
const nodes = [];
|
|
86
|
+
for (const [, child] of branch.children) {
|
|
87
|
+
if (child.kind === "leaf") {
|
|
88
|
+
nodes.push(child);
|
|
89
|
+
} else {
|
|
90
|
+
nodes.push({
|
|
91
|
+
kind: "node",
|
|
92
|
+
prefix: child.prefix,
|
|
93
|
+
label: child.label,
|
|
94
|
+
children: convertBranchToNodes(child.branch)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
nodes.sort((a, b) => {
|
|
99
|
+
if (a.kind !== b.kind) return a.kind === "node" ? -1 : 1;
|
|
100
|
+
const aLabel = a.kind === "node" ? a.label : a.name;
|
|
101
|
+
const bLabel = b.kind === "node" ? b.label : b.name;
|
|
102
|
+
return aLabel < bLabel ? -1 : aLabel > bLabel ? 1 : 0;
|
|
103
|
+
});
|
|
104
|
+
return nodes;
|
|
105
|
+
}
|
|
106
|
+
function collectLeafNames(node) {
|
|
107
|
+
if (node.kind === "leaf") return [node.name];
|
|
108
|
+
const names = [];
|
|
109
|
+
for (const child of node.children) {
|
|
110
|
+
names.push(...collectLeafNames(child));
|
|
111
|
+
}
|
|
112
|
+
return names;
|
|
113
|
+
}
|
|
114
|
+
function countLeaves(node) {
|
|
115
|
+
if (node.kind === "leaf") {
|
|
116
|
+
return { total: 1, disabled: node.disabled ? 1 : 0 };
|
|
117
|
+
}
|
|
118
|
+
let total = 0;
|
|
119
|
+
let disabled = 0;
|
|
120
|
+
for (const child of node.children) {
|
|
121
|
+
const counts = countLeaves(child);
|
|
122
|
+
total += counts.total;
|
|
123
|
+
disabled += counts.disabled;
|
|
124
|
+
}
|
|
125
|
+
return { total, disabled };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/client/ToolsManagerPanel.tsx
|
|
129
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
130
|
+
var sectionStyle = {
|
|
131
|
+
display: "flex",
|
|
132
|
+
flexDirection: "column",
|
|
133
|
+
gap: 12,
|
|
134
|
+
color: "var(--dsw-alias-label-primary)"
|
|
135
|
+
};
|
|
136
|
+
var titleStyle = {
|
|
137
|
+
margin: 0,
|
|
138
|
+
fontSize: 16,
|
|
139
|
+
lineHeight: "24px",
|
|
140
|
+
fontWeight: 500,
|
|
141
|
+
color: "var(--dsw-alias-label-primary)"
|
|
142
|
+
};
|
|
143
|
+
var introStyle = {
|
|
144
|
+
margin: 0,
|
|
145
|
+
fontSize: 14,
|
|
146
|
+
lineHeight: "22px",
|
|
147
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
148
|
+
};
|
|
149
|
+
var treeStyle = {
|
|
150
|
+
margin: "8px 0 0",
|
|
151
|
+
display: "flex",
|
|
152
|
+
flexDirection: "column",
|
|
153
|
+
gap: 1
|
|
154
|
+
};
|
|
155
|
+
var errorStyle = {
|
|
156
|
+
margin: 0,
|
|
157
|
+
fontSize: 12,
|
|
158
|
+
lineHeight: "18px",
|
|
159
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
160
|
+
};
|
|
161
|
+
var emptyStyle = {
|
|
162
|
+
margin: "12px 0",
|
|
163
|
+
fontSize: 14,
|
|
164
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
165
|
+
};
|
|
166
|
+
var metaStyle = {
|
|
167
|
+
fontSize: 11,
|
|
168
|
+
lineHeight: "16px",
|
|
169
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
170
|
+
fontFamily: "ui-monospace, monospace"
|
|
171
|
+
};
|
|
172
|
+
var INDENT = 20;
|
|
173
|
+
function nodeHeaderStyle(depth) {
|
|
174
|
+
return {
|
|
175
|
+
display: "flex",
|
|
176
|
+
alignItems: "center",
|
|
177
|
+
gap: 6,
|
|
178
|
+
padding: "5px 8px",
|
|
179
|
+
cursor: "pointer",
|
|
180
|
+
borderRadius: 6,
|
|
181
|
+
userSelect: "none",
|
|
182
|
+
marginLeft: depth * INDENT
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var nodeHeaderClass = "tm-node-header";
|
|
186
|
+
var nodeLabelStyle = {
|
|
187
|
+
fontSize: 13,
|
|
188
|
+
fontWeight: 600,
|
|
189
|
+
color: "var(--dsw-alias-label-primary)",
|
|
190
|
+
fontFamily: "ui-monospace, monospace"
|
|
191
|
+
};
|
|
192
|
+
var nodeActionsStyle = {
|
|
193
|
+
display: "flex",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
gap: 6,
|
|
196
|
+
marginLeft: "auto"
|
|
197
|
+
};
|
|
198
|
+
function leafRowStyle(depth) {
|
|
199
|
+
return {
|
|
200
|
+
display: "flex",
|
|
201
|
+
alignItems: "flex-start",
|
|
202
|
+
gap: 8,
|
|
203
|
+
padding: "5px 8px",
|
|
204
|
+
borderRadius: 6,
|
|
205
|
+
marginLeft: depth * INDENT
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
var leafRowClass = "tm-leaf-row";
|
|
209
|
+
var leafInfoStyle = {
|
|
210
|
+
flex: 1,
|
|
211
|
+
minWidth: 0
|
|
212
|
+
};
|
|
213
|
+
var leafNameStyle = {
|
|
214
|
+
fontSize: 13,
|
|
215
|
+
fontWeight: 500,
|
|
216
|
+
color: "var(--dsw-alias-label-primary)",
|
|
217
|
+
fontFamily: "ui-monospace, monospace",
|
|
218
|
+
wordBreak: "break-all"
|
|
219
|
+
};
|
|
220
|
+
var leafDescStyle = {
|
|
221
|
+
fontSize: 12,
|
|
222
|
+
lineHeight: "18px",
|
|
223
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
224
|
+
marginTop: 2,
|
|
225
|
+
wordBreak: "break-word",
|
|
226
|
+
display: "-webkit-box",
|
|
227
|
+
WebkitLineClamp: 2,
|
|
228
|
+
WebkitBoxOrient: "vertical",
|
|
229
|
+
overflow: "hidden"
|
|
230
|
+
};
|
|
231
|
+
var leafToggleStyle = {
|
|
232
|
+
flexShrink: 0,
|
|
233
|
+
display: "flex",
|
|
234
|
+
alignItems: "center",
|
|
235
|
+
gap: 6
|
|
236
|
+
};
|
|
237
|
+
function Chevron({ open }) {
|
|
238
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
239
|
+
import_dsh_client_ui_primitives.IconChevronRightOutline14,
|
|
240
|
+
{
|
|
241
|
+
size: 14,
|
|
242
|
+
className: open ? "chevron-open" : "chevron-closed"
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
function ToolsManagerPanel(_props) {
|
|
247
|
+
const [plugins, setPlugins] = (0, import_react.useState)([]);
|
|
248
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
249
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
250
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
251
|
+
const [pendingTools, setPendingTools] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
252
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
253
|
+
try {
|
|
254
|
+
const res = await fetch("/tools-manager/api/list", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "content-type": "application/json" },
|
|
257
|
+
body: "{}"
|
|
258
|
+
});
|
|
259
|
+
const body = await res.json();
|
|
260
|
+
if (body.ok === true && body.value !== void 0) {
|
|
261
|
+
setPlugins(body.value.plugins);
|
|
262
|
+
setError(void 0);
|
|
263
|
+
} else {
|
|
264
|
+
setError(body.error?.message ?? "\u52A0\u8F7D\u5931\u8D25");
|
|
265
|
+
}
|
|
266
|
+
} catch (caught) {
|
|
267
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
268
|
+
} finally {
|
|
269
|
+
setLoading(false);
|
|
270
|
+
}
|
|
271
|
+
}, []);
|
|
272
|
+
(0, import_react.useEffect)(() => {
|
|
273
|
+
void refresh();
|
|
274
|
+
}, [refresh]);
|
|
275
|
+
const toggleOne = (0, import_react.useCallback)(async (toolName, disabled) => {
|
|
276
|
+
setBusy(true);
|
|
277
|
+
setPendingTools((prev) => new Set(prev).add(toolName));
|
|
278
|
+
setError(void 0);
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch("/tools-manager/api/set", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "content-type": "application/json" },
|
|
283
|
+
body: JSON.stringify({ toolName, disabled })
|
|
284
|
+
});
|
|
285
|
+
const body = await res.json();
|
|
286
|
+
if (body.ok === true && body.value !== void 0) {
|
|
287
|
+
setPlugins(body.value.plugins);
|
|
288
|
+
} else {
|
|
289
|
+
setError(body.error?.message ?? "\u5207\u6362\u5931\u8D25");
|
|
290
|
+
}
|
|
291
|
+
} catch (caught) {
|
|
292
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
293
|
+
} finally {
|
|
294
|
+
setBusy(false);
|
|
295
|
+
setPendingTools((prev) => {
|
|
296
|
+
const next = new Set(prev);
|
|
297
|
+
next.delete(toolName);
|
|
298
|
+
return next;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}, []);
|
|
302
|
+
const toggleNode = (0, import_react.useCallback)(async (node) => {
|
|
303
|
+
const names = collectLeafNames(node);
|
|
304
|
+
if (names.length === 0) return;
|
|
305
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
306
|
+
for (const g of plugins) {
|
|
307
|
+
for (const t of g.tools) toolMap.set(t.name, t.disabled);
|
|
308
|
+
}
|
|
309
|
+
const anyEnabled = names.some((n) => toolMap.get(n) !== true);
|
|
310
|
+
const targetDisabled = anyEnabled;
|
|
311
|
+
const toToggle = names.filter((n) => toolMap.get(n) !== targetDisabled);
|
|
312
|
+
if (toToggle.length === 0) return;
|
|
313
|
+
setBusy(true);
|
|
314
|
+
setPendingTools((prev) => {
|
|
315
|
+
const next = new Set(prev);
|
|
316
|
+
for (const n of toToggle) next.add(n);
|
|
317
|
+
return next;
|
|
318
|
+
});
|
|
319
|
+
setError(void 0);
|
|
320
|
+
try {
|
|
321
|
+
let lastBody;
|
|
322
|
+
for (const name of toToggle) {
|
|
323
|
+
const res = await fetch("/tools-manager/api/set", {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: { "content-type": "application/json" },
|
|
326
|
+
body: JSON.stringify({ toolName: name, disabled: targetDisabled })
|
|
327
|
+
});
|
|
328
|
+
lastBody = await res.json();
|
|
329
|
+
if (lastBody.ok !== true) {
|
|
330
|
+
setError(lastBody.error?.message ?? `\u6279\u91CF\u5207\u6362\u5931\u8D25: ${name}`);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (lastBody?.ok === true && lastBody.value !== void 0) {
|
|
335
|
+
setPlugins(lastBody.value.plugins);
|
|
336
|
+
}
|
|
337
|
+
} catch (caught) {
|
|
338
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
339
|
+
} finally {
|
|
340
|
+
setBusy(false);
|
|
341
|
+
setPendingTools((prev) => {
|
|
342
|
+
const next = new Set(prev);
|
|
343
|
+
for (const n of toToggle) next.delete(n);
|
|
344
|
+
return next;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}, [plugins]);
|
|
348
|
+
const allTools = (0, import_react.useMemo)(() => {
|
|
349
|
+
const tools = [];
|
|
350
|
+
for (const g of plugins) tools.push(...g.tools);
|
|
351
|
+
return tools;
|
|
352
|
+
}, [plugins]);
|
|
353
|
+
const tree = (0, import_react.useMemo)(() => buildPrefixTree(allTools), [allTools]);
|
|
354
|
+
const totalTools = allTools.length;
|
|
355
|
+
const disabledCount = allTools.filter((t) => t.disabled).length;
|
|
356
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sectionStyle, children: [
|
|
357
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: titleStyle, children: "\u5DE5\u5177\u7BA1\u7406" }),
|
|
358
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: introStyle, children: "\u6309\u5DE5\u5177\u540D\u524D\u7F00\u5206\u7EC4\u7684\u53EF\u6298\u53E0\u6811\u3002\u70B9\u51FB\u8282\u70B9\u5C55\u5F00/\u6298\u53E0\uFF0C\u5185\u90E8\u8282\u70B9\u652F\u6301\u6279\u91CF\u542F\u505C\u5B50\u5DE5\u5177\u3002\u7981\u7528\u7684\u5DE5\u5177\u5BF9\u6A21\u578B\u4E0D\u53EF\u89C1\u4E14\u6267\u884C\u88AB\u62D2\uFF0C\u8DE8\u4F1A\u8BDD\u6301\u4E45\u5316\u3002" }),
|
|
359
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: metaStyle, children: [
|
|
360
|
+
"\u5171 ",
|
|
361
|
+
plugins.length,
|
|
362
|
+
" \u4E2A\u63D2\u4EF6 \xB7 ",
|
|
363
|
+
totalTools,
|
|
364
|
+
" \u4E2A\u5DE5\u5177 \xB7 ",
|
|
365
|
+
disabledCount,
|
|
366
|
+
" \u4E2A\u5DF2\u7981\u7528"
|
|
367
|
+
] }),
|
|
368
|
+
error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: errorStyle, children: error }),
|
|
369
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: introStyle, children: "\u52A0\u8F7D\u4E2D\u2026" }) : totalTools === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: emptyStyle, children: "\u5F53\u524D\u6CA1\u6709\u5DF2\u6CE8\u518C\u7684\u5DE5\u5177\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: treeStyle, children: tree.map((node) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
370
|
+
TreeEntry,
|
|
371
|
+
{
|
|
372
|
+
node,
|
|
373
|
+
depth: 0,
|
|
374
|
+
pendingTools,
|
|
375
|
+
busy,
|
|
376
|
+
onToggleTool: toggleOne,
|
|
377
|
+
onToggleNode: toggleNode
|
|
378
|
+
},
|
|
379
|
+
node.kind === "node" ? node.prefix : node.name
|
|
380
|
+
)) }),
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
|
|
382
|
+
.chevron-closed { transition: transform 0.15s ease; transform: rotate(0deg); }
|
|
383
|
+
.chevron-open { transition: transform 0.15s ease; transform: rotate(90deg); }
|
|
384
|
+
.tm-node-header:hover { background: var(--dsw-alias-bg-module-platform, rgba(0,0,0,0.04)); }
|
|
385
|
+
.tm-leaf-row:hover { background: var(--dsw-alias-bg-module-platform, rgba(0,0,0,0.04)); }
|
|
386
|
+
` })
|
|
387
|
+
] });
|
|
388
|
+
}
|
|
389
|
+
function TreeEntry(props) {
|
|
390
|
+
const { node, depth, pendingTools, busy, onToggleTool, onToggleNode } = props;
|
|
391
|
+
if (node.kind === "leaf") {
|
|
392
|
+
const pending = pendingTools.has(node.name);
|
|
393
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: leafRowStyle(depth), className: leafRowClass, children: [
|
|
394
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: leafInfoStyle, children: [
|
|
395
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: leafNameStyle, children: node.name }),
|
|
396
|
+
node.description ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: leafDescStyle, children: node.description }) : null
|
|
397
|
+
] }),
|
|
398
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: leafToggleStyle, children: [
|
|
399
|
+
node.disabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Pill, { children: "\u5DF2\u7981\u7528" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Pill, { active: true, children: "\u5DF2\u542F\u7528" }),
|
|
400
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
401
|
+
import_dsh_client_ui_primitives.Button,
|
|
402
|
+
{
|
|
403
|
+
onClick: () => onToggleTool(node.name, !node.disabled),
|
|
404
|
+
disabled: busy && pending,
|
|
405
|
+
children: node.disabled ? "\u542F\u7528" : "\u7981\u7528"
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
] })
|
|
409
|
+
] });
|
|
410
|
+
}
|
|
411
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
412
|
+
PrefixNodeEntry,
|
|
413
|
+
{
|
|
414
|
+
node,
|
|
415
|
+
depth,
|
|
416
|
+
pendingTools,
|
|
417
|
+
busy,
|
|
418
|
+
onToggleTool,
|
|
419
|
+
onToggleNode
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
function PrefixNodeEntry(props) {
|
|
424
|
+
const { node, depth, pendingTools, busy, onToggleTool, onToggleNode } = props;
|
|
425
|
+
const [open, setOpen] = (0, import_react.useState)(depth < 1);
|
|
426
|
+
if (node.kind !== "node") return null;
|
|
427
|
+
const counts = countLeaves(node);
|
|
428
|
+
const allDisabled = counts.disabled === counts.total;
|
|
429
|
+
const anyEnabled = counts.disabled < counts.total;
|
|
430
|
+
const nodePending = collectLeafNames(node).some((n) => pendingTools.has(n));
|
|
431
|
+
const statusPill = allDisabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Pill, { children: "\u5168\u90E8\u5DF2\u7981\u7528" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dsh_client_ui_primitives.Pill, { active: true, children: [
|
|
432
|
+
counts.total - counts.disabled,
|
|
433
|
+
"/",
|
|
434
|
+
counts.total
|
|
435
|
+
] });
|
|
436
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
437
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
438
|
+
"div",
|
|
439
|
+
{
|
|
440
|
+
style: nodeHeaderStyle(depth),
|
|
441
|
+
className: nodeHeaderClass,
|
|
442
|
+
onClick: () => setOpen(!open),
|
|
443
|
+
children: [
|
|
444
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chevron, { open }),
|
|
445
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: nodeLabelStyle, children: node.label }),
|
|
446
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: nodeActionsStyle, children: [
|
|
447
|
+
statusPill,
|
|
448
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
449
|
+
import_dsh_client_ui_primitives.Button,
|
|
450
|
+
{
|
|
451
|
+
onClick: (e) => {
|
|
452
|
+
e.stopPropagation();
|
|
453
|
+
onToggleNode(node);
|
|
454
|
+
},
|
|
455
|
+
disabled: busy && nodePending,
|
|
456
|
+
children: anyEnabled ? "\u5168\u90E8\u7981\u7528" : "\u5168\u90E8\u542F\u7528"
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
] })
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
),
|
|
463
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 1 }, children: node.children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
464
|
+
TreeEntry,
|
|
465
|
+
{
|
|
466
|
+
node: child,
|
|
467
|
+
depth: depth + 1,
|
|
468
|
+
pendingTools,
|
|
469
|
+
busy,
|
|
470
|
+
onToggleTool,
|
|
471
|
+
onToggleNode
|
|
472
|
+
},
|
|
473
|
+
child.kind === "node" ? child.prefix : child.name
|
|
474
|
+
)) })
|
|
475
|
+
] });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/client/index.ts
|
|
479
|
+
var inject = ["slots"];
|
|
480
|
+
function apply(ctx) {
|
|
481
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
482
|
+
name: "settings.section",
|
|
483
|
+
id: "dsh-tools-manager",
|
|
484
|
+
order: 62,
|
|
485
|
+
// after MCP (61)
|
|
486
|
+
label: () => "\u5DE5\u5177\u7BA1\u7406",
|
|
487
|
+
inject: () => ({})
|
|
488
|
+
}, ToolsManagerPanel));
|
|
489
|
+
}
|
|
490
|
+
return module.exports; } });
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — composition-layer schema and resolved config shape.
|
|
3
|
+
*
|
|
4
|
+
* The composition `Config` (cordis.patch.yml) is the first-boot seed; the
|
|
5
|
+
* settings user layer composes on top of it at runtime. `resolveConfig`
|
|
6
|
+
* normalises any combination of partial source values into a fully-populated
|
|
7
|
+
* {@link ResolvedConfig}.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-tools-manager/config
|
|
10
|
+
*/
|
|
11
|
+
import z from 'schemastery';
|
|
12
|
+
/** Schemastery schema for the composition entry and the `tools-manager` settings namespace. */
|
|
13
|
+
export const Config = z.object({
|
|
14
|
+
disabled: z.array(z.string()).default([]).description('Globally disabled tool names; hidden from the model and denied at execution.'),
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Resolve config with fallbacks for missing / invalid values.
|
|
18
|
+
* @param config - raw config from cordis.yml or settings scope.
|
|
19
|
+
* @returns a fully-populated {@link ResolvedConfig}.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveConfig(config) {
|
|
22
|
+
const disabled = Array.isArray(config.disabled)
|
|
23
|
+
? config.disabled.filter((name) => typeof name === 'string' && name !== '')
|
|
24
|
+
: [];
|
|
25
|
+
return { disabled };
|
|
26
|
+
}
|