dsh-mcp-connector 0.2.24 → 0.2.25

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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.25] - 2026-08-27
8
+
9
+ ### Added
10
+
11
+ - 新增 Update Provider 适配层,将版本发现与安装执行解耦;UI 通过统一能力调用更新检查、任务、回滚和重启,为 DSH 原生或其他插件市场预留适配边界。
12
+ - 首个 DSH Market API v1 Provider 适配器:能力探测通过后可一键更新插件,并轮询展示安装进度、稳定失败码、兼容回滚结果及按 Web/Desktop 宿主能力提供的刷新或重启操作。
13
+
14
+ ### Compatibility
15
+
16
+ - Provider 未安装、协议不兼容、广告跨源/无效端点或版本检查不一致时不调用私有更新路由,继续使用现有“前往插件市场”/npm 页面降级路径。
17
+
18
+ ### Verification
19
+
20
+ - 125 项自动测试、lint、npm 发布包白名单/敏感内容扫描通过;已在 web profile 的 dshmarket 1.33.0 上验证 `/dsh-market/api/v1/capabilities` 与 Connector 版本检查响应。
21
+
7
22
  ## [0.2.24] - 2026-08-27
8
23
 
9
24
  ### Added
@@ -399,7 +414,8 @@
399
414
  - 外部 URL 与导入 Header 执行安全校验。
400
415
  - iframe 消息校验同源和消息来源。
401
416
 
