@zaimokuza/dsh-plugin-hub 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1**。支持 Web 与 Desktop、中英文界面;从侧边栏打开,保留当前会话。
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
 
@@ -23,6 +23,8 @@ dsh plugin --profile <当前-profile> add @zaimokuza/dsh-plugin-hub
23
23
 
24
24
  MCP 的描述、版本和图标来自服务初始化信息。宿主未公开持续连接状态时显示“未知”,独立连接测试单独显示结果。建议在启动 DSH 时设置凭据环境变量,在 YAML 中通过 `!!js process.env.MCP_TOKEN` 引用;JSON 等价写法为 `{ "__jsExpr": "process.env.MCP_TOKEN" }`。
25
25
 
26
+ Desktop 实验性功能通过宿主的 `window.dshDesktop.experiments` v1 接口执行,宿主完成空闲检查、配置事务和后端重启。缺少接口的 Desktop 显示不支持管理。
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
 
28
30
  ## 开发与发布
package/lib/client.js CHANGED
@@ -267,7 +267,7 @@ function SkillPreview({ file, t, close }) {
267
267
  }, [file]);
268
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") } })));
269
269
  }
270
- function ResourceHub({ ctx, request, t, brand, panelId }) {
270
+ function ResourceHub({ ctx, request, experiments, t, brand, panelId }) {
271
271
  (0, import_react.useSyncExternalStore)((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot().active);
272
272
  const [tab, setTab] = (0, import_react.useState)("skills");
273
273
  const [scope, setScope] = (0, import_react.useState)("");
@@ -291,9 +291,14 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
291
291
  const refresh = async (signal) => {
292
292
  const token = ++generation.current;
293
293
  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) {
294
+ const result = await request("resources", { workspaceId: scope }, { signal });
295
+ try {
296
+ result.experiments = await experiments.list(result.profile, { signal });
297
+ } catch (error2) {
298
+ result.experiments = [];
299
+ result.experimentsError = error2.message;
300
+ }
301
+ if (mounted.current && token === generation.current && !signal?.aborted) {
297
302
  setData(result);
298
303
  setError("");
299
304
  }
@@ -314,7 +319,7 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
314
319
  const result = await request(route, { ...body, workspaceId: scope, profile: data.profile.directory });
315
320
  if (mounted.current && result.skills) {
316
321
  generation.current++;
317
- setData((previous) => ({ ...result, experiments: previous.experiments }));
322
+ setData((previous) => ({ ...result, experiments: previous.experiments, experimentsError: previous.experimentsError }));
318
323
  }
319
324
  return result;
320
325
  } catch (error2) {
@@ -324,6 +329,30 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
324
329
  if (mounted.current) setBusy(false);
325
330
  }
326
331
  };
332
+ const changeExperiment = async (row, enabled, operation = "toggle") => {
333
+ setBusy(true);
334
+ setError("");
335
+ generation.current++;
336
+ let failure;
337
+ try {
338
+ await experiments.change(data.profile, row, enabled, operation);
339
+ } catch (error2) {
340
+ failure = error2.message;
341
+ } finally {
342
+ if (mounted.current) {
343
+ try {
344
+ const latest = await experiments.list(data.profile);
345
+ if (mounted.current) setData((previous) => ({ ...previous, experiments: latest, experimentsError: void 0 }));
346
+ } catch (error2) {
347
+ failure ??= error2.message;
348
+ }
349
+ if (mounted.current) {
350
+ if (failure) setError(failure);
351
+ setBusy(false);
352
+ }
353
+ }
354
+ }
355
+ };
327
356
  const run = (body, route) => {
328
357
  void action(body, route).catch(() => {
329
358
  });
@@ -348,13 +377,11 @@ function ResourceHub({ ctx, request, t, brand, panelId }) {
348
377
  void refresh().finally(() => {
349
378
  if (mounted.current) setBusy(false);
350
379
  });
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: () => {
380
+ }, 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)), tab === "experiments" && data?.experimentsError && /* @__PURE__ */ import_react.default.createElement("p", { role: "alert" }, t(data.experimentsError)), !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.busy || !row.canToggle, onChange: (enabled) => {
381
+ void changeExperiment(row, enabled);
382
+ } }) : 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.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_react.default.createElement("span", null, t(row.enabled ? "\u914D\u7F6E\u5DF2\u542F\u7528" : "\u914D\u7F6E\u5DF2\u7981\u7528")), !row.supported && /* @__PURE__ */ import_react.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_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", disabled: busy || row.busy, onClick: () => {
383
+ void changeExperiment(row, row.enabled, "install");
384
+ } }, t("\u5B89\u88C5 Agent Teams"))), 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: () => {
358
385
  setBusy(true);
359
386
  void action({ id: row.id }, "skill-file").then(setFile).catch(() => {
360
387
  });
@@ -373,6 +400,17 @@ function HubSidebarButton({ wide, usePanelInfo, ctx, panelId, brand }) {
373
400
 
374
401
  // src/client/locale.js
375
402
  var resourceWords = {
403
+ "\u65E0\u6548\u7684\u5B9E\u9A8C\u6027\u529F\u80FD\u8BF7\u6C42": "Invalid experiment request.",
404
+ "\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7BA1\u7406\u6B64\u5B9E\u9A8C\u6027\u529F\u80FD": "This host does not support managing this experiment.",
405
+ "\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.",
406
+ "\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.",
407
+ "\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.",
408
+ "\u5BBF\u4E3B\u5B9E\u9A8C\u6027\u529F\u80FD\u63A5\u53E3\u8FD4\u56DE\u4E86\u65E0\u6548\u72B6\u6001": "The host experiment API returned an invalid state.",
409
+ "Desktop \u5B9E\u9A8C\u6027\u529F\u80FD\u9700\u901A\u8FC7\u5BBF\u4E3B\u63A5\u53E3\u64CD\u4F5C": "Desktop experiments must be managed through the host API.",
410
+ "\u8FD0\u884C\u72B6\u6001\u672A\u77E5": "Runtime state unknown",
411
+ "\u914D\u7F6E\u5DF2\u542F\u7528": "Configured to enable",
412
+ "\u914D\u7F6E\u5DF2\u7981\u7528": "Configured to disable",
413
+ "\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.",
376
414
  "HTTP \u5FC5\u586B": "Required for HTTP",
377
415
  "HTTP \u8BF7\u6C42\u5934\uFF0C\u53EF\u5F15\u7528\u73AF\u5883\u53D8\u91CF\u3002": "HTTP request headers; environment references are supported.",
378
416
  "Hub profile \u914D\u7F6E\u4E0E\u5F53\u524D\u5B9E\u4F8B\u4E0D\u4E00\u81F4": "Hub's configured profile does not match this instance.",
@@ -545,6 +583,62 @@ function connectionRequest(connection, marketId) {
545
583
  };
546
584
  }
547
585
 
586
+ // src/experiment-definitions.js
587
+ var agentTeams = Object.freeze({
588
+ id: "agent-teams",
589
+ name: "Agent Teams",
590
+ 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"
591
+ });
592
+
593
+ // src/client/experiments.js
594
+ var errors = {
595
+ "invalid-request": "\u65E0\u6548\u7684\u5B9E\u9A8C\u6027\u529F\u80FD\u8BF7\u6C42",
596
+ "profile-changed": "\u5B9E\u4F8B profile \u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0",
597
+ unsupported: "\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7BA1\u7406\u6B64\u5B9E\u9A8C\u6027\u529F\u80FD",
598
+ busy: "\u5B9E\u9A8C\u6027\u529F\u80FD\u6B63\u5728\u66F4\u65B0",
599
+ "state-changed": "\u5B9E\u9A8C\u6027\u529F\u80FD\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5",
600
+ "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",
601
+ failed: "\u5B9E\u9A8C\u6027\u529F\u80FD\u5207\u6362\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u786E\u8BA4\u5F53\u524D\u72B6\u6001"
602
+ };
603
+ function desktopBridge(desktop) {
604
+ const bridge = desktop?.experiments;
605
+ return bridge?.version === 1 && typeof bridge.list === "function" && typeof bridge.setEnabled === "function" ? bridge : null;
606
+ }
607
+ function validateSnapshot(snapshot, profile) {
608
+ if (snapshot?.profile !== profile.directory) throw new Error("\u5B9E\u4F8B profile \u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0");
609
+ if (!Array.isArray(snapshot.features)) throw new Error("\u5BBF\u4E3B\u5B9E\u9A8C\u6027\u529F\u80FD\u63A5\u53E3\u8FD4\u56DE\u4E86\u65E0\u6548\u72B6\u6001");
610
+ const rows = snapshot.features.filter((row) => row?.id === "agent-teams");
611
+ 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");
612
+ return rows;
613
+ }
614
+ function experimentClient(request, desktop, reload) {
615
+ return {
616
+ async list(profile, options = {}) {
617
+ if (profile.installation !== "desktop") return request("experiments", {}, options);
618
+ const baseline = [{ ...agentTeams, enabled: null, activeEnabled: null, installed: false, busy: false, pendingRestart: false }];
619
+ const bridge = desktopBridge(desktop);
620
+ const native = bridge ? validateSnapshot(await bridge.list(), profile) : [];
621
+ return baseline.map((row) => {
622
+ const feature = native.find((item) => item.id === row.id);
623
+ return feature ? { ...row, ...feature, canInstall: false, supported: true, pendingRestart: feature.activeEnabled !== null && feature.enabled !== feature.activeEnabled } : { ...row, canToggle: false, canInstall: false, supported: false };
624
+ });
625
+ },
626
+ async change(profile, row, enabled, action = "toggle") {
627
+ if (profile.installation !== "desktop") return request("experiment-mutate", { profile: profile.directory, id: row.id, action, enabled, expectedEnabled: row.enabled });
628
+ const bridge = desktopBridge(desktop);
629
+ 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");
630
+ const result = await bridge.setEnabled({ profile: profile.directory, id: row.id, enabled, expectedEnabled: row.enabled });
631
+ if (result?.ok === false) {
632
+ throw new Error(errors[result.error?.code] ?? errors.failed);
633
+ }
634
+ 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");
635
+ validateSnapshot(result.value, profile);
636
+ if (result.reloadRequired) reload();
637
+ return result.value;
638
+ }
639
+ };
640
+ }
641
+
548
642
  // src/client/index.jsx
549
643
  var NS = "@zaimokuza/dsh-plugin-hub";
550
644
  var MARKET_ID = "hub";
@@ -556,8 +650,9 @@ function apply(ctx) {
556
650
  ctx.effect(() => ctx.locale.register(NS, words), "Plugin Hub: dictionaries");
557
651
  const t = translate(ctx.locale.bind(NS));
558
652
  const request = connectionRequest(ctx.connection, MARKET_ID);
653
+ const experiments = experimentClient(request, window.dshDesktop, () => window.location.reload());
559
654
  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 })));
655
+ ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: panelId, locale: NS }, () => /* @__PURE__ */ import_react2.default.createElement(ResourceHub, { ctx, request, experiments, t, brand: BRAND, panelId })));
561
656
  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 })));
562
657
  }
563
658
  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.1",
4
4
  "private": false,
5
5
  "description": "Native DSH resource hub: Skills, MCP, third-party plugins and experiments",
6
6
  "type": "module",
@@ -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 });