dsh-mcp-connector 0.2.23 → 0.2.24
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 +16 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/USER-GUIDE.md +5 -4
- package/lib/client.js +127 -1
- package/lib/index.js +17 -0
- package/lib/version-status.js +216 -0
- package/lib/web.js +1 -0
- package/package.json +2 -2
- package/ui/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.24] - 2026-08-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- MCP连接器标题显示当前插件版本,并由服务端缓存检查 npm `latest` 与 GitHub Release;发现新版本时引导用户前往 DSH 插件市场安全更新。
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- 市场页原“刷新”按钮改为“刷新连接器目录”,明确该操作仅更新 Registry 卡片,不会升级插件本身。
|
|
16
|
+
|
|
17
|
+
### Verification
|
|
18
|
+
|
|
19
|
+
- 123 项自动测试、lint、npm 发布包白名单/敏感内容扫描和 npm/GitHub 真实版本源验收通过。
|
|
20
|
+
|
|
7
21
|
## [0.2.23] - 2026-08-26
|
|
8
22
|
|
|
9
23
|
### Fixed
|
|
@@ -385,7 +399,8 @@
|
|
|
385
399
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
386
400
|
- iframe 消息校验同源和消息来源。
|
|
387
401
|
|
|
388
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
402
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.24...HEAD
|
|
403
|
+
[0.2.24]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.23...v0.2.24
|
|
389
404
|
[0.2.23]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.22...v0.2.23
|
|
390
405
|
[0.2.22]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.21...v0.2.22
|
|
391
406
|
[0.2.21]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.20...v0.2.21
|
package/README.en.md
CHANGED
|
@@ -28,6 +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
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.
|
|
32
33
|
|
|
33
34
|
<!-- catalog-stats:start -->
|
|
@@ -111,7 +112,7 @@ npm run dev:ui
|
|
|
111
112
|
|
|
112
113
|
Every Registry merge regenerates `catalog-stats.json`; an hourly workflow in this repository synchronizes the Chinese and English product copy plus a local stats snapshot. The static npm README updates with package releases, while the live badges above read the Registry directly and therefore stay current without another npm release.
|
|
113
114
|
|
|
114
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
115
|
+
The current public version is [`dsh-mcp-connector@0.2.24`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.24](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.24).
|
|
115
116
|
|
|
116
117
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
117
118
|
|
package/README.md
CHANGED
|
@@ -28,6 +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
32
|
- Registry 工具链:Schema/唯一性/密钥审计、MCP/OAuth 无凭据探针、每周健康巡检。
|
|
32
33
|
- 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;检测到旧插件仍启用并管理同名 Server 时阻断重复连接,避免凭据相互覆盖。
|
|
33
34
|
- 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`health_check`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
|
|
@@ -106,7 +107,7 @@ npm run dev:ui
|
|
|
106
107
|
|
|
107
108
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
108
109
|
|
|
109
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
110
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.24`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.24](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.24)。
|
|
110
111
|
|
|
111
112
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
112
113
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -42,7 +42,8 @@ dsh web
|
|
|
42
42
|
- **已安装**:查看已经保存到本机的连接,页签徽标显示连接数。
|
|
43
43
|
- **搜索**:按名称、服务商、简介和标签查找。
|
|
44
44
|
- **添加连接**:导入 JSON、手动配置 HTTP/stdio,或从市场描述 URL 安装。
|
|
45
|
-
-
|
|
45
|
+
- **版本与更新提示**:标题旁显示当前插件版本。检测到 npm 有新版本时,点击更新提示前往 DSH 插件市场执行安全升级;升级后按页面提示完全重启 DSH。
|
|
46
|
+
- **刷新连接器目录**:重新拉取 Registry 卡片,并更新连接健康状态;该操作不会升级 MCP连接器插件本身。
|
|
46
47
|
|
|
47
48
|
## 3. 浏览市场与分类
|
|
48
49
|
|
|
@@ -64,7 +65,7 @@ dsh web
|
|
|
64
65
|
| `配置` | 需要 Bearer Token 或 API Key | 录入服务商签发的凭据并验证 |
|
|
65
66
|
| `需重新授权` | OAuth 授权过期、被撤销或不可用 | 点击后重新完成 OAuth 授权 |
|
|
66
67
|
| `自动重试中` | OAuth Token 刷新遇到网络或服务端暂时故障 | 无需重新授权;保持 DSH 运行,插件会按退避策略自动恢复 |
|
|
67
|
-
| `已配置` | 本机已有配置,尚未完成本轮健康确认 |
|
|
68
|
+
| `已配置` | 本机已有配置,尚未完成本轮健康确认 | 点击“刷新连接器目录”或进入详情检查 |
|
|
68
69
|
| `已连接` | 最近一次健康检查通过 | 可直接在会话中使用对应 MCP 工具 |
|
|
69
70
|
| `部分异常` / `连接异常` | 一个或多个 Server 未通过检查 | 打开详情查看提示,核对网络、权限和服务状态 |
|
|
70
71
|
|
|
@@ -140,7 +141,7 @@ stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、
|
|
|
140
141
|
- 执行健康检查;
|
|
141
142
|
- 断开连接并删除该连接的本机配置。
|
|
142
143
|
|
|
143
|
-
OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端点时仍会删除 DSH
|
|
144
|
+
OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端点时仍会删除 DSH 本机授权记录。连接状态发生变化后,可点击“刷新连接器目录”重新检查。
|
|
144
145
|
|
|
145
146
|
## 8. 安全边界
|
|
146
147
|
|
|
@@ -160,7 +161,7 @@ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端
|
|
|
160
161
|
|
|
161
162
|
### 市场里没有最新卡片
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
点击“刷新连接器目录”。如果远程 Registry 暂时不可用,页面会继续显示缓存或内置目录;稍后恢复网络再刷新即可。
|
|
164
165
|
|
|
165
166
|
### Token/API Key 一直验证失败
|
|
166
167
|
|
package/lib/client.js
CHANGED
|
@@ -19,6 +19,9 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
];
|
|
20
20
|
const PROMPT_REQUEST_TYPE = "mcp-connector:start-session";
|
|
21
21
|
const PROMPT_RESULT_TYPE = "mcp-connector:start-session-result";
|
|
22
|
+
const VERSION_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
23
|
+
const VERSION_CHECK_RETRY_MS = 5 * 60 * 1e3;
|
|
24
|
+
const NPM_PACKAGE_URL = "https://www.npmjs.com/package/dsh-mcp-connector";
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Desktop 内置的社区插件市场也注册在 sidebar.footer.action。
|
|
@@ -86,6 +89,31 @@ window.__ModuleLoader__.load({
|
|
|
86
89
|
.mcpConnectorMarketPanel { background: #ffffff; color: #111827; }
|
|
87
90
|
.mcpConnectorMarketHeader { border-color: #e5e7eb; }
|
|
88
91
|
.mcpConnectorMarketTitle { color: #111827; }
|
|
92
|
+
.mcpConnectorVersion {
|
|
93
|
+
color: #6b7280;
|
|
94
|
+
background: #f3f4f6;
|
|
95
|
+
border-radius: 999px;
|
|
96
|
+
padding: 2px 8px;
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
}
|
|
100
|
+
.mcpConnectorUpdateButton {
|
|
101
|
+
color: #4338ca;
|
|
102
|
+
background: #eef2ff;
|
|
103
|
+
border: 1px solid #c7d2fe;
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
padding: 5px 9px;
|
|
106
|
+
font: inherit;
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
.mcpConnectorUpdateButton:hover,
|
|
112
|
+
.mcpConnectorUpdateButton:focus-visible {
|
|
113
|
+
background: #e0e7ff;
|
|
114
|
+
outline: 2px solid #6366f1;
|
|
115
|
+
outline-offset: 1px;
|
|
116
|
+
}
|
|
89
117
|
.mcpConnectorMarketClose { color: #6b7280; }
|
|
90
118
|
.mcpConnectorMarketClose:hover,
|
|
91
119
|
.mcpConnectorMarketClose:focus-visible {
|
|
@@ -99,6 +127,10 @@ window.__ModuleLoader__.load({
|
|
|
99
127
|
.mcpConnectorMarketPanel { background: #1b1e25; color: #f3f4f6; }
|
|
100
128
|
.mcpConnectorMarketHeader { border-color: #30343d; }
|
|
101
129
|
.mcpConnectorMarketTitle { color: #f3f4f6; }
|
|
130
|
+
.mcpConnectorVersion { color: #d1d5db; background: #30343d; }
|
|
131
|
+
.mcpConnectorUpdateButton { color: #c7d2fe; background: #312e81; border-color: #4f46e5; }
|
|
132
|
+
.mcpConnectorUpdateButton:hover,
|
|
133
|
+
.mcpConnectorUpdateButton:focus-visible { background: #3730a3; }
|
|
102
134
|
.mcpConnectorMarketClose { color: #c4c8d0; }
|
|
103
135
|
.mcpConnectorMarketClose:hover,
|
|
104
136
|
.mcpConnectorMarketClose:focus-visible { background: #30343d !important; color: #ffffff; }
|
|
@@ -135,6 +167,50 @@ window.__ModuleLoader__.load({
|
|
|
135
167
|
return mount;
|
|
136
168
|
}
|
|
137
169
|
|
|
170
|
+
async function fetchVersionStatus() {
|
|
171
|
+
const response = await window.fetch("/mcp-connector/api", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "content-type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ method: "versionStatus", params: {} })
|
|
175
|
+
});
|
|
176
|
+
const result = await response.json();
|
|
177
|
+
if (!response.ok || result.ok !== true || result.detail === void 0) {
|
|
178
|
+
throw new Error(result.message ?? `HTTP ${response.status}`);
|
|
179
|
+
}
|
|
180
|
+
return result.detail;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* DSH 暂未公开“打开指定设置分区”服务:只依赖稳定的 sidebar.settings
|
|
185
|
+
* slot 和可访问文本打开插件市场,不依赖构建生成的 CSS 类名。
|
|
186
|
+
*/
|
|
187
|
+
function openDshPluginMarket(actions) {
|
|
188
|
+
const settingsHost = document.querySelector('[data-slot="sidebar.settings"]');
|
|
189
|
+
const settingsTrigger = settingsHost?.querySelector?.('button[aria-haspopup="dialog"]');
|
|
190
|
+
if (settingsTrigger === null || settingsTrigger === void 0) {
|
|
191
|
+
window.open(NPM_PACKAGE_URL, "_blank", "noopener,noreferrer");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
actions.close();
|
|
195
|
+
window.setTimeout(() => {
|
|
196
|
+
settingsTrigger.click();
|
|
197
|
+
let attempts = 0;
|
|
198
|
+
const selectMarket = () => {
|
|
199
|
+
const marketButton = [...document.querySelectorAll('[role="dialog"] nav button')].find((button) => {
|
|
200
|
+
const label = button.textContent?.trim() ?? "";
|
|
201
|
+
return /^(\u63d2\u4ef6\u5e02\u573a|Plugin Market|Plugin Marketplace)$/i.test(label);
|
|
202
|
+
});
|
|
203
|
+
if (marketButton !== void 0) {
|
|
204
|
+
marketButton.click();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
attempts += 1;
|
|
208
|
+
if (attempts < 20) window.setTimeout(selectMarket, 50);
|
|
209
|
+
};
|
|
210
|
+
window.requestAnimationFrame(selectMarket);
|
|
211
|
+
}, 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
138
214
|
/** 创建弹框 store(使用 DSH defineStore,与社区插件市场一致)。 */
|
|
139
215
|
function createMarketViewStore() {
|
|
140
216
|
return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
@@ -181,6 +257,7 @@ window.__ModuleLoader__.load({
|
|
|
181
257
|
const { wide, useStore, actions, startPromptSession: launchPromptSession } = props;
|
|
182
258
|
const open = useStore((state) => state.open);
|
|
183
259
|
const detailOpen = useStore((state) => state.detailOpen);
|
|
260
|
+
const [versionStatus, setVersionStatus] = (0, react.useState)(null);
|
|
184
261
|
const frameRef = (0, react.useRef)(null);
|
|
185
262
|
const closeRef = (0, react.useRef)(null);
|
|
186
263
|
(0, react.useEffect)(() => {
|
|
@@ -219,6 +296,38 @@ window.__ModuleLoader__.load({
|
|
|
219
296
|
window.addEventListener("message", onMessage);
|
|
220
297
|
return () => { window.removeEventListener("message", onMessage); };
|
|
221
298
|
}, [actions, launchPromptSession]);
|
|
299
|
+
(0, react.useEffect)(() => {
|
|
300
|
+
if (!open) return void 0;
|
|
301
|
+
let disposed = false;
|
|
302
|
+
let pollTimer;
|
|
303
|
+
const schedule = (delayMs) => {
|
|
304
|
+
window.clearTimeout(pollTimer);
|
|
305
|
+
pollTimer = window.setTimeout(check, Math.max(1e3, delayMs));
|
|
306
|
+
};
|
|
307
|
+
const check = () => {
|
|
308
|
+
fetchVersionStatus().then((status) => {
|
|
309
|
+
if (disposed) return;
|
|
310
|
+
setVersionStatus(status);
|
|
311
|
+
if (status.checking) {
|
|
312
|
+
schedule(1e3);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const nextCheckAt = Date.parse(status.nextCheckAt ?? "");
|
|
316
|
+
const delay = Number.isFinite(nextCheckAt)
|
|
317
|
+
? Math.min(VERSION_CHECK_INTERVAL_MS, nextCheckAt - Date.now())
|
|
318
|
+
: VERSION_CHECK_INTERVAL_MS;
|
|
319
|
+
schedule(delay);
|
|
320
|
+
}, (error) => {
|
|
321
|
+
console.warn("[mcp-connector] plugin update check failed:", error);
|
|
322
|
+
if (!disposed) schedule(VERSION_CHECK_RETRY_MS);
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
check();
|
|
326
|
+
return () => {
|
|
327
|
+
disposed = true;
|
|
328
|
+
window.clearTimeout(pollTimer);
|
|
329
|
+
};
|
|
330
|
+
}, [open]);
|
|
222
331
|
(0, react.useEffect)(() => {
|
|
223
332
|
if (!open) return void 0;
|
|
224
333
|
closeRef.current?.focus?.();
|
|
@@ -273,7 +382,24 @@ window.__ModuleLoader__.load({
|
|
|
273
382
|
style: { display: "flex", alignItems: "center", gap: 10 },
|
|
274
383
|
children: [
|
|
275
384
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 22 }, children: "\u{1F9E9}" }),
|
|
276
|
-
|
|
385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { id: "mcp-connector-market-title", className: "mcpConnectorMarketTitle", style: { fontSize: 18, fontWeight: 600 }, children: "MCP连接器" }),
|
|
386
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
387
|
+
className: "mcpConnectorVersion",
|
|
388
|
+
title: versionStatus === null ? "正在检查插件版本" : `当前版本 v${versionStatus.installedVersion}`,
|
|
389
|
+
children: versionStatus === null ? "v…" : `v${versionStatus.installedVersion}`
|
|
390
|
+
}),
|
|
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
|
+
}),
|
|
398
|
+
versionStatus?.releasePending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
399
|
+
className: "mcpConnectorVersion",
|
|
400
|
+
title: "GitHub Release 已发布,等待 npm 同步后即可更新",
|
|
401
|
+
children: `v${versionStatus.release.version} 正在同步`
|
|
402
|
+
})
|
|
277
403
|
]
|
|
278
404
|
}),
|
|
279
405
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
package/lib/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { classifyConnectionError, validateConnectionRecords } from './mcp-valida
|
|
|
33
33
|
import { listMcpTools, McpHttpError } from './mcp-http.js';
|
|
34
34
|
import { registerTools } from './tools.js';
|
|
35
35
|
import { mountWebRoutes } from './web.js';
|
|
36
|
+
import { createVersionStatusService } from './version-status.js';
|
|
36
37
|
import { assertSafeUrl } from './util.js';
|
|
37
38
|
import { readLegacyGrantCandidates, planLegacyMigration, toConnectorGrant, toConnectionRecords } from './migration.js';
|
|
38
39
|
import { classifyRefreshFailure, refreshRetryDelay } from './grant-lifecycle.js';
|
|
@@ -83,6 +84,10 @@ export const Config = z.object({
|
|
|
83
84
|
|
|
84
85
|
export async function apply(ctx, config) {
|
|
85
86
|
const logger = ctx.logger('mcp-connector');
|
|
87
|
+
const versionStatusService = createVersionStatusService({
|
|
88
|
+
timeoutMs: config.requestTimeoutMs,
|
|
89
|
+
logger,
|
|
90
|
+
});
|
|
86
91
|
|
|
87
92
|
const domain = await ctx.storageDomain.open(defineConnectorDomain());
|
|
88
93
|
ctx.effect(() => () => {
|
|
@@ -856,6 +861,17 @@ export async function apply(ctx, config) {
|
|
|
856
861
|
/* ───────────────────────── api 门面 ───────────────────────── */
|
|
857
862
|
|
|
858
863
|
const api = {
|
|
864
|
+
async versionStatus(force = false) {
|
|
865
|
+
const detail = versionStatusService.status({ force });
|
|
866
|
+
let message = `当前版本 v${detail.installedVersion}`;
|
|
867
|
+
if (detail.checking) message += ',正在检查更新';
|
|
868
|
+
else if (detail.updateAvailable) message += `,可更新到 v${detail.latestVersion}`;
|
|
869
|
+
else if (detail.releasePending) message += ',新版本正在同步到 npm';
|
|
870
|
+
else if (detail.status === 'unavailable') message += ',暂时无法检查更新';
|
|
871
|
+
else message += ',已是 npm 最新版';
|
|
872
|
+
return { ok: true, message, detail };
|
|
873
|
+
},
|
|
874
|
+
|
|
859
875
|
async catalog({ category, keyword } = {}) {
|
|
860
876
|
const items = listCatalog(state.merged, { category, keyword, publishedOnly: true }).map((d) => {
|
|
861
877
|
const health = connectorHealth(d.id);
|
|
@@ -1640,6 +1656,7 @@ export async function apply(ctx, config) {
|
|
|
1640
1656
|
ctx.effect(() => () => {
|
|
1641
1657
|
disposeTools();
|
|
1642
1658
|
disposeWeb?.();
|
|
1659
|
+
versionStatusService.dispose();
|
|
1643
1660
|
for (const g of state.grants.values()) if (g.timer) clearTimeout(g.timer);
|
|
1644
1661
|
});
|
|
1645
1662
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
|
+
|
|
5
|
+
export const INSTALLED_PLUGIN_VERSION = packageJson.version;
|
|
6
|
+
export const NPM_LATEST_URL = 'https://registry.npmjs.org/dsh-mcp-connector/latest';
|
|
7
|
+
export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-mcp-connector';
|
|
8
|
+
export const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/duhu2000/dsh-mcp-connector/releases/latest';
|
|
9
|
+
export const GITHUB_RELEASES_URL = 'https://github.com/duhu2000/dsh-mcp-connector/releases';
|
|
10
|
+
export const VERSION_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
11
|
+
export const VERSION_CHECK_FAILURE_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
function normalizeVersion(value) {
|
|
14
|
+
if (typeof value !== 'string') return undefined;
|
|
15
|
+
const normalized = value.trim().replace(/^v/i, '');
|
|
16
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized)
|
|
17
|
+
? normalized
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseVersion(value) {
|
|
22
|
+
const normalized = normalizeVersion(value);
|
|
23
|
+
if (!normalized) return undefined;
|
|
24
|
+
const [withoutBuild] = normalized.split('+');
|
|
25
|
+
const [core, prerelease] = withoutBuild.split('-', 2);
|
|
26
|
+
return {
|
|
27
|
+
normalized,
|
|
28
|
+
core: core.split('.').map(Number),
|
|
29
|
+
prerelease: prerelease?.split('.') ?? [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Compare two semantic versions. Returns a positive number when `left` is newer. */
|
|
34
|
+
export function compareVersions(left, right) {
|
|
35
|
+
const a = parseVersion(left);
|
|
36
|
+
const b = parseVersion(right);
|
|
37
|
+
if (!a || !b) return 0;
|
|
38
|
+
for (let index = 0; index < 3; index += 1) {
|
|
39
|
+
if (a.core[index] !== b.core[index]) return a.core[index] - b.core[index];
|
|
40
|
+
}
|
|
41
|
+
if (a.prerelease.length === 0 && b.prerelease.length > 0) return 1;
|
|
42
|
+
if (a.prerelease.length > 0 && b.prerelease.length === 0) return -1;
|
|
43
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
44
|
+
for (let index = 0; index < length; index += 1) {
|
|
45
|
+
const aPart = a.prerelease[index];
|
|
46
|
+
const bPart = b.prerelease[index];
|
|
47
|
+
if (aPart === undefined) return -1;
|
|
48
|
+
if (bPart === undefined) return 1;
|
|
49
|
+
if (aPart === bPart) continue;
|
|
50
|
+
const aNumeric = /^\d+$/.test(aPart);
|
|
51
|
+
const bNumeric = /^\d+$/.test(bPart);
|
|
52
|
+
if (aNumeric && bNumeric) return Number(aPart) - Number(bPart);
|
|
53
|
+
if (aNumeric) return -1;
|
|
54
|
+
if (bNumeric) return 1;
|
|
55
|
+
return aPart.localeCompare(bPart);
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isVersionNewer(candidate, installed) {
|
|
61
|
+
return compareVersions(candidate, installed) > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function errorMessage(error) {
|
|
65
|
+
return error instanceof Error ? error.message : String(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchJson(fetchImpl, url, { timeoutMs, headers, activeControllers }) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
activeControllers.add(controller);
|
|
71
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetchImpl(url, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
accept: 'application/json',
|
|
77
|
+
'user-agent': `dsh-mcp-connector/${INSTALLED_PLUGIN_VERSION}`,
|
|
78
|
+
...headers,
|
|
79
|
+
},
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!response?.ok) throw new Error(`HTTP ${response?.status ?? 'unknown'}`);
|
|
83
|
+
return await response.json();
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
activeControllers.delete(controller);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check npm and GitHub without exposing those origins to the iframe CSP.
|
|
92
|
+
* npm `latest` is the update authority; GitHub is supplemental release metadata.
|
|
93
|
+
*/
|
|
94
|
+
export function createVersionStatusService({
|
|
95
|
+
installedVersion = INSTALLED_PLUGIN_VERSION,
|
|
96
|
+
fetchImpl = globalThis.fetch,
|
|
97
|
+
timeoutMs = 15_000,
|
|
98
|
+
cacheTtlMs = VERSION_CHECK_TTL_MS,
|
|
99
|
+
failureTtlMs = VERSION_CHECK_FAILURE_TTL_MS,
|
|
100
|
+
now = () => Date.now(),
|
|
101
|
+
logger,
|
|
102
|
+
} = {}) {
|
|
103
|
+
let cached;
|
|
104
|
+
let expiresAt = 0;
|
|
105
|
+
let inFlight;
|
|
106
|
+
const activeControllers = new Set();
|
|
107
|
+
|
|
108
|
+
async function query() {
|
|
109
|
+
const checkedAtMs = now();
|
|
110
|
+
const [npmResult, githubResult] = await Promise.allSettled([
|
|
111
|
+
fetchJson(fetchImpl, NPM_LATEST_URL, { timeoutMs, activeControllers }),
|
|
112
|
+
fetchJson(fetchImpl, GITHUB_LATEST_RELEASE_URL, {
|
|
113
|
+
timeoutMs,
|
|
114
|
+
activeControllers,
|
|
115
|
+
headers: { accept: 'application/vnd.github+json' },
|
|
116
|
+
}),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const npmVersion = npmResult.status === 'fulfilled' ? normalizeVersion(npmResult.value?.version) : undefined;
|
|
120
|
+
const githubVersion = githubResult.status === 'fulfilled' ? normalizeVersion(githubResult.value?.tag_name) : undefined;
|
|
121
|
+
const npmOk = npmVersion !== undefined;
|
|
122
|
+
const githubOk = githubVersion !== undefined;
|
|
123
|
+
const errors = [];
|
|
124
|
+
if (!npmOk) errors.push(`npm: ${npmResult.status === 'rejected' ? errorMessage(npmResult.reason) : '返回了无效版本'}`);
|
|
125
|
+
if (!githubOk) errors.push(`GitHub: ${githubResult.status === 'rejected' ? errorMessage(githubResult.reason) : '返回了无效版本'}`);
|
|
126
|
+
if (errors.length > 0) logger?.warn?.(`plugin update check partial failure: ${errors.join('; ')}`);
|
|
127
|
+
|
|
128
|
+
const updateAvailable = npmOk && isVersionNewer(npmVersion, installedVersion);
|
|
129
|
+
const releasePending = githubOk && isVersionNewer(githubVersion, installedVersion)
|
|
130
|
+
&& (!npmOk || isVersionNewer(githubVersion, npmVersion));
|
|
131
|
+
const status = npmOk && githubOk ? 'ok' : (npmOk || githubOk ? 'partial' : 'unavailable');
|
|
132
|
+
// npm is authoritative for an actionable update. A GitHub-only failure must not
|
|
133
|
+
// turn into a five-minute retry loop against the unauthenticated GitHub API.
|
|
134
|
+
const resultTtlMs = npmOk ? cacheTtlMs : failureTtlMs;
|
|
135
|
+
const result = {
|
|
136
|
+
installedVersion,
|
|
137
|
+
latestVersion: npmVersion ?? null,
|
|
138
|
+
updateAvailable,
|
|
139
|
+
releasePending,
|
|
140
|
+
checking: false,
|
|
141
|
+
status,
|
|
142
|
+
checkedAt: new Date(checkedAtMs).toISOString(),
|
|
143
|
+
nextCheckAt: new Date(checkedAtMs + resultTtlMs).toISOString(),
|
|
144
|
+
npmPackageUrl: NPM_PACKAGE_URL,
|
|
145
|
+
releasesUrl: GITHUB_RELEASES_URL,
|
|
146
|
+
release: githubOk ? {
|
|
147
|
+
version: githubVersion,
|
|
148
|
+
tagName: githubResult.value.tag_name,
|
|
149
|
+
url: typeof githubResult.value.html_url === 'string' ? githubResult.value.html_url : GITHUB_RELEASES_URL,
|
|
150
|
+
publishedAt: githubResult.value.published_at ?? null,
|
|
151
|
+
} : null,
|
|
152
|
+
sources: {
|
|
153
|
+
npm: npmOk ? { ok: true, version: npmVersion } : { ok: false },
|
|
154
|
+
github: githubOk ? { ok: true, version: githubVersion } : { ok: false },
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
expiresAt = checkedAtMs + resultTtlMs;
|
|
158
|
+
cached = result;
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function check({ force = false } = {}) {
|
|
163
|
+
if (!force && cached && now() < expiresAt) return cached;
|
|
164
|
+
if (inFlight) return inFlight;
|
|
165
|
+
if (typeof fetchImpl !== 'function') {
|
|
166
|
+
cached = {
|
|
167
|
+
installedVersion,
|
|
168
|
+
latestVersion: null,
|
|
169
|
+
updateAvailable: false,
|
|
170
|
+
releasePending: false,
|
|
171
|
+
checking: false,
|
|
172
|
+
status: 'unavailable',
|
|
173
|
+
checkedAt: new Date(now()).toISOString(),
|
|
174
|
+
nextCheckAt: new Date(now() + failureTtlMs).toISOString(),
|
|
175
|
+
npmPackageUrl: NPM_PACKAGE_URL,
|
|
176
|
+
releasesUrl: GITHUB_RELEASES_URL,
|
|
177
|
+
release: null,
|
|
178
|
+
sources: { npm: { ok: false }, github: { ok: false } },
|
|
179
|
+
};
|
|
180
|
+
expiresAt = now() + failureTtlMs;
|
|
181
|
+
return cached;
|
|
182
|
+
}
|
|
183
|
+
inFlight = query().finally(() => { inFlight = undefined; });
|
|
184
|
+
return inFlight;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function status({ force = false } = {}) {
|
|
188
|
+
const stale = !cached || now() >= expiresAt;
|
|
189
|
+
if ((force || stale) && !inFlight) {
|
|
190
|
+
void check({ force: true }).catch((error) => {
|
|
191
|
+
logger?.warn?.(`plugin update check failed: ${errorMessage(error)}`);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const current = cached ?? {
|
|
195
|
+
installedVersion,
|
|
196
|
+
latestVersion: null,
|
|
197
|
+
updateAvailable: false,
|
|
198
|
+
releasePending: false,
|
|
199
|
+
status: 'checking',
|
|
200
|
+
checkedAt: null,
|
|
201
|
+
nextCheckAt: null,
|
|
202
|
+
npmPackageUrl: NPM_PACKAGE_URL,
|
|
203
|
+
releasesUrl: GITHUB_RELEASES_URL,
|
|
204
|
+
release: null,
|
|
205
|
+
sources: { npm: { ok: false }, github: { ok: false } },
|
|
206
|
+
};
|
|
207
|
+
return { ...current, checking: inFlight !== undefined };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function dispose() {
|
|
211
|
+
for (const controller of activeControllers) controller.abort();
|
|
212
|
+
activeControllers.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { status, check, dispose };
|
|
216
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -17,6 +17,7 @@ const UI_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'ui');
|
|
|
17
17
|
|
|
18
18
|
/** GET /mcp-connector/api 允许调度的方法 → api 门面(白名单,杜绝任意反射调用)。 */
|
|
19
19
|
const API_WHITELIST = new Map([
|
|
20
|
+
['versionStatus', (api, p) => api.versionStatus(p?.force === true)],
|
|
20
21
|
['catalog', (api, p) => api.catalog(p ?? {})],
|
|
21
22
|
['status', (api) => api.status()],
|
|
22
23
|
['healthCheck', (api, p) => api.healthCheck(p?.connectorId)],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
35
|
"test": "node --test",
|
|
36
|
-
"lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/oauth.js && node --check lib/grant-lifecycle.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/sync-registry-stats.mjs",
|
|
36
|
+
"lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/version-status.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/oauth.js && node --check lib/grant-lifecycle.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/sync-registry-stats.mjs",
|
|
37
37
|
"registry:build": "node scripts/build-registry.mjs",
|
|
38
38
|
"registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
|
|
39
39
|
"dev:ui": "node scripts/ui-harness.mjs",
|
package/ui/index.html
CHANGED
|
@@ -400,7 +400,7 @@
|
|
|
400
400
|
<input id="search" type="text" placeholder="搜索连接器…" />
|
|
401
401
|
</div>
|
|
402
402
|
<button class="icon-btn primary" id="add-connection" type="button">+ 添加连接</button>
|
|
403
|
-
<button class="icon-btn" id="refresh"
|
|
403
|
+
<button class="icon-btn" id="refresh" title="刷新连接器目录">刷新连接器目录</button>
|
|
404
404
|
<div class="market-nav" id="market-nav" aria-label="市场分类导航"></div>
|
|
405
405
|
</header>
|
|
406
406
|
<main id="main"></main>
|