@zaimokuza/dsh-plugin-hub 0.2.0 → 0.2.2
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 +6 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/lib/client.js +244 -124
- package/package.json +9 -4
- package/src/experiment-definitions.js +6 -0
- package/src/experiments.js +6 -4
- package/src/index.js +2 -1
- package/src/mcp-native.js +76 -0
- package/src/mcp-probe.js +53 -0
- package/src/resources.js +12 -62
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# DSH Plugin Hub
|
|
2
2
|
|
|
3
|
-
DSH 原生资源面板,当前版本 **0.2.
|
|
3
|
+
DSH 原生资源面板,当前版本 **0.2.2**。支持 Web 与 Desktop、中英文界面;从右上角 Plugin Hub 图标打开,保留当前会话。
|
|
4
4
|
|
|
5
5
|
- **Skill**:搜索、按工作区筛选、启停、打开目录,使用 DSH 原生 Markdown 组件预览正文。
|
|
6
6
|
- **MCP**:JSON/YAML 配置编辑与格式化、环境变量引用、字段帮助、启停、重连、独立连接测试和删除。
|
|
7
7
|
- **Plugin**:查看当前 profile 的非官方原生插件,展示版本与来源;桌面端提供原生管理窗口入口说明。
|
|
8
|
-
- **实验性功能**:管理 Agent Teams;Web
|
|
8
|
+
- **实验性功能**:管理 Agent Teams;Web 支持安装与启停,支持实验性功能接口的 Desktop 可直接在 Hub 启停。
|
|
9
9
|
|
|
10
10
|
## 安装
|
|
11
11
|
|
|
@@ -15,13 +15,15 @@ dsh plugin --profile <当前-profile> add @zaimokuza/dsh-plugin-hub
|
|
|
15
15
|
|
|
16
16
|
重启 DSH 后打开 Plugin Hub。Desktop 从自身的 **Desktop Plugins** 窗口安装。
|
|
17
17
|
|
|
18
|
-
要求 Node.js `^22.19.0 || >=24.0.0
|
|
18
|
+
要求 Node.js `^22.19.0 || >=24.0.0`;兼容性验证覆盖 DSH `0.1.5-rc.1`、`0.1.5-rc.2` 和 `0.1.6-alpha.1`。
|
|
19
19
|
|
|
20
20
|
## 使用说明
|
|
21
21
|
|
|
22
22
|
所有管理操作绑定当前实例的 profile。Skill 启停不会修改共享文件;MCP 删除会移除 Hub 创建的配置或当前 profile 中独立的 YAML 定义,共享文件或安装包中的定义需在来源处删除。
|
|
23
23
|
|
|
24
|
-
MCP
|
|
24
|
+
MCP 配置校验和字段帮助直接读取当前 DSH 的原生规则与默认值。独立连接测试支持新旧 MCP 协议,描述、版本和图标来自服务元数据,服务端指令不作为描述。宿主未公开持续连接状态时显示“未知”,独立连接测试单独显示结果。建议在启动 DSH 时设置凭据环境变量,在 YAML 中通过 `!!js process.env.MCP_TOKEN` 引用;JSON 等价写法为 `{ "__jsExpr": "process.env.MCP_TOKEN" }`。
|
|
25
|
+
|
|
26
|
+
Desktop 实验性功能通过宿主的 `window.dshDesktop.experiments` v1 接口执行,宿主完成空闲检查、配置事务和后端重启。缺少接口的 Desktop 显示不支持管理。
|
|
25
27
|
|
|
26
28
|
目录操作作用于运行 DSH 的电脑。Desktop 原生管理窗口使用 macOS `Cmd+,`、Windows `Ctrl+,` 或系统菜单 **Desktop Plugins**。更多实现边界见 [原生资源说明](https://github.com/zaimokuza-yoshiteru/dsh-plugin-hub/blob/main/docs/native-resources.md)。
|
|
27
29
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -16,6 +16,6 @@ The above copyright notice and this permission notice shall be included in all c
|
|
|
16
16
|
|
|
17
17
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
18
18
|
|
|
19
|
-
Runtime dependencies @modelcontextprotocol/
|
|
19
|
+
Runtime dependencies @modelcontextprotocol/client and yaml, and development/test dependencies @modelcontextprotocol/sdk, @modelcontextprotocol/server, @modelcontextprotocol/node, @deepseek-ai/schemastery, esbuild and React retain their own licenses. Their precise versions are recorded in package-lock.json.
|
|
20
20
|
|
|
21
21
|
The UI consumes the host-provided `@deepseek-ai/dsh-client-ui-primitives` (Input, Button, Menu and Modal). These components and their styles are resolved from the DSH module table, not copied or bundled into this plugin.
|
package/lib/client.js
CHANGED
|
@@ -40,11 +40,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
40
40
|
var define_HUB_BRAND_default = { title: "Plugin Hub", subTitle: "DSH EXTENSIONS", primaryColor: "#596579", navTitle: "Plugin Hub" };
|
|
41
41
|
|
|
42
42
|
// src/client/index.jsx
|
|
43
|
-
var
|
|
43
|
+
var import_react5 = __toESM(require("react"), 1);
|
|
44
44
|
|
|
45
45
|
// src/client/resources.jsx
|
|
46
|
-
var
|
|
47
|
-
var
|
|
46
|
+
var import_react4 = __toESM(require("react"), 1);
|
|
47
|
+
var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
48
48
|
|
|
49
49
|
// src/client/resources.css
|
|
50
50
|
var resources_default = `/* Reuse marketplace layout and DSH controls; only adapt the main-panel container. */
|
|
@@ -78,12 +78,6 @@ var resources_default = `/* Reuse marketplace layout and DSH controls; only adap
|
|
|
78
78
|
.hub-resource-card-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 28px; }
|
|
79
79
|
.hub-resource-card-heading > :last-child { flex-shrink: 0; }
|
|
80
80
|
.hub-resource-tab[aria-selected="true"], .hub-resource-tab[aria-pressed="true"] { background: var(--dsw-alias-interactive-bg-active); }
|
|
81
|
-
/* The sidebar exports a slot, not its Settings trigger; match that trigger's geometry. */
|
|
82
|
-
.hub-sidebar-button { box-sizing: border-box; flex: 1; min-width: 0; height: 42px; margin: 4px -2px; padding: 0 10px 0 8px; gap: 8px; justify-content: flex-start; border-radius: 12px; font-family: var(--dsw-font-family); }
|
|
83
|
-
.hub-sidebar-button[aria-pressed="true"] { background: var(--dsw-alias-interactive-bg-active); }
|
|
84
|
-
.hub-sidebar-button > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
85
|
-
.hub-sidebar-button.is-rail { flex: none; width: 36px; height: 36px; margin: 8px 0 10px; padding: 0; justify-content: center; border-radius: 50%; }
|
|
86
|
-
|
|
87
81
|
.hub-resource-page .hub-search-input input::placeholder { color: var(--dsw-alias-label-tertiary); opacity: 1; }
|
|
88
82
|
/* Typography matches DSH's 14px controls; secondary metadata uses 12px. */
|
|
89
83
|
.hub-resource-page .hub-market { font: inherit; }
|
|
@@ -119,53 +113,13 @@ var resources_default = `/* Reuse marketplace layout and DSH controls; only adap
|
|
|
119
113
|
.hub-mcp-endpoint code { font: 11px/18px var(--ds-font-family-code); max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
120
114
|
`;
|
|
121
115
|
|
|
122
|
-
// src/client/mcp
|
|
123
|
-
var
|
|
124
|
-
|
|
125
|
-
["transport", "stdio \u4E3A\u672C\u5730\u8FDB\u7A0B\uFF1Bstreamable-http \u4E3A HTTP \u670D\u52A1\u3002", "\u5FC5\u586B"],
|
|
126
|
-
["command", "stdio \u53EF\u6267\u884C\u6587\u4EF6\uFF1BWindows \u53EF\u4F7F\u7528 node.exe \u7684\u5B8C\u6574\u8DEF\u5F84\u3002", "stdio \u5FC5\u586B"],
|
|
127
|
-
["args", "\u4F20\u7ED9\u53EF\u6267\u884C\u6587\u4EF6\u7684\u5B57\u7B26\u4E32\u6570\u7EC4\uFF0C\u4E0D\u7ECF\u8FC7 shell\u3002", "[]"],
|
|
128
|
-
["env", "\u663E\u5F0F\u4F20\u7ED9\u670D\u52A1\u7684\u73AF\u5883\u53D8\u91CF\uFF0C\u53EF\u7528 !!js process.env.NAME \u5F15\u7528\u3002", "{}"],
|
|
129
|
-
["cwd", "stdio \u5DE5\u4F5C\u76EE\u5F55\uFF0C\u7559\u7A7A\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u76EE\u5F55\u3002", '""'],
|
|
130
|
-
["url", "Streamable HTTP \u5730\u5740\u3002", "HTTP \u5FC5\u586B"],
|
|
131
|
-
["headers", "HTTP \u8BF7\u6C42\u5934\uFF0C\u53EF\u5F15\u7528\u73AF\u5883\u53D8\u91CF\u3002", "{}"],
|
|
132
|
-
["toolCallTimeoutMs", "\u5355\u6B21\u5DE5\u5177\u8C03\u7528\u8D85\u65F6\uFF0C\u5355\u4F4D\u6BEB\u79D2\u3002", "60000"],
|
|
133
|
-
["failOnStartupError", "\u521D\u59CB\u5316\u5931\u8D25\u65F6\u662F\u5426\u8BA9\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\u3002", "false"],
|
|
134
|
-
["reconnect.enabled", "\u8FDE\u63A5\u4E22\u5931\u540E\u81EA\u52A8\u91CD\u8FDE\u3002", "true"],
|
|
135
|
-
["reconnect.initialDelayMs", "\u9996\u6B21\u91CD\u8FDE\u7B49\u5F85\u6BEB\u79D2\u6570\uFF0C\u4E4B\u540E\u6307\u6570\u9012\u589E\u3002", "500"],
|
|
136
|
-
["reconnect.maxDelayMs", "\u91CD\u8FDE\u7B49\u5F85\u4E0A\u9650\uFF0C\u5355\u4F4D\u6BEB\u79D2\u3002", "30000"],
|
|
137
|
-
["reconnect.maxAttempts", "\u4E00\u6B21\u8FDE\u63A5\u6545\u969C\u7684\u8FDE\u7EED\u91CD\u8BD5\u6B21\u6570\u4E0A\u9650\u3002", "10"]
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
// src/client/market.css
|
|
141
|
-
var market_default = "/* Shared page layout; interactive controls remain DSH primitives. */\n.hub-market {\n box-sizing: border-box;\n --red: #596579;\n --ink: var(--dsw-alias-label-primary, #25272b);\n --muted: var(--dsw-alias-label-tertiary, #7b7e86);\n --line: var(--dsw-alias-border-l4, #e9e9ed);\n color: var(--ink);\n font: var(--dsw-font-xs-13);\n padding: 5px 3px 20px;\n width: 100%;\n position: relative;\n isolation: isolate;\n container-type: inline-size;\n}\n.hub-topline { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 20px; }\n.hub-brand { display: flex; align-items: center; gap: 8px; font-size: 10px; letter-spacing: 1px; font-weight: 700; color: var(--muted); }\n.hub-brandmark { display: grid; place-items: center; width: 25px; height: 25px; color: white; background: var(--red); border-radius: 50%; letter-spacing: 0; }\n.hub-divider { width: 1px; height: 13px; background: var(--line); margin: 0 3px; }\n.hub-header { display: flex; justify-content: space-between; align-items: center; gap: 18px; margin-bottom: 18px; }\n.hub-eyebrow { font-size: 9px; letter-spacing: 1.9px; color: var(--red); font-weight: 700; }\n.hub-header h1 { font-size: 30px; letter-spacing: -1px; margin: 4px 0 5px; font-weight: 650; line-height: 1.4; }\n.hub-header h1 span { color: var(--red); }\n.hub-header p { margin: 0; color: var(--muted); font-size: 12px; }\n.hub-header-art { position: relative; display: grid; place-items: center; color: var(--red); background: linear-gradient(145deg, color-mix(in srgb, var(--red) 5%, var(--dsw-alias-bg-layer-2)), color-mix(in srgb, var(--red) 14%, var(--dsw-alias-bg-layer-2))); width: 80px; height: 74px; border-radius: 20px; transform: rotate(-8deg); flex-shrink: 0; border: 1px solid color-mix(in srgb, var(--red) 15%, transparent); }\n.hub-art-plus { position: absolute; right: -5px; bottom: 6px; background: var(--dsw-alias-bg-layer-3); box-shadow: 0 3px 9px #47172515; border-radius: 9px; width: 28px; height: 28px; text-align: center; font-size: 21px; line-height: 26px; transform: rotate(8deg); }\n.hub-art-dot { position: absolute; width: 8px; height: 8px; background: color-mix(in srgb, var(--red) 40%, var(--dsw-alias-bg-layer-3)); border-radius: 50%; top: 9px; left: -6px; }\n.hub-tabs { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--line); margin-bottom: 14px; }\n.hub-tab-count { font-size: 10px; color: var(--muted); margin-left: 5px; }\n.hub-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }\n.hub-search-input { flex: 1; min-width: 180px; }\n.hub-results { display: flex; justify-content: space-between; align-items: center; gap: 10px; color: var(--muted); font-size: 11px; margin: 20px 0 12px; }\n.hub-results b { margin-left: 4px; font-weight: 500; }\n.hub-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; }\n.hub-card { border: 1px solid var(--line); border-radius: 11px; padding: 16px; background: var(--dsw-alias-bg-layer-2, #fff); display: flex; flex-direction: column; min-width: 0; }\n.hub-description { font-size: 12px; color: var(--muted); line-height: 1.85; min-height: 44px; margin: 13px 0 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }\n@container (max-width: 470px) { .hub-grid { grid-template-columns: 1fr; } .hub-topline { flex-wrap: wrap; } .hub-header-art { display: none; } }\n";
|
|
116
|
+
// src/client/mcp.jsx
|
|
117
|
+
var import_react2 = __toESM(require("react"), 1);
|
|
118
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
142
119
|
|
|
143
|
-
// src/client/
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const label = row.name.split("/").pop().replace(/^@/, "").slice(0, 1).toLocaleUpperCase();
|
|
147
|
-
return /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-item-icon", "aria-hidden": "true" }, row.icon && !failed ? /* @__PURE__ */ import_react.default.createElement("img", { src: row.icon, alt: "", referrerPolicy: "no-referrer", onError: () => setFailed(true) }) : label);
|
|
148
|
-
}
|
|
149
|
-
function ScopePicker({ data, scope, setScope, t, disabled }) {
|
|
150
|
-
const [open, setOpen] = (0, import_react.useState)(false);
|
|
151
|
-
const items = [{ id: "", label: t("\u5168\u5C40") }, ...(data?.workspaces ?? []).map((row) => ({ id: row.id, label: row.title }))];
|
|
152
|
-
return /* @__PURE__ */ import_react.default.createElement(
|
|
153
|
-
import_dsh_client_ui_primitives.Menu,
|
|
154
|
-
{
|
|
155
|
-
open,
|
|
156
|
-
onClose: () => setOpen(false),
|
|
157
|
-
items,
|
|
158
|
-
selectedId: scope,
|
|
159
|
-
portal: true,
|
|
160
|
-
align: "end",
|
|
161
|
-
onSelect: (id) => {
|
|
162
|
-
setOpen(false);
|
|
163
|
-
setScope(id);
|
|
164
|
-
},
|
|
165
|
-
anchor: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", disabled, "aria-label": t("\u8D44\u6E90\u8303\u56F4"), "aria-expanded": open, "aria-haspopup": "menu", onClick: () => setOpen(!open) }, items.find((row) => row.id === scope)?.label ?? t("\u5168\u5C40"))
|
|
166
|
-
}
|
|
167
|
-
);
|
|
168
|
-
}
|
|
120
|
+
// src/client/resource-modal.jsx
|
|
121
|
+
var import_react = __toESM(require("react"), 1);
|
|
122
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
169
123
|
function ResourceModal(props) {
|
|
170
124
|
(0, import_react.useEffect)(() => {
|
|
171
125
|
if (!props.open) return;
|
|
@@ -180,23 +134,52 @@ function ResourceModal(props) {
|
|
|
180
134
|
}, [props.open, props.onClose]);
|
|
181
135
|
return /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Modal, { ...props });
|
|
182
136
|
}
|
|
137
|
+
|
|
138
|
+
// src/client/mcp-help.js
|
|
139
|
+
var mcpDescriptions = {
|
|
140
|
+
"serverName": "\u5DE5\u5177\u547D\u540D\u7A7A\u95F4\uFF0C1\u201332 \u4F4D\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u6216\u8FDE\u5B57\u7B26\u3002",
|
|
141
|
+
"transport": "stdio \u4E3A\u672C\u5730\u8FDB\u7A0B\uFF1Bstreamable-http \u4E3A HTTP \u670D\u52A1\u3002",
|
|
142
|
+
"command": "stdio \u53EF\u6267\u884C\u6587\u4EF6\uFF1BWindows \u53EF\u4F7F\u7528 node.exe \u7684\u5B8C\u6574\u8DEF\u5F84\u3002",
|
|
143
|
+
"args": "\u4F20\u7ED9\u53EF\u6267\u884C\u6587\u4EF6\u7684\u5B57\u7B26\u4E32\u6570\u7EC4\uFF0C\u4E0D\u7ECF\u8FC7 shell\u3002",
|
|
144
|
+
"env": "\u663E\u5F0F\u4F20\u7ED9\u670D\u52A1\u7684\u73AF\u5883\u53D8\u91CF\uFF0C\u53EF\u7528 !!js process.env.NAME \u5F15\u7528\u3002",
|
|
145
|
+
"cwd": "stdio \u5DE5\u4F5C\u76EE\u5F55\uFF0C\u7559\u7A7A\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u76EE\u5F55\u3002",
|
|
146
|
+
"url": "Streamable HTTP \u5730\u5740\u3002",
|
|
147
|
+
"headers": "HTTP \u8BF7\u6C42\u5934\uFF0C\u53EF\u5F15\u7528\u73AF\u5883\u53D8\u91CF\u3002",
|
|
148
|
+
"toolCallTimeoutMs": "\u5355\u6B21\u5DE5\u5177\u8C03\u7528\u6216\u8D44\u6E90\u8BF7\u6C42\u8D85\u65F6\uFF0C\u5355\u4F4D\u6BEB\u79D2\u3002",
|
|
149
|
+
"maxInstructionBytes": "\u670D\u52A1\u7AEF\u6307\u4EE4\u7684 UTF-8 \u5B57\u8282\u4E0A\u9650\uFF0C\u5305\u542B\u670D\u52A1\u540D\u79F0\u524D\u7F00\uFF1B\u8D85\u51FA\u65F6\u8FDE\u63A5\u5931\u8D25\u3002",
|
|
150
|
+
"failOnStartupError": "\u521D\u59CB\u5316\u5931\u8D25\u65F6\u662F\u5426\u8BA9\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\u3002",
|
|
151
|
+
"reconnect.enabled": "\u8FDE\u63A5\u4E22\u5931\u540E\u81EA\u52A8\u91CD\u8FDE\u3002",
|
|
152
|
+
"reconnect.initialDelayMs": "\u9996\u6B21\u91CD\u8FDE\u7B49\u5F85\u6BEB\u79D2\u6570\uFF0C\u4E4B\u540E\u6307\u6570\u9012\u589E\u3002",
|
|
153
|
+
"reconnect.maxDelayMs": "\u91CD\u8FDE\u7B49\u5F85\u4E0A\u9650\uFF0C\u5355\u4F4D\u6BEB\u79D2\u3002",
|
|
154
|
+
"reconnect.maxAttempts": "\u4E00\u6B21\u8FDE\u63A5\u6545\u969C\u7684\u8FDE\u7EED\u91CD\u8BD5\u6B21\u6570\u4E0A\u9650\u3002"
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/client/mcp.jsx
|
|
183
158
|
function McpEditor({ row, t, busy, save, close, request, profile }) {
|
|
184
|
-
const [format, setFormat] = (0,
|
|
185
|
-
const [help, setHelp] = (0,
|
|
186
|
-
const [
|
|
187
|
-
const [
|
|
188
|
-
const [
|
|
189
|
-
const [
|
|
190
|
-
(0,
|
|
191
|
-
|
|
159
|
+
const [format, setFormat] = (0, import_react2.useState)("json");
|
|
160
|
+
const [help, setHelp] = (0, import_react2.useState)(false);
|
|
161
|
+
const [fields, setFields] = (0, import_react2.useState)([]);
|
|
162
|
+
const [text, setText] = (0, import_react2.useState)(JSON.stringify({ serverName: "my_server", transport: "stdio", command: "npx", args: ["-y", "mcp-server-package"] }, null, 2));
|
|
163
|
+
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
164
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
165
|
+
const [ready, setReady] = (0, import_react2.useState)(false);
|
|
166
|
+
(0, import_react2.useEffect)(() => {
|
|
192
167
|
const controller = new AbortController();
|
|
193
|
-
|
|
168
|
+
setLoading(true);
|
|
169
|
+
Promise.all([
|
|
170
|
+
request("mcp-schema", { profile }, { signal: controller.signal }),
|
|
171
|
+
row ? request("mcp-config", { id: row.id, revision: row.revision, profile, format: "json" }, { signal: controller.signal }) : Promise.resolve(null)
|
|
172
|
+
]).then(([schema, config]) => {
|
|
194
173
|
if (!controller.signal.aborted) {
|
|
195
|
-
|
|
174
|
+
setFields(schema.fields);
|
|
175
|
+
if (config) setText(config.text);
|
|
196
176
|
setReady(true);
|
|
197
177
|
}
|
|
198
178
|
}).catch((error2) => {
|
|
199
|
-
if (!controller.signal.aborted)
|
|
179
|
+
if (!controller.signal.aborted) {
|
|
180
|
+
setReady(false);
|
|
181
|
+
setError(error2.message);
|
|
182
|
+
}
|
|
200
183
|
}).finally(() => {
|
|
201
184
|
if (!controller.signal.aborted) setLoading(false);
|
|
202
185
|
});
|
|
@@ -225,7 +208,7 @@ function McpEditor({ row, t, busy, save, close, request, profile }) {
|
|
|
225
208
|
}
|
|
226
209
|
};
|
|
227
210
|
const blocked = busy || loading;
|
|
228
|
-
return /* @__PURE__ */
|
|
211
|
+
return /* @__PURE__ */ import_react2.default.createElement(
|
|
229
212
|
ResourceModal,
|
|
230
213
|
{
|
|
231
214
|
open: true,
|
|
@@ -235,13 +218,13 @@ function McpEditor({ row, t, busy, save, close, request, profile }) {
|
|
|
235
218
|
closeLabel: t("\u5173\u95ED"),
|
|
236
219
|
onClose: blocked ? () => {
|
|
237
220
|
} : close,
|
|
238
|
-
footer: /* @__PURE__ */
|
|
221
|
+
footer: /* @__PURE__ */ import_react2.default.createElement("div", { className: "hub-resource-actions" }, /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Button, { size: "md", disabled: blocked, onClick: close }, t("\u53D6\u6D88")), /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Button, { size: "md", variant: "primary", type: "submit", form: "hub-mcp-editor", disabled: blocked || !ready }, t("\u4FDD\u5B58")))
|
|
239
222
|
},
|
|
240
|
-
/* @__PURE__ */
|
|
223
|
+
/* @__PURE__ */ import_react2.default.createElement("div", { className: "hub-editor-columns" }, /* @__PURE__ */ import_react2.default.createElement("form", { id: "hub-mcp-editor", className: "hub-resource-form", onSubmit: submit }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "hub-resource-actions", role: "group", "aria-label": t("\u914D\u7F6E\u683C\u5F0F") }, ["json", "yaml"].map((value) => /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Button, { key: value, size: "md", className: "hub-resource-tab", "aria-pressed": format === value, disabled: blocked || !ready, onClick: () => {
|
|
241
224
|
void convert(value);
|
|
242
|
-
} }, value.toUpperCase())), /* @__PURE__ */
|
|
225
|
+
} }, value.toUpperCase())), /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Button, { size: "md", disabled: blocked || !ready, onClick: () => {
|
|
243
226
|
void convert(format);
|
|
244
|
-
} }, t("\u683C\u5F0F\u5316"))), /* @__PURE__ */
|
|
227
|
+
} }, t("\u683C\u5F0F\u5316"))), /* @__PURE__ */ import_react2.default.createElement("div", { className: "hub-resource-editor-note" }, t("\u63A8\u8350\u5728\u542F\u52A8 DSH \u7684\u73AF\u5883\u4E2D\u8BBE\u7F6E\u51ED\u636E\u3002YAML \u4F7F\u7528"), " ", /* @__PURE__ */ import_react2.default.createElement("code", null, "!!js process.env.MCP_TOKEN"), t("\uFF1BJSON \u4F7F\u7528"), " ", /* @__PURE__ */ import_react2.default.createElement("code", null, '{"__jsExpr":"process.env.MCP_TOKEN"}'), /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Button, { size: "md", "aria-expanded": help, onClick: () => setHelp(!help) }, t("\u5E2E\u52A9"))), loading && /* @__PURE__ */ import_react2.default.createElement("p", { role: "status" }, t("\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E")), /* @__PURE__ */ import_react2.default.createElement("label", { className: "hub-resource-field" }, t("MCP \u914D\u7F6E"), /* @__PURE__ */ import_react2.default.createElement("textarea", { className: "hub-resource-code", value: ready ? text : "", onChange: (event) => setText(event.target.value), disabled: blocked || !ready, spellCheck: false, autoCapitalize: "off", autoComplete: "off", maxLength: 24e3, required: true })), error && /* @__PURE__ */ import_react2.default.createElement("p", { role: "alert" }, t(error))), help && /* @__PURE__ */ import_react2.default.createElement("aside", { className: "hub-editor-help" }, /* @__PURE__ */ import_react2.default.createElement("h3", null, t("MCP \u914D\u7F6E\u5E2E\u52A9")), /* @__PURE__ */ import_react2.default.createElement("p", null, "macOS / Linux: ", /* @__PURE__ */ import_react2.default.createElement("code", null, "export MCP_TOKEN=\u2026"), /* @__PURE__ */ import_react2.default.createElement("br", null), "Windows PowerShell: ", /* @__PURE__ */ import_react2.default.createElement("code", null, "$env:MCP_TOKEN=\u2026")), /* @__PURE__ */ import_react2.default.createElement("p", null, t("\u4E0B\u5217\u9ED8\u8BA4\u503C\u7531 DSH \u63D0\u4F9B\uFF1B\u5207\u6362\u683C\u5F0F\u4E0D\u4F1A\u8865\u5165\u8FD9\u4E9B\u5B57\u6BB5\u3002")), /* @__PURE__ */ import_react2.default.createElement("dl", null, fields.map((field) => /* @__PURE__ */ import_react2.default.createElement("div", { key: field.key + field.transports.join() }, /* @__PURE__ */ import_react2.default.createElement("dt", null, /* @__PURE__ */ import_react2.default.createElement("code", null, field.key), field.transports.length === 1 && /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tag, { tone: "neutral" }, field.transports[0])), /* @__PURE__ */ import_react2.default.createElement("dd", null, t(mcpDescriptions[field.key] ?? "\u7531\u5F53\u524D DSH \u63D0\u4F9B\u7684\u914D\u7F6E\u9879\u3002"), /* @__PURE__ */ import_react2.default.createElement("span", null, t(field.required ? "\u8981\u6C42" : "\u9ED8\u8BA4\u503C"), " \xB7 ", field.required ? t("\u5FC5\u586B") : Object.hasOwn(field, "default") ? JSON.stringify(field.default) : t("\u672A\u63D0\u4F9B\u9ED8\u8BA4\u503C"))))))))
|
|
245
228
|
);
|
|
246
229
|
}
|
|
247
230
|
function McpSummary({ row, t, activation }) {
|
|
@@ -255,33 +238,82 @@ function McpSummary({ row, t, activation }) {
|
|
|
255
238
|
}
|
|
256
239
|
}
|
|
257
240
|
const loaded = activation(row.activation);
|
|
258
|
-
return /* @__PURE__ */
|
|
241
|
+
return /* @__PURE__ */ import_react2.default.createElement("div", { className: "hub-mcp-summary" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "hub-mcp-statuses" }, /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tooltip, { label: `${t("\u52A0\u8F7D\u72B6\u6001")} \xB7 ${loaded}` }, /* @__PURE__ */ import_react2.default.createElement("span", { tabIndex: 0 }, /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tag, { tone: { active: "success", loading: "info", unloading: "info", failed: "danger" }[row.activation] ?? "neutral" }, loaded === t("\u672A\u77E5") ? t("\u52A0\u8F7D\u672A\u77E5") : loaded))), /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tooltip, { label: t("\u5BBF\u4E3B\u672A\u516C\u5F00\u6301\u7EED\u8FDE\u63A5\u72B6\u6001\uFF1B\u5DF2\u52A0\u8F7D\u4E0D\u4EE3\u8868\u8FDE\u63A5\u6210\u529F\u3002") }, /* @__PURE__ */ import_react2.default.createElement("span", { tabIndex: 0 }, /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tag, { tone: "neutral" }, t("\u8FDE\u63A5\u672A\u77E5"))))), /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tooltip, { label: `${row.transport}${endpoint ? " \xB7 " + endpoint : ""}` }, /* @__PURE__ */ import_react2.default.createElement("span", { className: "hub-mcp-endpoint", tabIndex: 0 }, /* @__PURE__ */ import_react2.default.createElement(import_dsh_client_ui_primitives2.Tag, { tone: "neutral" }, row.transport === "streamable-http" ? "HTTP" : row.transport === "unknown" ? t("\u672A\u77E5") : row.transport), endpointLabel && /* @__PURE__ */ import_react2.default.createElement("code", null, endpointLabel))));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/client/entry.jsx
|
|
245
|
+
var import_react3 = __toESM(require("react"), 1);
|
|
246
|
+
var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
247
|
+
var css = `
|
|
248
|
+
.hub-entry-button { width: 32px; height: 32px; padding: 0; flex: none; }
|
|
249
|
+
.hub-entry-fallback { position: absolute; top: 16px; right: 24px; }
|
|
250
|
+
body:has(.hub-header-entry) .hub-entry-fallback { display: none; }
|
|
251
|
+
`;
|
|
252
|
+
function HubIconButton({ label, selected = false, onClick }) {
|
|
253
|
+
return /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement("style", null, css), /* @__PURE__ */ import_react3.default.createElement(import_dsh_client_ui_primitives3.Tooltip, { label, side: "bottom" }, /* @__PURE__ */ import_react3.default.createElement(import_dsh_client_ui_primitives3.Button, { className: "hub-entry-button", "aria-label": label, "aria-pressed": selected, icon: /* @__PURE__ */ import_react3.default.createElement(import_dsh_client_ui_primitives3.IconCordisPluginOutline14, { size: 18 }), onClick })));
|
|
254
|
+
}
|
|
255
|
+
function HubHeaderEntry({ ctx, panelId, brand }) {
|
|
256
|
+
return /* @__PURE__ */ import_react3.default.createElement("span", { className: "hub-header-entry" }, /* @__PURE__ */ import_react3.default.createElement(HubIconButton, { label: brand.navTitle, onClick: () => ctx.layout.selectPanel(panelId) }));
|
|
257
|
+
}
|
|
258
|
+
function HubEmptyEntry({ usePanelInfo, ctx, panelId, brand }) {
|
|
259
|
+
const conversation = usePanelInfo((info) => info.activePanelId === null);
|
|
260
|
+
return conversation ? /* @__PURE__ */ import_react3.default.createElement("div", { className: "hub-entry-fallback" }, /* @__PURE__ */ import_react3.default.createElement(HubIconButton, { label: brand.navTitle, onClick: () => ctx.layout.selectPanel(panelId) })) : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/client/market.css
|
|
264
|
+
var market_default = "/* Shared page layout; interactive controls remain DSH primitives. */\n.hub-market {\n box-sizing: border-box;\n --red: #596579;\n --ink: var(--dsw-alias-label-primary, #25272b);\n --muted: var(--dsw-alias-label-tertiary, #7b7e86);\n --line: var(--dsw-alias-border-l4, #e9e9ed);\n color: var(--ink);\n font: var(--dsw-font-xs-13);\n padding: 5px 3px 20px;\n width: 100%;\n position: relative;\n isolation: isolate;\n container-type: inline-size;\n}\n.hub-topline { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 20px; }\n.hub-brand { display: flex; align-items: center; gap: 8px; font-size: 10px; letter-spacing: 1px; font-weight: 700; color: var(--muted); }\n.hub-brandmark { display: grid; place-items: center; width: 25px; height: 25px; color: white; background: var(--red); border-radius: 50%; letter-spacing: 0; }\n.hub-divider { width: 1px; height: 13px; background: var(--line); margin: 0 3px; }\n.hub-header { display: flex; justify-content: space-between; align-items: center; gap: 18px; margin-bottom: 18px; }\n.hub-eyebrow { font-size: 9px; letter-spacing: 1.9px; color: var(--red); font-weight: 700; }\n.hub-header h1 { font-size: 30px; letter-spacing: -1px; margin: 4px 0 5px; font-weight: 650; line-height: 1.4; }\n.hub-header h1 span { color: var(--red); }\n.hub-header p { margin: 0; color: var(--muted); font-size: 12px; }\n.hub-header-art { position: relative; display: grid; place-items: center; color: var(--red); background: linear-gradient(145deg, color-mix(in srgb, var(--red) 5%, var(--dsw-alias-bg-layer-2)), color-mix(in srgb, var(--red) 14%, var(--dsw-alias-bg-layer-2))); width: 80px; height: 74px; border-radius: 20px; transform: rotate(-8deg); flex-shrink: 0; border: 1px solid color-mix(in srgb, var(--red) 15%, transparent); }\n.hub-art-plus { position: absolute; right: -5px; bottom: 6px; background: var(--dsw-alias-bg-layer-3); box-shadow: 0 3px 9px #47172515; border-radius: 9px; width: 28px; height: 28px; text-align: center; font-size: 21px; line-height: 26px; transform: rotate(8deg); }\n.hub-art-dot { position: absolute; width: 8px; height: 8px; background: color-mix(in srgb, var(--red) 40%, var(--dsw-alias-bg-layer-3)); border-radius: 50%; top: 9px; left: -6px; }\n.hub-tabs { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--line); margin-bottom: 14px; }\n.hub-tab-count { font-size: 10px; color: var(--muted); margin-left: 5px; }\n.hub-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }\n.hub-search-input { flex: 1; min-width: 180px; }\n.hub-results { display: flex; justify-content: space-between; align-items: center; gap: 10px; color: var(--muted); font-size: 11px; margin: 20px 0 12px; }\n.hub-results b { margin-left: 4px; font-weight: 500; }\n.hub-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; }\n.hub-card { border: 1px solid var(--line); border-radius: 11px; padding: 16px; background: var(--dsw-alias-bg-layer-2, #fff); display: flex; flex-direction: column; min-width: 0; }\n.hub-description { font-size: 12px; color: var(--muted); line-height: 1.85; min-height: 44px; margin: 13px 0 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }\n@container (max-width: 470px) { .hub-grid { grid-template-columns: 1fr; } .hub-topline { flex-wrap: wrap; } .hub-header-art { display: none; } }\n";
|
|
265
|
+
|
|
266
|
+
// src/client/resources.jsx
|
|
267
|
+
function ItemIcon({ row }) {
|
|
268
|
+
const [failed, setFailed] = (0, import_react4.useState)(false);
|
|
269
|
+
const label = row.name.split("/").pop().replace(/^@/, "").slice(0, 1).toLocaleUpperCase();
|
|
270
|
+
return /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-item-icon", "aria-hidden": "true" }, row.icon && !failed ? /* @__PURE__ */ import_react4.default.createElement("img", { src: row.icon, alt: "", referrerPolicy: "no-referrer", onError: () => setFailed(true) }) : label);
|
|
271
|
+
}
|
|
272
|
+
function ScopePicker({ data, scope, setScope, t, disabled }) {
|
|
273
|
+
const [open, setOpen] = (0, import_react4.useState)(false);
|
|
274
|
+
const items = [{ id: "", label: t("\u5168\u5C40") }, ...(data?.workspaces ?? []).map((row) => ({ id: row.id, label: row.title }))];
|
|
275
|
+
return /* @__PURE__ */ import_react4.default.createElement(
|
|
276
|
+
import_dsh_client_ui_primitives4.Menu,
|
|
277
|
+
{
|
|
278
|
+
open,
|
|
279
|
+
onClose: () => setOpen(false),
|
|
280
|
+
items,
|
|
281
|
+
selectedId: scope,
|
|
282
|
+
portal: true,
|
|
283
|
+
align: "end",
|
|
284
|
+
onSelect: (id) => {
|
|
285
|
+
setOpen(false);
|
|
286
|
+
setScope(id);
|
|
287
|
+
},
|
|
288
|
+
anchor: /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "md", disabled, "aria-label": t("\u8D44\u6E90\u8303\u56F4"), "aria-expanded": open, "aria-haspopup": "menu", onClick: () => setOpen(!open) }, items.find((row) => row.id === scope)?.label ?? t("\u5168\u5C40"))
|
|
289
|
+
}
|
|
290
|
+
);
|
|
259
291
|
}
|
|
260
292
|
function SkillPreview({ file, t, close }) {
|
|
261
|
-
const view = (0,
|
|
293
|
+
const view = (0, import_react4.useRef)(null);
|
|
262
294
|
const frontmatter = file.text.match(/^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)(?:\r?\n|$)/);
|
|
263
295
|
const title = file.path.split(/[\\/]/).slice(-2, -1).join("") + "/skill.md";
|
|
264
296
|
const body = frontmatter ? file.text.slice(frontmatter[0].length) : file.text;
|
|
265
|
-
(0,
|
|
297
|
+
(0, import_react4.useEffect)(() => {
|
|
266
298
|
if (window.matchMedia("(max-width: 850px)").matches) view.current?.scrollIntoView({ block: "start" });
|
|
267
299
|
}, [file]);
|
|
268
|
-
return /* @__PURE__ */
|
|
300
|
+
return /* @__PURE__ */ import_react4.default.createElement("aside", { className: "hub-skill-view", ref: view, "aria-label": title }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-card-heading" }, /* @__PURE__ */ import_react4.default.createElement("h2", { title }, title), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", onClick: close, icon: /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconCloseOutline16, null) }, t("\u5173\u95ED"))), /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-skill-markdown" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.MarkdownText, { text: body, labels: { code: { copyLabel: t("\u590D\u5236"), copiedLabel: t("\u5DF2\u590D\u5236") }, footnotes: t("\u811A\u6CE8") } })));
|
|
269
301
|
}
|
|
270
|
-
function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
271
|
-
(0,
|
|
272
|
-
const [tab, setTab] = (0,
|
|
273
|
-
const [scope, setScope] = (0,
|
|
274
|
-
const [file, setFile] = (0,
|
|
275
|
-
const [desktopHelp, setDesktopHelp] = (0,
|
|
276
|
-
const [data, setData] = (0,
|
|
277
|
-
const [query, setQuery] = (0,
|
|
278
|
-
const [error, setError] = (0,
|
|
279
|
-
const [busy, setBusy] = (0,
|
|
280
|
-
const [editor, setEditor] = (0,
|
|
281
|
-
const [deleting, setDeleting] = (0,
|
|
282
|
-
const generation = (0,
|
|
283
|
-
const mounted = (0,
|
|
284
|
-
(0,
|
|
302
|
+
function ResourceHub({ ctx, request, experiments, t, brand, panelId }) {
|
|
303
|
+
(0, import_react4.useSyncExternalStore)((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot().active);
|
|
304
|
+
const [tab, setTab] = (0, import_react4.useState)("skills");
|
|
305
|
+
const [scope, setScope] = (0, import_react4.useState)("");
|
|
306
|
+
const [file, setFile] = (0, import_react4.useState)(null);
|
|
307
|
+
const [desktopHelp, setDesktopHelp] = (0, import_react4.useState)(false);
|
|
308
|
+
const [data, setData] = (0, import_react4.useState)(null);
|
|
309
|
+
const [query, setQuery] = (0, import_react4.useState)("");
|
|
310
|
+
const [error, setError] = (0, import_react4.useState)("");
|
|
311
|
+
const [busy, setBusy] = (0, import_react4.useState)(false);
|
|
312
|
+
const [editor, setEditor] = (0, import_react4.useState)(null);
|
|
313
|
+
const [deleting, setDeleting] = (0, import_react4.useState)(null);
|
|
314
|
+
const generation = (0, import_react4.useRef)(0);
|
|
315
|
+
const mounted = (0, import_react4.useRef)(true);
|
|
316
|
+
(0, import_react4.useEffect)(() => {
|
|
285
317
|
mounted.current = true;
|
|
286
318
|
return () => {
|
|
287
319
|
mounted.current = false;
|
|
@@ -291,9 +323,14 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
|
291
323
|
const refresh = async (signal) => {
|
|
292
324
|
const token = ++generation.current;
|
|
293
325
|
try {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
326
|
+
const result = await request("resources", { workspaceId: scope }, { signal });
|
|
327
|
+
try {
|
|
328
|
+
result.experiments = await experiments.list(result.profile, { signal });
|
|
329
|
+
} catch (error2) {
|
|
330
|
+
result.experiments = [];
|
|
331
|
+
result.experimentsError = error2.message;
|
|
332
|
+
}
|
|
333
|
+
if (mounted.current && token === generation.current && !signal?.aborted) {
|
|
297
334
|
setData(result);
|
|
298
335
|
setError("");
|
|
299
336
|
}
|
|
@@ -301,7 +338,7 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
|
301
338
|
if (!signal?.aborted && mounted.current && token === generation.current) setError(error2.message);
|
|
302
339
|
}
|
|
303
340
|
};
|
|
304
|
-
(0,
|
|
341
|
+
(0, import_react4.useEffect)(() => {
|
|
305
342
|
const controller = new AbortController();
|
|
306
343
|
setData(null);
|
|
307
344
|
void refresh(controller.signal);
|
|
@@ -314,7 +351,7 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
|
314
351
|
const result = await request(route, { ...body, workspaceId: scope, profile: data.profile.directory });
|
|
315
352
|
if (mounted.current && result.skills) {
|
|
316
353
|
generation.current++;
|
|
317
|
-
setData((previous) => ({ ...result, experiments: previous.experiments }));
|
|
354
|
+
setData((previous) => ({ ...result, experiments: previous.experiments, experimentsError: previous.experimentsError }));
|
|
318
355
|
}
|
|
319
356
|
return result;
|
|
320
357
|
} catch (error2) {
|
|
@@ -324,6 +361,30 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
|
324
361
|
if (mounted.current) setBusy(false);
|
|
325
362
|
}
|
|
326
363
|
};
|
|
364
|
+
const changeExperiment = async (row, enabled, operation = "toggle") => {
|
|
365
|
+
setBusy(true);
|
|
366
|
+
setError("");
|
|
367
|
+
generation.current++;
|
|
368
|
+
let failure;
|
|
369
|
+
try {
|
|
370
|
+
await experiments.change(data.profile, row, enabled, operation);
|
|
371
|
+
} catch (error2) {
|
|
372
|
+
failure = error2.message;
|
|
373
|
+
} finally {
|
|
374
|
+
if (mounted.current) {
|
|
375
|
+
try {
|
|
376
|
+
const latest = await experiments.list(data.profile);
|
|
377
|
+
if (mounted.current) setData((previous) => ({ ...previous, experiments: latest, experimentsError: void 0 }));
|
|
378
|
+
} catch (error2) {
|
|
379
|
+
failure ??= error2.message;
|
|
380
|
+
}
|
|
381
|
+
if (mounted.current) {
|
|
382
|
+
if (failure) setError(failure);
|
|
383
|
+
setBusy(false);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
327
388
|
const run = (body, route) => {
|
|
328
389
|
void action(body, route).catch(() => {
|
|
329
390
|
});
|
|
@@ -335,7 +396,7 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
|
335
396
|
setQuery("");
|
|
336
397
|
};
|
|
337
398
|
const activation = (state) => t({ active: "\u5DF2\u52A0\u8F7D", loading: "\u52A0\u8F7D\u4E2D", pending: "\u672A\u52A0\u8F7D", disabled: "\u5DF2\u7981\u7528", failed: "\u52A0\u8F7D\u5931\u8D25", disposed: "\u672A\u52A0\u8F7D", unloading: "\u52A0\u8F7D\u4E2D" }[state] ?? "\u672A\u77E5");
|
|
338
|
-
return /* @__PURE__ */
|
|
399
|
+
return /* @__PURE__ */ import_react4.default.createElement("section", { className: `hub-resource-page ${file ? "with-file" : ""}`, "aria-label": brand.title }, /* @__PURE__ */ import_react4.default.createElement("style", null, market_default, resources_default), /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-market hub-resource-content", style: { "--red": brand.primaryColor } }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-topline" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-brand" }, /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-brandmark" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconCordisPluginOutline14, { size: 17 })), brand.title, /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-divider" }), brand.subTitle), /* @__PURE__ */ import_react4.default.createElement(HubIconButton, { label: t("\u8FD4\u56DE\u4F1A\u8BDD"), selected: true, onClick: () => ctx.layout.selectPanel(null) })), /* @__PURE__ */ import_react4.default.createElement("header", { className: "hub-header" }, /* @__PURE__ */ import_react4.default.createElement("div", null, /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-eyebrow" }, "SKILLS \xB7 MCP \xB7 PLUGINS"), /* @__PURE__ */ import_react4.default.createElement("h1", null, t("\u8D44\u6E90\u7BA1\u7406"), /* @__PURE__ */ import_react4.default.createElement("span", null, ".")), /* @__PURE__ */ import_react4.default.createElement("p", null, t("\u5728\u5F53\u524D\u5B9E\u4F8B\u4E2D\u7BA1\u7406 Skill\u3001MCP \u548C\u63D2\u4EF6\u3002"))), /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-header-art", "aria-hidden": "true" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconCordisPluginOutline14, { size: 38 }), /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-art-plus" }, "+"), /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-art-dot" }))), /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-profile" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Pill, null, t("\u5F53\u524D profile"), " \xB7 ", data?.profile.name ?? "\u2026"), /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-resource-path", title: data?.profile.directory }, data?.profile.directory ?? "\u2026")), /* @__PURE__ */ import_react4.default.createElement("div", { role: "tablist", className: "hub-tabs", "aria-label": brand.title, onKeyDown: (event) => {
|
|
339
400
|
const index = tabs.findIndex((row) => row.id === tab);
|
|
340
401
|
const next = event.key === "ArrowRight" ? (index + 1) % tabs.length : event.key === "ArrowLeft" ? (index + tabs.length - 1) % tabs.length : event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : -1;
|
|
341
402
|
if (next !== -1) {
|
|
@@ -343,37 +404,47 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
|
|
|
343
404
|
switchTab(tabs[next].id);
|
|
344
405
|
event.currentTarget.querySelectorAll('[role="tab"]')[next].focus();
|
|
345
406
|
}
|
|
346
|
-
} }, tabs.map((row) => /* @__PURE__ */
|
|
407
|
+
} }, tabs.map((row) => /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { key: row.id, role: "tab", id: `${panelId}-${row.id}`, "aria-controls": `${panelId}-content`, "aria-selected": tab === row.id, tabIndex: tab === row.id ? 0 : -1, size: "md", className: "hub-resource-tab", onClick: () => switchTab(row.id) }, row.label, /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-tab-count" }, data?.[row.id]?.length ?? "\u2014")))), /* @__PURE__ */ import_react4.default.createElement("div", { role: "tabpanel", id: `${panelId}-content`, "aria-labelledby": `${panelId}-${tab}` }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-toolbar" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Input, { className: "hub-search-input", icon: /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconSearchOutline16, null), "aria-label": t("\u641C\u7D22\u540D\u79F0\u3001\u63CF\u8FF0\u6216\u6765\u6E90"), placeholder: t("\u641C\u7D22\u540D\u79F0\u3001\u63CF\u8FF0\u6216\u6765\u6E90"), value: query, onChange: (event) => setQuery(event.target.value) }), tab === "skills" && /* @__PURE__ */ import_react4.default.createElement(ScopePicker, { data, scope, setScope, t, disabled: busy }), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "md", disabled: busy, onClick: () => {
|
|
347
408
|
setBusy(true);
|
|
348
409
|
void refresh().finally(() => {
|
|
349
410
|
if (mounted.current) setBusy(false);
|
|
350
411
|
});
|
|
351
|
-
}, icon: /* @__PURE__ */
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
void request("experiment-mutate", { id: row.id, action: "install", profile: data.profile.directory }).then(() => refresh()).catch((error2) => setError(error2.message)).finally(() => setBusy(false));
|
|
357
|
-
} }, t("\u5B89\u88C5 Agent Teams")), data.profile.installation === "desktop" && /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", onClick: () => setDesktopHelp(true) }, t("\u684C\u9762\u63D2\u4EF6\u7BA1\u7406"))), tab !== "experiments" && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-card-footer" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-actions" }, tab === "plugins" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: busy, icon: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconFolderOpenOutline16, null), onClick: () => run({ id: row.name, kind: "plugin" }, "open-directory") }, t("\u6253\u5F00\u76EE\u5F55"))), tab === "skills" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: !row.directory || busy, onClick: () => {
|
|
412
|
+
}, icon: /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconRefreshOutline16, null) }, t("\u5237\u65B0")), tab === "plugins" && data?.profile.installation === "desktop" && /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { onClick: () => setDesktopHelp(true) }, t("\u684C\u9762\u63D2\u4EF6\u7BA1\u7406")), tab === "mcps" && /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "md", variant: "primary", disabled: !data || busy, onClick: () => setEditor({}) }, t("\u6DFB\u52A0 MCP"))), /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-results" }, /* @__PURE__ */ import_react4.default.createElement("span", null, t("\u5F53\u524D\u5217\u8868"), " ", /* @__PURE__ */ import_react4.default.createElement("b", null, rows.length)), /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-resource-scope-note" }, t(tab === "skills" ? "\u5F00\u5173\u53EA\u5F71\u54CD\u5F53\u524D profile\uFF1BSkill \u6E90\u6587\u4EF6\u4FDD\u6301\u539F\u6837\u3002" : tab === "mcps" ? "MCP \u4E3A\u5F53\u524D profile \u7684\u5168\u5C40\u670D\u52A1\uFF1B\u5DE5\u4F5C\u533A\u8303\u56F4\u53EA\u7B5B\u9009 Skill\u3002" : tab === "plugins" ? "\u4EC5\u5C55\u793A\u5DF2\u5B89\u88C5\u7684\u975E\u5B98\u65B9\u63D2\u4EF6" : "\u5B9E\u9A8C\u6027\u529F\u80FD"))), error && /* @__PURE__ */ import_react4.default.createElement("p", { role: "alert" }, t(error)), tab === "experiments" && data?.experimentsError && /* @__PURE__ */ import_react4.default.createElement("p", { role: "alert" }, t(data.experimentsError)), !data && !error && /* @__PURE__ */ import_react4.default.createElement("p", { role: "status" }, t("\u6B63\u5728\u8BFB\u53D6\u8D44\u6E90")), data && tab === "skills" && !data.skillsComplete && /* @__PURE__ */ import_react4.default.createElement("p", { role: "status" }, t("\u8D44\u6E90\u5C1A\u672A\u5B8C\u6574\u52A0\u8F7D\uFF0C\u8BF7\u5237\u65B0\u91CD\u8BD5")), /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-grid hub-resource-grid" }, rows.map((row) => /* @__PURE__ */ import_react4.default.createElement("article", { key: row.id ?? row.name, className: "hub-card hub-resource-card" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-card-heading" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-identity" }, /* @__PURE__ */ import_react4.default.createElement(ItemIcon, { row }), /* @__PURE__ */ import_react4.default.createElement("h2", { className: "hub-resource-card-name", title: row.name }, row.name, (tab === "mcps" || tab === "plugins") && /* @__PURE__ */ import_react4.default.createElement("span", { className: "hub-resource-version" }, "@", row.version || t("\u672A\u77E5")))), tab !== "plugins" && tab !== "experiments" ? /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Switch, { checked: Boolean(row.enabled), label: `${t(row.enabled ? "\u7981\u7528" : "\u542F\u7528")} ${row.name}`, disabled: busy || tab === "skills" && (!row.canToggle || !row.enabled && !row.managed), title: tab === "skills" && !row.canToggle ? t("\u6B64\u5F00\u5173\u7EE7\u627F\u81EA\u5176\u5B83\u8303\u56F4\uFF0C\u8BF7\u5207\u6362\u8303\u56F4\u4FEE\u6539") : void 0, onChange: (enabled) => run({ action: tab === "skills" ? "skill-toggle" : "mcp-toggle", id: row.id, revision: row.revision, enabled }) }) : tab === "experiments" ? /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Switch, { checked: Boolean(row.enabled), label: `${t(row.enabled ? "\u7981\u7528" : "\u542F\u7528")} ${row.name}`, disabled: busy || row.busy || !row.canToggle, onChange: (enabled) => {
|
|
413
|
+
void changeExperiment(row, enabled);
|
|
414
|
+
} }) : null), tab === "mcps" && /* @__PURE__ */ import_react4.default.createElement(McpSummary, { row, t, activation }), /* @__PURE__ */ import_react4.default.createElement("p", { className: "hub-description", title: tab === "experiments" ? t(row.description) : row.description }, (tab === "experiments" ? t(row.description) : row.description) || t(tab === "mcps" ? row.probe?.status === "success" ? "\u670D\u52A1\u672A\u63D0\u4F9B\u63CF\u8FF0\u3002" : "\u670D\u52A1\u5C1A\u672A\u63D0\u4F9B\u63CF\u8FF0\uFF0C\u53EF\u6D4B\u8BD5\u8FDE\u63A5\u8BFB\u53D6\u3002" : "\u6682\u65E0\u63CF\u8FF0")), tab === "mcps" && /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, row.probe && /* @__PURE__ */ import_react4.default.createElement("p", { className: "hub-resource-meta" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Pill, null, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.StateDot, { state: row.probe.status === "success" ? "done" : "error" }), " ", t(row.probe.status === "success" ? "\u6D4B\u8BD5\u6210\u529F" : "\u6D4B\u8BD5\u5931\u8D25"), " \xB7 ", new Date(row.probe.at).toLocaleTimeString(), row.probe.tools !== void 0 && ` \xB7 ${t("\u5DE5\u5177\u6570")} ${row.probe.tools}`))), tab === "experiments" && /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-actions" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Pill, null, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.StateDot, { state: row.pendingRestart ? "warning" : row.activeEnabled ? "done" : "idle" }), t(row.busy || busy ? "\u5B9E\u9A8C\u6027\u529F\u80FD\u6B63\u5728\u66F4\u65B0" : row.pendingRestart ? "\u91CD\u542F DSH \u540E\u751F\u6548" : row.activeEnabled === null ? "\u8FD0\u884C\u72B6\u6001\u672A\u77E5" : row.activeEnabled ? "\u5DF2\u542F\u7528" : "\u5DF2\u7981\u7528")), row.pendingRestart && /* @__PURE__ */ import_react4.default.createElement("span", null, t(row.enabled ? "\u914D\u7F6E\u5DF2\u542F\u7528" : "\u914D\u7F6E\u5DF2\u7981\u7528")), !row.supported && /* @__PURE__ */ import_react4.default.createElement("span", null, t("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7BA1\u7406\u6B64\u5B9E\u9A8C\u6027\u529F\u80FD")), !row.installed && row.canInstall && /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: busy || row.busy, onClick: () => {
|
|
415
|
+
void changeExperiment(row, row.enabled, "install");
|
|
416
|
+
} }, t("\u5B89\u88C5 Agent Teams"))), tab !== "experiments" && /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-card-footer" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-actions" }, tab === "plugins" && /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: busy, icon: /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconFolderOpenOutline16, null), onClick: () => run({ id: row.name, kind: "plugin" }, "open-directory") }, t("\u6253\u5F00\u76EE\u5F55"))), tab === "skills" && /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: !row.directory || busy, onClick: () => {
|
|
358
417
|
setBusy(true);
|
|
359
418
|
void action({ id: row.id }, "skill-file").then(setFile).catch(() => {
|
|
360
419
|
});
|
|
361
|
-
} }, t("\u67E5\u770B SKILL.md")), /* @__PURE__ */
|
|
420
|
+
} }, t("\u67E5\u770B SKILL.md")), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: !row.directory || busy, onClick: () => run({ id: row.id }, "open-directory"), icon: /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.IconFolderOpenOutline16, null) }, t("\u6253\u5F00\u76EE\u5F55"))), tab === "mcps" && /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Tooltip, { label: t("\u72EC\u7ACB\u6D4B\u8BD5\u4F1A\u4E34\u65F6\u8FDE\u63A5\u670D\u52A1\u5E76\u8BFB\u53D6\u5DE5\u5177\u5217\u8868\uFF0C\u4E0D\u6267\u884C\u5DE5\u5177\u3002") }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: busy || !row.enabled, onClick: () => run({ id: row.id, revision: row.revision }, "mcp-test") }, t("\u6D4B\u8BD5\u8FDE\u63A5"))), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: busy || !row.enabled, onClick: () => run({ action: "mcp-reconnect", id: row.id, revision: row.revision }) }, t("\u91CD\u65B0\u8FDE\u63A5")), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: busy, onClick: () => setEditor(row) }, t("\u7F16\u8F91")), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Tooltip, { label: row.canDelete ? t("\u5220\u9664") : t("\u6765\u81EA\u5171\u4EAB\u914D\u7F6E\u6216\u5BBF\u4E3B\u5305\uFF0C\u8BF7\u5728\u6765\u6E90\u5904\u5220\u9664") }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", disabled: busy || !row.canDelete, onClick: () => setDeleting(row) }, t("\u5220\u9664"))))), /* @__PURE__ */ import_react4.default.createElement("p", { className: "hub-resource-path hub-resource-source", title: row.directory ?? row.source }, t("\u6765\u6E90"), " \xB7 ", row.directory ?? row.source))))), data && !rows.length && /* @__PURE__ */ import_react4.default.createElement("p", { className: "hub-resource-empty" }, t("\u672A\u627E\u5230\u5339\u914D\u8D44\u6E90")))), file && /* @__PURE__ */ import_react4.default.createElement(SkillPreview, { key: file.path, file, t, close: () => setFile(null) }), /* @__PURE__ */ import_react4.default.createElement(ResourceModal, { className: "hub-resource-dialog", open: desktopHelp, title: t("\u684C\u9762\u63D2\u4EF6\u7BA1\u7406"), closeLabel: t("\u5173\u95ED"), onClose: () => setDesktopHelp(false) }, /* @__PURE__ */ import_react4.default.createElement("p", null, t("\u5F53\u524D DSH \u4E3B\u7A97\u53E3\u672A\u63D0\u4F9B\u6253\u5F00\u7BA1\u7406\u7A97\u53E3\u7684\u63A5\u53E3\u3002\u8BF7\u4F7F\u7528\u7CFB\u7EDF\u83DC\u5355\u7684 Desktop Plugins\uFF0C\u6216\u6309"), " ", /* @__PURE__ */ import_react4.default.createElement("kbd", null, data?.profile.platform === "darwin" ? "\u2318 ," : "Ctrl+,"))), editor && /* @__PURE__ */ import_react4.default.createElement(McpEditor, { request, profile: data.profile.directory, row: editor.id ? editor : null, t, busy, close: () => setEditor(null), save: async (body) => {
|
|
362
421
|
await action(body);
|
|
363
422
|
setEditor(null);
|
|
364
|
-
} }), /* @__PURE__ */
|
|
423
|
+
} }), /* @__PURE__ */ import_react4.default.createElement(ResourceModal, { className: "hub-resource-dialog", contentClassName: "hub-resource-dialog-scroll", open: Boolean(deleting), title: t("\u786E\u8BA4\u5220\u9664"), closeLabel: t("\u5173\u95ED"), onClose: () => setDeleting(null), footer: /* @__PURE__ */ import_react4.default.createElement("div", { className: "hub-resource-actions" }, /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { disabled: busy, onClick: () => setDeleting(null) }, t("\u53D6\u6D88")), /* @__PURE__ */ import_react4.default.createElement(import_dsh_client_ui_primitives4.Button, { variant: "primary", disabled: busy, onClick: () => {
|
|
365
424
|
void action({ action: "mcp-delete", id: deleting.id, revision: deleting.revision }).then(() => setDeleting(null)).catch(() => {
|
|
366
425
|
});
|
|
367
|
-
} }, t("\u5220\u9664"))) }, /* @__PURE__ */
|
|
368
|
-
}
|
|
369
|
-
function HubSidebarButton({ wide, usePanelInfo, ctx, panelId, brand }) {
|
|
370
|
-
const selected = usePanelInfo((state) => state.activePanelId === panelId);
|
|
371
|
-
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("style", null, resources_default), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Tooltip, { label: brand.navTitle, disabled: wide }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { className: `hub-sidebar-button ${wide ? "is-wide" : "is-rail"}`, "aria-label": brand.navTitle, "aria-pressed": selected, icon: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconCordisPluginOutline14, { size: 18 }), onClick: () => ctx.layout.selectPanel(selected ? null : panelId) }, wide && /* @__PURE__ */ import_react.default.createElement("span", null, brand.navTitle))));
|
|
426
|
+
} }, t("\u5220\u9664"))) }, /* @__PURE__ */ import_react4.default.createElement("p", null, deleting?.name), /* @__PURE__ */ import_react4.default.createElement("p", null, t("\u5220\u9664\u4F1A\u65AD\u5F00\u8BE5 MCP\uFF0C\u5E76\u79FB\u9664\u5BF9\u5E94\u7684\u914D\u7F6E\u6761\u76EE\u3002"))));
|
|
372
427
|
}
|
|
373
428
|
|
|
374
429
|
// src/client/locale.js
|
|
375
430
|
var resourceWords = {
|
|
376
|
-
"
|
|
431
|
+
"\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u7528\u7684 MCP \u914D\u7F6E\u89C4\u5219": "This DSH host does not expose a supported MCP configuration schema.",
|
|
432
|
+
"MCP \u914D\u7F6E\u4E0D\u7B26\u5408\u5F53\u524D DSH \u7684\u89C4\u5219\uFF0C\u8BF7\u6838\u5BF9\u5B57\u6BB5\u7C7B\u578B\u548C\u53D6\u503C": "The MCP configuration does not match this DSH host's schema. Check field types and values.",
|
|
433
|
+
"\u670D\u52A1\u7AEF\u6307\u4EE4\u7684 UTF-8 \u5B57\u8282\u4E0A\u9650\uFF0C\u5305\u542B\u670D\u52A1\u540D\u79F0\u524D\u7F00\uFF1B\u8D85\u51FA\u65F6\u8FDE\u63A5\u5931\u8D25\u3002": "Maximum UTF-8 bytes of server instructions, including the server-name prefix. Exceeding it fails the connection.",
|
|
434
|
+
"\u5355\u6B21\u5DE5\u5177\u8C03\u7528\u6216\u8D44\u6E90\u8BF7\u6C42\u8D85\u65F6\uFF0C\u5355\u4F4D\u6BEB\u79D2\u3002": "Timeout per tool call or resource request, in milliseconds.",
|
|
435
|
+
"\u7531\u5F53\u524D DSH \u63D0\u4F9B\u7684\u914D\u7F6E\u9879\u3002": "Configuration field provided by this DSH host.",
|
|
436
|
+
"\u672A\u63D0\u4F9B\u9ED8\u8BA4\u503C": "No default specified",
|
|
437
|
+
"\u65E0\u6548\u7684\u5B9E\u9A8C\u6027\u529F\u80FD\u8BF7\u6C42": "Invalid experiment request.",
|
|
438
|
+
"\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7BA1\u7406\u6B64\u5B9E\u9A8C\u6027\u529F\u80FD": "This host does not support managing this experiment.",
|
|
439
|
+
"\u5B9E\u9A8C\u6027\u529F\u80FD\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5": "The experiment state has changed. Refresh and try again.",
|
|
440
|
+
"\u4ECD\u6709\u4EFB\u52A1\u6216\u8BF7\u6C42\u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u6216\u505C\u6B62\u540E\u518D\u5207\u6362": "Tasks or requests are still running. Wait for them to finish or stop them before switching.",
|
|
441
|
+
"\u5B9E\u9A8C\u6027\u529F\u80FD\u5207\u6362\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u786E\u8BA4\u5F53\u524D\u72B6\u6001": "The experiment change failed. Refresh to check its current state.",
|
|
442
|
+
"\u5BBF\u4E3B\u5B9E\u9A8C\u6027\u529F\u80FD\u63A5\u53E3\u8FD4\u56DE\u4E86\u65E0\u6548\u72B6\u6001": "The host experiment API returned an invalid state.",
|
|
443
|
+
"Desktop \u5B9E\u9A8C\u6027\u529F\u80FD\u9700\u901A\u8FC7\u5BBF\u4E3B\u63A5\u53E3\u64CD\u4F5C": "Desktop experiments must be managed through the host API.",
|
|
444
|
+
"\u8FD0\u884C\u72B6\u6001\u672A\u77E5": "Runtime state unknown",
|
|
445
|
+
"\u914D\u7F6E\u5DF2\u542F\u7528": "Configured to enable",
|
|
446
|
+
"\u914D\u7F6E\u5DF2\u7981\u7528": "Configured to disable",
|
|
447
|
+
"\u5B9E\u9A8C\u6027\u591A Agent \u534F\u4F5C\u5DE5\u5177\u4E0E\u56E2\u961F\u9762\u677F\u3002Web \u5207\u6362\u540E\u9700\u91CD\u542F DSH\uFF1B\u652F\u6301\u7BA1\u7406\u7684 Desktop \u4F1A\u81EA\u52A8\u91CD\u542F\u540E\u7AEF\uFF0C\u4F1A\u8BDD\u6570\u636E\u4FDD\u7559\u3002": "Experimental multi-agent tools and team panel. Restart DSH after Web changes; supported Desktop hosts restart the backend automatically. Conversations are retained.",
|
|
377
448
|
"HTTP \u8BF7\u6C42\u5934\uFF0C\u53EF\u5F15\u7528\u73AF\u5883\u53D8\u91CF\u3002": "HTTP request headers; environment references are supported.",
|
|
378
449
|
"Hub profile \u914D\u7F6E\u4E0E\u5F53\u524D\u5B9E\u4F8B\u4E0D\u4E00\u81F4": "Hub's configured profile does not match this instance.",
|
|
379
450
|
"Hub \u6B63\u5728\u5173\u95ED": "Hub is shutting down.",
|
|
@@ -383,7 +454,6 @@ var resourceWords = {
|
|
|
383
454
|
"JSON / YAML \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u7F29\u8FDB\u3001\u5F15\u53F7\u548C\u91CD\u590D\u5B57\u6BB5": "Invalid JSON or YAML. Check indentation, quotes and duplicate fields.",
|
|
384
455
|
"MCP URL \u5FC5\u987B\u4F7F\u7528 HTTP(S)\uFF0C\u51ED\u636E\u8BF7\u653E\u5165 headers": "The MCP URL must use HTTP(S). Put credentials in headers.",
|
|
385
456
|
"MCP \u4E3A\u5F53\u524D profile \u7684\u5168\u5C40\u670D\u52A1\uFF1B\u5DE5\u4F5C\u533A\u8303\u56F4\u53EA\u7B5B\u9009 Skill\u3002": "MCP services belong to this profile. The workspace selector filters Skills only.",
|
|
386
|
-
"MCP \u540D\u79F0\u53EA\u5141\u8BB8 1\u201332 \u4F4D\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26": "MCP names must contain 1\u201332 letters, digits, underscores or hyphens.",
|
|
387
457
|
"MCP \u5DF2\u4E0D\u5B58\u5728": "This MCP no longer exists.",
|
|
388
458
|
"MCP \u914D\u7F6E": "MCP configuration",
|
|
389
459
|
"MCP \u914D\u7F6E\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5": "The MCP configuration has changed. Refresh and try again.",
|
|
@@ -392,15 +462,10 @@ var resourceWords = {
|
|
|
392
462
|
"Skill \u6587\u4EF6\u4E0D\u5728\u6765\u6E90\u76EE\u5F55\u5185": "The Skill file is outside its source directory.",
|
|
393
463
|
"Skill \u6587\u4EF6\u8FC7\u5927\uFF0C\u8BF7\u6253\u5F00\u76EE\u5F55\u67E5\u770B": "The Skill file is too large to preview. Open its directory to view it.",
|
|
394
464
|
"Streamable HTTP \u5730\u5740\u3002": "The Streamable HTTP endpoint URL.",
|
|
395
|
-
"args \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4": "args must be an array of strings.",
|
|
396
465
|
"cwd \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84": "cwd must be an absolute path.",
|
|
397
|
-
"env / headers \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u952E\u503C\u5BF9\u8C61": "env and headers must be objects with string values.",
|
|
398
|
-
"failOnStartupError \u5FC5\u987B\u662F\u5E03\u5C14\u503C": "failOnStartupError must be a boolean.",
|
|
399
|
-
"reconnect \u914D\u7F6E\u65E0\u6548": "Invalid reconnect configuration.",
|
|
400
466
|
"stdio \u4E3A\u672C\u5730\u8FDB\u7A0B\uFF1Bstreamable-http \u4E3A HTTP \u670D\u52A1\u3002": "stdio runs a local process; streamable-http connects to an HTTP service.",
|
|
401
467
|
"stdio \u53EF\u6267\u884C\u6587\u4EF6\uFF1BWindows \u53EF\u4F7F\u7528 node.exe \u7684\u5B8C\u6574\u8DEF\u5F84\u3002": "The stdio executable. On Windows, a full path to node.exe is supported.",
|
|
402
468
|
"stdio \u5DE5\u4F5C\u76EE\u5F55\uFF0C\u7559\u7A7A\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u76EE\u5F55\u3002": "The stdio working directory. Leave empty to use the host's working directory.",
|
|
403
|
-
"stdio \u5FC5\u586B": "Required for stdio",
|
|
404
469
|
"type \u4E0E transport \u4E0D\u4E00\u81F4": "type and transport do not match.",
|
|
405
470
|
"\u4E00\u6B21\u8FDE\u63A5\u6545\u969C\u7684\u8FDE\u7EED\u91CD\u8BD5\u6B21\u6570\u4E0A\u9650\u3002": "Maximum consecutive retries for one connection failure.",
|
|
406
471
|
"\u4E0B\u5217\u9ED8\u8BA4\u503C\u7531 DSH \u63D0\u4F9B\uFF1B\u5207\u6362\u683C\u5F0F\u4E0D\u4F1A\u8865\u5165\u8FD9\u4E9B\u5B57\u6BB5\u3002": "DSH supplies these defaults. Switching formats does not insert them.",
|
|
@@ -419,7 +484,6 @@ var resourceWords = {
|
|
|
419
484
|
"\u52A0\u8F7D\u5931\u8D25": "Activation failed",
|
|
420
485
|
"\u52A0\u8F7D\u672A\u77E5": "Activation unknown",
|
|
421
486
|
"\u52A0\u8F7D\u72B6\u6001": "Activation",
|
|
422
|
-
"\u5355\u6B21\u5DE5\u5177\u8C03\u7528\u8D85\u65F6\uFF0C\u5355\u4F4D\u6BEB\u79D2\u3002": "Timeout for each tool call, in milliseconds.",
|
|
423
487
|
"\u539F\u751F DSH \u5B89\u88C5\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u6B64 DSH \u7248\u672C\u63D0\u4F9B Agent Teams \u5305": "Native DSH installation failed. Check whether this DSH version provides Agent Teams packages.",
|
|
424
488
|
"\u53D6\u6D88": "Cancel",
|
|
425
489
|
"\u542F\u7528": "Enable",
|
|
@@ -432,7 +496,6 @@ var resourceWords = {
|
|
|
432
496
|
"\u5B9E\u4F8B\u6216\u529F\u80FD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0": "The instance or feature has changed. Refresh to continue.",
|
|
433
497
|
"\u5B9E\u9A8C\u6027\u529F\u80FD": "Experimental",
|
|
434
498
|
"\u5B9E\u9A8C\u6027\u529F\u80FD\u6B63\u5728\u66F4\u65B0": "An experimental feature is being updated.",
|
|
435
|
-
"\u5B9E\u9A8C\u6027\u591A Agent \u534F\u4F5C\u5DE5\u5177\u4E0E\u56E2\u961F\u9762\u677F\u3002\u5207\u6362\u540E\u9700\u91CD\u542F DSH\uFF0C\u4F1A\u8BDD\u6570\u636E\u4FDD\u7559\u3002": "Experimental multi-agent tools and team panel. Changes require restarting DSH; conversations are preserved.",
|
|
436
499
|
"\u5BBF\u4E3B\u65E0\u6CD5\u6253\u5F00\u76EE\u5F55\uFF0C\u8BF7\u6839\u636E\u6765\u6E90\u8DEF\u5F84\u624B\u52A8\u6253\u5F00": "The host could not open the directory. Open it manually using the source path.",
|
|
437
500
|
"\u5BBF\u4E3B\u672A\u516C\u5F00\u6301\u7EED\u8FDE\u63A5\u72B6\u6001\uFF1B\u5DF2\u52A0\u8F7D\u4E0D\u4EE3\u8868\u8FDE\u63A5\u6210\u529F\u3002": "The host does not expose continuous connection status. Activation does not imply a successful connection.",
|
|
438
501
|
"\u5DE5\u5177\u547D\u540D\u7A7A\u95F4\uFF0C1\u201332 \u4F4D\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u6216\u8FDE\u5B57\u7B26\u3002": "Tool namespace: 1\u201332 letters, digits, underscores or hyphens.",
|
|
@@ -460,6 +523,7 @@ var resourceWords = {
|
|
|
460
523
|
"\u65E0\u6548\u7684\u5F00\u5173\u503C": "Invalid toggle value.",
|
|
461
524
|
"\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u5B9E\u4F8B\u7684 profile\uFF0C\u5DF2\u505C\u6B62\u7BA1\u7406\u64CD\u4F5C": "Cannot identify this instance's profile. Management operations are disabled.",
|
|
462
525
|
"\u663E\u5F0F\u4F20\u7ED9\u670D\u52A1\u7684\u73AF\u5883\u53D8\u91CF\uFF0C\u53EF\u7528 !!js process.env.NAME \u5F15\u7528\u3002": "Environment variables passed to the server. Use !!js process.env.NAME for references.",
|
|
526
|
+
"\u670D\u52A1\u672A\u63D0\u4F9B\u63CF\u8FF0\u3002": "The server did not provide a description.",
|
|
463
527
|
"\u6682\u65E0\u63CF\u8FF0": "No description",
|
|
464
528
|
"\u670D\u52A1\u5C1A\u672A\u63D0\u4F9B\u63CF\u8FF0\uFF0C\u53EF\u6D4B\u8BD5\u8FDE\u63A5\u8BFB\u53D6\u3002": "No server description available. Test the connection to retrieve it.",
|
|
465
529
|
"\u672A\u52A0\u8F7D": "Not loaded",
|
|
@@ -496,11 +560,9 @@ var resourceWords = {
|
|
|
496
560
|
"\u8BF7\u5148\u542F\u7528 MCP": "Enable the MCP first.",
|
|
497
561
|
"\u8BF7\u5148\u5B89\u88C5 Agent Teams": "Install Agent Teams first.",
|
|
498
562
|
"\u8BF7\u63D0\u4F9B\u4E00\u4E2A MCP \u914D\u7F6E\u5BF9\u8C61": "Provide one MCP configuration object.",
|
|
499
|
-
"\u8BF7\u901A\u8FC7 Desktop \u539F\u751F\u63D2\u4EF6\u7BA1\u7406\u7A97\u53E3\u5207\u6362 Agent Teams": "Use the native Desktop Plugins window to toggle Agent Teams.",
|
|
500
563
|
"\u8D44\u6E90\u5C1A\u672A\u5B8C\u6574\u52A0\u8F7D\uFF0C\u8BF7\u5237\u65B0\u91CD\u8BD5": "Resource discovery is incomplete. Refresh to retry.",
|
|
501
564
|
"\u8D44\u6E90\u7BA1\u7406": "Resource management",
|
|
502
565
|
"\u8D44\u6E90\u8303\u56F4": "Resource scope",
|
|
503
|
-
"\u8D85\u65F6\u65F6\u95F4\u5FC5\u987B\u5728 1000\u2013300000 \u6BEB\u79D2\u4E4B\u95F4": "The timeout must be between 1000 and 300000 milliseconds.",
|
|
504
566
|
"\u8FD4\u56DE\u4F1A\u8BDD": "Back to conversation",
|
|
505
567
|
"\u8FD9\u4E2A\u8D44\u6E90\u6CA1\u6709\u672C\u5730\u76EE\u5F55": "This resource has no local directory.",
|
|
506
568
|
"\u8FDE\u63A5\u4E22\u5931\u540E\u81EA\u52A8\u91CD\u8FDE\u3002": "Reconnect automatically after losing the connection.",
|
|
@@ -545,6 +607,62 @@ function connectionRequest(connection, marketId) {
|
|
|
545
607
|
};
|
|
546
608
|
}
|
|
547
609
|
|
|
610
|
+
// src/experiment-definitions.js
|
|
611
|
+
var agentTeams = Object.freeze({
|
|
612
|
+
id: "agent-teams",
|
|
613
|
+
name: "Agent Teams",
|
|
614
|
+
description: "\u5B9E\u9A8C\u6027\u591A Agent \u534F\u4F5C\u5DE5\u5177\u4E0E\u56E2\u961F\u9762\u677F\u3002Web \u5207\u6362\u540E\u9700\u91CD\u542F DSH\uFF1B\u652F\u6301\u7BA1\u7406\u7684 Desktop \u4F1A\u81EA\u52A8\u91CD\u542F\u540E\u7AEF\uFF0C\u4F1A\u8BDD\u6570\u636E\u4FDD\u7559\u3002"
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// src/client/experiments.js
|
|
618
|
+
var errors = {
|
|
619
|
+
"invalid-request": "\u65E0\u6548\u7684\u5B9E\u9A8C\u6027\u529F\u80FD\u8BF7\u6C42",
|
|
620
|
+
"profile-changed": "\u5B9E\u4F8B profile \u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0",
|
|
621
|
+
unsupported: "\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7BA1\u7406\u6B64\u5B9E\u9A8C\u6027\u529F\u80FD",
|
|
622
|
+
busy: "\u5B9E\u9A8C\u6027\u529F\u80FD\u6B63\u5728\u66F4\u65B0",
|
|
623
|
+
"state-changed": "\u5B9E\u9A8C\u6027\u529F\u80FD\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5",
|
|
624
|
+
"tasks-running": "\u4ECD\u6709\u4EFB\u52A1\u6216\u8BF7\u6C42\u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u6216\u505C\u6B62\u540E\u518D\u5207\u6362",
|
|
625
|
+
failed: "\u5B9E\u9A8C\u6027\u529F\u80FD\u5207\u6362\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u786E\u8BA4\u5F53\u524D\u72B6\u6001"
|
|
626
|
+
};
|
|
627
|
+
function desktopBridge(desktop) {
|
|
628
|
+
const bridge = desktop?.experiments;
|
|
629
|
+
return bridge?.version === 1 && typeof bridge.list === "function" && typeof bridge.setEnabled === "function" ? bridge : null;
|
|
630
|
+
}
|
|
631
|
+
function validateSnapshot(snapshot, profile) {
|
|
632
|
+
if (snapshot?.profile !== profile.directory) throw new Error("\u5B9E\u4F8B profile \u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0");
|
|
633
|
+
if (!Array.isArray(snapshot.features)) throw new Error("\u5BBF\u4E3B\u5B9E\u9A8C\u6027\u529F\u80FD\u63A5\u53E3\u8FD4\u56DE\u4E86\u65E0\u6548\u72B6\u6001");
|
|
634
|
+
const rows = snapshot.features.filter((row) => row?.id === "agent-teams");
|
|
635
|
+
if (rows.length > 1 || rows.some((row) => typeof row.enabled !== "boolean" || ![true, false, null].includes(row.activeEnabled) || ["installed", "canToggle", "busy"].some((key) => typeof row[key] !== "boolean"))) throw new Error("\u5BBF\u4E3B\u5B9E\u9A8C\u6027\u529F\u80FD\u63A5\u53E3\u8FD4\u56DE\u4E86\u65E0\u6548\u72B6\u6001");
|
|
636
|
+
return rows;
|
|
637
|
+
}
|
|
638
|
+
function experimentClient(request, desktop, reload) {
|
|
639
|
+
return {
|
|
640
|
+
async list(profile, options = {}) {
|
|
641
|
+
if (profile.installation !== "desktop") return request("experiments", {}, options);
|
|
642
|
+
const baseline = [{ ...agentTeams, enabled: null, activeEnabled: null, installed: false, busy: false, pendingRestart: false }];
|
|
643
|
+
const bridge = desktopBridge(desktop);
|
|
644
|
+
const native = bridge ? validateSnapshot(await bridge.list(), profile) : [];
|
|
645
|
+
return baseline.map((row) => {
|
|
646
|
+
const feature = native.find((item) => item.id === row.id);
|
|
647
|
+
return feature ? { ...row, ...feature, canInstall: false, supported: true, pendingRestart: feature.activeEnabled !== null && feature.enabled !== feature.activeEnabled } : { ...row, canToggle: false, canInstall: false, supported: false };
|
|
648
|
+
});
|
|
649
|
+
},
|
|
650
|
+
async change(profile, row, enabled, action = "toggle") {
|
|
651
|
+
if (profile.installation !== "desktop") return request("experiment-mutate", { profile: profile.directory, id: row.id, action, enabled, expectedEnabled: row.enabled });
|
|
652
|
+
const bridge = desktopBridge(desktop);
|
|
653
|
+
if (!bridge || !row.supported || !row.canToggle || action !== "toggle") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7BA1\u7406\u6B64\u5B9E\u9A8C\u6027\u529F\u80FD");
|
|
654
|
+
const result = await bridge.setEnabled({ profile: profile.directory, id: row.id, enabled, expectedEnabled: row.enabled });
|
|
655
|
+
if (result?.ok === false) {
|
|
656
|
+
throw new Error(errors[result.error?.code] ?? errors.failed);
|
|
657
|
+
}
|
|
658
|
+
if (result?.ok !== true || typeof result.reloadRequired !== "boolean") throw new Error("\u5BBF\u4E3B\u5B9E\u9A8C\u6027\u529F\u80FD\u63A5\u53E3\u8FD4\u56DE\u4E86\u65E0\u6548\u72B6\u6001");
|
|
659
|
+
validateSnapshot(result.value, profile);
|
|
660
|
+
if (result.reloadRequired) reload();
|
|
661
|
+
return result.value;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
548
666
|
// src/client/index.jsx
|
|
549
667
|
var NS = "@zaimokuza/dsh-plugin-hub";
|
|
550
668
|
var MARKET_ID = "hub";
|
|
@@ -556,8 +674,10 @@ function apply(ctx) {
|
|
|
556
674
|
ctx.effect(() => ctx.locale.register(NS, words), "Plugin Hub: dictionaries");
|
|
557
675
|
const t = translate(ctx.locale.bind(NS));
|
|
558
676
|
const request = connectionRequest(ctx.connection, MARKET_ID);
|
|
677
|
+
const experiments = experimentClient(request, window.dshDesktop, () => window.location.reload());
|
|
559
678
|
const panelId = "plugin-hub-" + MARKET_ID;
|
|
560
|
-
ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: panelId, locale: NS }, () => /* @__PURE__ */
|
|
561
|
-
ctx.slots.inject("
|
|
679
|
+
ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: panelId, locale: NS }, () => /* @__PURE__ */ import_react5.default.createElement(ResourceHub, { ctx, request, experiments, t, brand: BRAND, panelId })));
|
|
680
|
+
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({ name: "conversation.session.header.utilities", id: panelId, order: 100, locale: NS }, () => /* @__PURE__ */ import_react5.default.createElement(HubHeaderEntry, { ctx, panelId, brand: BRAND })));
|
|
681
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({ name: "shell.overlay", id: panelId, locale: NS }, (props) => /* @__PURE__ */ import_react5.default.createElement(HubEmptyEntry, { ...props, ctx, panelId, brand: BRAND })));
|
|
562
682
|
}
|
|
563
683
|
return module.exports; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaimokuza/dsh-plugin-hub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Native DSH resource hub: Skills, MCP, third-party plugins and experiments",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@deepseek-ai/dsh-client-locale",
|
|
23
23
|
"@deepseek-ai/dsh-client-connection",
|
|
24
24
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
25
|
-
"@deepseek-ai/dsh-client-ui-
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
26
26
|
],
|
|
27
27
|
"platform": "web"
|
|
28
28
|
}
|
|
@@ -36,10 +36,14 @@
|
|
|
36
36
|
"verify:native": "node scripts/verify-native.mjs"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/
|
|
39
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
40
40
|
"yaml": "2.9.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
44
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
45
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
46
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
43
47
|
"esbuild": "0.28.1",
|
|
44
48
|
"react": "18.3.1"
|
|
45
49
|
},
|
|
@@ -70,7 +74,8 @@
|
|
|
70
74
|
"hostCompatibility": "capability-based",
|
|
71
75
|
"testedDshVersions": [
|
|
72
76
|
"0.1.5-rc.1",
|
|
73
|
-
"0.1.5-rc.2"
|
|
77
|
+
"0.1.5-rc.2",
|
|
78
|
+
"0.1.6-alpha.1"
|
|
74
79
|
]
|
|
75
80
|
}
|
|
76
81
|
}
|
package/src/experiments.js
CHANGED
|
@@ -3,9 +3,10 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { atomicWrite } from './profile-files.js';
|
|
6
|
+
import { agentTeams } from './experiment-definitions.js';
|
|
6
7
|
const bundles = ['@deepseek-ai/dsh-experimental-agent-team-profile', '@deepseek-ai/dsh-experimental-agent-team-web-profile'];
|
|
7
8
|
export class Experiments {
|
|
8
|
-
constructor(environment) { this.environment = environment; this.file = join(environment.profileDir, 'package.json'); this.initial =
|
|
9
|
+
constructor(environment) { this.environment = environment; this.file = join(environment.profileDir, 'package.json'); this.initial = undefined; this.busy = false; }
|
|
9
10
|
async snapshot() {
|
|
10
11
|
const manifest = JSON.parse(await readFile(this.file, 'utf8'));
|
|
11
12
|
const configuredBundles = manifest.dsh?.profile?.bundles;
|
|
@@ -13,17 +14,18 @@ export class Experiments {
|
|
|
13
14
|
const enabled = this.environment.installation === 'desktop'
|
|
14
15
|
? (typeof desktopSetting === 'boolean' ? desktopSetting : null)
|
|
15
16
|
: (Array.isArray(configuredBundles) ? bundles.every(name => configuredBundles.includes(name)) : null);
|
|
16
|
-
this.initial
|
|
17
|
+
if (this.initial === undefined) this.initial = enabled;
|
|
17
18
|
const resolver = createRequire(this.file);
|
|
18
19
|
const installed = bundles.every(name => { try { resolver.resolve(name + '/package.json'); return true; } catch { return false; } });
|
|
19
|
-
return [{
|
|
20
|
+
return [{ ...agentTeams, installed, enabled, activeEnabled: this.environment.installation === 'cli' ? this.initial : null, supported: this.environment.installation === 'cli', pendingRestart: this.environment.installation === 'cli' && this.initial !== null && this.initial !== enabled, canToggle: this.environment.installation === 'cli' && Array.isArray(configuredBundles) && installed, canInstall: this.environment.installation === 'cli', busy: this.busy }];
|
|
20
21
|
}
|
|
21
22
|
async mutate(data) {
|
|
22
23
|
if (data.profile !== this.environment.profileDir || data.id !== 'agent-teams') throw new Error('实例或功能已变化,请刷新');
|
|
23
24
|
if (this.busy) throw new Error('实验性功能正在更新');
|
|
24
|
-
if (this.environment.installation !== 'cli') throw new Error('
|
|
25
|
+
if (this.environment.installation !== 'cli') throw new Error('Desktop 实验性功能需通过宿主接口操作');
|
|
25
26
|
this.busy = true;
|
|
26
27
|
try {
|
|
28
|
+
if (data.expectedEnabled !== undefined && data.expectedEnabled !== (await this.snapshot())[0].enabled) throw new Error('实验性功能状态已变化,请刷新后重试');
|
|
27
29
|
if (data.action === 'install') {
|
|
28
30
|
for (const name of bundles) await new Promise((resolve, reject) => {
|
|
29
31
|
const child = spawn(process.execPath, [this.environment.cli, 'plugin', '--profile', this.environment.profile, 'add', `${name}@${this.environment.host.dsh}`, '--save-exact'], { cwd: this.environment.profileDir, env: { ...process.env, DSH_HOME: this.environment.home }, stdio: 'ignore', windowsHide: true, shell: false });
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function apply(ctx, options = {}) {
|
|
|
24
24
|
if (path === 'resources') return resources.snapshot(data.workspaceId, data.presetId);
|
|
25
25
|
if (path === 'resource-mutate') return resources.mutate(data);
|
|
26
26
|
if (path === 'mcp-config') return resources.configuration(data);
|
|
27
|
+
if (path === 'mcp-schema') return resources.configurationSchema(data);
|
|
27
28
|
if (path === 'mcp-format') return resources.formatConfiguration(data);
|
|
28
29
|
if (path === 'mcp-test') return resources.test(data, signal);
|
|
29
30
|
if (path === 'open-directory') return resources.open(data);
|
|
@@ -34,7 +35,7 @@ export async function apply(ctx, options = {}) {
|
|
|
34
35
|
};
|
|
35
36
|
// The shared RPC interceptor belongs to DSH's Gateway. Exact Fetch routes
|
|
36
37
|
// compose ahead of it on both the HTTP carrier and Desktop's worker carrier.
|
|
37
|
-
for (const path of ['resources', 'resource-mutate', 'mcp-config', 'mcp-format', 'mcp-test', 'open-directory', 'skill-file', 'experiments', 'experiment-mutate']) {
|
|
38
|
+
for (const path of ['resources', 'resource-mutate', 'mcp-config', 'mcp-schema', 'mcp-format', 'mcp-test', 'open-directory', 'skill-file', 'experiments', 'experiment-mutate']) {
|
|
38
39
|
ctx.effect(() => ctx.connection.fetch.register({ path: `/api/${identity.service}/${path}`, methods: ['POST'], requestBody: 'buffered', async fetch(request) {
|
|
39
40
|
let envelope;
|
|
40
41
|
try { envelope = await request.json(); } catch { return Response.json({ error: 'Invalid JSON' }, { status: 400 }); }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const MCP_MODULE = '@deepseek-ai/dsh-mcp-client';
|
|
4
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
|
|
6
|
+
function variants(schema) {
|
|
7
|
+
const nodes = schema?.type === 'union' ? schema.list : [schema];
|
|
8
|
+
if (!nodes?.length || nodes.some(node => node?.type !== 'object' || !node.dict?.transport)) {
|
|
9
|
+
throw new Error('当前 DSH 未提供可用的 MCP 配置规则');
|
|
10
|
+
}
|
|
11
|
+
return nodes;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Schemastery preserves unknown keys. Reject typos using the host's field tree,
|
|
15
|
+
// without maintaining a second list of supported MCP options in Hub.
|
|
16
|
+
function knownFields(value, schema) {
|
|
17
|
+
if (!object(value) || schema.type !== 'object') return;
|
|
18
|
+
for (const [key, child] of Object.entries(value)) {
|
|
19
|
+
if (!Object.hasOwn(schema.dict, key)) throw new Error('配置包含宿主不支持的字段,请核对 MCP 配置');
|
|
20
|
+
knownFields(child, schema.dict[key]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The host owns types/defaults; Hub owns its editor's filesystem/URL boundary. */
|
|
25
|
+
export function validateNativeMcp(Config, input) {
|
|
26
|
+
const branch = variants(Config).find(node => node.dict.transport.value === input?.transport);
|
|
27
|
+
if (!branch) throw new Error('不支持的 MCP 协议');
|
|
28
|
+
knownFields(input, branch);
|
|
29
|
+
let config;
|
|
30
|
+
try { config = Config(structuredClone(input)); }
|
|
31
|
+
catch { throw new Error('MCP 配置不符合当前 DSH 的规则,请核对字段类型和取值'); }
|
|
32
|
+
if (config.transport === 'stdio') {
|
|
33
|
+
if (!config.command?.trim() || config.command.includes('\0')) throw new Error('需要有效的可执行命令');
|
|
34
|
+
if (config.cwd && !isAbsolute(config.cwd)) throw new Error('cwd 必须是绝对路径');
|
|
35
|
+
} else if (config.transport === 'streamable-http') {
|
|
36
|
+
let url; try { url = new URL(config.url); } catch { throw new Error('需要有效的 MCP URL'); }
|
|
37
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('MCP URL 必须使用 HTTP(S),凭据请放入 headers');
|
|
38
|
+
}
|
|
39
|
+
if (config.reconnect?.initialDelayMs > config.reconnect?.maxDelayMs) throw new Error('重连初始间隔不能超过最大间隔');
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Only schema metadata crosses RPC; never resolved configuration or env values. */
|
|
44
|
+
export function mcpSchemaFields(Config) {
|
|
45
|
+
const fields = new Map();
|
|
46
|
+
function visit(schema, transport, prefix = '') {
|
|
47
|
+
for (const [name, node] of Object.entries(schema.dict)) {
|
|
48
|
+
const key = prefix + name;
|
|
49
|
+
if (node.type === 'object') { visit(node, transport, key + '.'); continue; }
|
|
50
|
+
const meta = node.meta ?? {};
|
|
51
|
+
const field = { key, type: node.type, required: Boolean(meta.required) || node.type === 'const',
|
|
52
|
+
...(Object.hasOwn(meta, 'default') ? { default: meta.default } : {}), transports: [transport] };
|
|
53
|
+
const previous = fields.get(key);
|
|
54
|
+
// Keep distinct rows if transports ever acquire different defaults/types.
|
|
55
|
+
const id = previous && JSON.stringify({ ...previous, transports: [] }) !== JSON.stringify({ ...field, transports: [] }) ? `${key}:${transport}` : key;
|
|
56
|
+
if (fields.has(id)) fields.get(id).transports.push(transport);
|
|
57
|
+
else fields.set(id, field);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const branch of variants(Config)) visit(branch, branch.dict.transport.value);
|
|
61
|
+
return [...fields.values()];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class NativeMcp {
|
|
65
|
+
constructor(loader) { this.loader = loader; }
|
|
66
|
+
async module() {
|
|
67
|
+
this.pending ??= this.loader.import(MCP_MODULE).then(module => {
|
|
68
|
+
if (typeof module.Config !== 'function') throw new Error('当前 DSH 未提供可用的 MCP 配置规则');
|
|
69
|
+
variants(module.Config);
|
|
70
|
+
return module;
|
|
71
|
+
}).catch(error => { this.pending = undefined; throw error; });
|
|
72
|
+
return this.pending;
|
|
73
|
+
}
|
|
74
|
+
async validate(input) { return validateNativeMcp((await this.module()).Config, input); }
|
|
75
|
+
async fields() { return mcpSchemaFields((await this.module()).Config); }
|
|
76
|
+
}
|
package/src/mcp-probe.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
|
3
|
+
|
|
4
|
+
const safeIcon = value => { try { const url = new URL(value); return url.protocol === 'https:' && !url.username && !url.password && !url.search ? url.href : null; } catch { return null; } };
|
|
5
|
+
|
|
6
|
+
/** A bounded, independent observation, never the host's live connection state. */
|
|
7
|
+
export async function probeMcp(config, { signal, timeoutMs = 15000 } = {}) {
|
|
8
|
+
const abort = AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(timeoutMs)]);
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
const client = new Client({ name: 'dsh-plugin-hub-probe', version: '1.0.0' }, {
|
|
11
|
+
capabilities: {}, versionNegotiation: { mode: 'auto' },
|
|
12
|
+
});
|
|
13
|
+
let transport;
|
|
14
|
+
let onAbort;
|
|
15
|
+
try {
|
|
16
|
+
abort.throwIfAborted();
|
|
17
|
+
transport = config.transport === 'stdio' ? new StdioClientTransport({ command: config.command, args: config.args,
|
|
18
|
+
env: { ...Object.fromEntries(['PATH', 'HOME', 'USERPROFILE', 'SystemRoot', 'TMPDIR', 'TEMP', 'LANG'].filter(k => process.env[k]).map(k => [k, process.env[k]])), ...config.env },
|
|
19
|
+
cwd: config.cwd || undefined, stderr: 'ignore',
|
|
20
|
+
}) : new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: config.headers } });
|
|
21
|
+
const cancellation = new Promise((_, reject) => {
|
|
22
|
+
onAbort = () => { reject(abort.reason); };
|
|
23
|
+
abort.addEventListener('abort', onAbort, { once: true });
|
|
24
|
+
});
|
|
25
|
+
const result = await Promise.race([(async () => {
|
|
26
|
+
await client.connect(transport, { signal: abort, timeout: timeoutMs });
|
|
27
|
+
abort.throwIfAborted();
|
|
28
|
+
// Match the host's instruction budget when its schema supplies one. Older
|
|
29
|
+
// hosts omit this option; the independent probe must not invent a limit.
|
|
30
|
+
const instructions = client.getInstructions();
|
|
31
|
+
if (instructions && config.maxInstructionBytes !== undefined &&
|
|
32
|
+
Buffer.byteLength(`### MCP server: ${config.serverName}\n\n${instructions}`) > config.maxInstructionBytes) {
|
|
33
|
+
throw new Error('MCP server instructions exceed the host limit');
|
|
34
|
+
}
|
|
35
|
+
// SDK owns pagination and supports resource-only servers with zero tools.
|
|
36
|
+
const { tools } = client.getServerCapabilities()?.tools ? await client.listTools(undefined, { signal: abort, timeout: timeoutMs }) : { tools: [] };
|
|
37
|
+
const info = client.getServerVersion();
|
|
38
|
+
const capabilities = client.getServerCapabilities();
|
|
39
|
+
return { status: 'success', tools: tools.length, version: info?.version ?? null,
|
|
40
|
+
icon: safeIcon(info?.icons?.[0]?.src), description: String(info?.description ?? '').slice(0, 8000),
|
|
41
|
+
capabilities: { tools: Boolean(capabilities?.tools), resources: Boolean(capabilities?.resources), prompts: Boolean(capabilities?.prompts) },
|
|
42
|
+
};
|
|
43
|
+
})(), cancellation]);
|
|
44
|
+
return { ...result, durationMs: Date.now() - started, at: new Date().toISOString() };
|
|
45
|
+
} catch {
|
|
46
|
+
return { status: 'failed', durationMs: Date.now() - started, at: new Date().toISOString() };
|
|
47
|
+
} finally {
|
|
48
|
+
if (onAbort) abort.removeEventListener('abort', onAbort);
|
|
49
|
+
// During negotiation the transport (and its probe sibling) may not yet be
|
|
50
|
+
// attached to Client. Both owners must be closed on cancellation/failure.
|
|
51
|
+
await Promise.allSettled([client.close(), transport?.close()]);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/resources.js
CHANGED
|
@@ -2,15 +2,13 @@ import { readFile, mkdir, realpath } from 'node:fs/promises';
|
|
|
2
2
|
import { join, relative, isAbsolute } from 'node:path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import { NativeMcp, MCP_MODULE as MCP } from './mcp-native.js';
|
|
6
|
+
import { probeMcp } from './mcp-probe.js';
|
|
8
7
|
|
|
9
8
|
import { parseMcpDocument, formatMcpDocument, parseConfigText, resolveEnvironment } from './mcp-config.js';
|
|
10
9
|
|
|
11
10
|
import { findMcpDefinition, deleteMcpDefinition, atomicWrite } from './profile-files.js';
|
|
12
11
|
|
|
13
|
-
const MCP = '@deepseek-ai/dsh-mcp-client';
|
|
14
12
|
const fingerprint = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
15
13
|
const inside = (root, path) => { const rel = relative(root, path); return !rel.startsWith('..') && !isAbsolute(rel); };
|
|
16
14
|
const record = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -24,47 +22,11 @@ export function describeMcp(config = {}) {
|
|
|
24
22
|
};
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
export function validateMcp(input) {
|
|
28
|
-
if (!record(input) || !/^[A-Za-z0-9_-]{1,32}$/.test(input.serverName)) throw new Error('MCP 名称只允许 1–32 位字母、数字、下划线和连字符');
|
|
29
|
-
const common = ['serverName', 'transport', 'toolCallTimeoutMs', 'failOnStartupError', 'reconnect'];
|
|
30
|
-
const allowed = [...common, ...(input.transport === 'stdio' ? ['command', 'args', 'env', 'cwd'] : ['url', 'headers'])];
|
|
31
|
-
if (Object.keys(input).some(key => !allowed.includes(key))) throw new Error('配置包含宿主不支持的字段,请核对 MCP 配置');
|
|
32
|
-
const config = { serverName: input.serverName, transport: input.transport };
|
|
33
|
-
if (input.transport === 'stdio') {
|
|
34
|
-
if (typeof input.command !== 'string' || !input.command.trim() || input.command.includes('\0')) throw new Error('需要有效的可执行命令');
|
|
35
|
-
config.command = input.command;
|
|
36
|
-
config.args = input.args ?? [];
|
|
37
|
-
if (!Array.isArray(config.args) || config.args.some(x => typeof x !== 'string')) throw new Error('args 必须是字符串数组');
|
|
38
|
-
config.env = input.env ?? {};
|
|
39
|
-
config.cwd = input.cwd ?? '';
|
|
40
|
-
if (typeof config.cwd !== 'string' || (config.cwd && !isAbsolute(config.cwd))) throw new Error('cwd 必须是绝对路径');
|
|
41
|
-
} else if (input.transport === 'streamable-http') {
|
|
42
|
-
let url; try { url = new URL(input.url); } catch { throw new Error('需要有效的 MCP URL'); }
|
|
43
|
-
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('MCP URL 必须使用 HTTP(S),凭据请放入 headers');
|
|
44
|
-
config.url = url.href; config.headers = input.headers ?? {};
|
|
45
|
-
} else throw new Error('不支持的 MCP 协议');
|
|
46
|
-
for (const map of [config.env, config.headers].filter(Boolean)) {
|
|
47
|
-
if (!record(map) || Object.values(map).some(x => typeof x !== 'string')) throw new Error('env / headers 必须是字符串键值对象');
|
|
48
|
-
}
|
|
49
|
-
config.toolCallTimeoutMs = input.toolCallTimeoutMs ?? 60000;
|
|
50
|
-
if (!Number.isInteger(config.toolCallTimeoutMs) || config.toolCallTimeoutMs < 1000 || config.toolCallTimeoutMs > 300000) throw new Error('超时时间必须在 1000–300000 毫秒之间');
|
|
51
|
-
config.failOnStartupError = input.failOnStartupError ?? false;
|
|
52
|
-
if (typeof config.failOnStartupError !== 'boolean') throw new Error('failOnStartupError 必须是布尔值');
|
|
53
|
-
if (input.reconnect !== undefined) {
|
|
54
|
-
if (!record(input.reconnect) || Object.keys(input.reconnect).some(key => !['enabled', 'initialDelayMs', 'maxDelayMs', 'maxAttempts'].includes(key))) throw new Error('reconnect 配置无效');
|
|
55
|
-
config.reconnect = { ...input.reconnect };
|
|
56
|
-
for (const [key, value] of Object.entries(config.reconnect)) {
|
|
57
|
-
if (key === 'enabled' ? typeof value !== 'boolean' : !Number.isSafeInteger(value) || value < 1 || (key !== 'maxAttempts' && value > 2147483647)) throw new Error('reconnect 配置无效');
|
|
58
|
-
}
|
|
59
|
-
if (config.reconnect.initialDelayMs && config.reconnect.maxDelayMs && config.reconnect.initialDelayMs > config.reconnect.maxDelayMs) throw new Error('重连初始间隔不能超过最大间隔');
|
|
60
|
-
}
|
|
61
|
-
return config;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
25
|
/** Native resource adapter. Its durable state belongs exclusively to the boot profile. */
|
|
65
26
|
export class NativeResources {
|
|
66
27
|
constructor(ctx, environment, directory, inventory) {
|
|
67
28
|
Object.assign(this, { ctx, environment, directory, inventory });
|
|
29
|
+
this.mcp = new NativeMcp(ctx.loader);
|
|
68
30
|
this.file = join(directory, 'resources.json');
|
|
69
31
|
this.state = { version: 1, skills: [], mcps: [], overrides: {} };
|
|
70
32
|
this.tail = Promise.resolve(); this.fibers = new Map(); this.applied = new WeakMap(); this.probes = new Map(); this.loadErrors = new Map();
|
|
@@ -150,8 +112,8 @@ export class NativeResources {
|
|
|
150
112
|
for (const row of this.state.mcps) {
|
|
151
113
|
if (row.enabled && !this.fibers.has(row.id)) {
|
|
152
114
|
try {
|
|
153
|
-
const module = await this.
|
|
154
|
-
this.fibers.set(row.id, this.ctx.plugin(module,
|
|
115
|
+
const module = await this.mcp.module();
|
|
116
|
+
this.fibers.set(row.id, this.ctx.plugin(module, await this.mcp.validate(resolveEnvironment(row.config))));
|
|
155
117
|
this.loadErrors.delete(row.id);
|
|
156
118
|
} catch (error) { this.loadErrors.set(row.id, error.message); }
|
|
157
119
|
}
|
|
@@ -218,6 +180,10 @@ export class NativeResources {
|
|
|
218
180
|
return { text: formatMcpDocument(config, data.format ?? 'json') };
|
|
219
181
|
});
|
|
220
182
|
}
|
|
183
|
+
async configurationSchema(data) {
|
|
184
|
+
if (data.profile !== this.environment.profileDir) throw new Error('实例 profile 已变化,请刷新');
|
|
185
|
+
return { fields: await this.mcp.fields() };
|
|
186
|
+
}
|
|
221
187
|
formatConfiguration(data) {
|
|
222
188
|
if (data.profile !== this.environment.profileDir) throw new Error('实例 profile 已变化,请刷新');
|
|
223
189
|
return { text: formatMcpDocument(parseConfigText(data.text), data.format) };
|
|
@@ -243,7 +209,7 @@ export class NativeResources {
|
|
|
243
209
|
const previous = next.mcps.find(row => row.id === data.id);
|
|
244
210
|
const entry = this.entries().find(entry => this.entryId(entry) === data.id);
|
|
245
211
|
const raw = data.text !== undefined ? parseMcpDocument(data.text) : { ...(previous?.config ?? entry?.options.config ?? {}), ...data.config };
|
|
246
|
-
|
|
212
|
+
await this.mcp.validate(resolveEnvironment(raw));
|
|
247
213
|
const config = raw;
|
|
248
214
|
if (snapshot.mcps.some(row => row.name === config.serverName && row.id !== data.id)) throw new Error('当前 profile 已有同名 MCP');
|
|
249
215
|
if (data.action === 'mcp-add') next.mcps.push({ id: `hub-${randomUUID()}`, config, enabled: true });
|
|
@@ -300,24 +266,8 @@ export class NativeResources {
|
|
|
300
266
|
const raw = row?.config ?? entry?.options.config;
|
|
301
267
|
const config = raw && resolveEnvironment(raw);
|
|
302
268
|
if (!config) throw new Error('当前 MCP 没有可用的已解析配置,请先启用');
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
const transport = config.transport === 'stdio' ? new StdioClientTransport({ command: config.command, args: config.args ?? [],
|
|
306
|
-
env: { ...Object.fromEntries(['PATH', 'HOME', 'USERPROFILE', 'SystemRoot', 'TMPDIR', 'TEMP', 'LANG'].filter(k => process.env[k]).map(k => [k, process.env[k]])), ...config.env },
|
|
307
|
-
cwd: config.cwd || undefined, stderr: 'ignore',
|
|
308
|
-
}) : new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: config.headers } });
|
|
309
|
-
const started = Date.now();
|
|
310
|
-
let onAbort;
|
|
311
|
-
const cancellation = new Promise((_, reject) => {
|
|
312
|
-
onAbort = () => { void transport.close(); reject(abort.reason); };
|
|
313
|
-
abort.addEventListener('abort', onAbort, { once: true });
|
|
314
|
-
});
|
|
315
|
-
try {
|
|
316
|
-
if (abort.aborted) throw abort.reason;
|
|
317
|
-
const result = await Promise.race([(async () => { await client.connect(transport); return client.getServerCapabilities()?.tools ? client.listTools({}, { signal: abort, timeout: 15000 }) : { tools: [] }; })(), cancellation]);
|
|
318
|
-
this.probes.set(data.id, { status: 'success', tools: result.tools.length, version: client.getServerVersion()?.version ?? null, icon: safeIcon(client.getServerVersion()?.icons?.[0]?.src), description: String(client.getServerVersion()?.description ?? client.getInstructions() ?? '').slice(0, 8000), durationMs: Date.now() - started, at: new Date().toISOString() });
|
|
319
|
-
} catch { this.probes.set(data.id, { status: 'failed', at: new Date().toISOString(), durationMs: Date.now() - started }); }
|
|
320
|
-
finally { abort.removeEventListener('abort', onAbort); await client.close().catch(() => {}); }
|
|
269
|
+
const validated = await this.mcp.validate(config);
|
|
270
|
+
this.probes.set(data.id, await probeMcp(validated, { signal: AbortSignal.any([this.lifecycle.signal, ...(signal ? [signal] : [])]) }));
|
|
321
271
|
return this.snapshot(data.workspaceId, data.presetId);
|
|
322
272
|
}
|
|
323
273
|
async skillFile(data) {
|