@double-coding/dsh-connectors 0.0.0-stage → 0.1.0

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.
Files changed (125) hide show
  1. package/CONTRIBUTING.md +173 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +156 -0
  4. package/README.md +153 -2
  5. package/assets/readme/architecture.webp +0 -0
  6. package/assets/readme/hero-en.svg +62 -0
  7. package/assets/readme/hero.svg +62 -0
  8. package/assets/readme/panel.webp +0 -0
  9. package/cordis.patch.yml +8 -0
  10. package/dist/client.js +5385 -0
  11. package/dist/client.js.map +7 -0
  12. package/dist/host/catalog.d.ts +7 -0
  13. package/dist/host/catalog.js +15 -0
  14. package/dist/host/catalog.js.map +1 -0
  15. package/dist/host/cli/adapters.d.ts +69 -0
  16. package/dist/host/cli/adapters.js +577 -0
  17. package/dist/host/cli/adapters.js.map +1 -0
  18. package/dist/host/cli/exec.d.ts +34 -0
  19. package/dist/host/cli/exec.js +186 -0
  20. package/dist/host/cli/exec.js.map +1 -0
  21. package/dist/host/cli/service.d.ts +44 -0
  22. package/dist/host/cli/service.js +711 -0
  23. package/dist/host/cli/service.js.map +1 -0
  24. package/dist/host/cli/version.d.ts +1 -0
  25. package/dist/host/cli/version.js +16 -0
  26. package/dist/host/cli/version.js.map +1 -0
  27. package/dist/host/connections.d.ts +106 -0
  28. package/dist/host/connections.js +1207 -0
  29. package/dist/host/connections.js.map +1 -0
  30. package/dist/host/errors.d.ts +4 -0
  31. package/dist/host/errors.js +8 -0
  32. package/dist/host/errors.js.map +1 -0
  33. package/dist/host/instanceUrl.d.ts +1 -0
  34. package/dist/host/instanceUrl.js +27 -0
  35. package/dist/host/instanceUrl.js.map +1 -0
  36. package/dist/host/mcpServers.d.ts +19 -0
  37. package/dist/host/mcpServers.js +243 -0
  38. package/dist/host/mcpServers.js.map +1 -0
  39. package/dist/host/oauth.d.ts +49 -0
  40. package/dist/host/oauth.js +215 -0
  41. package/dist/host/oauth.js.map +1 -0
  42. package/dist/host/openExternal.d.ts +1 -0
  43. package/dist/host/openExternal.js +45 -0
  44. package/dist/host/openExternal.js.map +1 -0
  45. package/dist/host/providers.d.ts +105 -0
  46. package/dist/host/providers.js +199 -0
  47. package/dist/host/providers.js.map +1 -0
  48. package/dist/host/routes.d.ts +18 -0
  49. package/dist/host/routes.js +514 -0
  50. package/dist/host/routes.js.map +1 -0
  51. package/dist/host/sse-bridge.d.ts +1 -0
  52. package/dist/host/sse-bridge.js +168 -0
  53. package/dist/host/sse-bridge.js.map +1 -0
  54. package/dist/host/store.d.ts +184 -0
  55. package/dist/host/store.js +98 -0
  56. package/dist/host/store.js.map +1 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +35 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/shared/categories.d.ts +12 -0
  61. package/dist/shared/categories.js +59 -0
  62. package/dist/shared/categories.js.map +1 -0
  63. package/dist/shared/customConfig.d.ts +1 -0
  64. package/dist/shared/customConfig.js +2 -0
  65. package/dist/shared/customConfig.js.map +1 -0
  66. package/dist/shared/types.d.ts +222 -0
  67. package/dist/shared/types.js +2 -0
  68. package/dist/shared/types.js.map +1 -0
  69. package/package.json +96 -4
  70. package/providers/README.md +44 -0
  71. package/providers/amap/icon.png +0 -0
  72. package/providers/amap/provider.json +34 -0
  73. package/providers/baidu-netdisk/icon.png +0 -0
  74. package/providers/baidu-netdisk/provider.json +36 -0
  75. package/providers/circleci/icon.svg +1 -0
  76. package/providers/circleci/provider.json +29 -0
  77. package/providers/cloudflare-docs/icon.svg +1 -0
  78. package/providers/cloudflare-docs/provider.json +26 -0
  79. package/providers/context7/icon.png +0 -0
  80. package/providers/context7/provider.json +25 -0
  81. package/providers/deepwiki/icon.svg +5 -0
  82. package/providers/deepwiki/provider.json +26 -0
  83. package/providers/firecrawl/icon.png +0 -0
  84. package/providers/firecrawl/provider.json +40 -0
  85. package/providers/gitee/icon.svg +1 -0
  86. package/providers/gitee/provider.json +32 -0
  87. package/providers/github/icon.svg +1 -0
  88. package/providers/github/provider.json +33 -0
  89. package/providers/gitlab/icon.svg +1 -0
  90. package/providers/gitlab/provider.json +31 -0
  91. package/providers/gitlab-self-hosted/icon.svg +1 -0
  92. package/providers/gitlab-self-hosted/provider.json +39 -0
  93. package/providers/grafana/icon.svg +1 -0
  94. package/providers/grafana/provider.json +32 -0
  95. package/providers/linear/icon.svg +1 -0
  96. package/providers/linear/provider.json +41 -0
  97. package/providers/microsoft-learn/icon.svg +6 -0
  98. package/providers/microsoft-learn/provider.json +26 -0
  99. package/providers/neon/icon.svg +1 -0
  100. package/providers/neon/provider.json +41 -0
  101. package/providers/notion/icon.svg +1 -0
  102. package/providers/notion/provider.json +29 -0
  103. package/providers/posthog/icon.svg +1 -0
  104. package/providers/posthog/provider.json +38 -0
  105. package/providers/postman/icon.svg +1 -0
  106. package/providers/postman/provider.json +29 -0
  107. package/providers/qcc/icon.png +0 -0
  108. package/providers/qcc/provider.json +50 -0
  109. package/providers/sentry/icon.svg +1 -0
  110. package/providers/sentry/provider.json +30 -0
  111. package/providers/sourcegraph/icon.png +0 -0
  112. package/providers/sourcegraph/provider.json +32 -0
  113. package/providers/supabase/icon.svg +1 -0
  114. package/providers/supabase/provider.json +52 -0
  115. package/providers/tavily/icon.svg +4 -0
  116. package/providers/tavily/provider.json +41 -0
  117. package/providers/tencent-docs/icon.png +0 -0
  118. package/providers/tencent-docs/provider.json +34 -0
  119. package/providers/tencent-yuanbao-search/icon.png +0 -0
  120. package/providers/tencent-yuanbao-search/provider.json +32 -0
  121. package/providers/tianyancha/icon.png +0 -0
  122. package/providers/tianyancha/provider.json +34 -0
  123. package/providers/yunxiao/icon.svg +1 -0
  124. package/providers/yunxiao/provider.json +30 -0
  125. package/schema/provider.schema.json +325 -0
