@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 CHANGED
@@ -1,11 +1,11 @@
1
1
  # DSH Plugin Hub
2
2
 
3
- DSH 原生资源面板,当前版本 **0.2.0**。支持 Web 与 Desktop、中英文界面;从侧边栏打开,保留当前会话。
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 支持安装与启停,Desktop 通过原生管理窗口操作。
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`;已验证 DSH `0.1.5-rc.1` `0.1.5-rc.2`。
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 的描述、版本和图标来自服务初始化信息。宿主未公开持续连接状态时显示“未知”,独立连接测试单独显示结果。建议在启动 DSH 时设置凭据环境变量,在 YAML 中通过 `!!js process.env.MCP_TOKEN` 引用;JSON 等价写法为 `{ "__jsExpr": "process.env.MCP_TOKEN" }`。
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
 
@@ -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/sdk and yaml, and build dependencies esbuild and React retain their own licenses. Their precise versions are recorded in package-lock.json.
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 import_react2 = __toESM(require("react"), 1);
43
+ var import_react5 = __toESM(require("react"), 1);
44
44
 
45
45
  // src/client/resources.jsx
46
- var import_react = __toESM(require("react"), 1);
47
- var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
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-help.js
123
- var mcpFields = [
124
- ["serverName", "\u5DE5\u5177\u547D\u540D\u7A7A\u95F4\uFF0C1\u201332 \u4F4D\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u6216\u8FDE\u5B57\u7B26\u3002", "\u5FC5\u586B"],
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/resources.jsx
144
- function ItemIcon({ row }) {
145
- const [failed, setFailed] = (0, import_react.useState)(false);
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, import_react.useState)("json");
185
- const [help, setHelp] = (0, import_react.useState)(false);
186
- const [text, setText] = (0, import_react.useState)(JSON.stringify({ serverName: "my_server", transport: "stdio", command: "npx", args: ["-y", "mcp-server-package"] }, null, 2));
187
- const [loading, setLoading] = (0, import_react.useState)(Boolean(row));
188
- const [error, setError] = (0, import_react.useState)("");
189
- const [ready, setReady] = (0, import_react.useState)(!row);
190
- (0, import_react.useEffect)(() => {
191
- if (!row) return;
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
- request("mcp-config", { id: row.id, revision: row.revision, profile, format: "json" }, { signal: controller.signal }).then((result) => {
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
- setText(result.text);
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) setError(error2.message);
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__ */ import_react.default.createElement(
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__ */ import_react.default.createElement("div", { className: "hub-resource-actions" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", disabled: blocked, onClick: close }, t("\u53D6\u6D88")), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", variant: "primary", type: "submit", form: "hub-mcp-editor", disabled: blocked || !ready }, t("\u4FDD\u5B58")))
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__ */ import_react.default.createElement("div", { className: "hub-editor-columns" }, /* @__PURE__ */ import_react.default.createElement("form", { id: "hub-mcp-editor", className: "hub-resource-form", onSubmit: submit }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-actions", role: "group", "aria-label": t("\u914D\u7F6E\u683C\u5F0F") }, ["json", "yaml"].map((value) => /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { key: value, size: "md", className: "hub-resource-tab", "aria-pressed": format === value, disabled: blocked || !ready, onClick: () => {
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__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", disabled: blocked || !ready, onClick: () => {
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__ */ import_react.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_react.default.createElement("code", null, "!!js process.env.MCP_TOKEN"), t("\uFF1BJSON \u4F7F\u7528"), " ", /* @__PURE__ */ import_react.default.createElement("code", null, '{"__jsExpr":"process.env.MCP_TOKEN"}'), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", "aria-expanded": help, onClick: () => setHelp(!help) }, t("\u5E2E\u52A9"))), loading && /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, t("\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E")), /* @__PURE__ */ import_react.default.createElement("label", { className: "hub-resource-field" }, t("MCP \u914D\u7F6E"), /* @__PURE__ */ import_react.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_react.default.createElement("p", { role: "alert" }, t(error))), help && /* @__PURE__ */ import_react.default.createElement("aside", { className: "hub-editor-help" }, /* @__PURE__ */ import_react.default.createElement("h3", null, t("MCP \u914D\u7F6E\u5E2E\u52A9")), /* @__PURE__ */ import_react.default.createElement("p", null, "macOS / Linux: ", /* @__PURE__ */ import_react.default.createElement("code", null, "export MCP_TOKEN=\u2026"), /* @__PURE__ */ import_react.default.createElement("br", null), "Windows PowerShell: ", /* @__PURE__ */ import_react.default.createElement("code", null, "$env:MCP_TOKEN=\u2026")), /* @__PURE__ */ import_react.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_react.default.createElement("dl", null, mcpFields.map(([key, description, fallback]) => /* @__PURE__ */ import_react.default.createElement("div", { key }, /* @__PURE__ */ import_react.default.createElement("dt", null, /* @__PURE__ */ import_react.default.createElement("code", null, key)), /* @__PURE__ */ import_react.default.createElement("dd", null, t(description), /* @__PURE__ */ import_react.default.createElement("span", null, t(fallback.includes("\u5FC5\u586B") ? "\u8981\u6C42" : "\u9ED8\u8BA4\u503C"), " \xB7 ", fallback.includes("\u5FC5\u586B") ? t(fallback) : fallback)))))))
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__ */ import_react.default.createElement("div", { className: "hub-mcp-summary" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-mcp-statuses" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Tooltip, { label: `${t("\u52A0\u8F7D\u72B6\u6001")} \xB7 ${loaded}` }, /* @__PURE__ */ import_react.default.createElement("span", { tabIndex: 0 }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement("span", { tabIndex: 0 }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Tag, { tone: "neutral" }, t("\u8FDE\u63A5\u672A\u77E5"))))), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Tooltip, { label: `${row.transport}${endpoint ? " \xB7 " + endpoint : ""}` }, /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-mcp-endpoint", tabIndex: 0 }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Tag, { tone: "neutral" }, row.transport === "streamable-http" ? "HTTP" : row.transport === "unknown" ? t("\u672A\u77E5") : row.transport), endpointLabel && /* @__PURE__ */ import_react.default.createElement("code", null, endpointLabel))));
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, import_react.useRef)(null);
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, import_react.useEffect)(() => {
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__ */ import_react.default.createElement("aside", { className: "hub-skill-view", ref: view, "aria-label": title }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-card-heading" }, /* @__PURE__ */ import_react.default.createElement("h2", { title }, title), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", onClick: close, icon: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconCloseOutline16, null) }, t("\u5173\u95ED"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-skill-markdown" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.MarkdownText, { text: body, labels: { code: { copyLabel: t("\u590D\u5236"), copiedLabel: t("\u5DF2\u590D\u5236") }, footnotes: t("\u811A\u6CE8") } })));
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, import_react.useSyncExternalStore)((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot().active);
272
- const [tab, setTab] = (0, import_react.useState)("skills");
273
- const [scope, setScope] = (0, import_react.useState)("");
274
- const [file, setFile] = (0, import_react.useState)(null);
275
- const [desktopHelp, setDesktopHelp] = (0, import_react.useState)(false);
276
- const [data, setData] = (0, import_react.useState)(null);
277
- const [query, setQuery] = (0, import_react.useState)("");
278
- const [error, setError] = (0, import_react.useState)("");
279
- const [busy, setBusy] = (0, import_react.useState)(false);
280
- const [editor, setEditor] = (0, import_react.useState)(null);
281
- const [deleting, setDeleting] = (0, import_react.useState)(null);
282
- const generation = (0, import_react.useRef)(0);
283
- const mounted = (0, import_react.useRef)(true);
284
- (0, import_react.useEffect)(() => {
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 [result, experiments] = await Promise.all([request("resources", { workspaceId: scope }, { signal }), request("experiments", {}, { signal })]);
295
- result.experiments = experiments;
296
- if (mounted.current && token === generation.current) {
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, import_react.useEffect)(() => {
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__ */ import_react.default.createElement("section", { className: `hub-resource-page ${file ? "with-file" : ""}`, "aria-label": brand.title }, /* @__PURE__ */ import_react.default.createElement("style", null, market_default, resources_default), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-market hub-resource-content", style: { "--red": brand.primaryColor } }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-topline" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-brand" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-brandmark" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconCordisPluginOutline14, { size: 17 })), brand.title, /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-divider" }), brand.subTitle), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", onClick: () => ctx.layout.selectPanel(null), icon: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconCloseOutline16, null) }, t("\u8FD4\u56DE\u4F1A\u8BDD"))), /* @__PURE__ */ import_react.default.createElement("header", { className: "hub-header" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-eyebrow" }, "SKILLS \xB7 MCP \xB7 PLUGINS"), /* @__PURE__ */ import_react.default.createElement("h1", null, t("\u8D44\u6E90\u7BA1\u7406"), /* @__PURE__ */ import_react.default.createElement("span", null, ".")), /* @__PURE__ */ import_react.default.createElement("p", null, t("\u5728\u5F53\u524D\u5B9E\u4F8B\u4E2D\u7BA1\u7406 Skill\u3001MCP \u548C\u63D2\u4EF6\u3002"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-header-art", "aria-hidden": "true" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconCordisPluginOutline14, { size: 38 }), /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-art-plus" }, "+"), /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-art-dot" }))), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-profile" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Pill, null, t("\u5F53\u524D profile"), " \xB7 ", data?.profile.name ?? "\u2026"), /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-resource-path", title: data?.profile.directory }, data?.profile.directory ?? "\u2026")), /* @__PURE__ */ import_react.default.createElement("div", { role: "tablist", className: "hub-tabs", "aria-label": brand.title, onKeyDown: (event) => {
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__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement("span", { className: "hub-tab-count" }, data?.[row.id]?.length ?? "\u2014")))), /* @__PURE__ */ import_react.default.createElement("div", { role: "tabpanel", id: `${panelId}-content`, "aria-labelledby": `${panelId}-${tab}` }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-toolbar" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Input, { className: "hub-search-input", icon: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement(ScopePicker, { data, scope, setScope, t, disabled: busy }), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", disabled: busy, onClick: () => {
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__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconRefreshOutline16, null) }, t("\u5237\u65B0")), tab === "plugins" && data?.profile.installation === "desktop" && /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { onClick: () => setDesktopHelp(true) }, t("\u684C\u9762\u63D2\u4EF6\u7BA1\u7406")), tab === "mcps" && /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "md", variant: "primary", disabled: !data || busy, onClick: () => setEditor({}) }, t("\u6DFB\u52A0 MCP"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-results" }, /* @__PURE__ */ import_react.default.createElement("span", null, t("\u5F53\u524D\u5217\u8868"), " ", /* @__PURE__ */ import_react.default.createElement("b", null, rows.length)), /* @__PURE__ */ import_react.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_react.default.createElement("p", { role: "alert" }, t(error)), !data && !error && /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, t("\u6B63\u5728\u8BFB\u53D6\u8D44\u6E90")), data && tab === "skills" && !data.skillsComplete && /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, t("\u8D44\u6E90\u5C1A\u672A\u5B8C\u6574\u52A0\u8F7D\uFF0C\u8BF7\u5237\u65B0\u91CD\u8BD5")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-grid hub-resource-grid" }, rows.map((row) => /* @__PURE__ */ import_react.default.createElement("article", { key: row.id ?? row.name, className: "hub-card hub-resource-card" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-card-heading" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-resource-identity" }, /* @__PURE__ */ import_react.default.createElement(ItemIcon, { row }), /* @__PURE__ */ import_react.default.createElement("h2", { className: "hub-resource-card-name", title: row.name }, row.name, (tab === "mcps" || tab === "plugins") && /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-resource-version" }, "@", row.version || t("\u672A\u77E5")))), tab !== "plugins" && tab !== "experiments" ? /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement(import_dsh_client_ui_primitives.Switch, { checked: Boolean(row.enabled), label: `${t(row.enabled ? "\u7981\u7528" : "\u542F\u7528")} ${row.name}`, disabled: busy || !row.canToggle, onChange: (enabled) => {
352
- setBusy(true);
353
- void request("experiment-mutate", { id: row.id, enabled, profile: data.profile.directory }).then(() => refresh()).catch((error2) => setError(error2.message)).finally(() => setBusy(false));
354
- } }) : null), tab === "mcps" && /* @__PURE__ */ import_react.default.createElement(McpSummary, { row, t, activation }), /* @__PURE__ */ import_react.default.createElement("p", { className: "hub-description", title: tab === "experiments" ? t(row.description) : row.description }, (tab === "experiments" ? t(row.description) : row.description) || t(tab === "mcps" ? "\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_react.default.createElement(import_react.default.Fragment, null, row.probe && /* @__PURE__ */ import_react.default.createElement("p", { className: "hub-resource-meta" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Pill, null, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement("div", { className: "hub-resource-actions" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Pill, null, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.StateDot, { state: row.pendingRestart ? "warning" : row.enabled ? "done" : "idle" }), t(row.pendingRestart ? "\u91CD\u542F DSH \u540E\u751F\u6548" : row.enabled === null ? "\u672A\u77E5" : row.enabled ? "\u5DF2\u542F\u7528" : "\u5DF2\u7981\u7528")), !row.installed && /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: busy || !row.canInstall, onClick: () => {
355
- setBusy(true);
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__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: !row.directory || busy, onClick: () => run({ id: row.id }, "open-directory"), icon: /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.IconFolderOpenOutline16, null) }, t("\u6253\u5F00\u76EE\u5F55"))), tab === "mcps" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: busy || !row.enabled, onClick: () => run({ id: row.id, revision: row.revision }, "mcp-test") }, t("\u6D4B\u8BD5\u8FDE\u63A5"))), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: busy, onClick: () => setEditor(row) }, t("\u7F16\u8F91")), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.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_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: busy || !row.canDelete, onClick: () => setDeleting(row) }, t("\u5220\u9664"))))), /* @__PURE__ */ import_react.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_react.default.createElement("p", { className: "hub-resource-empty" }, t("\u672A\u627E\u5230\u5339\u914D\u8D44\u6E90")))), file && /* @__PURE__ */ import_react.default.createElement(SkillPreview, { key: file.path, file, t, close: () => setFile(null) }), /* @__PURE__ */ import_react.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_react.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_react.default.createElement("kbd", null, data?.profile.platform === "darwin" ? "\u2318 ," : "Ctrl+,"))), editor && /* @__PURE__ */ import_react.default.createElement(McpEditor, { request, profile: data.profile.directory, row: editor.id ? editor : null, t, busy, close: () => setEditor(null), save: async (body) => {
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__ */ import_react.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_react.default.createElement("div", { className: "hub-resource-actions" }, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { disabled: busy, onClick: () => setDeleting(null) }, t("\u53D6\u6D88")), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: busy, onClick: () => {
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__ */ import_react.default.createElement("p", null, deleting?.name), /* @__PURE__ */ import_react.default.createElement("p", null, t("\u5220\u9664\u4F1A\u65AD\u5F00\u8BE5 MCP\uFF0C\u5E76\u79FB\u9664\u5BF9\u5E94\u7684\u914D\u7F6E\u6761\u76EE\u3002"))));
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
- "HTTP \u5FC5\u586B": "Required for HTTP",
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__ */ import_react2.default.createElement(ResourceHub, { ctx, request, t, brand: BRAND, panelId })));
561
- ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({ name: "sidebar.footer.action", id: panelId, order: 39, locale: NS }, (props) => /* @__PURE__ */ import_react2.default.createElement(HubSidebarButton, { ...props, ctx, panelId, brand: BRAND })));
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.0",
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-sidebar"
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/sdk": "1.30.0",
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
  }
@@ -0,0 +1,6 @@
1
+ /** Shared presentation metadata; each host supplies its own capabilities and state. */
2
+ export const agentTeams = Object.freeze({
3
+ id: 'agent-teams',
4
+ name: 'Agent Teams',
5
+ description: '实验性多 Agent 协作工具与团队面板。Web 切换后需重启 DSH;支持管理的 Desktop 会自动重启后端,会话数据保留。',
6
+ });
@@ -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 = null; this.busy = false; }
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 ??= enabled;
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 [{ id: 'agent-teams', name: 'Agent Teams', description: '实验性多 Agent 协作工具与团队面板。切换后需重启 DSH,会话数据保留。', installed, enabled, pendingRestart: this.initial !== enabled, canToggle: this.environment.installation === 'cli' && installed, canInstall: this.environment.installation === 'cli', busy: this.busy }];
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('请通过 Desktop 原生插件管理窗口切换 Agent Teams');
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
+ }
@@ -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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
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.ctx.loader.import(MCP);
154
- this.fibers.set(row.id, this.ctx.plugin(module, validateMcp(resolveEnvironment(row.config))));
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
- validateMcp(resolveEnvironment(raw));
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 client = new Client({ name: 'dsh-plugin-hub-probe', version: '1.0.0' });
304
- const abort = AbortSignal.any([this.lifecycle.signal, signal ?? new AbortController().signal, AbortSignal.timeout(15000)]);
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) {