402
- [Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.24...HEAD
417
+ [Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.25...HEAD
418
+ [0.2.25]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.24...v0.2.25
403
419
  [0.2.24]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.23...v0.2.24
404
420
  [0.2.23]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.22...v0.2.23
405
421
  [0.2.22]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.21...v0.2.22
package/README.en.md CHANGED
@@ -28,7 +28,7 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
28
28
  - Persistent connection lifecycle management: restore on restart, enable/disable, disconnect, retry transient OAuth refresh failures with bounded backoff, and revoke authorization. Cards declaring issuer-level sharing reuse one grant; DCR client secrets remain local to that grant.
29
29
  - Built-in, remote, and local catalogs with `published` and `featured` controls.
30
30
  - A standalone remote Registry, allowing new marketplace cards to appear after refresh without publishing a new npm version.
31
- - Plugin version awareness: the MCP Connector title shows the installed version, while a server-side cached check compares npm `latest` with GitHub Releases and routes available updates through the DSH plugin marketplace.
31
+ - Plugin version and one-click updates: version discovery is independent of the installation source, while an Update Provider adapter layer negotiates safe mutation capabilities. DSH Market API v1 is the first adapter and supports progress, normalized failures, rollback, and capability-gated restart/refresh actions; without a compatible provider, the UI falls back to the current plugin marketplace or npm.
32
32
  - Explicit, non-destructive migration from the two earlier Qichacha OAuth plugins, plus active-plugin conflict detection that blocks duplicate server management and credential overwrites.
33
33
 
34
34
  <!-- catalog-stats:start -->
package/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
  - 生命周期管理:连接持久化、重启恢复、启停、断开、OAuth 自动刷新/退避恢复与撤销;同 issuer 卡片可共享一次授权,DCR 返回的客户端密钥与 Token 一同只保存在本机。
29
29
  - 目录运营:内置目录、远程 registry、本地覆盖,支持 `published` 上下架与 `featured` 精选。
30
30
  - 独立远程 Registry:新市场卡片合并后客户端刷新即可见,无需重新发布 npm;远程不可用时自动回退内置目录。
31
- - 插件版本感知:MCP连接器标题显示当前版本,服务端缓存检查 npm 与 GitHub Release;发现 npm 新版本时引导到 DSH 插件市场更新。
31
+ - 插件版本与一键更新:版本发现独立于安装来源;页面通过 Update Provider 适配层探测安全更新能力。DSH Market API v1 是首个适配器,支持进度、稳定失败码、回滚及按宿主能力提供的重启/刷新操作;无可用 Provider 时回退到当前插件市场或 npm。
32
32
  - Registry 工具链:Schema/唯一性/密钥审计、MCP/OAuth 无凭据探针、每周健康巡检。
33
33
  - 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;检测到旧插件仍启用并管理同名 Server 时阻断重复连接,避免凭据相互覆盖。
34
34
  - 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`health_check`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
@@ -0,0 +1,34 @@
1
+ # MCP连接器插件更新协议
2
+
3
+ MCP连接器把“刷新连接器目录”和“升级插件代码”视为两个独立动作:前者只重新拉取 Registry 卡片,后者必须由当前 DSH 宿主中可用的安全 Update Provider 执行。
4
+
5
+ ## 分层与 Provider 适配
6
+
7
+ 版本发现与更新执行彻底分离:
8
+
9
+ - MCP连接器自己的 Host 检查 npm `latest` 与 GitHub Release,所有安装方式都可显示已安装版本和新版本提示。
10
+ - 客户端通过 `UPDATE_PROVIDER_ADAPTERS` 按顺序探测 Provider。UI 只调用统一的 `probe` / `check` / `start` / `operation` / `rollback` / `restart` 能力,不持有市场私有路由。
11
+ - DSH Market API v1 是首个 Provider 适配器。后续可在不改动更新 UI 状态机的前提下增加 DSH 原生能力或其他插件市场的版本化协议适配器。
12
+
13
+ ## 能力协商
14
+
15
+ 每个 Provider 适配器只硬编码自己的 capabilities 入口,其余更新、任务、回滚和重启端点均从能力响应中读取。客户端只接受当前 DSH 页面的相对同源端点,拒绝跨域 URL、协议不匹配、缺少必要端点或未声明 `features.update` 的 Provider。
16
+
17
+ DSH Market 适配器从 `GET /dsh-market/api/v1/capabilities` 开始,要求 `schema: dsh-market/update-api/v1` 与 `apiVersion: 1`。未安装任何可用 Provider、接口返回 404、协议不兼容或检查失败时,页面不会尝试私有更新路由,而是回退到“前往插件市场”;设置入口不可用时再打开 npm 包页面。
18
+
19
+ ## 更新流程
20
+
21
+ 1. 插件自己的服务端缓存检查 npm `latest` 与 GitHub Release,只在 npm 已有可安装新版本时提示升级。
22
+ 2. 选中的 Provider 再检查本 profile 中 `dsh-mcp-connector` 的实际安装版本和目标版本。
23
+ 3. 用户点击“一键更新”后,Provider 立即返回 `operationId`;页面通过适配器轮询任务状态,展示排队、解析、下载和安装进度。
24
+ 4. 成功后按 Provider 返回的激活结论展示“刷新生效”或重启提示。Web 且 Provider 明确允许进程重启时才展示“立即重启”;Desktop 等由外壳管理生命周期的宿主只提示用户重启,不自行杀进程。
25
+ 5. Provider 保留恢复点时展示“回滚”。回滚仍由 Provider 执行,MCP连接器不直接修改 profile、lockfile 或 `node_modules`。
26
+
27
+ 失败结果使用稳定代码区分正在运行的 Agent、其他安装任务占用、镜像同步、版本未变化、超时和权限拒绝。可重试失败保留重试操作;Registry 刚发布但镜像仍未同步时,用户可在看到明确提示后选择强制重试。
28
+
29
+ ## 安全边界
30
+
31
+ - 所有变更请求均为浏览器同源 POST,并继续接受 Provider 自身的 Origin、环回地址和运行中任务保护。
32
+ - MCP连接器不执行 `npm`/`pnpm`、不保存回滚 Token,也不复制任何 Provider 的安装算法。
33
+ - 重启按钮完全服从能力响应;接口未声明支持时不得构造其他进程控制路径。
34
+ - 更新任务属于当前 Host 进程。Host 已更换后,旧页面的任务编号自然失效,避免把旧进度误认为新进程状态。
@@ -42,7 +42,7 @@ dsh web
42
42
  - **已安装**:查看已经保存到本机的连接,页签徽标显示连接数。
43
43
  - **搜索**:按名称、服务商、简介和标签查找。
44
44
  - **添加连接**:导入 JSON、手动配置 HTTP/stdio,或从市场描述 URL 安装。
45
- - **版本与更新提示**:标题旁显示当前插件版本。检测到 npm 有新版本时,点击更新提示前往 DSH 插件市场执行安全升级;升级后按页面提示完全重启 DSH。
45
+ - **版本与更新提示**:标题旁显示当前插件版本,版本检查不依赖安装所用的插件市场。检测到 npm 有新版本且当前宿主存在兼容 Update Provider 时,可在当前页面一键更新并查看进度、失败原因和可用回滚。DSH Market API v1 是当前首个 Provider;无可用 Provider 时自动回退为“前往插件市场”或 npm 页面。
46
46
  - **刷新连接器目录**:重新拉取 Registry 卡片,并更新连接健康状态;该操作不会升级 MCP连接器插件本身。
47
47
 
48
48
  ## 3. 浏览市场与分类
package/lib/client.js CHANGED
@@ -22,6 +22,17 @@ window.__ModuleLoader__.load({
22
22
  const VERSION_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
23
23
  const VERSION_CHECK_RETRY_MS = 5 * 60 * 1e3;
24
24
  const NPM_PACKAGE_URL = "https://www.npmjs.com/package/dsh-mcp-connector";
25
+ const PLUGIN_PACKAGE_NAME = "dsh-mcp-connector";
26
+ const UPDATE_PROVIDER_OPERATION_POLL_MS = 1e3;
27
+ const UPDATE_PROVIDER_ADAPTERS = Object.freeze([
28
+ createHttpUpdateProviderAdapter({
29
+ id: "dsh-market-v1",
30
+ label: "DSH Market",
31
+ schema: "dsh-market/update-api/v1",
32
+ apiVersion: 1,
33
+ capabilitiesUrl: "/dsh-market/api/v1/capabilities"
34
+ })
35
+ ]);
25
36
 
26
37
  /**
27
38
  * Desktop 内置的社区插件市场也注册在 sidebar.footer.action。
@@ -114,6 +125,37 @@ window.__ModuleLoader__.load({
114
125
  outline: 2px solid #6366f1;
115
126
  outline-offset: 1px;
116
127
  }
128
+ .mcpConnectorUpdateButton:disabled {
129
+ opacity: 0.65;
130
+ cursor: wait;
131
+ }
132
+ .mcpConnectorUpdateControls {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 6px;
136
+ min-width: 0;
137
+ }
138
+ .mcpConnectorUpdateStatus {
139
+ max-width: 240px;
140
+ overflow: hidden;
141
+ text-overflow: ellipsis;
142
+ white-space: nowrap;
143
+ color: #166534;
144
+ background: #f0fdf4;
145
+ border-radius: 999px;
146
+ padding: 3px 8px;
147
+ font-size: 12px;
148
+ font-weight: 600;
149
+ }
150
+ .mcpConnectorUpdateStatus[data-tone="error"] {
151
+ color: #b91c1c;
152
+ background: #fef2f2;
153
+ }
154
+ .mcpConnectorUpdateSecondary {
155
+ color: #4b5563;
156
+ background: transparent;
157
+ border-color: #d1d5db;
158
+ }
117
159
  .mcpConnectorMarketClose { color: #6b7280; }
118
160
  .mcpConnectorMarketClose:hover,
119
161
  .mcpConnectorMarketClose:focus-visible {
@@ -131,6 +173,9 @@ window.__ModuleLoader__.load({
131
173
  .mcpConnectorUpdateButton { color: #c7d2fe; background: #312e81; border-color: #4f46e5; }
132
174
  .mcpConnectorUpdateButton:hover,
133
175
  .mcpConnectorUpdateButton:focus-visible { background: #3730a3; }
176
+ .mcpConnectorUpdateStatus { color: #bbf7d0; background: #14532d; }
177
+ .mcpConnectorUpdateStatus[data-tone="error"] { color: #fecaca; background: #7f1d1d; }
178
+ .mcpConnectorUpdateSecondary { color: #d1d5db; background: transparent; border-color: #4b5563; }
134
179
  .mcpConnectorMarketClose { color: #c4c8d0; }
135
180
  .mcpConnectorMarketClose:hover,
136
181
  .mcpConnectorMarketClose:focus-visible { background: #30343d !important; color: #ffffff; }
@@ -180,6 +225,151 @@ window.__ModuleLoader__.load({
180
225
  return result.detail;
181
226
  }
182
227
 
228
+ /**
229
+ * 更新 Provider 只能广告当前 DSH 页面的同源端点。这个边界既避免
230
+ * Connector 跨域传递安装指令,也使后续市场适配器可复用同一客户端。
231
+ */
232
+ function sameOriginProviderEndpoint(value) {
233
+ if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) return null;
234
+ try {
235
+ const url = new URL(value, window.location.origin);
236
+ if (url.origin !== window.location.origin || url.search !== "" || url.hash !== "") return null;
237
+ return url.pathname;
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * 把一个版本化的同源 HTTP 更新协议适配为 Connector 内部通用能力。
245
+ * UI 不接触供应商路由或响应封装;新市场只需新增适配器。
246
+ */
247
+ function createHttpUpdateProviderAdapter(options) {
248
+ const { id, label, schema, apiVersion, capabilitiesUrl } = options;
249
+ async function readResponse(response) {
250
+ let result;
251
+ try {
252
+ result = await response.json();
253
+ } catch {
254
+ result = null;
255
+ }
256
+ if (!response.ok) {
257
+ const error = new Error(result?.error ?? result?.failure?.message ?? `HTTP ${response.status}`);
258
+ error.code = result?.failure?.code ?? `HTTP_${response.status}`;
259
+ error.retryable = result?.failure?.retryable === true;
260
+ throw error;
261
+ }
262
+ if (result?.schema !== schema) throw new Error(`${label} 更新接口版本不兼容`);
263
+ return result;
264
+ }
265
+
266
+ async function post(path, body) {
267
+ return readResponse(await window.fetch(path, {
268
+ method: "POST",
269
+ headers: { accept: "application/json", "content-type": "application/json" },
270
+ body: JSON.stringify(body)
271
+ }));
272
+ }
273
+
274
+ return Object.freeze({
275
+ id,
276
+ label,
277
+ async probe() {
278
+ const response = await window.fetch(capabilitiesUrl, { headers: { accept: "application/json" } });
279
+ if (response.status === 404) return null;
280
+ const result = await readResponse(response);
281
+ if (result.apiVersion !== apiVersion || result.features?.update !== true) return null;
282
+ const endpoints = {
283
+ updates: sameOriginProviderEndpoint(result.endpoints?.updates),
284
+ operations: sameOriginProviderEndpoint(result.endpoints?.operations),
285
+ rollback: sameOriginProviderEndpoint(result.endpoints?.rollback),
286
+ restart: sameOriginProviderEndpoint(result.endpoints?.restart)
287
+ };
288
+ if (endpoints.updates === null || endpoints.operations === null) return null;
289
+ if (result.features?.rollback === true && endpoints.rollback === null) return null;
290
+ if (result.restart?.supported === true && endpoints.restart === null) return null;
291
+ return {
292
+ providerId: id,
293
+ providerLabel: label,
294
+ runtime: result.runtime,
295
+ features: result.features,
296
+ restart: result.restart,
297
+ endpoints
298
+ };
299
+ },
300
+ async check(capabilities, packageName, force = false) {
301
+ const query = new URLSearchParams({ name: packageName });
302
+ if (force) query.set("force", "1");
303
+ const result = await readResponse(await window.fetch(`${capabilities.endpoints.updates}?${query}`, {
304
+ headers: { accept: "application/json" }
305
+ }));
306
+ return result.package;
307
+ },
308
+ async start(capabilities, packageName, force = false) {
309
+ const result = await post(capabilities.endpoints.updates, {
310
+ packageName,
311
+ ...(force ? { force: true } : {})
312
+ });
313
+ if (typeof result.operation?.operationId !== "string") {
314
+ throw new Error(`${label} 未返回更新任务编号`);
315
+ }
316
+ return result.operation;
317
+ },
318
+ async operation(capabilities, operationId) {
319
+ const query = new URLSearchParams({ operationId });
320
+ const result = await readResponse(await window.fetch(`${capabilities.endpoints.operations}?${query}`, {
321
+ headers: { accept: "application/json" }
322
+ }));
323
+ return result.operation;
324
+ },
325
+ async rollback(capabilities, operationId) {
326
+ if (capabilities.endpoints.rollback === null) throw new Error(`${label} 不支持回滚`);
327
+ const result = await post(capabilities.endpoints.rollback, { operationId });
328
+ return result.operation;
329
+ },
330
+ async restart(capabilities) {
331
+ if (capabilities.endpoints.restart === null) throw new Error(`${label} 不支持重启`);
332
+ return post(capabilities.endpoints.restart, {});
333
+ }
334
+ });
335
+ }
336
+
337
+ async function discoverUpdateProvider(packageName, force = false) {
338
+ for (const provider of UPDATE_PROVIDER_ADAPTERS) {
339
+ try {
340
+ const capabilities = await provider.probe();
341
+ if (capabilities === null) continue;
342
+ const update = await provider.check(capabilities, packageName, force);
343
+ return { provider, capabilities, update };
344
+ } catch (error) {
345
+ console.warn(`[mcp-connector] ${provider.id} update provider unavailable:`, error);
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+
351
+ function updateProgressLabel(operation) {
352
+ if (operation?.state === "queued") return "更新任务排队中…";
353
+ const percent = operation?.progress?.percent;
354
+ if (typeof percent === "number") return `正在更新 ${percent}%`;
355
+ const phase = operation?.progress?.phase;
356
+ if (typeof phase === "string" && phase !== "") return `正在更新:${phase}`;
357
+ return "正在更新…";
358
+ }
359
+
360
+ function updateFailureLabel(failure) {
361
+ const labels = {
362
+ AGENTS_RUNNING: "有任务正在运行,请结束后重试",
363
+ OPERATION_BUSY: "插件市场正在处理其他任务",
364
+ RELEASE_TOO_FRESH: "新版本仍在镜像同步",
365
+ VERSION_UNCHANGED: "下载后版本未变化",
366
+ UPDATE_TIMEOUT: "更新超时",
367
+ UPDATE_FORBIDDEN: "更新请求被拒绝",
368
+ UPDATE_REJECTED: "无法执行本次更新"
369
+ };
370
+ return labels[failure?.code] ?? failure?.message ?? "更新失败";
371
+ }
372
+
183
373
  /**
184
374
  * DSH 暂未公开“打开指定设置分区”服务:只依赖稳定的 sidebar.settings
185
375
  * slot 和可访问文本打开插件市场,不依赖构建生成的 CSS 类名。
@@ -258,6 +448,14 @@ window.__ModuleLoader__.load({
258
448
  const open = useStore((state) => state.open);
259
449
  const detailOpen = useStore((state) => state.detailOpen);
260
450
  const [versionStatus, setVersionStatus] = (0, react.useState)(null);
451
+ const [updateProviderState, setUpdateProviderState] = (0, react.useState)("checking");
452
+ const [updateProviderSelection, setUpdateProviderSelection] = (0, react.useState)(null);
453
+ const [providerUpdateCheck, setProviderUpdateCheck] = (0, react.useState)(null);
454
+ const [updateOperation, setUpdateOperation] = (0, react.useState)(null);
455
+ const [updateError, setUpdateError] = (0, react.useState)(null);
456
+ const [updateStarting, setUpdateStarting] = (0, react.useState)(false);
457
+ const [rollbackRunning, setRollbackRunning] = (0, react.useState)(false);
458
+ const [restarting, setRestarting] = (0, react.useState)(false);
261
459
  const frameRef = (0, react.useRef)(null);
262
460
  const closeRef = (0, react.useRef)(null);
263
461
  (0, react.useEffect)(() => {
@@ -328,6 +526,65 @@ window.__ModuleLoader__.load({
328
526
  window.clearTimeout(pollTimer);
329
527
  };
330
528
  }, [open]);
529
+ (0, react.useEffect)(() => {
530
+ if (!open) return void 0;
531
+ let disposed = false;
532
+ setUpdateProviderState("checking");
533
+ discoverUpdateProvider(PLUGIN_PACKAGE_NAME, true).then((selection) => {
534
+ if (disposed) return;
535
+ if (selection === null) {
536
+ setUpdateProviderSelection(null);
537
+ setProviderUpdateCheck(null);
538
+ setUpdateProviderState("unavailable");
539
+ return;
540
+ }
541
+ setUpdateProviderSelection(selection);
542
+ setProviderUpdateCheck(selection.update);
543
+ setUpdateProviderState("ready");
544
+ }).catch((error) => {
545
+ console.warn("[mcp-connector] update provider discovery failed:", error);
546
+ if (disposed) return;
547
+ setUpdateProviderSelection(null);
548
+ setProviderUpdateCheck(null);
549
+ setUpdateProviderState("unavailable");
550
+ });
551
+ return () => { disposed = true; };
552
+ }, [open]);
553
+ (0, react.useEffect)(() => {
554
+ const operationId = updateOperation?.operationId;
555
+ const provider = updateProviderSelection?.provider;
556
+ const capabilities = updateProviderSelection?.capabilities;
557
+ if (!open || provider === void 0 || capabilities === void 0
558
+ || typeof operationId !== "string" || !["queued", "running"].includes(updateOperation.state)) {
559
+ return void 0;
560
+ }
561
+ let disposed = false;
562
+ let timer;
563
+ const poll = () => {
564
+ provider.operation(capabilities, operationId).then((operation) => {
565
+ if (disposed) return;
566
+ setUpdateError(null);
567
+ setUpdateOperation(operation);
568
+ if (["queued", "running"].includes(operation.state)) {
569
+ timer = window.setTimeout(poll, UPDATE_PROVIDER_OPERATION_POLL_MS);
570
+ return;
571
+ }
572
+ if (operation.state === "succeeded") {
573
+ provider.check(capabilities, PLUGIN_PACKAGE_NAME, true).then(setProviderUpdateCheck, () => {});
574
+ }
575
+ }, (error) => {
576
+ console.warn("[mcp-connector] update operation polling failed:", error);
577
+ if (disposed) return;
578
+ setUpdateError(error instanceof Error ? error.message : String(error));
579
+ timer = window.setTimeout(poll, UPDATE_PROVIDER_OPERATION_POLL_MS * 2);
580
+ });
581
+ };
582
+ timer = window.setTimeout(poll, UPDATE_PROVIDER_OPERATION_POLL_MS);
583
+ return () => {
584
+ disposed = true;
585
+ window.clearTimeout(timer);
586
+ };
587
+ }, [open, updateOperation?.operationId, updateProviderSelection?.provider?.id]);
331
588
  (0, react.useEffect)(() => {
332
589
  if (!open) return void 0;
333
590
  closeRef.current?.focus?.();
@@ -336,6 +593,201 @@ window.__ModuleLoader__.load({
336
593
  return () => { window.removeEventListener("keydown", onKeyDown); };
337
594
  }, [open, actions]);
338
595
  if (!open) return null;
596
+ const updateRunning = updateStarting || updateOperation !== null && ["queued", "running"].includes(updateOperation.state);
597
+ const startUpdate = (force = false) => {
598
+ const provider = updateProviderSelection?.provider;
599
+ const capabilities = updateProviderSelection?.capabilities;
600
+ if (updateRunning || provider === void 0 || capabilities === void 0) return;
601
+ setUpdateError(null);
602
+ setUpdateStarting(true);
603
+ provider.start(capabilities, PLUGIN_PACKAGE_NAME, force).then((operation) => {
604
+ setUpdateStarting(false);
605
+ setUpdateOperation(operation);
606
+ }, (error) => {
607
+ console.error("[mcp-connector] update start failed:", error);
608
+ setUpdateStarting(false);
609
+ if (error?.retryable === true) {
610
+ setUpdateOperation({
611
+ state: "failed",
612
+ failure: {
613
+ code: error.code ?? "UPDATE_FAILED",
614
+ message: error instanceof Error ? error.message : String(error),
615
+ retryable: true
616
+ },
617
+ outcome: { rollback: { available: false, state: "unavailable" } }
618
+ });
619
+ } else {
620
+ setUpdateError(error instanceof Error ? error.message : String(error));
621
+ }
622
+ });
623
+ };
624
+ const rollbackUpdate = () => {
625
+ const provider = updateProviderSelection?.provider;
626
+ const capabilities = updateProviderSelection?.capabilities;
627
+ if (rollbackRunning || provider === void 0 || capabilities === void 0
628
+ || typeof updateOperation?.operationId !== "string") return;
629
+ setUpdateError(null);
630
+ setRollbackRunning(true);
631
+ provider.rollback(capabilities, updateOperation.operationId).then((operation) => {
632
+ setRollbackRunning(false);
633
+ setUpdateOperation(operation);
634
+ }, (error) => {
635
+ console.error("[mcp-connector] rollback failed:", error);
636
+ setRollbackRunning(false);
637
+ setUpdateError(error instanceof Error ? error.message : String(error));
638
+ });
639
+ };
640
+ const restartHost = () => {
641
+ const provider = updateProviderSelection?.provider;
642
+ const capabilities = updateProviderSelection?.capabilities;
643
+ if (restarting || provider === void 0 || capabilities === void 0) return;
644
+ setRestarting(true);
645
+ setUpdateError(null);
646
+ provider.restart(capabilities).catch((error) => {
647
+ console.error("[mcp-connector] restart failed:", error);
648
+ setRestarting(false);
649
+ setUpdateError(error instanceof Error ? error.message : String(error));
650
+ });
651
+ };
652
+ const renderUpdateControls = () => {
653
+ if (!versionStatus?.updateAvailable && updateOperation === null && updateError === null) return null;
654
+ if (updateRunning) {
655
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
656
+ className: "mcpConnectorUpdateControls",
657
+ children: [
658
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
659
+ type: "button",
660
+ className: "mcpConnectorUpdateButton",
661
+ disabled: true,
662
+ children: updateStarting ? "正在启动更新…" : updateProgressLabel(updateOperation)
663
+ }),
664
+ updateError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
665
+ className: "mcpConnectorUpdateStatus",
666
+ "data-tone": "error",
667
+ title: updateError,
668
+ children: "暂时无法读取进度,正在重试"
669
+ })
670
+ ]
671
+ });
672
+ }
673
+ if (updateOperation?.state === "failed") {
674
+ const force = ["RELEASE_TOO_FRESH", "VERSION_UNCHANGED"].includes(updateOperation.failure?.code);
675
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
676
+ className: "mcpConnectorUpdateControls",
677
+ children: [
678
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
679
+ className: "mcpConnectorUpdateStatus",
680
+ "data-tone": "error",
681
+ title: updateOperation.failure?.message ?? "更新失败",
682
+ children: updateFailureLabel(updateOperation.failure)
683
+ }),
684
+ updateOperation.failure?.retryable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
685
+ type: "button",
686
+ className: "mcpConnectorUpdateButton",
687
+ onClick: () => startUpdate(force),
688
+ children: force ? "强制重试" : "重试更新"
689
+ }),
690
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
691
+ type: "button",
692
+ className: "mcpConnectorUpdateButton mcpConnectorUpdateSecondary",
693
+ onClick: () => openDshPluginMarket(actions),
694
+ children: "打开插件市场"
695
+ })
696
+ ]
697
+ });
698
+ }
699
+ if (["succeeded", "rolled-back"].includes(updateOperation?.state)) {
700
+ const rolledBack = updateOperation.state === "rolled-back";
701
+ const restartRequired = updateOperation.outcome?.restartRequired === true;
702
+ const refreshRequired = updateOperation.outcome?.refreshRequired === true;
703
+ const rollbackFailed = updateOperation.outcome?.rollback?.state === "failed";
704
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
705
+ className: "mcpConnectorUpdateControls",
706
+ children: [
707
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
708
+ className: "mcpConnectorUpdateStatus",
709
+ children: rolledBack
710
+ ? "已回滚"
711
+ : `已安装 v${updateOperation.installedVersion ?? versionStatus?.latestVersion}`
712
+ }),
713
+ rollbackFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
714
+ className: "mcpConnectorUpdateStatus",
715
+ "data-tone": "error",
716
+ title: updateOperation.outcome.rollback.detail ?? "回滚失败",
717
+ children: "回滚失败"
718
+ }),
719
+ restartRequired && updateProviderSelection?.capabilities?.restart?.supported === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
720
+ type: "button",
721
+ className: "mcpConnectorUpdateButton",
722
+ disabled: restarting,
723
+ onClick: restartHost,
724
+ children: restarting ? "正在重启…" : "立即重启"
725
+ }),
726
+ restartRequired && updateProviderSelection?.capabilities?.restart?.supported !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
727
+ className: "mcpConnectorUpdateStatus",
728
+ title: `重启由 ${updateProviderSelection?.capabilities?.restart?.managedBy ?? "宿主"} 管理`,
729
+ children: updateProviderSelection?.capabilities?.runtime === "desktop" ? "请重启 DSH Desktop" : "请重启 DSH"
730
+ }),
731
+ !restartRequired && refreshRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
732
+ type: "button",
733
+ className: "mcpConnectorUpdateButton",
734
+ onClick: () => window.location.reload(),
735
+ children: "刷新生效"
736
+ }),
737
+ updateOperation.outcome?.rollback?.available === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
738
+ type: "button",
739
+ className: "mcpConnectorUpdateButton mcpConnectorUpdateSecondary",
740
+ disabled: rollbackRunning,
741
+ onClick: rollbackUpdate,
742
+ children: rollbackRunning ? "正在回滚…" : rollbackFailed ? "重试回滚" : "回滚"
743
+ })
744
+ ]
745
+ });
746
+ }
747
+ if (updateError !== null) {
748
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
749
+ className: "mcpConnectorUpdateControls",
750
+ children: [
751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
752
+ className: "mcpConnectorUpdateStatus",
753
+ "data-tone": "error",
754
+ title: updateError,
755
+ children: "更新服务调用失败"
756
+ }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
758
+ type: "button",
759
+ className: "mcpConnectorUpdateButton mcpConnectorUpdateSecondary",
760
+ onClick: () => openDshPluginMarket(actions),
761
+ children: "打开插件市场"
762
+ })
763
+ ]
764
+ });
765
+ }
766
+ if (updateProviderState === "checking") {
767
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
768
+ type: "button",
769
+ className: "mcpConnectorUpdateButton",
770
+ disabled: true,
771
+ children: "正在准备更新…"
772
+ });
773
+ }
774
+ if (updateProviderState === "ready" && providerUpdateCheck?.updateAvailable === true) {
775
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
776
+ type: "button",
777
+ className: "mcpConnectorUpdateButton",
778
+ title: `由 ${updateProviderSelection?.provider?.label ?? "更新服务"} 安全更新到 v${providerUpdateCheck.latestVersion}`,
779
+ onClick: () => startUpdate(false),
780
+ children: `一键更新到 v${providerUpdateCheck.latestVersion}`
781
+ });
782
+ }
783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
784
+ type: "button",
785
+ className: "mcpConnectorUpdateButton",
786
+ title: `npm 已发布 v${versionStatus?.latestVersion}`,
787
+ onClick: () => openDshPluginMarket(actions),
788
+ children: `前往插件市场更新到 v${versionStatus?.latestVersion}`
789
+ });
790
+ };
339
791
  const src = window.location.origin + "/mcp-connector/ui/";
340
792
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
341
793
  style: {
@@ -379,7 +831,7 @@ window.__ModuleLoader__.load({
379
831
  },
380
832
  children: [
381
833
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
- style: { display: "flex", alignItems: "center", gap: 10 },
834
+ style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", minWidth: 0, flex: 1 },
383
835
  children: [
384
836
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 22 }, children: "\u{1F9E9}" }),
385
837
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { id: "mcp-connector-market-title", className: "mcpConnectorMarketTitle", style: { fontSize: 18, fontWeight: 600 }, children: "MCP连接器" }),
@@ -388,13 +840,7 @@ window.__ModuleLoader__.load({
388
840
  title: versionStatus === null ? "正在检查插件版本" : `当前版本 v${versionStatus.installedVersion}`,
389
841
  children: versionStatus === null ? "v…" : `v${versionStatus.installedVersion}`
390
842
  }),
391
- versionStatus?.updateAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
392
- type: "button",
393
- className: "mcpConnectorUpdateButton",
394
- title: `npm 已发布 v${versionStatus.latestVersion}`,
395
- onClick: () => openDshPluginMarket(actions),
396
- children: `前往插件市场更新到 v${versionStatus.latestVersion}`
397
- }),
843
+ renderUpdateControls(),
398
844
  versionStatus?.releasePending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
399
845
  className: "mcpConnectorVersion",
400
846
  title: "GitHub Release 已发布,等待 npm 同步后即可更新",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "General-purpose MCP connector, connection manager, plugin extension, and integration marketplace for DeepSeek Harness, initiated and maintained by Qichacha/QCC.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -19,6 +19,7 @@
19
19
  "registry",
20
20
  "docs/DESKTOP-E2E.md",
21
21
  "docs/MARKET-REGISTRATION.md",
22
+ "docs/PLUGIN-UPDATE.md",
22
23
  "docs/STDIO-SUPPORT.md",
23
24
  "docs/USER-GUIDE.md",
24
25
  "cordis.patch.yml",