@@ -0,0 +1,1207 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { BUILTIN_CATEGORIES, MAX_EXAMPLES, resolveCategory, validateCategory } from '../shared/categories.js';
3
+ import { CUSTOM_SECRET_PLACEHOLDER } from '../shared/customConfig.js';
4
+ import { ROUTE_PREFIX } from '../shared/types.js';
5
+ import { ConnectError } from './errors.js';
6
+ import { resolveInstanceUrl } from './instanceUrl.js';
7
+ import { assertPrivateUrlsAllowed, parseMcpServers } from './mcpServers.js';
8
+ import { openExternal } from './openExternal.js';
9
+ import { buildAuthorizeUrl, createPkce, createState, exchangeCode, fetchOAuthMetadata, refreshAccessToken, registerOAuthClient, } from './oauth.js';
10
+ import { defineConnectorDomain } from './store.js';
11
+ const SSE_BRIDGE_PATH = fileURLToPath(new URL('./sse-bridge.js', import.meta.url));
12
+ const MCP_CLIENT = '@deepseek-ai/dsh-mcp-client';
13
+ const ENTRY_PREFIX = 'dsh-connectors';
14
+ const OAUTH_PENDING_TTL_MS = 10 * 60 * 1000;
15
+ const OAUTH_FLOW_TTL_MS = 2 * 60 * 1000;
16
+ const OAUTH_REFRESH_SKEW_MS = 60 * 1000;
17
+ const OAUTH_METADATA_TTL_MS = 10 * 60 * 1000;
18
+ // GitLab.com 的 initialize + tools/list 实测可达 15s,5s 会让正常连接误判失败
19
+ const TOOL_WAIT_TIMEOUT_MS = 30_000;
20
+ // stdio 首次由 npx 拉包实测要 80s 以上才完成 initialize,按 HTTP 的 30s 会误判
21
+ const STDIO_TOOL_WAIT_TIMEOUT_MS = 120_000;
22
+ function toolWaitTimeout(transport) {
23
+ return transport === 'stdio' ? STDIO_TOOL_WAIT_TIMEOUT_MS : TOOL_WAIT_TIMEOUT_MS;
24
+ }
25
+ function toolWaitMessage(transport, timeoutMs, lead) {
26
+ const seconds = timeoutMs / 1000;
27
+ if (transport === 'stdio') {
28
+ return `${lead}${seconds} 秒内没有发现工具:启动命令没有就绪。请先在终端运行该命令确认能启动,首次用 npx 运行还需要先下载依赖`;
29
+ }
30
+ return `${lead}${seconds} 秒内没有发现工具,请检查服务地址或凭据`;
31
+ }
32
+ function entryId(connectorId) {
33
+ return `${ENTRY_PREFIX}-${connectorId}`;
34
+ }
35
+ function headersFor(record) {
36
+ const headers = { ...(record.headers ?? {}) };
37
+ if (record.oauth !== undefined) {
38
+ headers.Authorization = `Bearer ${record.oauth.accessToken}`;
39
+ }
40
+ else if (record.auth !== undefined) {
41
+ headers[record.auth.header] = `${record.auth.prefix}${record.auth.token}`;
42
+ }
43
+ return headers;
44
+ }
45
+ function redactCustomUrl(value) {
46
+ try {
47
+ const url = new URL(value);
48
+ if (url.search === '' && url.hash === '')
49
+ return value;
50
+ return `${url.origin}${url.pathname}?${CUSTOM_SECRET_PLACEHOLDER}`;
51
+ }
52
+ catch {
53
+ return value;
54
+ }
55
+ }
56
+ function restoreCustomUrl(value, previous) {
57
+ const marker = `?${CUSTOM_SECRET_PLACEHOLDER}`;
58
+ if (!value.endsWith(marker) || previous === undefined)
59
+ return value;
60
+ try {
61
+ const url = new URL(previous);
62
+ return `${value.slice(0, -marker.length)}${url.search}${url.hash}`;
63
+ }
64
+ catch {
65
+ return value;
66
+ }
67
+ }
68
+ function redactValues(values) {
69
+ return Object.fromEntries(Object.keys(values).map(key => [key, CUSTOM_SECRET_PLACEHOLDER]));
70
+ }
71
+ function mergeSecretValues(next, previous, label) {
72
+ if (next === undefined)
73
+ return undefined;
74
+ const merged = {};
75
+ for (const [key, value] of Object.entries(next)) {
76
+ if (value !== CUSTOM_SECRET_PLACEHOLDER) {
77
+ merged[key] = value;
78
+ continue;
79
+ }
80
+ const stored = previous?.[key];
81
+ if (stored === undefined) {
82
+ throw new ConnectError('invalid_custom_config', `${label}.${key} 是占位值,但没有已保存的值,请填写新值`);
83
+ }
84
+ merged[key] = stored;
85
+ }
86
+ return merged;
87
+ }
88
+ function sameStringArray(left, right) {
89
+ if (left === undefined || right === undefined)
90
+ return left === right;
91
+ return left.length === right.length && left.every((item, index) => item === right[index]);
92
+ }
93
+ function sameStringMap(left, right) {
94
+ if (left === undefined || right === undefined)
95
+ return left === right;
96
+ const leftKeys = Object.keys(left).sort();
97
+ const rightKeys = Object.keys(right).sort();
98
+ return leftKeys.length === rightKeys.length
99
+ && leftKeys.every((key, index) => key === rightKeys[index] && left[key] === right[key]);
100
+ }
101
+ function sameConnectionConfig(left, right) {
102
+ return left.url === right.url
103
+ && left.command === right.command
104
+ && left.cwd === right.cwd
105
+ && sameStringArray(left.args, right.args)
106
+ && sameStringMap(left.env, right.env)
107
+ && sameStringMap(left.headers, right.headers);
108
+ }
109
+ function entryConfig(record, failOnStartupError) {
110
+ if (record.transport === 'sse') {
111
+ if (record.url === undefined)
112
+ throw new ConnectError('invalid_connector', 'SSE 连接缺少地址');
113
+ return {
114
+ transport: 'stdio',
115
+ serverName: record.serverName,
116
+ command: process.execPath,
117
+ args: [SSE_BRIDGE_PATH],
118
+ env: {
119
+ DSH_SSE_URL: record.url,
120
+ DSH_SSE_HEADERS: JSON.stringify(headersFor(record)),
121
+ ELECTRON_RUN_AS_NODE: '1',
122
+ },
123
+ failOnStartupError,
124
+ };
125
+ }
126
+ if (record.transport === 'stdio') {
127
+ if (record.command === undefined)
128
+ throw new ConnectError('invalid_connector', 'stdio 连接缺少启动命令');
129
+ return {
130
+ transport: 'stdio',
131
+ serverName: record.serverName,
132
+ command: record.command,
133
+ args: record.args ?? [],
134
+ ...(record.env === undefined ? {} : { env: record.env }),
135
+ ...(record.cwd === undefined ? {} : { cwd: record.cwd }),
136
+ failOnStartupError,
137
+ };
138
+ }
139
+ if (record.url === undefined)
140
+ throw new ConnectError('invalid_connector', 'HTTP 连接缺少地址');
141
+ return {
142
+ transport: 'streamable-http',
143
+ serverName: record.serverName,
144
+ url: record.url,
145
+ headers: headersFor(record),
146
+ failOnStartupError,
147
+ };
148
+ }
149
+ function secretValues(record) {
150
+ const values = [
151
+ ...(record.auth === undefined ? [] : [record.auth.token]),
152
+ ...(record.oauth === undefined
153
+ ? []
154
+ : [
155
+ record.oauth.accessToken,
156
+ ...(record.oauth.refreshToken === undefined ? [] : [record.oauth.refreshToken]),
157
+ ...(record.oauth.clientSecret === undefined ? [] : [record.oauth.clientSecret]),
158
+ ]),
159
+ ...Object.values(record.headers ?? {}),
160
+ ...Object.values(record.env ?? {}),
161
+ ];
162
+ return values.filter(value => value.length >= 4).sort((left, right) => right.length - left.length);
163
+ }
164
+ function describeError(error, record) {
165
+ let message = error instanceof Error ? error.message : String(error);
166
+ if (record !== undefined) {
167
+ for (const secret of secretValues(record))
168
+ message = message.split(secret).join('***');
169
+ }
170
+ return message.length > 600 ? `${message.slice(0, 600)}…` : message;
171
+ }
172
+ function parseRedirectUri(value) {
173
+ let url;
174
+ try {
175
+ url = new URL(value);
176
+ }
177
+ catch {
178
+ throw new ConnectError('invalid_request', 'OAuth 回调地址不合法');
179
+ }
180
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:')
181
+ || url.pathname !== `${ROUTE_PREFIX}/api/oauth/callback`
182
+ || url.search !== ''
183
+ || url.hash !== '') {
184
+ throw new ConnectError('invalid_request', 'OAuth 回调地址不合法');
185
+ }
186
+ const host = url.hostname;
187
+ if (host !== '127.0.0.1' && host !== 'localhost' && host !== '[::1]') {
188
+ throw new ConnectError('invalid_request', 'OAuth 回调地址必须是本机地址');
189
+ }
190
+ return url.toString();
191
+ }
192
+ export class ConnectionService {
193
+ ctx;
194
+ catalog;
195
+ domainPromise;
196
+ oauthPending = new Map();
197
+ oauthFlows = new Map();
198
+ oauthClients = new Map();
199
+ oauthMetadata = new Map();
200
+ disposing = false;
201
+ constructor(ctx, catalog) {
202
+ this.ctx = ctx;
203
+ this.catalog = catalog;
204
+ if (ctx.loader === undefined)
205
+ throw new Error('loader service is unavailable');
206
+ if (ctx.storageDomain === undefined)
207
+ throw new Error('storageDomain service is unavailable');
208
+ this.domainPromise = ctx.storageDomain.open(defineConnectorDomain());
209
+ }
210
+ async table() {
211
+ const domain = await this.domainPromise;
212
+ return domain.table('connections');
213
+ }
214
+ async customTable() {
215
+ const domain = await this.domainPromise;
216
+ return domain.table('custom_connectors');
217
+ }
218
+ async dispose() {
219
+ this.disposing = true;
220
+ const domain = await this.domainPromise;
221
+ await domain.close();
222
+ }
223
+ async list() {
224
+ const table = await this.table();
225
+ return [...table.entries()].map(([, record]) => this.view(record));
226
+ }
227
+ async listCards() {
228
+ const customTable = await this.customTable();
229
+ const custom = [...customTable.entries()]
230
+ .sort(([, left], [, right]) => left.createdAt - right.createdAt)
231
+ .map(([, record]) => this.customCard(record));
232
+ // 全部 Tab 按分类块连续排列:分类顺序沿用 Tab 顺序,组内保持 provider order,自定义接在同分类内置之后
233
+ const buckets = new Map();
234
+ const labels = [];
235
+ for (const card of [...this.catalog(), ...custom]) {
236
+ const bucket = buckets.get(card.category);
237
+ if (bucket === undefined) {
238
+ buckets.set(card.category, [card]);
239
+ labels.push(card.category);
240
+ }
241
+ else {
242
+ bucket.push(card);
243
+ }
244
+ }
245
+ const rank = new Map(BUILTIN_CATEGORIES.map((category, index) => [category, index]));
246
+ const fallback = BUILTIN_CATEGORIES.length;
247
+ labels.sort((left, right) => {
248
+ const leftRank = rank.get(left) ?? fallback;
249
+ const rightRank = rank.get(right) ?? fallback;
250
+ return leftRank - rightRank || left.localeCompare(right, 'zh-CN');
251
+ });
252
+ return labels.flatMap(label => buckets.get(label) ?? []);
253
+ }
254
+ async restore() {
255
+ const table = await this.table();
256
+ for (const [, record] of table.entries()) {
257
+ if (!record.enabled || this.disposing)
258
+ continue;
259
+ const card = this.cardFor(record.connectorId);
260
+ if (card?.instance !== undefined && record.url === undefined)
261
+ continue;
262
+ if (this.needsAuth(card) && record.auth === undefined && record.oauth === undefined)
263
+ continue;
264
+ try {
265
+ const fresh = await this.ensureFreshOAuth(record);
266
+ if (fresh !== record)
267
+ await table.put(fresh.key, fresh);
268
+ await this.provision(fresh, false);
269
+ }
270
+ catch (error) {
271
+ await this.rememberError(record.key, describeError(error, record));
272
+ }
273
+ }
274
+ }
275
+ async connect(connectorId, token, instanceUrl) {
276
+ const builtinCard = this.catalog().find(item => item.id === connectorId);
277
+ const table = await this.table();
278
+ const previous = table.get(connectorId);
279
+ const now = Date.now();
280
+ let record;
281
+ if (builtinCard !== undefined && builtinCard.published) {
282
+ if (builtinCard.instance !== undefined) {
283
+ return this.connectInstance(builtinCard, token, instanceUrl);
284
+ }
285
+ if (builtinCard.auth.type === 'oauth2')
286
+ return this.installOAuth(builtinCard);
287
+ if ((builtinCard.auth.type === 'bearer' || builtinCard.auth.type === 'header')
288
+ && (token === undefined || token.trim() === '')) {
289
+ return this.installToken(builtinCard);
290
+ }
291
+ const server = builtinCard.server;
292
+ record = {
293
+ key: builtinCard.id,
294
+ connectorId: builtinCard.id,
295
+ name: builtinCard.name,
296
+ serverName: server.serverName,
297
+ transport: server.transport,
298
+ url: server.url,
299
+ command: server.command,
300
+ args: server.args,
301
+ auth: this.authFor(builtinCard, token),
302
+ enabled: true,
303
+ createdAt: previous?.createdAt ?? now,
304
+ updatedAt: now,
305
+ };
306
+ }
307
+ else {
308
+ const custom = (await this.customTable()).get(connectorId);
309
+ if (custom === undefined)
310
+ throw new ConnectError('not_found', '连接器不存在或未发布');
311
+ record = {
312
+ key: custom.connectorId,
313
+ connectorId: custom.connectorId,
314
+ name: custom.name,
315
+ serverName: custom.serverName,
316
+ transport: custom.transport,
317
+ url: custom.url,
318
+ command: custom.command,
319
+ args: custom.args,
320
+ env: custom.env,
321
+ cwd: custom.cwd,
322
+ headers: custom.headers,
323
+ enabled: true,
324
+ createdAt: previous?.createdAt ?? now,
325
+ updatedAt: now,
326
+ };
327
+ }
328
+ try {
329
+ await this.provision(record, true);
330
+ const timeoutMs = toolWaitTimeout(record.transport);
331
+ const tools = await this.waitForTools(record.serverName, timeoutMs);
332
+ if (tools.length === 0) {
333
+ await this.removeEntry(record.connectorId);
334
+ throw new ConnectError('connect_failed', toolWaitMessage(record.transport, timeoutMs, '连接已提交,但 '));
335
+ }
336
+ }
337
+ catch (error) {
338
+ if (previous !== undefined)
339
+ await this.rememberError(record.key, describeError(error, record));
340
+ if (error instanceof ConnectError)
341
+ throw error;
342
+ throw new ConnectError('connect_failed', describeError(error, record));
343
+ }
344
+ await table.put(record.key, record);
345
+ return this.view(record);
346
+ }
347
+ async connectInstance(card, token, instanceUrl) {
348
+ const table = await this.table();
349
+ const previous = table.get(card.id);
350
+ const now = Date.now();
351
+ const raw = instanceUrl?.trim();
352
+ let url = previous?.url;
353
+ if (raw !== undefined && raw !== '')
354
+ url = resolveInstanceUrl(raw);
355
+ const base = {
356
+ key: card.id,
357
+ connectorId: card.id,
358
+ name: card.name,
359
+ serverName: card.server.serverName,
360
+ transport: card.server.transport,
361
+ enabled: true,
362
+ createdAt: previous?.createdAt ?? now,
363
+ updatedAt: now,
364
+ };
365
+ if (url === undefined) {
366
+ await this.removeEntry(card.id);
367
+ await table.put(base.key, base);
368
+ return this.view(base);
369
+ }
370
+ if (card.auth.type === 'oauth2') {
371
+ await this.removeEntry(card.id);
372
+ const record = { ...base, url };
373
+ await table.put(record.key, record);
374
+ return this.view(record);
375
+ }
376
+ if ((card.auth.type === 'bearer' || card.auth.type === 'header')
377
+ && (token === undefined || token.trim() === '')) {
378
+ await this.removeEntry(card.id);
379
+ const record = { ...base, url };
380
+ await table.put(record.key, record);
381
+ return this.view(record);
382
+ }
383
+ const record = { ...base, url, auth: this.authFor(card, token) };
384
+ try {
385
+ await this.provision(record, true);
386
+ const timeoutMs = TOOL_WAIT_TIMEOUT_MS;
387
+ const tools = await this.waitForTools(record.serverName, timeoutMs);
388
+ if (tools.length === 0) {
389
+ await this.removeEntry(record.connectorId);
390
+ throw new ConnectError('connect_failed', `连接已提交,但 ${timeoutMs / 1000} 秒内没有发现工具,请检查服务地址或凭据`);
391
+ }
392
+ }
393
+ catch (error) {
394
+ if (previous !== undefined)
395
+ await this.rememberError(record.key, describeError(error, record));
396
+ if (error instanceof ConnectError)
397
+ throw error;
398
+ throw new ConnectError('connect_failed', describeError(error, record));
399
+ }
400
+ await table.put(record.key, record);
401
+ return this.view(record);
402
+ }
403
+ async disconnect(connectorId) {
404
+ const table = await this.table();
405
+ await this.removeEntry(connectorId);
406
+ await table.delete(connectorId);
407
+ }
408
+ async revokeOAuth(connectorId) {
409
+ const table = await this.table();
410
+ const record = table.get(connectorId);
411
+ if (record === undefined)
412
+ throw new ConnectError('not_found', '连接记录不存在');
413
+ if (record.oauth === undefined)
414
+ return this.view(record);
415
+ await this.removeEntry(connectorId);
416
+ const { oauth: _oauth, ...rest } = record;
417
+ const updated = {
418
+ ...rest,
419
+ enabled: true,
420
+ lastError: undefined,
421
+ updatedAt: Date.now(),
422
+ };
423
+ await table.put(connectorId, updated);
424
+ return this.view(updated);
425
+ }
426
+ async reconfigureInstance(connectorId, instanceUrl) {
427
+ const card = this.catalog().find(item => item.id === connectorId);
428
+ if (card === undefined || !card.published || card.instance === undefined) {
429
+ throw new ConnectError('not_found', '连接器不存在或不支持更换地址');
430
+ }
431
+ const table = await this.table();
432
+ const previous = table.get(connectorId);
433
+ if (previous === undefined)
434
+ throw new ConnectError('not_installed', '请先安装连接器');
435
+ const url = resolveInstanceUrl(instanceUrl);
436
+ for (const [state, pending] of this.oauthPending) {
437
+ if (pending.connectorId === connectorId)
438
+ this.oauthPending.delete(state);
439
+ }
440
+ for (const [state, flow] of this.oauthFlows) {
441
+ if (flow.connectorId === connectorId)
442
+ this.oauthFlows.delete(state);
443
+ }
444
+ if (previous.url === url)
445
+ return this.view(previous);
446
+ await this.removeEntry(connectorId);
447
+ const { oauth: _oauth, auth: _auth, ...rest } = previous;
448
+ const updated = {
449
+ ...rest,
450
+ url,
451
+ enabled: true,
452
+ lastError: undefined,
453
+ updatedAt: Date.now(),
454
+ };
455
+ await table.put(connectorId, updated);
456
+ return this.view(updated);
457
+ }
458
+ async setEnabled(connectorId, enabled) {
459
+ const table = await this.table();
460
+ const record = table.get(connectorId);
461
+ if (record === undefined)
462
+ throw new ConnectError('not_found', '连接记录不存在');
463
+ if (record.enabled === enabled)
464
+ return this.view(record);
465
+ if (!enabled) {
466
+ await this.removeEntry(connectorId);
467
+ const updated = {
468
+ ...record,
469
+ enabled: false,
470
+ lastError: undefined,
471
+ updatedAt: Date.now(),
472
+ };
473
+ await table.put(connectorId, updated);
474
+ return this.view(updated);
475
+ }
476
+ if (this.needsAuth(this.cardFor(connectorId)) && record.auth === undefined && record.oauth === undefined) {
477
+ const updated = {
478
+ ...record,
479
+ enabled: true,
480
+ lastError: undefined,
481
+ updatedAt: Date.now(),
482
+ };
483
+ await table.put(connectorId, updated);
484
+ return this.view(updated);
485
+ }
486
+ const updated = {
487
+ ...record,
488
+ enabled: true,
489
+ lastError: undefined,
490
+ updatedAt: Date.now(),
491
+ };
492
+ let fresh = updated;
493
+ try {
494
+ fresh = await this.ensureFreshOAuth(updated);
495
+ await this.provision(fresh, true);
496
+ const timeoutMs = toolWaitTimeout(fresh.transport);
497
+ const tools = await this.waitForTools(fresh.serverName, timeoutMs);
498
+ if (tools.length === 0) {
499
+ await this.removeEntry(connectorId);
500
+ throw new ConnectError('connect_failed', toolWaitMessage(fresh.transport, timeoutMs, '启用失败:'));
501
+ }
502
+ }
503
+ catch (error) {
504
+ await this.removeEntry(connectorId).catch(() => undefined);
505
+ const message = describeError(error, fresh);
506
+ await table.put(connectorId, {
507
+ ...fresh,
508
+ enabled: false,
509
+ lastError: message,
510
+ updatedAt: Date.now(),
511
+ });
512
+ if (error instanceof ConnectError)
513
+ throw error;
514
+ throw new ConnectError('connect_failed', message);
515
+ }
516
+ await table.put(connectorId, fresh);
517
+ return this.view(fresh);
518
+ }
519
+ async installOAuth(card) {
520
+ if (card.auth.type !== 'oauth2') {
521
+ throw new ConnectError('oauth_required', `${card.name} 需要 OAuth 授权,请使用授权流程`);
522
+ }
523
+ if (card.instance !== undefined) {
524
+ throw new ConnectError('invalid_connector', '带 instance 的连接器需要先填写服务地址');
525
+ }
526
+ const server = card.server;
527
+ if (server.url === undefined)
528
+ throw new ConnectError('invalid_connector', '连接器没有可用的 MCP Server');
529
+ const table = await this.table();
530
+ const previous = table.get(card.id);
531
+ if (previous?.oauth !== undefined)
532
+ return this.view(previous);
533
+ const url = server.url;
534
+ const now = Date.now();
535
+ const record = {
536
+ key: card.id,
537
+ connectorId: card.id,
538
+ name: card.name,
539
+ serverName: server.serverName,
540
+ transport: server.transport,
541
+ url,
542
+ command: server.command,
543
+ args: server.args,
544
+ enabled: true,
545
+ createdAt: previous?.createdAt ?? now,
546
+ updatedAt: now,
547
+ };
548
+ await this.removeEntry(card.id);
549
+ await table.put(record.key, record);
550
+ return this.view(record);
551
+ }
552
+ async installToken(card) {
553
+ const server = card.server;
554
+ const table = await this.table();
555
+ const previous = table.get(card.id);
556
+ if (previous?.auth !== undefined)
557
+ return this.view(previous);
558
+ const now = Date.now();
559
+ const record = {
560
+ key: card.id,
561
+ connectorId: card.id,
562
+ name: card.name,
563
+ serverName: server.serverName,
564
+ transport: server.transport,
565
+ url: server.url,
566
+ command: server.command,
567
+ args: server.args,
568
+ enabled: true,
569
+ createdAt: previous?.createdAt ?? now,
570
+ updatedAt: now,
571
+ };
572
+ await this.removeEntry(card.id);
573
+ await table.put(record.key, record);
574
+ return this.view(record);
575
+ }
576
+ needsAuth(card) {
577
+ return card?.auth.type === 'oauth2' || card?.auth.type === 'bearer' || card?.auth.type === 'header';
578
+ }
579
+ cardFor(connectorId) {
580
+ return this.catalog().find(item => item.id === connectorId);
581
+ }
582
+ async startOAuth(connectorId, redirectUri) {
583
+ const card = this.catalog().find(item => item.id === connectorId);
584
+ if (card === undefined || !card.published || card.auth.type !== 'oauth2') {
585
+ throw new ConnectError('not_found', '连接器不存在或未发布');
586
+ }
587
+ const redirect = parseRedirectUri(redirectUri);
588
+ const table = await this.table();
589
+ const previous = table.get(connectorId);
590
+ if (previous === undefined) {
591
+ throw new ConnectError('not_installed', '请先安装连接器,再发起授权');
592
+ }
593
+ const serverUrl = previous.url;
594
+ if (serverUrl === undefined) {
595
+ throw card.instance !== undefined
596
+ ? new ConnectError('instance_required', '请先填写服务地址')
597
+ : new ConnectError('invalid_connector', '连接记录缺少服务地址');
598
+ }
599
+ const auth = card.auth;
600
+ let metadata;
601
+ if (auth.endpoints !== undefined) {
602
+ metadata = {
603
+ authorizationEndpoint: auth.endpoints.authorization,
604
+ tokenEndpoint: auth.endpoints.token,
605
+ scope: auth.scopes === undefined ? '' : auth.scopes.join(' '),
606
+ ...(auth.authorizeParams === undefined ? {} : { authorizeParams: auth.authorizeParams }),
607
+ };
608
+ }
609
+ else {
610
+ const cachedMetadata = this.oauthMetadata.get(serverUrl);
611
+ const baseMetadata = cachedMetadata !== undefined && Date.now() - cachedMetadata.at < OAUTH_METADATA_TTL_MS
612
+ ? cachedMetadata.metadata
613
+ : await fetchOAuthMetadata(serverUrl);
614
+ this.oauthMetadata.set(serverUrl, { metadata: baseMetadata, at: Date.now() });
615
+ const scope = auth.scopes !== undefined ? auth.scopes.join(' ') : baseMetadata.scope;
616
+ metadata = { ...baseMetadata, scope };
617
+ }
618
+ const clientKey = `${serverUrl}|${redirect}`;
619
+ const cached = auth.registration === 'dcr' ? this.oauthClients.get(clientKey) : undefined;
620
+ const staticClient = auth.registration === 'dcr' || auth.registration === 'user' ? undefined : auth.registration;
621
+ const previousClient = previous.oauth !== undefined
622
+ && previous.oauth.redirectUri === redirect
623
+ && (auth.registration === 'dcr' || staticClient?.clientId === previous.oauth.clientId)
624
+ ? { clientId: previous.oauth.clientId, clientSecret: previous.oauth.clientSecret }
625
+ : undefined;
626
+ let client;
627
+ if (auth.registration === 'user') {
628
+ const stored = previous.oauthClient
629
+ ?? (previous.oauth !== undefined && previous.oauth.clientSecret !== undefined
630
+ ? { clientId: previous.oauth.clientId, clientSecret: previous.oauth.clientSecret }
631
+ : undefined);
632
+ if (stored === undefined) {
633
+ throw new ConnectError('oauth_client_required', `${card.name} 需要先填写应用的 AppKey 与 SecretKey`);
634
+ }
635
+ client = stored;
636
+ }
637
+ else {
638
+ client = previousClient ?? cached ?? await this.oauthClientFor(auth, metadata, redirect);
639
+ }
640
+ if (auth.registration === 'dcr')
641
+ this.oauthClients.set(clientKey, client);
642
+ const usePkce = auth.pkce !== false;
643
+ const { verifier, challenge } = usePkce ? createPkce() : { verifier: '', challenge: undefined };
644
+ const state = createState();
645
+ const now = Date.now();
646
+ this.pruneOAuthPending();
647
+ this.pruneOAuthFlows();
648
+ this.oauthPending.set(state, {
649
+ connectorId,
650
+ verifier,
651
+ redirectUri: redirect,
652
+ client,
653
+ metadata,
654
+ tokenMethod: auth.tokenMethod ?? 'POST',
655
+ serverUrl,
656
+ createdAt: now,
657
+ });
658
+ this.oauthFlows.set(state, { connectorId, status: 'pending', createdAt: now });
659
+ const authorizeUrl = buildAuthorizeUrl(metadata, {
660
+ clientId: client.clientId,
661
+ redirectUri: redirect,
662
+ state,
663
+ ...(challenge === undefined ? {} : { challenge }),
664
+ });
665
+ const opened = await openExternal(authorizeUrl);
666
+ return { authorizeUrl, flowId: state, opened };
667
+ }
668
+ async saveOAuthClient(connectorId, clientId, clientSecret) {
669
+ const card = this.cardFor(connectorId);
670
+ if (card === undefined || card.auth.type !== 'oauth2' || card.auth.registration !== 'user') {
671
+ throw new ConnectError('invalid_request', '该连接器不需要配置应用凭据');
672
+ }
673
+ const table = await this.table();
674
+ const record = table.get(connectorId);
675
+ if (record === undefined)
676
+ throw new ConnectError('not_installed', '请先安装连接器,再配置应用');
677
+ if (record.oauthClient?.clientId === clientId && record.oauthClient.clientSecret === clientSecret) {
678
+ return this.view(record);
679
+ }
680
+ const next = {
681
+ ...record,
682
+ oauthClient: { clientId, clientSecret },
683
+ updatedAt: Date.now(),
684
+ };
685
+ delete next.oauth;
686
+ delete next.lastError;
687
+ await this.removeEntry(connectorId);
688
+ await table.put(next.key, next);
689
+ return this.view(next);
690
+ }
691
+ async oauthClientFor(auth, metadata, redirect) {
692
+ if (auth.registration === 'dcr') {
693
+ if (metadata.registrationEndpoint === undefined) {
694
+ throw new ConnectError('oauth_failed', '该实例未提供动态客户端注册,暂不支持;请升级实例或使用自定义连接器');
695
+ }
696
+ return registerOAuthClient(metadata, redirect, auth.clientName ?? 'DSH 连接器');
697
+ }
698
+ if (auth.registration === 'user') {
699
+ throw new ConnectError('oauth_client_required', '请先填写应用的 AppKey 与 SecretKey');
700
+ }
701
+ return { clientId: auth.registration.clientId };
702
+ }
703
+ async completeOAuth(state, code) {
704
+ this.pruneOAuthPending();
705
+ const pending = this.oauthPending.get(state);
706
+ if (pending === undefined) {
707
+ throw new ConnectError('oauth_state_invalid', '授权状态无效或已过期,请回到连接器面板重新授权');
708
+ }
709
+ this.oauthPending.delete(state);
710
+ try {
711
+ const view = await this.finishOAuth(pending, code);
712
+ this.oauthFlows.set(state, {
713
+ connectorId: pending.connectorId,
714
+ status: 'done',
715
+ createdAt: pending.createdAt,
716
+ });
717
+ return view;
718
+ }
719
+ catch (error) {
720
+ this.oauthFlows.set(state, {
721
+ connectorId: pending.connectorId,
722
+ status: 'error',
723
+ message: error instanceof Error ? error.message : String(error),
724
+ createdAt: pending.createdAt,
725
+ });
726
+ throw error;
727
+ }
728
+ }
729
+ oauthFlowStatus(flowId) {
730
+ this.pruneOAuthFlows();
731
+ const flow = this.oauthFlows.get(flowId);
732
+ if (flow === undefined)
733
+ return { status: 'expired' };
734
+ return {
735
+ status: flow.status,
736
+ ...(flow.message === undefined ? {} : { message: flow.message }),
737
+ };
738
+ }
739
+ failOAuthFlow(flowId, message) {
740
+ this.pruneOAuthFlows();
741
+ const flow = this.oauthFlows.get(flowId);
742
+ if (flow === undefined)
743
+ return;
744
+ this.oauthPending.delete(flowId);
745
+ this.oauthFlows.set(flowId, { ...flow, status: 'error', message });
746
+ }
747
+ cancelOAuth(flowId) {
748
+ this.pruneOAuthPending();
749
+ this.oauthPending.delete(flowId);
750
+ this.oauthFlows.delete(flowId);
751
+ }
752
+ async finishOAuth(pending, code) {
753
+ const card = this.catalog().find(item => item.id === pending.connectorId);
754
+ if (card === undefined || !card.published)
755
+ throw new ConnectError('not_found', '连接器已下架');
756
+ const server = card.server;
757
+ const tokens = await exchangeCode(pending.metadata.tokenEndpoint, {
758
+ code,
759
+ redirectUri: pending.redirectUri,
760
+ clientId: pending.client.clientId,
761
+ ...(pending.client.clientSecret === undefined ? {} : { clientSecret: pending.client.clientSecret }),
762
+ ...(pending.verifier === '' ? {} : { verifier: pending.verifier }),
763
+ ...(pending.metadata.resource === undefined ? {} : { resource: pending.metadata.resource }),
764
+ tokenMethod: pending.tokenMethod,
765
+ });
766
+ const table = await this.table();
767
+ const previous = table.get(card.id);
768
+ const now = Date.now();
769
+ const record = {
770
+ key: card.id,
771
+ connectorId: card.id,
772
+ name: card.name,
773
+ serverName: server.serverName,
774
+ transport: server.transport,
775
+ url: pending.serverUrl,
776
+ command: server.command,
777
+ args: server.args,
778
+ oauth: {
779
+ provider: card.id,
780
+ clientId: pending.client.clientId,
781
+ ...(pending.client.clientSecret === undefined ? {} : { clientSecret: pending.client.clientSecret }),
782
+ redirectUri: pending.redirectUri,
783
+ tokenEndpoint: pending.metadata.tokenEndpoint,
784
+ tokenMethod: pending.tokenMethod,
785
+ authorizationEndpoint: pending.metadata.authorizationEndpoint,
786
+ accessToken: tokens.accessToken,
787
+ ...(tokens.refreshToken === undefined ? {} : { refreshToken: tokens.refreshToken }),
788
+ ...(tokens.expiresAt === undefined ? {} : { expiresAt: tokens.expiresAt }),
789
+ scope: tokens.scope ?? pending.metadata.scope,
790
+ ...(pending.metadata.resource === undefined ? {} : { resource: pending.metadata.resource }),
791
+ },
792
+ enabled: true,
793
+ ...(pending.client.clientSecret === undefined
794
+ ? (previous?.oauthClient === undefined ? {} : { oauthClient: previous.oauthClient })
795
+ : { oauthClient: { clientId: pending.client.clientId, clientSecret: pending.client.clientSecret } }),
796
+ createdAt: previous?.createdAt ?? now,
797
+ updatedAt: now,
798
+ };
799
+ try {
800
+ await this.provision(record, true);
801
+ const tools = await this.waitForTools(record.serverName, toolWaitTimeout(record.transport));
802
+ if (tools.length === 0) {
803
+ await this.removeEntry(record.connectorId);
804
+ throw new ConnectError('connect_failed', `授权成功,但 ${toolWaitTimeout(record.transport) / 1000} 秒内没有发现 ${card.name} 工具,请稍后重试`);
805
+ }
806
+ }
807
+ catch (error) {
808
+ await this.removeEntry(record.connectorId).catch(() => undefined);
809
+ const message = describeError(error, record);
810
+ await table.put(record.key, {
811
+ ...record,
812
+ enabled: false,
813
+ lastError: message,
814
+ updatedAt: Date.now(),
815
+ });
816
+ if (error instanceof ConnectError)
817
+ throw error;
818
+ throw new ConnectError('connect_failed', message);
819
+ }
820
+ await table.put(record.key, record);
821
+ return this.view(record);
822
+ }
823
+ async ensureFreshOAuth(record) {
824
+ const oauth = record.oauth;
825
+ if (oauth === undefined || oauth.expiresAt === undefined)
826
+ return record;
827
+ if (oauth.expiresAt - Date.now() > OAUTH_REFRESH_SKEW_MS)
828
+ return record;
829
+ if (oauth.refreshToken === undefined) {
830
+ throw new ConnectError('oauth_expired', `${record.name} 授权已过期,请回到连接器面板重新授权`);
831
+ }
832
+ const tokens = await refreshAccessToken(oauth.tokenEndpoint, {
833
+ refreshToken: oauth.refreshToken,
834
+ clientId: oauth.clientId,
835
+ ...(oauth.clientSecret === undefined ? {} : { clientSecret: oauth.clientSecret }),
836
+ ...(oauth.resource === undefined ? {} : { resource: oauth.resource }),
837
+ ...(oauth.tokenMethod === undefined ? {} : { tokenMethod: oauth.tokenMethod }),
838
+ });
839
+ return {
840
+ ...record,
841
+ oauth: {
842
+ ...oauth,
843
+ accessToken: tokens.accessToken,
844
+ refreshToken: tokens.refreshToken ?? oauth.refreshToken,
845
+ expiresAt: tokens.expiresAt ?? oauth.expiresAt,
846
+ scope: tokens.scope ?? oauth.scope,
847
+ },
848
+ updatedAt: Date.now(),
849
+ };
850
+ }
851
+ pruneOAuthPending() {
852
+ const now = Date.now();
853
+ for (const [state, pending] of this.oauthPending) {
854
+ if (now - pending.createdAt > OAUTH_PENDING_TTL_MS)
855
+ this.oauthPending.delete(state);
856
+ }
857
+ }
858
+ pruneOAuthFlows() {
859
+ const now = Date.now();
860
+ for (const [state, flow] of this.oauthFlows) {
861
+ const ttl = flow.status === 'pending' ? OAUTH_PENDING_TTL_MS : OAUTH_FLOW_TTL_MS;
862
+ if (now - flow.createdAt > ttl)
863
+ this.oauthFlows.delete(state);
864
+ }
865
+ }
866
+ async importCustom(text, allowPrivate, options = {}) {
867
+ const seeds = parseMcpServers(text);
868
+ assertPrivateUrlsAllowed(seeds, allowPrivate);
869
+ if (options.category !== undefined) {
870
+ const problem = validateCategory(options.category);
871
+ if (problem !== undefined)
872
+ throw new ConnectError('invalid_category', problem);
873
+ }
874
+ const batchDescription = options.description?.trim();
875
+ if (batchDescription !== undefined && batchDescription !== '' && Array.from(batchDescription).length > 120) {
876
+ throw new ConnectError('invalid_description', '描述最多 120 字');
877
+ }
878
+ const batchExamples = options.examples === undefined
879
+ ? undefined
880
+ : options.examples.map(item => item.trim()).filter(item => item !== '');
881
+ if (batchExamples !== undefined && batchExamples.length > MAX_EXAMPLES) {
882
+ throw new ConnectError('invalid_examples', `示例提示词最多 ${MAX_EXAMPLES} 条`);
883
+ }
884
+ const categoryOverride = options.category === undefined || options.category.trim() === ''
885
+ ? undefined
886
+ : options.category;
887
+ const descriptionOverride = batchDescription === undefined || batchDescription === ''
888
+ ? undefined
889
+ : batchDescription;
890
+ const examplesOverride = batchExamples === undefined || batchExamples.length === 0
891
+ ? undefined
892
+ : batchExamples;
893
+ const customTable = await this.customTable();
894
+ const connections = await this.table();
895
+ const usedNames = new Set();
896
+ for (const card of this.catalog()) {
897
+ usedNames.add(card.server.serverName);
898
+ }
899
+ for (const [, record] of customTable.entries())
900
+ usedNames.add(record.serverName);
901
+ for (const [, record] of connections.entries())
902
+ usedNames.add(record.serverName);
903
+ for (const seed of seeds) {
904
+ if (usedNames.has(seed.serverName)) {
905
+ const internal = seed.displayName === seed.serverName ? '' : `(内部名称 "${seed.serverName}")`;
906
+ throw new ConnectError('server_name_conflict', `serverName "${seed.displayName}"${internal} 已被占用,请改名后重试`);
907
+ }
908
+ usedNames.add(seed.serverName);
909
+ }
910
+ const now = Date.now();
911
+ const cards = [];
912
+ for (const seed of seeds) {
913
+ const rawCategory = categoryOverride ?? seed.category;
914
+ if (rawCategory !== undefined) {
915
+ const problem = validateCategory(rawCategory);
916
+ if (problem !== undefined) {
917
+ throw new ConnectError('invalid_category', `${seed.displayName}: ${problem}`);
918
+ }
919
+ }
920
+ const category = resolveCategory(rawCategory);
921
+ const rawDescription = descriptionOverride ?? seed.description;
922
+ const description = rawDescription?.trim();
923
+ if (description !== undefined && description !== '' && Array.from(description).length > 120) {
924
+ throw new ConnectError('invalid_description', `${seed.displayName}: 描述最多 120 字`);
925
+ }
926
+ const examples = examplesOverride ?? seed.examples ?? [];
927
+ const record = {
928
+ key: seed.connectorId,
929
+ connectorId: seed.connectorId,
930
+ name: seed.displayName,
931
+ serverName: seed.serverName,
932
+ transport: seed.transport,
933
+ url: seed.url,
934
+ headers: seed.headers,
935
+ command: seed.command,
936
+ args: seed.args,
937
+ env: seed.env,
938
+ cwd: seed.cwd,
939
+ category,
940
+ ...(description === undefined || description === '' ? {} : { description }),
941
+ ...(examples.length === 0 ? {} : { examples }),
942
+ createdAt: now,
943
+ updatedAt: now,
944
+ };
945
+ await customTable.put(record.key, record);
946
+ cards.push(this.customCard(record));
947
+ }
948
+ return cards;
949
+ }
950
+ async removeCustom(connectorId) {
951
+ const customTable = await this.customTable();
952
+ if (customTable.get(connectorId) === undefined) {
953
+ throw new ConnectError('not_found', '自定义连接器不存在');
954
+ }
955
+ const connections = await this.table();
956
+ if (connections.get(connectorId) !== undefined) {
957
+ await this.removeEntry(connectorId);
958
+ await connections.delete(connectorId);
959
+ }
960
+ await customTable.delete(connectorId);
961
+ }
962
+ async customConfig(connectorId) {
963
+ const record = (await this.customTable()).get(connectorId);
964
+ if (record === undefined)
965
+ throw new ConnectError('not_found', '自定义连接器不存在');
966
+ const server = {};
967
+ if (record.url !== undefined)
968
+ server.url = redactCustomUrl(record.url);
969
+ if (record.transport === 'sse')
970
+ server.type = 'sse';
971
+ if (record.command !== undefined)
972
+ server.command = record.command;
973
+ if (record.args !== undefined)
974
+ server.args = record.args;
975
+ if (record.env !== undefined)
976
+ server.env = redactValues(record.env);
977
+ if (record.cwd !== undefined)
978
+ server.cwd = record.cwd;
979
+ if (record.headers !== undefined)
980
+ server.headers = redactValues(record.headers);
981
+ const metadata = {};
982
+ if (record.category !== undefined)
983
+ metadata.category = record.category;
984
+ if (record.description !== undefined)
985
+ metadata.description = record.description;
986
+ if (record.examples !== undefined)
987
+ metadata.examples = record.examples;
988
+ if (Object.keys(metadata).length > 0)
989
+ server['x-dsh'] = metadata;
990
+ const connections = await this.table();
991
+ return {
992
+ json: JSON.stringify({ mcpServers: { [record.name]: server } }, null, 2),
993
+ installed: connections.get(connectorId) !== undefined,
994
+ };
995
+ }
996
+ async updateCustom(connectorId, text, allowPrivate, options = {}) {
997
+ const customTable = await this.customTable();
998
+ const previous = customTable.get(connectorId);
999
+ if (previous === undefined)
1000
+ throw new ConnectError('not_found', '自定义连接器不存在');
1001
+ const seeds = parseMcpServers(text);
1002
+ if (seeds.length !== 1) {
1003
+ throw new ConnectError('invalid_custom_config', '编辑时只能保留一个 Server');
1004
+ }
1005
+ const seed = seeds[0];
1006
+ if (seed === undefined) {
1007
+ throw new ConnectError('invalid_custom_config', '编辑时只能保留一个 Server');
1008
+ }
1009
+ if (seed.serverName !== previous.serverName) {
1010
+ throw new ConnectError('server_name_conflict', `编辑时不能修改 serverName(内部名称 "${previous.serverName}")`);
1011
+ }
1012
+ assertPrivateUrlsAllowed(seeds, allowPrivate);
1013
+ if (options.category !== undefined) {
1014
+ const problem = validateCategory(options.category);
1015
+ if (problem !== undefined)
1016
+ throw new ConnectError('invalid_category', problem);
1017
+ }
1018
+ const batchDescription = options.description?.trim();
1019
+ if (batchDescription !== undefined && batchDescription !== '' && Array.from(batchDescription).length > 120) {
1020
+ throw new ConnectError('invalid_description', '描述最多 120 字');
1021
+ }
1022
+ const batchExamples = options.examples === undefined
1023
+ ? undefined
1024
+ : options.examples.map(item => item.trim()).filter(item => item !== '');
1025
+ if (batchExamples !== undefined && batchExamples.length > MAX_EXAMPLES) {
1026
+ throw new ConnectError('invalid_examples', `示例提示词最多 ${MAX_EXAMPLES} 条`);
1027
+ }
1028
+ const rawCategory = options.category ?? seed.category;
1029
+ if (rawCategory !== undefined) {
1030
+ const problem = validateCategory(rawCategory);
1031
+ if (problem !== undefined) {
1032
+ throw new ConnectError('invalid_category', `${seed.displayName}: ${problem}`);
1033
+ }
1034
+ }
1035
+ const category = resolveCategory(rawCategory);
1036
+ const rawDescription = batchDescription === undefined || batchDescription === ''
1037
+ ? seed.description
1038
+ : batchDescription;
1039
+ const description = rawDescription?.trim();
1040
+ if (description !== undefined && description !== '' && Array.from(description).length > 120) {
1041
+ throw new ConnectError('invalid_description', `${seed.displayName}: 描述最多 120 字`);
1042
+ }
1043
+ const examples = batchExamples !== undefined && batchExamples.length > 0
1044
+ ? batchExamples
1045
+ : seed.examples ?? [];
1046
+ const url = seed.url === undefined ? undefined : restoreCustomUrl(seed.url, previous.url);
1047
+ if (url !== undefined && url.endsWith(`?${CUSTOM_SECRET_PLACEHOLDER}`)) {
1048
+ throw new ConnectError('invalid_custom_config', 'URL 中的占位值没有已保存的查询参数,请填写完整地址');
1049
+ }
1050
+ const headers = mergeSecretValues(seed.headers, previous.headers, `${seed.displayName}.headers`);
1051
+ const env = mergeSecretValues(seed.env, previous.env, `${seed.displayName}.env`);
1052
+ const { description: _oldDescription, examples: _oldExamples, ...rest } = previous;
1053
+ const record = {
1054
+ ...rest,
1055
+ name: seed.displayName,
1056
+ transport: seed.transport,
1057
+ url,
1058
+ command: seed.command,
1059
+ args: seed.args,
1060
+ env,
1061
+ cwd: seed.cwd,
1062
+ headers,
1063
+ category,
1064
+ ...(description === undefined || description === '' ? {} : { description }),
1065
+ ...(examples.length === 0 ? {} : { examples }),
1066
+ updatedAt: Date.now(),
1067
+ };
1068
+ await customTable.put(connectorId, record);
1069
+ const changed = !sameConnectionConfig(previous, record);
1070
+ let disconnected = false;
1071
+ if (changed) {
1072
+ const connections = await this.table();
1073
+ if (connections.get(connectorId) !== undefined) {
1074
+ await this.removeEntry(connectorId);
1075
+ await connections.delete(connectorId);
1076
+ disconnected = true;
1077
+ }
1078
+ }
1079
+ return { card: this.customCard(record), disconnected };
1080
+ }
1081
+ authFor(card, token) {
1082
+ const auth = card.auth;
1083
+ if (auth.type === 'none')
1084
+ return undefined;
1085
+ if (auth.type === 'oauth2') {
1086
+ throw new ConnectError('oauth_required', `${card.name} 需要 OAuth 授权,请使用安装流程中的授权按钮`);
1087
+ }
1088
+ const value = token?.trim();
1089
+ if (value === undefined || value === '') {
1090
+ throw new ConnectError('missing_token', `请填写 ${auth.label}`);
1091
+ }
1092
+ if (auth.type === 'bearer') {
1093
+ return { mode: 'bearer', header: 'Authorization', prefix: 'Bearer ', token: value };
1094
+ }
1095
+ return { mode: 'api-key', header: auth.header, prefix: auth.prefix ?? '', token: value };
1096
+ }
1097
+ toolDetailsFor(serverName) {
1098
+ const schemas = this.ctx.tools?.schemas?.() ?? [];
1099
+ const prefix = `mcp__${serverName}__`;
1100
+ return schemas
1101
+ .filter(schema => typeof schema.name === 'string' && schema.name.startsWith(prefix))
1102
+ .map(schema => ({
1103
+ name: schema.name,
1104
+ ...(typeof schema.description === 'string' && schema.description !== ''
1105
+ ? { description: schema.description }
1106
+ : {}),
1107
+ }))
1108
+ .sort((left, right) => left.name.localeCompare(right.name));
1109
+ }
1110
+ toolsFor(serverName) {
1111
+ return this.toolDetailsFor(serverName).map(tool => tool.name);
1112
+ }
1113
+ async waitForTools(serverName, timeoutMs = 5000) {
1114
+ const deadline = Date.now() + timeoutMs;
1115
+ for (;;) {
1116
+ const tools = this.toolsFor(serverName);
1117
+ if (tools.length > 0 || Date.now() >= deadline)
1118
+ return tools;
1119
+ await new Promise(resolve => setTimeout(resolve, 200));
1120
+ }
1121
+ }
1122
+ view(record) {
1123
+ const toolDetails = record.enabled ? this.toolDetailsFor(record.serverName) : [];
1124
+ const card = this.cardFor(record.connectorId);
1125
+ const endpointReady = card?.instance === undefined || record.url !== undefined;
1126
+ return {
1127
+ connectorId: record.connectorId,
1128
+ name: record.name,
1129
+ serverName: record.serverName,
1130
+ transport: record.transport,
1131
+ enabled: record.enabled,
1132
+ authorized: endpointReady
1133
+ && (!this.needsAuth(card) || record.auth !== undefined || record.oauth !== undefined),
1134
+ ...(card?.instance === undefined ? {} : { instanceReady: record.url !== undefined }),
1135
+ ...(card?.auth.type === 'oauth2' && card.auth.registration === 'user'
1136
+ ? { oauthClientReady: record.oauthClient !== undefined || record.oauth !== undefined }
1137
+ : {}),
1138
+ tools: toolDetails.map(tool => tool.name),
1139
+ toolDetails,
1140
+ error: record.lastError,
1141
+ createdAt: record.createdAt,
1142
+ };
1143
+ }
1144
+ customCard(record) {
1145
+ const stdio = record.transport === 'stdio';
1146
+ return {
1147
+ id: record.connectorId,
1148
+ name: record.name,
1149
+ vendor: '自定义',
1150
+ category: resolveCategory(record.category),
1151
+ summary: record.description ?? (stdio
1152
+ ? '通过 mcpServers 导入的本地 stdio MCP Server。'
1153
+ : record.transport === 'sse'
1154
+ ? '通过 mcpServers 导入的 SSE MCP Server。'
1155
+ : '通过 mcpServers 导入的 Streamable HTTP MCP Server。'),
1156
+ ...(record.description === undefined ? {} : { description: record.description }),
1157
+ ...(record.examples === undefined ? {} : { examples: record.examples }),
1158
+ tags: ['自定义', resolveCategory(record.category), record.transport],
1159
+ published: true,
1160
+ auth: { type: 'none' },
1161
+ source: 'custom',
1162
+ server: {
1163
+ serverName: record.serverName,
1164
+ transport: record.transport,
1165
+ ...(record.url === undefined ? {} : { url: record.url }),
1166
+ },
1167
+ };
1168
+ }
1169
+ async rememberError(connectorId, message) {
1170
+ const table = await this.table();
1171
+ const current = table.get(connectorId);
1172
+ if (current === undefined)
1173
+ return;
1174
+ await table.put(connectorId, { ...current, lastError: message, updatedAt: Date.now() });
1175
+ }
1176
+ async provision(record, failOnStartupError) {
1177
+ const loader = this.ctx.loader;
1178
+ if (loader === undefined)
1179
+ throw new ConnectError('loader_unavailable', 'DSH loader 服务不可用');
1180
+ const id = entryId(record.connectorId);
1181
+ const config = entryConfig(record, failOnStartupError);
1182
+ if (this.hasEntry(id)) {
1183
+ await loader.update(id, { config, disabled: false });
1184
+ }
1185
+ else {
1186
+ await loader.create({ id, name: MCP_CLIENT, config, disabled: false });
1187
+ }
1188
+ }
1189
+ async removeEntry(connectorId) {
1190
+ const loader = this.ctx.loader;
1191
+ if (loader === undefined)
1192
+ return;
1193
+ const id = entryId(connectorId);
1194
+ if (!this.hasEntry(id))
1195
+ return;
1196
+ await loader.remove(id);
1197
+ }
1198
+ hasEntry(id) {
1199
+ try {
1200
+ return this.ctx.loader?.resolve(id) !== undefined;
1201
+ }
1202
+ catch {
1203
+ return false;
1204
+ }
1205
+ }
1206
+ }
1207
+ //# sourceMappingURL=connections.js.map