dsh-mcp-connector 0.2.3 → 0.2.4
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 +19 -1
- package/README.en.md +2 -2
- package/README.md +3 -3
- package/lib/index.js +135 -4
- package/lib/mcp-validation.js +14 -3
- package/lib/tools.js +20 -2
- package/lib/web.js +1 -0
- package/package.json +1 -1
- package/scripts/ui-harness.mjs +10 -1
- package/ui/index.html +77 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.4] - 2026-08-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 新增主动连接健康检查,支持 OAuth grant、Bearer/API Key 与无鉴权 MCP initialize 握手,并以 4 并发、5 秒单连接上限避免刷新风暴。
|
|
12
|
+
- 新增 `mcp_connector_health_check` 对话工具和同源 Web API,可检查单个或全部已配置连接器。
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- 市场卡片和“已安装”列表不再把“本机存在配置”等同于“当前可用”,改为展示已配置、已连接、需重新授权、部分异常、连接异常或已停用。
|
|
17
|
+
- 首次打开市场会在后台执行一次限流健康检查;用户点击“刷新”时同步刷新目录和连接状态。
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- OAuth 过期/缺失或历史 Key 失效后,不再继续显示“已连接”;详情页会暂停 Prompt 发送并提供重新授权/重新配置入口。
|
|
22
|
+
- 网络、DNS、TLS 或服务端异常与凭据异常分开呈现,避免误导用户重复录入 Key。
|
|
23
|
+
|
|
7
24
|
## [0.2.3] - 2026-08-21
|
|
8
25
|
|
|
9
26
|
### Added
|
|
@@ -102,7 +119,8 @@
|
|
|
102
119
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
103
120
|
- iframe 消息校验同源和消息来源。
|
|
104
121
|
|
|
105
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
122
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.4...HEAD
|
|
123
|
+
[0.2.4]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.3...v0.2.4
|
|
106
124
|
[0.2.3]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.2...v0.2.3
|
|
107
125
|
[0.2.2]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.1...v0.2.2
|
|
108
126
|
[0.2.1]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.0...v0.2.1
|
package/README.en.md
CHANGED
|
@@ -13,7 +13,7 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- A primary sidebar entry below New Conversation and above workspaces/conversations, with a public footer-slot fallback for incompatible DSH DOM versions.
|
|
16
|
-
- Searchable Marketplace and Installed views with
|
|
16
|
+
- Searchable Marketplace and Installed views with catalog refresh and active health checks that distinguish Configured, Connected, Reauthorization Required, Partially Unavailable, and Connection Error states.
|
|
17
17
|
- OAuth 2.0 Authorization Code with PKCE, API key/Bearer/unauthenticated URL configuration, and `mcpServers` JSON import.
|
|
18
18
|
- Installation from a credential-free connector descriptor URL.
|
|
19
19
|
- Credential and MCP initialize validation before API-key connectors are saved as installed.
|
|
@@ -97,7 +97,7 @@ npm run dev:ui
|
|
|
97
97
|
|
|
98
98
|
`npm run check` performs syntax checks, automated tests, and an npm package allowlist/sensitive-content audit. Tags matching `v*` trigger GitHub Actions; the tag must match `package.json`. npm releases use Trusted Publishing through GitHub OIDC and do not require a long-lived `NPM_TOKEN`.
|
|
99
99
|
|
|
100
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
100
|
+
The current public version is [`dsh-mcp-connector@0.2.4`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.4](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.4).
|
|
101
101
|
|
|
102
102
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
103
103
|
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
## 功能
|
|
14
14
|
|
|
15
15
|
- 左侧主导航入口:目标位置为“新会话”下方、“工作区/会话列表”上方;若 DSH DOM 结构不兼容,自动回退到底部公开插槽。
|
|
16
|
-
-
|
|
16
|
+
- 图形化市场:市场/已安装、搜索、刷新与主动健康检查;区分“已配置”、“已连接”、“需重新授权”、“部分异常”和“连接异常”。
|
|
17
17
|
- 图形化添加:手动 URL/鉴权、`mcpServers` JSON、连接器描述 URL 三种入口,失败时保留表单并给出修复建议。
|
|
18
18
|
- 连接器详情:精选 Prompt 优先展示,点击可带入 DSH 新会话;工具按 Server 分组,支持描述、搜索和独立滚动。
|
|
19
19
|
- Prompt 模板:使用 `{{company}}` 等变量,发送前填写真实查询主体。
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- 独立远程 Registry:新市场卡片合并后客户端刷新即可见,无需重新发布 npm;远程不可用时自动回退内置目录。
|
|
25
25
|
- Registry 工具链:Schema/唯一性/密钥审计、MCP/OAuth 无凭据探针、每周健康巡检。
|
|
26
26
|
- 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;不删除原插件或原凭据。
|
|
27
|
-
- 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
|
|
27
|
+
- 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`health_check`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
|
|
28
28
|
|
|
29
29
|
当前市场前 4 张为企查查连接器,第 5 张为“北大法宝·法律检索”,第 6 张为“Wind·股票数据”;插件架构不限定厂商。
|
|
30
30
|
|
|
@@ -93,7 +93,7 @@ npm run dev:ui
|
|
|
93
93
|
|
|
94
94
|
`check` 执行语法检查、自动测试和 npm 发布包白名单校验;`dev:ui` 启动不含真实凭据的本地 mock 市场。CI 使用 `--legacy-peer-deps` 安装显式测试依赖,DSH 运行期 peer 仍由 Host 提供。`v*` Tag 会触发 GitHub Actions;Tag 必须与 `package.json` 版本一致。Release 通过 npm Trusted Publishing (GitHub OIDC) 发布,不依赖长期 `NPM_TOKEN`。
|
|
95
95
|
|
|
96
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
96
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.4`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.4](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.4)。
|
|
97
97
|
|
|
98
98
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
99
99
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/lib/index.js
CHANGED
|
@@ -77,6 +77,7 @@ export async function apply(ctx, config) {
|
|
|
77
77
|
overrides: new Map(), // id -> { published?, featured? }
|
|
78
78
|
connections: new Map(), // key -> ConnectionRecord
|
|
79
79
|
grants: new Map(), // grantKey -> { grant, timer, refreshPromise, needsReauth }
|
|
80
|
+
health: new Map(), // connectorId -> 最近一次主动连通性检查摘要(不持久化)
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
/* ───────────────────────── 目录加载 ───────────────────────── */
|
|
@@ -195,6 +196,7 @@ export async function apply(ctx, config) {
|
|
|
195
196
|
|
|
196
197
|
async function persistRecord(record) {
|
|
197
198
|
state.connections.set(record.key, record);
|
|
199
|
+
if (record.connectorId) state.health.delete(record.connectorId);
|
|
198
200
|
if (config.persistSecrets) await connectionStore.put(record);
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -206,11 +208,73 @@ export async function apply(ctx, config) {
|
|
|
206
208
|
return merged;
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
function grantFailure(record) {
|
|
212
|
+
if (record.auth?.mode !== 'oauth') return null;
|
|
213
|
+
const grantKey = record.auth.grantKey;
|
|
214
|
+
const entry = grantKey ? state.grants.get(grantKey) : null;
|
|
215
|
+
if (!entry) return { ok: false, kind: 'auth', serverKey: record.serverKey, serverName: record.serverName, message: 'OAuth 授权缺失,请重新授权' };
|
|
216
|
+
if (entry.needsReauth || entry.grant.accessTokenExpiresAt <= Date.now()) {
|
|
217
|
+
return { ok: false, kind: 'auth', serverKey: record.serverKey, serverName: record.serverName, message: 'OAuth 授权已过期或刷新失败,请重新授权' };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function healthSummary(connectorId, records, results, checkedAt = Date.now()) {
|
|
223
|
+
const enabledRecords = records.filter((record) => record.enabled !== false);
|
|
224
|
+
const availableServers = results.filter((result) => result.ok).length;
|
|
225
|
+
const failedServers = results.length - availableServers;
|
|
226
|
+
const authFailures = results.filter((result) => !result.ok && result.kind === 'auth').length;
|
|
227
|
+
let connectionState = 'configured';
|
|
228
|
+
if (records.length === 0) connectionState = 'disconnected';
|
|
229
|
+
else if (enabledRecords.length === 0) connectionState = 'disabled';
|
|
230
|
+
else if (failedServers === 0 && availableServers > 0) connectionState = 'healthy';
|
|
231
|
+
else if (availableServers > 0) connectionState = 'degraded';
|
|
232
|
+
else if (authFailures > 0) connectionState = 'reauth';
|
|
233
|
+
else if (failedServers > 0) connectionState = 'unavailable';
|
|
234
|
+
const labels = {
|
|
235
|
+
disconnected: '未连接', configured: '已配置', disabled: '已停用', healthy: '已连接',
|
|
236
|
+
degraded: '部分异常', reauth: '需重新授权', unavailable: '连接异常',
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
connectorId,
|
|
240
|
+
connectionState,
|
|
241
|
+
label: labels[connectionState],
|
|
242
|
+
configuredServers: records.length,
|
|
243
|
+
enabledServers: enabledRecords.length,
|
|
244
|
+
availableServers,
|
|
245
|
+
failedServers,
|
|
246
|
+
checkedAt,
|
|
247
|
+
results,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function connectorHealth(connectorId) {
|
|
252
|
+
const records = [...state.connections.values()].filter((record) => record.connectorId === connectorId);
|
|
253
|
+
if (records.length === 0) return healthSummary(connectorId, records, [], null);
|
|
254
|
+
if (records.every((record) => record.enabled === false)) return healthSummary(connectorId, records, [], null);
|
|
255
|
+
const immediateFailures = records.filter((record) => record.enabled !== false).map(grantFailure).filter(Boolean);
|
|
256
|
+
if (immediateFailures.length > 0) return healthSummary(connectorId, records, immediateFailures, Date.now());
|
|
257
|
+
const cached = state.health.get(connectorId);
|
|
258
|
+
return cached ?? {
|
|
259
|
+
...healthSummary(connectorId, records, [], null),
|
|
260
|
+
connectionState: 'configured',
|
|
261
|
+
label: '已配置',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function cacheHealth(connectorId, records, results) {
|
|
266
|
+
const summary = healthSummary(connectorId, records, results);
|
|
267
|
+
state.health.set(connectorId, summary);
|
|
268
|
+
return summary;
|
|
269
|
+
}
|
|
270
|
+
|
|
209
271
|
/* ───────────────────────── api 门面 ───────────────────────── */
|
|
210
272
|
|
|
211
273
|
const api = {
|
|
212
274
|
async catalog({ category, keyword } = {}) {
|
|
213
|
-
const items = listCatalog(state.merged, { category, keyword, publishedOnly: true }).map((d) =>
|
|
275
|
+
const items = listCatalog(state.merged, { category, keyword, publishedOnly: true }).map((d) => {
|
|
276
|
+
const health = connectorHealth(d.id);
|
|
277
|
+
return ({
|
|
214
278
|
id: d.id,
|
|
215
279
|
name: d.name,
|
|
216
280
|
vendor: d.vendor,
|
|
@@ -234,13 +298,19 @@ export async function apply(ctx, config) {
|
|
|
234
298
|
probeReportUrl: d.probeReportUrl,
|
|
235
299
|
toolsSnapshot: d.toolsSnapshot ?? [],
|
|
236
300
|
connected: [...state.connections.values()].filter((r) => r.connectorId === d.id).map((r) => r.key),
|
|
301
|
+
connectionState: health.connectionState,
|
|
302
|
+
connectionLabel: health.label,
|
|
303
|
+
healthCheckedAt: health.checkedAt,
|
|
304
|
+
availableServers: health.availableServers,
|
|
305
|
+
failedServers: health.failedServers,
|
|
237
306
|
servers: (d.servers ?? []).map((s) => ({
|
|
238
307
|
serverKey: s.serverKey,
|
|
239
308
|
url: s.url,
|
|
240
309
|
transport: s.transport,
|
|
241
310
|
serverName: s.serverName,
|
|
242
311
|
})),
|
|
243
|
-
|
|
312
|
+
});
|
|
313
|
+
});
|
|
244
314
|
return {
|
|
245
315
|
ok: true,
|
|
246
316
|
message: `共 ${items.length} 个已上架连接器`,
|
|
@@ -374,6 +444,7 @@ export async function apply(ctx, config) {
|
|
|
374
444
|
await upsertRecord(record);
|
|
375
445
|
created.push(record.key);
|
|
376
446
|
}
|
|
447
|
+
cacheHealth(connector.id, records, validation.detail.results);
|
|
377
448
|
return {
|
|
378
449
|
ok: true,
|
|
379
450
|
message: `已配置并连接 "${connector.name}"(${created.length} 个 MCP Server)`,
|
|
@@ -435,6 +506,8 @@ export async function apply(ctx, config) {
|
|
|
435
506
|
async status() {
|
|
436
507
|
const items = [];
|
|
437
508
|
for (const [key, record] of state.connections) {
|
|
509
|
+
const health = record.connectorId ? connectorHealth(record.connectorId) : null;
|
|
510
|
+
const recordHealth = health?.results?.find((result) => result.serverKey === record.serverKey || result.serverName === record.serverName);
|
|
438
511
|
let grantInfo = null;
|
|
439
512
|
if (record.auth?.grantKey) {
|
|
440
513
|
const g = state.grants.get(record.auth.grantKey);
|
|
@@ -457,11 +530,57 @@ export async function apply(ctx, config) {
|
|
|
457
530
|
authMode: record.auth?.mode ?? 'none',
|
|
458
531
|
grant: grantInfo,
|
|
459
532
|
lastError: record.lastError ?? null,
|
|
533
|
+
connectionState: recordHealth?.ok ? 'healthy' : recordHealth ? health.connectionState : health?.connectionState ?? 'configured',
|
|
534
|
+
healthCheckedAt: health?.checkedAt ?? null,
|
|
535
|
+
healthMessage: recordHealth?.message ?? null,
|
|
460
536
|
});
|
|
461
537
|
}
|
|
462
538
|
return { ok: true, message: `共 ${items.length} 条连接`, detail: { items } };
|
|
463
539
|
},
|
|
464
540
|
|
|
541
|
+
async healthCheck(connectorId) {
|
|
542
|
+
const targetIds = connectorId
|
|
543
|
+
? [String(connectorId)]
|
|
544
|
+
: [...new Set([...state.connections.values()].map((record) => record.connectorId).filter(Boolean))];
|
|
545
|
+
const summaries = [];
|
|
546
|
+
for (const id of targetIds) {
|
|
547
|
+
const records = [...state.connections.values()].filter((record) => record.connectorId === id);
|
|
548
|
+
const enabledRecords = records.filter((record) => record.enabled !== false);
|
|
549
|
+
if (records.length === 0 || enabledRecords.length === 0) {
|
|
550
|
+
const summary = healthSummary(id, records, [], Date.now());
|
|
551
|
+
state.health.set(id, summary);
|
|
552
|
+
summaries.push(summary);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const immediateResults = [];
|
|
556
|
+
const checkable = [];
|
|
557
|
+
for (const record of enabledRecords) {
|
|
558
|
+
const failure = grantFailure(record);
|
|
559
|
+
if (failure) immediateResults.push(failure);
|
|
560
|
+
else checkable.push(record);
|
|
561
|
+
}
|
|
562
|
+
let checkedResults = [];
|
|
563
|
+
if (checkable.length > 0) {
|
|
564
|
+
const validation = await validateConnectionRecords(checkable, {
|
|
565
|
+
timeoutMs: Math.min(config.requestTimeoutMs, 5_000),
|
|
566
|
+
concurrency: 4,
|
|
567
|
+
grants: grantMap(),
|
|
568
|
+
});
|
|
569
|
+
checkedResults = validation.detail.results;
|
|
570
|
+
}
|
|
571
|
+
summaries.push(cacheHealth(id, records, [...immediateResults, ...checkedResults]));
|
|
572
|
+
}
|
|
573
|
+
const healthy = summaries.filter((item) => item.connectionState === 'healthy').length;
|
|
574
|
+
const attention = summaries.filter((item) => ['reauth', 'degraded', 'unavailable'].includes(item.connectionState)).length;
|
|
575
|
+
return {
|
|
576
|
+
ok: true,
|
|
577
|
+
message: targetIds.length === 0
|
|
578
|
+
? '暂无已配置连接'
|
|
579
|
+
: `已检查 ${summaries.length} 个连接器:${healthy} 个正常${attention ? `,${attention} 个需处理` : ''}`,
|
|
580
|
+
detail: { items: summaries, healthy, attention },
|
|
581
|
+
};
|
|
582
|
+
},
|
|
583
|
+
|
|
465
584
|
async migrationPreview({ scanStored = false } = {}) {
|
|
466
585
|
const { candidates, warnings } = await readLegacyGrantCandidates(ctx.storageDomain, { openClosed: scanStored });
|
|
467
586
|
const plans = planLegacyMigration(candidates, state.merged, state.connections);
|
|
@@ -533,6 +652,7 @@ export async function apply(ctx, config) {
|
|
|
533
652
|
if (record.auth?.grantKey) {
|
|
534
653
|
await remove(ctx, config, record);
|
|
535
654
|
state.connections.delete(key);
|
|
655
|
+
if (record.connectorId) state.health.delete(record.connectorId);
|
|
536
656
|
if (config.persistSecrets) await connectionStore.delete(key).catch(() => {});
|
|
537
657
|
const stillUsed = [...state.connections.values()].some((r) => r.auth?.grantKey === record.auth.grantKey);
|
|
538
658
|
if (!stillUsed) {
|
|
@@ -558,6 +678,7 @@ export async function apply(ctx, config) {
|
|
|
558
678
|
|
|
559
679
|
await remove(ctx, config, record);
|
|
560
680
|
state.connections.delete(key);
|
|
681
|
+
if (record.connectorId) state.health.delete(record.connectorId);
|
|
561
682
|
if (config.persistSecrets) await connectionStore.delete(key).catch(() => {});
|
|
562
683
|
return { ok: true, message: `已断开 "${record.name}"` };
|
|
563
684
|
},
|
|
@@ -640,7 +761,14 @@ export async function apply(ctx, config) {
|
|
|
640
761
|
const error = response.status === 401 || response.status === 403
|
|
641
762
|
? `凭据无效、已过期或权限不足(HTTP ${response.status})`
|
|
642
763
|
: `HTTP ${response.status}`;
|
|
643
|
-
servers.push({
|
|
764
|
+
servers.push({
|
|
765
|
+
serverKey: record.serverKey || record.name,
|
|
766
|
+
serverName: record.serverName,
|
|
767
|
+
ok: false,
|
|
768
|
+
error,
|
|
769
|
+
errorKind: response.status === 401 || response.status === 403 ? 'auth' : 'http',
|
|
770
|
+
tools: [],
|
|
771
|
+
});
|
|
644
772
|
continue;
|
|
645
773
|
}
|
|
646
774
|
|
|
@@ -682,6 +810,9 @@ export async function apply(ctx, config) {
|
|
|
682
810
|
const totalTools = servers.reduce((sum, s) => sum + (s.tools?.length || 0), 0);
|
|
683
811
|
const availableServers = servers.filter((server) => server.ok).length;
|
|
684
812
|
const failedServers = servers.length - availableServers;
|
|
813
|
+
const health = cacheHealth(connectorId, records, servers.map((server) => server.ok
|
|
814
|
+
? { ok: true, kind: 'connected', serverKey: server.serverKey, serverName: server.serverName }
|
|
815
|
+
: { ok: false, kind: server.errorKind ?? 'network', serverKey: server.serverKey, serverName: server.serverName, message: server.error }));
|
|
685
816
|
const failureSummary = servers.filter((server) => !server.ok).slice(0, 3)
|
|
686
817
|
.map((server) => `${server.serverName || server.serverKey}:${server.error}`).join(';');
|
|
687
818
|
return {
|
|
@@ -689,7 +820,7 @@ export async function apply(ctx, config) {
|
|
|
689
820
|
message: failedServers > 0
|
|
690
821
|
? `工具加载失败:${failureSummary}${failedServers > 3 ? `;另有 ${failedServers - 3} 个 Server 不可用` : ''}${availableServers > 0 ? `;${availableServers} 个 Server 仍可用` : ''}`
|
|
691
822
|
: `共 ${servers.length} 个 server,${totalTools} 个工具`,
|
|
692
|
-
detail: { connectorId, servers, totalTools, availableServers, failedServers },
|
|
823
|
+
detail: { connectorId, servers, totalTools, availableServers, failedServers, connectionState: health.connectionState },
|
|
693
824
|
};
|
|
694
825
|
},
|
|
695
826
|
};
|
package/lib/mcp-validation.js
CHANGED
|
@@ -82,7 +82,7 @@ function failed(record, kind, message, extra = {}) {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export async function validateConnectionRecord(record, { timeoutMs = 15_000, fetchImpl = fetch } = {}) {
|
|
85
|
+
export async function validateConnectionRecord(record, { timeoutMs = 15_000, fetchImpl = fetch, grants = new Map() } = {}) {
|
|
86
86
|
const controller = new AbortController();
|
|
87
87
|
const timer = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
|
88
88
|
try {
|
|
@@ -91,7 +91,7 @@ export async function validateConnectionRecord(record, { timeoutMs = 15_000, fet
|
|
|
91
91
|
headers: {
|
|
92
92
|
'Content-Type': 'application/json',
|
|
93
93
|
Accept: 'application/json, text/event-stream',
|
|
94
|
-
...authHeaders(record,
|
|
94
|
+
...authHeaders(record, grants),
|
|
95
95
|
},
|
|
96
96
|
body: JSON.stringify({
|
|
97
97
|
jsonrpc: '2.0',
|
|
@@ -153,7 +153,18 @@ export async function validateConnectionRecord(record, { timeoutMs = 15_000, fet
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
export async function validateConnectionRecords(records, options = {}) {
|
|
156
|
-
const
|
|
156
|
+
const concurrency = Math.max(1, Math.min(Number(options.concurrency) || records.length || 1, records.length || 1));
|
|
157
|
+
const results = new Array(records.length);
|
|
158
|
+
let cursor = 0;
|
|
159
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
160
|
+
for (;;) {
|
|
161
|
+
const index = cursor;
|
|
162
|
+
cursor += 1;
|
|
163
|
+
if (index >= records.length) return;
|
|
164
|
+
results[index] = await validateConnectionRecord(records[index], options);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
await Promise.all(workers);
|
|
157
168
|
const failures = results.filter((result) => !result.ok);
|
|
158
169
|
if (failures.length === 0) {
|
|
159
170
|
return { ok: true, message: `已验证 ${results.length} 个 MCP Server`, detail: { results } };
|
package/lib/tools.js
CHANGED
|
@@ -29,7 +29,7 @@ const catalogOutput = {
|
|
|
29
29
|
lines.push('');
|
|
30
30
|
for (const it of items) {
|
|
31
31
|
const star = it.featured ? '⭐ ' : '';
|
|
32
|
-
const connected = it.connected?.length ?
|
|
32
|
+
const connected = it.connected?.length ? `(${it.connectionLabel || '已配置'} ${it.connected.length})` : '';
|
|
33
33
|
const summary = it.summary ? ` — ${it.summary}` : '';
|
|
34
34
|
lines.push(`- ${star}[${it.id}] ${it.name} · ${it.category} · ${it.authMode}${connected}${summary}`);
|
|
35
35
|
}
|
|
@@ -47,7 +47,8 @@ const statusOutput = {
|
|
|
47
47
|
if (items.length) {
|
|
48
48
|
lines.push('');
|
|
49
49
|
for (const it of items) {
|
|
50
|
-
const
|
|
50
|
+
const stateLabels = { healthy: '已连接', configured: '已配置', reauth: '需重新授权', degraded: '部分异常', unavailable: '连接异常', disabled: '已停用' };
|
|
51
|
+
const state = it.enabled ? (stateLabels[it.connectionState] || '已配置') : '停用';
|
|
51
52
|
const auth = it.authMode === 'oauth'
|
|
52
53
|
? it.grant?.needsReauth ? 'oauth(需重新授权)' : it.grant?.missing ? 'oauth(授权缺失)' : 'oauth'
|
|
53
54
|
: it.authMode;
|
|
@@ -180,6 +181,23 @@ export function registerTools(ctx, api) {
|
|
|
180
181
|
},
|
|
181
182
|
});
|
|
182
183
|
|
|
184
|
+
reg({
|
|
185
|
+
name: 'mcp_connector_health_check',
|
|
186
|
+
description: '主动检查已配置 MCP 连接的 initialize 握手、凭据和网络状态。可指定 connectorId,留空则检查全部已配置连接器。',
|
|
187
|
+
parameters: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
connectorId: { type: 'string', description: '连接器 id(可选)' },
|
|
191
|
+
},
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
output: resultOutput,
|
|
195
|
+
timeoutMs: 120_000,
|
|
196
|
+
async execute(args) {
|
|
197
|
+
return api.healthCheck(args?.connectorId);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
183
201
|
reg({
|
|
184
202
|
name: 'mcp_connector_migration_preview',
|
|
185
203
|
description:
|
package/lib/web.js
CHANGED
|
@@ -19,6 +19,7 @@ const UI_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'ui');
|
|
|
19
19
|
const API_WHITELIST = new Map([
|
|
20
20
|
['catalog', (api, p) => api.catalog(p ?? {})],
|
|
21
21
|
['status', (api) => api.status()],
|
|
22
|
+
['healthCheck', (api, p) => api.healthCheck(p?.connectorId)],
|
|
22
23
|
['migrationPreview', (api, p) => api.migrationPreview(p ?? {})],
|
|
23
24
|
['migrateLegacy', (api, p) => api.migrateLegacy(p?.candidateIds ?? [])],
|
|
24
25
|
['connect', (api, p) => api.connect(p?.connectorId, p?.serverKey)],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "General-purpose MCP Connector Marketplace for DeepSeek Harness with OAuth 2.0 PKCE, API key/URL, JSON import, tool discovery, prompt templates, and connection management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/scripts/ui-harness.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const catalog = JSON.parse(await readFile(join(root, 'catalog/catalog.json'), 'u
|
|
|
11
11
|
.filter((item) => item.published !== false)
|
|
12
12
|
.map(normalizeConnectorDescriptor);
|
|
13
13
|
const connected = new Set(['qcc-company']);
|
|
14
|
+
const healthStates = new Map();
|
|
14
15
|
const bundledAssets = new Map([
|
|
15
16
|
['qcc-logo.svg', 'image/svg+xml'],
|
|
16
17
|
['pkulaw-logo.png', 'image/png'],
|
|
@@ -47,13 +48,21 @@ const server = createServer(async (req, res) => {
|
|
|
47
48
|
credentialDescription: item.auth.credentialDescription,
|
|
48
49
|
credentialHelpLabel: item.auth.credentialHelpLabel,
|
|
49
50
|
connected: connected.has(item.id) ? [`${item.id}-main`] : [],
|
|
51
|
+
connectionState: connected.has(item.id) ? (healthStates.get(item.id) ?? 'configured') : 'disconnected',
|
|
52
|
+
connectionLabel: healthStates.get(item.id) === 'healthy' ? '已连接' : connected.has(item.id) ? '已配置' : '未连接',
|
|
50
53
|
})) } }); return;
|
|
51
54
|
}
|
|
52
55
|
if (method === 'status') { json(res, { ok: true, detail: { items: [] } }); return; }
|
|
53
56
|
if (method === 'migrationPreview') { json(res, { ok: true, detail: { pendingCount: 0, items: [] } }); return; }
|
|
54
57
|
if (method === 'toolsList') {
|
|
55
58
|
const tools = Array.from({ length: 125 }, (_, index) => ({ name: `tool_${String(index + 1).padStart(3, '0')}`, title: `工具 ${index + 1}`, description: `用于验证分批渲染与搜索的第 ${index + 1} 个工具。` }));
|
|
56
|
-
|
|
59
|
+
healthStates.set(params.connectorId, 'healthy');
|
|
60
|
+
json(res, { ok: true, detail: { totalTools: tools.length, connectionState: 'healthy', servers: [{ serverKey: 'company', serverName: 'qcc-company', ok: true, tools }] } }); return;
|
|
61
|
+
}
|
|
62
|
+
if (method === 'healthCheck') {
|
|
63
|
+
const ids = params.connectorId ? [params.connectorId] : [...connected];
|
|
64
|
+
ids.forEach((id) => healthStates.set(id, 'healthy'));
|
|
65
|
+
json(res, { ok: true, message: `已检查 ${ids.length} 个连接器:${ids.length} 个正常`, detail: { items: ids.map((connectorId) => ({ connectorId, connectionState: 'healthy' })) } }); return;
|
|
57
66
|
}
|
|
58
67
|
if (method === 'connect') { connected.add(params.connectorId); json(res, { ok: true, message: 'Mock 连接成功' }); return; }
|
|
59
68
|
if (method === 'configure') {
|
package/ui/index.html
CHANGED
|
@@ -146,6 +146,10 @@
|
|
|
146
146
|
.btn.ghost:hover { border-color: var(--accent); color: var(--accent); }
|
|
147
147
|
.btn.danger { background: transparent; border: 1px solid #fecaca; color: var(--bad); }
|
|
148
148
|
.btn.danger:hover { border-color: var(--bad); background: var(--bad-bg); }
|
|
149
|
+
.btn.status-warning { background: var(--warn-bg); border: 1px solid #fde68a; color: #92400e; }
|
|
150
|
+
.btn.status-warning:hover { background: #fef3c7; border-color: var(--warn); }
|
|
151
|
+
.btn.status-bad { background: var(--bad-bg); border: 1px solid #fecaca; color: var(--bad); }
|
|
152
|
+
.btn.status-bad:hover { background: #fee2e2; border-color: var(--bad); }
|
|
149
153
|
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
150
154
|
|
|
151
155
|
/* Installed rows */
|
|
@@ -423,7 +427,8 @@
|
|
|
423
427
|
const toast = $('#toast');
|
|
424
428
|
const searchInput = $('#search');
|
|
425
429
|
const LABELS = {
|
|
426
|
-
details: '详情', connect: '连接', connected: '已连接', configure: '配置',
|
|
430
|
+
details: '详情', connect: '连接', connected: '已连接', configured: '已配置', configure: '配置',
|
|
431
|
+
reauthorize: '需重新授权', connectionError: '连接异常', partialError: '部分异常', disabled: '已停用',
|
|
427
432
|
send: '发送', fillSend: '填写并发送', connectToUse: '连接后使用', try: '💬 去试试',
|
|
428
433
|
connectTry: '🔗 连接后试用', configureConnect: '🔑 录入凭据并连接',
|
|
429
434
|
reconfigureCredential: '🔑 重新配置凭据', loading: '加载中…',
|
|
@@ -448,6 +453,7 @@
|
|
|
448
453
|
let detailConnector = null;
|
|
449
454
|
let detailToolServers = [];
|
|
450
455
|
let detailNeedsReconfigure = false;
|
|
456
|
+
let detailFailureState = '';
|
|
451
457
|
let toolRenderLimit = 50;
|
|
452
458
|
let detailLoadId = 0;
|
|
453
459
|
let catalogRenderLimit = 60;
|
|
@@ -457,6 +463,7 @@
|
|
|
457
463
|
const PROMPT_RESULT_TYPE = 'mcp-connector:start-session-result';
|
|
458
464
|
let promptRequestId = null;
|
|
459
465
|
let promptRequestTimer = null;
|
|
466
|
+
let initialHealthCheckStarted = false;
|
|
460
467
|
|
|
461
468
|
async function call(method, params) {
|
|
462
469
|
try {
|
|
@@ -658,7 +665,7 @@
|
|
|
658
665
|
|| (d.tags || []).some(t => t.toLowerCase().includes(q));
|
|
659
666
|
}
|
|
660
667
|
|
|
661
|
-
async function loadMarket() {
|
|
668
|
+
async function loadMarket({ checkHealth = true } = {}) {
|
|
662
669
|
main.innerHTML = '<div class="empty">加载中…</div>';
|
|
663
670
|
const r = await call('catalog', {});
|
|
664
671
|
if (!r.ok) { main.innerHTML = `<div class="empty">${esc(r.message)}</div>`; return; }
|
|
@@ -670,14 +677,19 @@
|
|
|
670
677
|
const count = filtered.length;
|
|
671
678
|
const visible = filtered.slice(0, catalogRenderLimit);
|
|
672
679
|
main.innerHTML = `<div class="stats"><span class="count">共 <strong>${count}</strong> 个连接器${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''} · 点击卡片查看能力,点击「连接」完成授权</span></div><div class="grid">${visible.map(cardHtml).join('')}</div>${visible.length < count ? `<button class="tools-more" id="catalog-more" type="button">再显示 ${Math.min(60, count - visible.length)} 个(剩余 ${count - visible.length})</button>` : ''}`;
|
|
680
|
+
if (checkHealth && !initialHealthCheckStarted && items.some((item) => item.connected?.length > 0)) {
|
|
681
|
+
initialHealthCheckStarted = true;
|
|
682
|
+
call('healthCheck', {}).then(() => {
|
|
683
|
+
if (view === 'market') loadMarket({ checkHealth: false });
|
|
684
|
+
});
|
|
685
|
+
}
|
|
673
686
|
}
|
|
674
687
|
|
|
675
688
|
function cardHtml(d) {
|
|
676
|
-
const connected = d.connected?.length > 0;
|
|
677
689
|
const icon = isImageIcon(d.icon)
|
|
678
690
|
? `<img src="${esc(d.icon)}" alt="" onerror="this.parentElement.innerHTML='🔌';" />`
|
|
679
691
|
: `<span>${esc(d.icon || '🔌')}</span>`;
|
|
680
|
-
const actions = actionHtml(d
|
|
692
|
+
const actions = actionHtml(d);
|
|
681
693
|
const probe = d.probeStatus && d.probeStatus !== 'unverified' ? `<span class="badge ${esc(d.probeStatus)}">${d.probeStatus}</span>` : '';
|
|
682
694
|
return `<div class="card ${d.featured ? 'featured' : ''}" data-id="${esc(d.id)}" role="button" tabindex="0" aria-label="查看 ${esc(d.name)} 详情" onclick="window.__mcp.showDetail('${esc(d.id)}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.__mcp.showDetail('${esc(d.id)}')}">
|
|
683
695
|
<div class="icon">${icon}</div>
|
|
@@ -689,17 +701,24 @@
|
|
|
689
701
|
</div>`;
|
|
690
702
|
}
|
|
691
703
|
|
|
692
|
-
function actionHtml(d
|
|
704
|
+
function actionHtml(d) {
|
|
693
705
|
const id = esc(d.id);
|
|
706
|
+
const configured = d.connected?.length > 0;
|
|
707
|
+
const state = d.connectionState || (configured ? 'configured' : 'disconnected');
|
|
708
|
+
if (configured) {
|
|
709
|
+
if (state === 'healthy') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('connected')}</button>`;
|
|
710
|
+
if (state === 'configured') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('configured')}</button>`;
|
|
711
|
+
if (state === 'disabled') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('disabled')}</button>`;
|
|
712
|
+
if (state === 'reauth') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('reauthorize')}</button>`;
|
|
713
|
+
if (state === 'degraded') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('partialError')}</button>`;
|
|
714
|
+
if (state === 'unavailable') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-bad" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('connectionError')}</button>`;
|
|
715
|
+
}
|
|
694
716
|
if (d.authMode === 'oauth2-pkce') {
|
|
695
|
-
if (connected) return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('connected')}</button>`;
|
|
696
717
|
return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn" type="button" onclick="event.stopPropagation();window.__mcp.connect('${id}')">${t('connect')}</button>`;
|
|
697
718
|
}
|
|
698
719
|
if (d.authMode === 'none') {
|
|
699
|
-
if (connected) return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('connected')}</button>`;
|
|
700
720
|
return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn" type="button" onclick="event.stopPropagation();window.__mcp.connect('${id}')">${t('connect')}</button>`;
|
|
701
721
|
}
|
|
702
|
-
if (connected) return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('connected')}</button>`;
|
|
703
722
|
return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn" type="button" onclick="event.stopPropagation();window.__mcp.openConfig('${id}')">${t('configure')}</button>`;
|
|
704
723
|
}
|
|
705
724
|
|
|
@@ -718,10 +737,13 @@
|
|
|
718
737
|
function rowHtml(r) {
|
|
719
738
|
const key = esc(r.key);
|
|
720
739
|
const authMode = r.authMode === 'oauth' ? '<span class="badge auth-oauth">OAuth</span>' : `<span class="badge">${esc(r.authMode || 'manual')}</span>`;
|
|
740
|
+
const healthLabels = { healthy: '已连接', configured: '已配置', reauth: '需重新授权', degraded: '部分异常', unavailable: '连接异常', disabled: '已停用' };
|
|
741
|
+
const healthClass = r.connectionState === 'healthy' ? 'pass' : ['reauth', 'degraded'].includes(r.connectionState) ? 'partial' : r.connectionState === 'unavailable' ? 'fail' : '';
|
|
742
|
+
const healthBadge = `<span class="badge ${healthClass}">${esc(healthLabels[r.connectionState] || '已配置')}</span>`;
|
|
721
743
|
const enabled = r.enabled !== false;
|
|
722
744
|
return `<div class="row" data-key="${key}">
|
|
723
745
|
<div class="info">
|
|
724
|
-
<div class="rname">${esc(r.name)} ${authMode}</div>
|
|
746
|
+
<div class="rname">${esc(r.name)} ${authMode} ${healthBadge}</div>
|
|
725
747
|
<div class="rmeta">${esc(r.serverName || '')} · ${esc(r.url || '')}</div>
|
|
726
748
|
</div>
|
|
727
749
|
<label class="switch" title="${enabled ? '停用' : '启用'}" aria-label="${enabled ? '停用' : '启用'} ${esc(r.name)}">
|
|
@@ -867,7 +889,10 @@
|
|
|
867
889
|
async connect(connectorId) {
|
|
868
890
|
const r = await call('connect', { connectorId });
|
|
869
891
|
showToast(r.message, r.ok);
|
|
870
|
-
if (r.ok)
|
|
892
|
+
if (r.ok) {
|
|
893
|
+
await call('healthCheck', { connectorId });
|
|
894
|
+
await refresh();
|
|
895
|
+
}
|
|
871
896
|
},
|
|
872
897
|
toggle(key, enabled) {
|
|
873
898
|
call('setEnabled', { key, enabled }).then((r) => showToast(r.message, r.ok));
|
|
@@ -984,13 +1009,15 @@
|
|
|
984
1009
|
const d = marketItems[connectorId];
|
|
985
1010
|
if (!d) return;
|
|
986
1011
|
detailConnector = d;
|
|
987
|
-
|
|
1012
|
+
detailFailureState = d.connectionState || '';
|
|
1013
|
+
detailNeedsReconfigure = ['reauth', 'unavailable'].includes(detailFailureState);
|
|
988
1014
|
const loadId = ++detailLoadId;
|
|
989
1015
|
$('#detail-icon').innerHTML = isImageIcon(d.icon)
|
|
990
1016
|
? `<img src="${esc(d.icon)}" alt="" onerror="this.parentElement.innerHTML='🔌';" />`
|
|
991
1017
|
: `<span style="font-size:24px;">${esc(d.icon || '')}</span>`;
|
|
992
1018
|
$('#detail-name').textContent = d.name || '';
|
|
993
|
-
|
|
1019
|
+
const initialStateLabel = d.connectionLabel || (d.connected?.length ? '已配置' : '未连接');
|
|
1020
|
+
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${initialStateLabel} ${d.connected?.length || 0} 个服务` : `${initialStateLabel} ${d.connected?.length || 0} 个服务`;
|
|
994
1021
|
$('#detail-desc').textContent = d.description || d.summary || '';
|
|
995
1022
|
detailPrompts = (d.prompts || []).filter((prompt) => prompt?.text || prompt?.title);
|
|
996
1023
|
promptPage = 0;
|
|
@@ -1004,20 +1031,29 @@
|
|
|
1004
1031
|
setDetailOpen(true);
|
|
1005
1032
|
$('#detail-disconnect-btn').style.display = d.connected?.length > 0 ? 'block' : 'none';
|
|
1006
1033
|
const needsCredential = ['bearer', 'api-key'].includes(d.authMode);
|
|
1007
|
-
$('#detail-try-btn').textContent =
|
|
1034
|
+
$('#detail-try-btn').textContent = detailNeedsReconfigure
|
|
1035
|
+
? d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
|
|
1036
|
+
: d.connected?.length > 0 ? t('try') : needsCredential ? t('configureConnect') : t('connectTry');
|
|
1008
1037
|
const toolsResult = await call('toolsList', { connectorId });
|
|
1009
1038
|
if (loadId !== detailLoadId || !$('#detail-overlay').classList.contains('active')) return;
|
|
1010
1039
|
if (!toolsResult.ok) {
|
|
1040
|
+
detailFailureState = toolsResult.detail?.connectionState || detailFailureState || 'unavailable';
|
|
1041
|
+
detailNeedsReconfigure = true;
|
|
1011
1042
|
$('#tools-summary').textContent = '工具加载失败';
|
|
1012
1043
|
$('#detail-tools').innerHTML = `<div class="tools-error">${esc(toolsResult.message)}</div>`;
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1044
|
+
const failureLabel = detailFailureState === 'reauth' ? (d.authMode === 'oauth2-pkce' ? '授权已失效' : '凭据已失效') : '当前连接异常';
|
|
1045
|
+
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${failureLabel}` : failureLabel;
|
|
1046
|
+
$('#detail-try-btn').textContent = d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth'
|
|
1047
|
+
? '🔐 重新授权'
|
|
1048
|
+
: needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查';
|
|
1049
|
+
renderPromptPage();
|
|
1019
1050
|
return;
|
|
1020
1051
|
}
|
|
1052
|
+
detailFailureState = toolsResult.detail?.connectionState || 'healthy';
|
|
1053
|
+
detailNeedsReconfigure = false;
|
|
1054
|
+
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${detailFailureState === 'degraded' ? '部分服务可用' : '已连接'} ${d.connected?.length || 0} 个服务` : `${detailFailureState === 'degraded' ? '部分服务可用' : '已连接'} ${d.connected?.length || 0} 个服务`;
|
|
1055
|
+
$('#detail-try-btn').textContent = t('try');
|
|
1056
|
+
renderPromptPage();
|
|
1021
1057
|
detailToolServers = toolsResult.detail?.servers || [];
|
|
1022
1058
|
updateToolsSummary();
|
|
1023
1059
|
renderTools();
|
|
@@ -1097,8 +1133,24 @@
|
|
|
1097
1133
|
renderTools();
|
|
1098
1134
|
});
|
|
1099
1135
|
$('#detail-try-btn').addEventListener('click', async () => {
|
|
1100
|
-
if (detailNeedsReconfigure
|
|
1101
|
-
|
|
1136
|
+
if (detailNeedsReconfigure) {
|
|
1137
|
+
if (detailFailureState === 'reauth' && ['bearer', 'api-key'].includes(detailConnector?.authMode)) {
|
|
1138
|
+
openAddConnection('manual', detailConnector);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (detailFailureState === 'reauth' && detailConnector?.authMode === 'oauth2-pkce') {
|
|
1142
|
+
const id = detailConnector.id;
|
|
1143
|
+
const r = await call('connect', { connectorId: id });
|
|
1144
|
+
showToast(r.message, r.ok);
|
|
1145
|
+
if (r.ok) {
|
|
1146
|
+
await call('healthCheck', { connectorId: id });
|
|
1147
|
+
closeDetail();
|
|
1148
|
+
await refresh();
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const id = detailConnector?.id;
|
|
1153
|
+
if (id) await window.__mcp.showDetail(id);
|
|
1102
1154
|
return;
|
|
1103
1155
|
}
|
|
1104
1156
|
if (!detailConnector?.connected?.length) {
|
|
@@ -1154,6 +1206,10 @@
|
|
|
1154
1206
|
$('#refresh').addEventListener('click', async () => {
|
|
1155
1207
|
const r = await call('refreshCatalog', {});
|
|
1156
1208
|
showToast(r.message, r.ok);
|
|
1209
|
+
if (r.ok) {
|
|
1210
|
+
const health = await call('healthCheck', {});
|
|
1211
|
+
showToast(`${r.message};${health.message}`, health.ok);
|
|
1212
|
+
}
|
|
1157
1213
|
await refresh();
|
|
1158
1214
|
});
|
|
1159
1215
|
|