dsh-mcp-connector 0.2.35 → 0.2.37
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 +35 -1
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/catalog/catalog.json +18 -12
- package/docs/STDIO-SUPPORT.md +2 -2
- package/docs/USER-GUIDE.md +9 -0
- package/docs/screenshots/01-market-overview.jpg +0 -0
- package/docs/screenshots/02-connector-detail.jpg +0 -0
- package/docs/screenshots/03-tool-discovery.jpg +0 -0
- package/docs/screenshots/04-json-import.jpg +0 -0
- package/docs/screenshots/README.md +4 -4
- package/docs/screenshots/assets.json +6 -6
- package/lib/catalog.js +28 -1
- package/lib/connectors/oauth-connector.js +5 -5
- package/lib/index.js +20 -3
- package/lib/mcp-provision.js +6 -1
- package/lib/schema.js +7 -1
- package/package.json +1 -1
- package/registry/schema/connector.schema.json +2 -0
- package/ui/index.html +46 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.37] - 2026-09-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- “企查查·智能文档解析”改为 OAuth 2.0 PKCE 一键授权,一次授权同时启用远程在线 URL 与本地文件解析两个 Server;无需再手工复制 API Key。
|
|
12
|
+
- 更新产品截图和演示 GIF,同步当前 109 张市场卡片、智能文档解析名称及 OAuth 双入口说明。
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- 新增受限制的 OAuth-to-stdio 运行时注入:目录必须将 `oauthResource` 精确绑定到同连接器的 HTTPS MCP resource,Access Token 只注入子进程声明的 `oauthTokenEnv`,不写入连接记录、市场输出或日志。
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- 从旧版 API Key 单入口升级时显示“升级一键授权”;只有 OAuth 与远程/本地两个 Server 全部启动成功后才原子替换旧连接,失败时保留旧配置。
|
|
21
|
+
|
|
22
|
+
## [0.2.36] - 2026-09-06
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- “企查查·文档报告”更名为“企查查·智能文档解析”,产品说明、搜索标签、官网入口和示例 Prompt 调整为 OCR、文档结构化解析及 Markdown 输出场景。
|
|
27
|
+
- 智能文档解析卡片改为一次录入完整 Authorization,同时配置远程在线链接 `qcc-document` 和本机文件 `qcc-document-mcp` 两个入口。
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- 市场目录会比较已安装连接与卡片声明的 Server;旧版本仅安装远程入口时显示“补充本地解析”,凭据失效时显示“重新配置凭据”,不再误报完整连接或提示 OAuth 重新授权。
|
|
32
|
+
- 配置页明确区分智能文档解析 API Key 与旧版企查查 OAuth 登录令牌,避免本地上传网关返回 `200215` 身份认证失败。
|
|
33
|
+
|
|
34
|
+
### Verification
|
|
35
|
+
|
|
36
|
+
- 196 项自动测试、lint、版本/营销元数据/商店截图门禁和 npm 发布包白名单/敏感内容扫描全部通过。
|
|
37
|
+
- 远端 `qcc-document` 已用公开 PDF 完成真实解析;本地 `qcc-document-mcp` 已完成 stdio 启动和工具注册验证,并确认旧 OAuth Token 会被上传网关按预期拒绝,必须改用智能文档解析 API Key。
|
|
38
|
+
|
|
7
39
|
## [0.2.35] - 2026-09-05
|
|
8
40
|
|
|
9
41
|
### Fixed
|
|
@@ -548,7 +580,9 @@
|
|
|
548
580
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
549
581
|
- iframe 消息校验同源和消息来源。
|
|
550
582
|
|
|
551
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
583
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.37...HEAD
|
|
584
|
+
[0.2.37]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.36...v0.2.37
|
|
585
|
+
[0.2.36]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.35...v0.2.36
|
|
552
586
|
[0.2.35]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.34...v0.2.35
|
|
553
587
|
[0.2.34]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.33...v0.2.34
|
|
554
588
|
[0.2.33]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.32...v0.2.33
|
package/README.en.md
CHANGED
|
@@ -66,7 +66,7 @@ If the plugin helps you connect an MCP server faster, consider [starring the rep
|
|
|
66
66
|
- Explicit, non-destructive migration from the two earlier Qichacha OAuth plugins, plus active-plugin conflict detection that blocks duplicate server management and credential overwrites.
|
|
67
67
|
|
|
68
68
|
<!-- catalog-stats:start -->
|
|
69
|
-
As of 2026-09-
|
|
69
|
+
As of 2026-09-05, the public Registry publishes 105 connector descriptors. After merging and deduplicating them with the 4 bundled Qichacha cards, the Marketplace exposes 109 cards across 9 business categories. Recommendations remain limited to the four Qichacha cards, PKULaw, and Wind, for 6 featured cards in total. The Registry evolves independently; the badge shown after a client refresh and the live badges above are the authoritative current counts.
|
|
70
70
|
<!-- catalog-stats:end -->
|
|
71
71
|
|
|
72
72
|
## Interface and demo
|
|
@@ -171,7 +171,7 @@ npm run dev:ui
|
|
|
171
171
|
|
|
172
172
|
Every Registry merge regenerates `catalog-stats.json`; an hourly workflow in this repository synchronizes the Chinese and English product copy plus a local stats snapshot. The static npm README updates with package releases, while the live badges above read the Registry directly and therefore stay current without another npm release.
|
|
173
173
|
|
|
174
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
174
|
+
The current public version is [`dsh-mcp-connector@0.2.37`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.37](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.37).
|
|
175
175
|
|
|
176
176
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
177
177
|
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
68
68
|
- 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`export_config`、`snapshot`、`install_from_url`、`status`、`scope`、`health_check`、`policy`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
|
|
69
69
|
|
|
70
70
|
<!-- catalog-stats:start -->
|
|
71
|
-
截至 2026-09-
|
|
71
|
+
截至 2026-09-05,公共 Registry 已发布 105 条连接器描述;与随包的 4 张企查查卡片合并去重后,市场页可浏览 109 张卡片,覆盖企业数据、金融投资、法律合规、开发工具、办公协作、调研分析、设计创意、效率工具、其他 9 类。推荐位严格保留 4 张企查查卡片、北大法宝和 Wind,共 6 张;其他连接器按业务分类展示。Registry 可独立持续更新,实际数量以客户端刷新后的市场页签徽标和上方实时统计徽标为准。
|
|
72
72
|
<!-- catalog-stats:end -->
|
|
73
73
|
|
|
74
74
|
## 界面与演示
|
|
@@ -167,7 +167,7 @@ npm run dev:ui
|
|
|
167
167
|
|
|
168
168
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
169
169
|
|
|
170
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
170
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.37`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.37](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.37)。
|
|
171
171
|
|
|
172
172
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
173
173
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/catalog/catalog.json
CHANGED
|
@@ -131,19 +131,16 @@
|
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"id": "qcc-document",
|
|
134
|
-
"name": "
|
|
134
|
+
"name": "企查查·智能文档解析",
|
|
135
135
|
"vendor": "企查查 Qichacha",
|
|
136
136
|
"icon": "/mcp-connector/ui/assets/qcc-logo.svg",
|
|
137
137
|
"category": "效率工具",
|
|
138
|
-
"summary": "
|
|
139
|
-
"description": "
|
|
140
|
-
"tags": ["
|
|
138
|
+
"summary": "本机文件 / 在线链接 · OCR 与结构化解析",
|
|
139
|
+
"description": "一次 OAuth 授权同时连接远程与本地 Agent:远程入口解析公网 PDF/图片 URL,本地入口读取对话中上传、粘贴或指定路径的本机文件,自动上传并输出 Markdown 与结构化内容。本地入口需 Node.js 20+。",
|
|
140
|
+
"tags": ["智能文档解析", "OCR", "本机文件", "在线文档", "PDF", "图片", "Markdown"],
|
|
141
141
|
"published": true,
|
|
142
142
|
"featured": true,
|
|
143
|
-
"homepage": "https://
|
|
144
|
-
"promptVariables": [
|
|
145
|
-
{ "name": "company", "label": "企业名称", "default": "小米科技有限责任公司", "placeholder": "例如:小米科技有限责任公司", "required": true }
|
|
146
|
-
],
|
|
143
|
+
"homepage": "https://agent.qcc.com/doc",
|
|
147
144
|
"auth": {
|
|
148
145
|
"mode": "oauth2-pkce",
|
|
149
146
|
"issuer": "https://agent.qcc.com",
|
|
@@ -153,12 +150,21 @@
|
|
|
153
150
|
"grantSharing": "issuer"
|
|
154
151
|
},
|
|
155
152
|
"servers": [
|
|
156
|
-
{ "serverKey": "document", "url": "https://agent.qcc.com/mcp/document/stream", "serverName": "qcc-document", "transport": "streamable-http", "headers": {} }
|
|
153
|
+
{ "serverKey": "document", "url": "https://agent.qcc.com/mcp/document/stream", "serverName": "qcc-document", "transport": "streamable-http", "headers": {} },
|
|
154
|
+
{
|
|
155
|
+
"serverKey": "document-local",
|
|
156
|
+
"serverName": "qcc-document-mcp",
|
|
157
|
+
"transport": "stdio",
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["-y", "qcc-document-mcp"],
|
|
160
|
+
"oauthResource": "https://agent.qcc.com/mcp/document/stream",
|
|
161
|
+
"oauthTokenEnv": "QCC_DOCUMENT_AUTHORIZATION"
|
|
162
|
+
}
|
|
157
163
|
],
|
|
158
164
|
"prompts": [
|
|
159
|
-
{ "title": "
|
|
160
|
-
{ "title": "
|
|
161
|
-
{ "title": "
|
|
165
|
+
{ "title": "解析在线文档", "text": "解析这份在线文档:请把公网可访问的 PDF 或图片 URL 粘贴到这里,并输出 Markdown 与关键字段。", "server": "document" },
|
|
166
|
+
{ "title": "解析上传文件", "text": "解析我在对话中上传或粘贴的这份 PDF 或图片,提取正文、表格和关键字段。", "server": "document-local" },
|
|
167
|
+
{ "title": "解析本机文件", "text": "解析本机文件:请把文件路径替换为 ~/Desktop/example.pdf,并输出 Markdown 与结构化摘要。", "server": "document-local" }
|
|
162
168
|
]
|
|
163
169
|
},
|
|
164
170
|
{
|
package/docs/STDIO-SUPPORT.md
CHANGED
|
@@ -152,7 +152,7 @@ const Config = z.union([
|
|
|
152
152
|
|
|
153
153
|
- stdio 是本地进程,密钥最终仍通过环境变量传递(如 `GITHUB_TOKEN`、`OPENAI_API_KEY`)。
|
|
154
154
|
- 无凭据的 stdio 连接器使用 `auth.mode: "none"`;需要用户输入的卡片使用 `bearer` 或 `api-key`,并在 `auth.credentialFields` 声明一个或多个输入字段。
|
|
155
|
-
- `servers[].credentialBindings` 只保存“环境变量名 → 凭据字段 key
|
|
155
|
+
- `servers[].credentialBindings` 只保存“环境变量名 → 凭据字段 key”的映射;OAuth stdio 只能用 `oauthResource` 引用同连接器的 HTTP resource,并通过 `oauthTokenEnv` 声明运行时 Token 环境变量。`servers[].env` 只能包含非敏感默认值。
|
|
156
156
|
- 用户输入存入本机 `ConnectionRecord.env` 并透传给 `dsh-mcp-client`,不回写目录,也不出现在 catalog/status/log 输出。
|
|
157
157
|
- Registry 探针只校验声明与命令形状,绝不执行本地 stdio 命令。
|
|
158
158
|
|
|
@@ -614,7 +614,7 @@ cwd: { type: 'string', description: 'transport=stdio 时的工作目录,默认
|
|
|
614
614
|
| stdio `command` 可执行任意本地命令 | command 来自(a)维护者审核的 catalog,或(b)用户主动 configure,风险可控;DSH 运行时沙箱机制兜底 |
|
|
615
615
|
| `env` 里的密钥泄露到日志 | dsh-mcp-client 的 `buildChildEnv` 已用 `scrubbedParentEnv()` 清理敏感父环境变量;我们持久化 env 到 storage domain 与现有 headers 鉴权一致,无新增泄露面 |
|
|
616
616
|
| `cwd` 空字符串导致 spawn 异常 | provisioning 层显式 `record.cwd \|\| process.cwd()` |
|
|
617
|
-
| 目录夹带密钥 | `auditRawDescriptor` 拒绝真实凭据字段,`auditDescriptor` 拒绝 `env` 中的 token/secret/API Key/password
|
|
617
|
+
| 目录夹带密钥 | `auditRawDescriptor` 拒绝真实凭据字段,`auditDescriptor` 拒绝 `env` 中的 token/secret/API Key/password 类变量;手工凭据只允许 `credentialBindings` 引用已声明字段,OAuth Token 只允许按同连接器 `oauthResource` 在运行时注入 `oauthTokenEnv` |
|
|
618
618
|
| 凭据映射错误或未使用 | Schema 与目录审计检查字段 key、env 名、重复映射、未知引用及未被任何 HTTP/stdio Server 使用的必填字段 |
|
|
619
619
|
|
|
620
620
|
---
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -75,6 +75,15 @@ Bearer/API Key 连接器会先执行 MCP `initialize` 验证。所有 Server 通
|
|
|
75
75
|
|
|
76
76
|
stdio 市场卡片也可能显示一个或多个凭据字段,例如 API Token、区域或租户标识。卡片目录只声明字段名称及其环境变量映射;提交后,真实值仅保存到 DSH 本机连接记录,并由插件注入该 stdio 进程的 `env`。市场、状态页和日志不会返回这些值。插件会等待 Host 完成首次 MCP 初始化与工具同步后再保存连接;失败或超时不会增加已安装数量,卡片也不会提前显示“已连接”。
|
|
77
77
|
|
|
78
|
+
“企查查·智能文档解析”会通过一次 OAuth 一键授权配置两个互不冲突的入口:
|
|
79
|
+
|
|
80
|
+
- `qcc-document`:远程 Agent,解析公网可访问的在线文档或图片 URL。
|
|
81
|
+
- `qcc-document-mcp`:本地 Agent,通过 `npx -y qcc-document-mcp` 读取对话中上传、粘贴或指定路径的本机文件,自动上传并提交解析;要求 Node.js 20+。
|
|
82
|
+
|
|
83
|
+
点击“连接”后只需完成一次企查查 OAuth 授权,插件会同时注册上述两个 Server。OAuth Access Token 仅在运行时注入本地子进程的 `QCC_DOCUMENT_AUTHORIZATION`,不会写入市场目录、连接记录、页面状态或日志;Token 刷新后本地 Agent 会自动更新。从旧版手工 API Key 配置升级时,点击“升级一键授权”,只有在新授权与两个 Server 都启动成功后才会原子替换旧连接;失败会保留原配置。
|
|
84
|
+
|
|
85
|
+
旧版本只配置过远程入口时,卡片会显示“升级一键授权”,不会把单入口误报为完整连接。完成 OAuth 后,插件先验证两个 Server,再原子保存;任一入口失败都不会留下半套配置。
|
|
86
|
+
|
|
78
87
|
OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据发现。动态客户端注册既支持无需客户端密钥的 `none`,也支持服务商签发密钥的 `client_secret_post` 与 `client_secret_basic`。客户端密钥仅与 OAuth Grant 一同保存在 DSH 本机,用于换取、刷新和撤销 Token;插件不会要求用户把 OAuth Token 或客户端密钥复制到聊天中。如服务商在动态客户端注册阶段返回 HTTP 403,页面会明确提示“客户端注册被拒绝、尚未进入浏览器授权”;这通常需要服务商审核或登记客户端,重试用户授权无法绕过。
|
|
79
88
|
|
|
80
89
|
当市场描述声明 `grantSharing: "issuer"` 时,同一账号下相同 issuer、scope 和客户端鉴权方式的卡片共享一组 Grant。首次授权仍只启用用户点击的卡片;之后连接同组卡片会直接复用现有授权,不再重复打开 OAuth 页面。升级时会验证并自动归并旧版本留下的同 issuer 多份 Grant;Desktop 与 `dsh web` 并发时,插件使用跨进程锁串行轮换,并从每 Grant 独立原子日志读取最新 Token,避免 DSH 整文件 storage 的旧进程快照覆盖新凭据。网络、OAuth 元数据发现或服务端 5xx 等暂时故障只进入“自动重试中”;确认 journal 中也没有更新 Token,且明确收到 Refresh Token/客户端失效错误时,才进入“需重新授权”。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# UI 素材说明
|
|
2
2
|
|
|
3
|
-
本目录用于保存 `dsh-mcp-connector` 的公开界面截图。素材于 2026-
|
|
3
|
+
本目录用于保存 `dsh-mcp-connector` 的公开界面截图。素材于 2026-09-06 从 `v0.2.37` 源码的
|
|
4
4
|
无凭据 UI harness 采集:外层浏览器视口为 1280×720,harness 复刻产品宿主在
|
|
5
5
|
`lib/client.js` 中定义的 800px 市场面板,因此桌面端稳定为一行 2 张卡片。公开 Registry
|
|
6
|
-
快照含
|
|
6
|
+
快照含 105 条描述,与 4 张随包唯一卡片合并后市场显示 109 张。画面只展示公开市场元数据、
|
|
7
7
|
示例 Prompt 和明确标识为 Mock 的工具数据,不包含 OAuth Token、API Key、本机路径、
|
|
8
8
|
用户会话或查询结果。
|
|
9
9
|
|
|
@@ -21,8 +21,8 @@ SHA-256、尺寸与 GIF 时长。
|
|
|
21
21
|
|
|
22
22
|
| 文件 | 展示内容 |
|
|
23
23
|
|---|---|
|
|
24
|
-
| `01-market-overview.jpg` |
|
|
25
|
-
| `02-connector-detail.jpg` |
|
|
24
|
+
| `01-market-overview.jpg` | 109 张合并市场卡片、桌面两列、推荐章节、9 分类和固定分类栏;展示“企查查·智能文档解析”新名称与未配置状态 |
|
|
25
|
+
| `02-connector-detail.jpg` | 智能文档解析一次 OAuth 授权同时连接 2 个 Server:远程 Agent 解析在线 URL,本地 Agent 通过 `npx -y qcc-document-mcp` 解析本机文件 |
|
|
26
26
|
| `03-tool-discovery.jpg` | Context7 无凭据 Mock 健康状态、Mock 工具数量/描述、搜索和独立滚动区 |
|
|
27
27
|
| `04-json-import.jpg` | Streamable HTTP / stdio JSON 导入与本机凭据提示 |
|
|
28
28
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"capture": {
|
|
4
|
-
"capturedOn": "2026-
|
|
4
|
+
"capturedOn": "2026-09-06",
|
|
5
5
|
"browserViewport": {
|
|
6
6
|
"width": 1280,
|
|
7
7
|
"height": 720
|
|
@@ -13,32 +13,32 @@
|
|
|
13
13
|
"screenshots": [
|
|
14
14
|
{
|
|
15
15
|
"path": "docs/screenshots/01-market-overview.jpg",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "9c9bc1c6f9e5b247c71ce283b86895b9e971f6283dc6eabaa9dbb1c42ae408c3",
|
|
17
17
|
"width": 1280,
|
|
18
18
|
"height": 720
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "docs/screenshots/02-connector-detail.jpg",
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "dbb7a1493378c30e743dd77c8a35c1779f443a2f8b720871de1efc61ee54eb27",
|
|
23
23
|
"width": 1280,
|
|
24
24
|
"height": 720
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "docs/screenshots/03-tool-discovery.jpg",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "5eeb896db75a5f141a8463f128f7733ffe89af6a447cecb2d1b19f8287a3e1ec",
|
|
29
29
|
"width": 1280,
|
|
30
30
|
"height": 720
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"path": "docs/screenshots/04-json-import.jpg",
|
|
34
|
-
"sha256": "
|
|
34
|
+
"sha256": "de0a87cc7f247fad40ac37188f5a1d026f4087571e98d64595e80f8bcd9410be",
|
|
35
35
|
"width": 1280,
|
|
36
36
|
"height": 720
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
39
|
"demo": {
|
|
40
40
|
"path": "docs/demo.gif",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "c9a84ce9c164c1b9f846f8b5a65afd8dd5c49741c9dfc530f1ad766e47b3d2b6",
|
|
42
42
|
"width": 960,
|
|
43
43
|
"height": 540,
|
|
44
44
|
"durationSeconds": 16
|
package/lib/catalog.js
CHANGED
|
@@ -43,6 +43,11 @@ export function loadBundledCatalog() {
|
|
|
43
43
|
|
|
44
44
|
/** 安全审计:目录禁止携带密钥、URL 协议白名单、header 名白名单。 */
|
|
45
45
|
export function auditDescriptor(descriptor) {
|
|
46
|
+
const oauthHttpResources = new Set(
|
|
47
|
+
descriptor.servers
|
|
48
|
+
.filter((server) => server.transport === 'streamable-http')
|
|
49
|
+
.map((server) => server.url),
|
|
50
|
+
);
|
|
46
51
|
const credentialFields = descriptor.auth.credentialFields ?? [];
|
|
47
52
|
const credentialKeys = new Set();
|
|
48
53
|
for (const field of credentialFields) {
|
|
@@ -59,7 +64,26 @@ export function auditDescriptor(descriptor) {
|
|
|
59
64
|
}
|
|
60
65
|
const bindings = server.credentialBindings ?? {};
|
|
61
66
|
if (descriptor.auth.mode === 'oauth2-pkce') {
|
|
62
|
-
|
|
67
|
+
if (Object.keys(bindings).length > 0) {
|
|
68
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth stdio server 不能同时声明 credentialBindings`);
|
|
69
|
+
}
|
|
70
|
+
if (!server.oauthResource || !server.oauthTokenEnv) {
|
|
71
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth stdio server 必须声明 oauthResource 和 oauthTokenEnv`);
|
|
72
|
+
}
|
|
73
|
+
assertSafeUrl(server.oauthResource);
|
|
74
|
+
if (!oauthHttpResources.has(server.oauthResource)) {
|
|
75
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth stdio oauthResource 必须引用同一连接器的 HTTP resource`);
|
|
76
|
+
}
|
|
77
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(server.oauthTokenEnv)) {
|
|
78
|
+
throw new Error(`connector "${descriptor.id}" oauthTokenEnv 环境变量名非法: ${server.oauthTokenEnv}`);
|
|
79
|
+
}
|
|
80
|
+
if (Object.hasOwn(server.env ?? {}, server.oauthTokenEnv)) {
|
|
81
|
+
throw new Error(`connector "${descriptor.id}" oauthTokenEnv 与 servers[].env 重复声明: ${server.oauthTokenEnv}`);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (server.oauthResource || server.oauthTokenEnv) {
|
|
86
|
+
throw new Error(`connector "${descriptor.id}" 只有 OAuth stdio server 可声明 oauthResource/oauthTokenEnv`);
|
|
63
87
|
}
|
|
64
88
|
if (descriptor.auth.mode === 'none' && Object.keys(bindings).length > 0) {
|
|
65
89
|
throw new Error(`connector "${descriptor.id}" 的 stdio credentialBindings 需要 bearer/api-key 凭据定义`);
|
|
@@ -89,6 +113,9 @@ export function auditDescriptor(descriptor) {
|
|
|
89
113
|
}
|
|
90
114
|
}
|
|
91
115
|
}
|
|
116
|
+
if (descriptor.auth.mode === 'oauth2-pkce' && oauthHttpResources.size === 0) {
|
|
117
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth 授权至少需要一个 HTTP resource`);
|
|
118
|
+
}
|
|
92
119
|
if (['bearer', 'api-key'].includes(descriptor.auth.mode)) {
|
|
93
120
|
const httpServers = descriptor.servers.filter((server) => server.transport === 'streamable-http');
|
|
94
121
|
if (httpServers.length > 0 && credentialFields[0]) referencedCredentials.add(credentialFields[0].key);
|
|
@@ -88,8 +88,8 @@ export function describeOAuthAuthorizationError(error) {
|
|
|
88
88
|
* @param {AbortSignal} [args.signal]
|
|
89
89
|
*/
|
|
90
90
|
export async function oauthAuthorize({ connector, config, logger, signal }) {
|
|
91
|
-
const entryServer = connector.servers
|
|
92
|
-
if (!entryServer) throw stagedError('resource-discovery', `connector "${connector.id}" has no
|
|
91
|
+
const entryServer = connector.servers.find((server) => server.url);
|
|
92
|
+
if (!entryServer) throw stagedError('resource-discovery', `connector "${connector.id}" has no HTTP OAuth resource`);
|
|
93
93
|
|
|
94
94
|
const protectedResource = await atOAuthStage(
|
|
95
95
|
'resource-discovery',
|
|
@@ -166,16 +166,16 @@ export async function oauthAuthorize({ connector, config, logger, signal }) {
|
|
|
166
166
|
let grantedResources;
|
|
167
167
|
if (grantedUrls) {
|
|
168
168
|
const granted = new Set(grantedUrls);
|
|
169
|
-
const grantedServers = connector.servers.filter((s) => granted.has(s.url));
|
|
169
|
+
const grantedServers = connector.servers.filter((s) => granted.has(s.oauthResource ?? s.url));
|
|
170
170
|
grantedKeys = grantedServers.map((s) => s.serverKey);
|
|
171
|
-
grantedResources = grantedServers.map((s) => s.url);
|
|
171
|
+
grantedResources = [...new Set(grantedServers.map((s) => s.oauthResource ?? s.url).filter(Boolean))];
|
|
172
172
|
if (grantedKeys.length === 0) {
|
|
173
173
|
grantedKeys = [entryServer.serverKey];
|
|
174
174
|
grantedResources = [entryServer.url];
|
|
175
175
|
}
|
|
176
176
|
} else {
|
|
177
177
|
grantedKeys = connector.servers.map((s) => s.serverKey);
|
|
178
|
-
grantedResources = connector.servers.map((s) => s.url);
|
|
178
|
+
grantedResources = [...new Set(connector.servers.map((s) => s.oauthResource ?? s.url).filter(Boolean))];
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
return {
|
package/lib/index.js
CHANGED
|
@@ -1051,7 +1051,7 @@ export async function apply(ctx, config) {
|
|
|
1051
1051
|
|
|
1052
1052
|
function oauthResourceOrigins(connector) {
|
|
1053
1053
|
return [...new Set((connector.servers ?? []).map((server) => {
|
|
1054
|
-
try { return new URL(server.url).origin; } catch { return ''; }
|
|
1054
|
+
try { return new URL(server.oauthResource ?? server.url).origin; } catch { return ''; }
|
|
1055
1055
|
}).filter(Boolean))].sort();
|
|
1056
1056
|
}
|
|
1057
1057
|
|
|
@@ -1084,6 +1084,7 @@ export async function apply(ctx, config) {
|
|
|
1084
1084
|
const servers = [];
|
|
1085
1085
|
for (const candidate of sharingConnectors) {
|
|
1086
1086
|
for (const server of candidate.servers) {
|
|
1087
|
+
if (!server.url) continue;
|
|
1087
1088
|
if (seen.has(server.url)) continue;
|
|
1088
1089
|
seen.add(server.url);
|
|
1089
1090
|
servers.push({ ...server, serverKey: `${candidate.id}:${server.serverKey}` });
|
|
@@ -1097,7 +1098,7 @@ export async function apply(ctx, config) {
|
|
|
1097
1098
|
const now = Date.now();
|
|
1098
1099
|
const records = [];
|
|
1099
1100
|
for (const connector of connectors) {
|
|
1100
|
-
for (const server of connector.servers.filter((candidate) => granted.has(candidate.url))) {
|
|
1101
|
+
for (const server of connector.servers.filter((candidate) => granted.has(candidate.oauthResource ?? candidate.url))) {
|
|
1101
1102
|
records.push({
|
|
1102
1103
|
key: `${connector.id}-${server.serverKey}`,
|
|
1103
1104
|
connectorId: connector.id,
|
|
@@ -1106,6 +1107,11 @@ export async function apply(ctx, config) {
|
|
|
1106
1107
|
serverKey: server.serverKey,
|
|
1107
1108
|
transport: server.transport,
|
|
1108
1109
|
url: server.url,
|
|
1110
|
+
command: server.command,
|
|
1111
|
+
args: server.args,
|
|
1112
|
+
env: server.env,
|
|
1113
|
+
oauthTokenEnv: server.oauthTokenEnv,
|
|
1114
|
+
cwd: server.cwd,
|
|
1109
1115
|
serverName: server.serverName,
|
|
1110
1116
|
headers: server.headers,
|
|
1111
1117
|
auth: { mode: 'oauth', grantKey },
|
|
@@ -1146,7 +1152,7 @@ export async function apply(ctx, config) {
|
|
|
1146
1152
|
continue;
|
|
1147
1153
|
}
|
|
1148
1154
|
}
|
|
1149
|
-
if (connector.servers.some((server) => entry.grant.authorizedResources.includes(server.url))) {
|
|
1155
|
+
if (connector.servers.some((server) => entry.grant.authorizedResources.includes(server.oauthResource ?? server.url))) {
|
|
1150
1156
|
return { grantKey, entry };
|
|
1151
1157
|
}
|
|
1152
1158
|
}
|
|
@@ -1709,6 +1715,13 @@ export async function apply(ctx, config) {
|
|
|
1709
1715
|
const visibleRecords = workspaceId
|
|
1710
1716
|
? connectorRecords.filter((record) => connectionVisibleInWorkspace(connectionScopes.binding(record.key), workspaceId))
|
|
1711
1717
|
: connectorRecords;
|
|
1718
|
+
const missingServers = (d.servers ?? []).filter((server) => !visibleRecords.some((record) => (
|
|
1719
|
+
record.serverKey === server.serverKey || record.serverName === server.serverName
|
|
1720
|
+
))).map((server) => ({
|
|
1721
|
+
serverKey: server.serverKey,
|
|
1722
|
+
serverName: server.serverName,
|
|
1723
|
+
transport: server.transport,
|
|
1724
|
+
}));
|
|
1712
1725
|
return ({
|
|
1713
1726
|
id: d.id,
|
|
1714
1727
|
name: d.name,
|
|
@@ -1734,6 +1747,8 @@ export async function apply(ctx, config) {
|
|
|
1734
1747
|
probeReportUrl: d.probeReportUrl,
|
|
1735
1748
|
toolsSnapshot: d.toolsSnapshot ?? [],
|
|
1736
1749
|
connected: visibleRecords.map((record) => record.key),
|
|
1750
|
+
connectionComplete: missingServers.length === 0,
|
|
1751
|
+
missingServers,
|
|
1737
1752
|
connectedScopes: connectorRecords.map((record) => ({ key: record.key, ...scopeDetail(record, workspaceId) })),
|
|
1738
1753
|
connectionState: health.connectionState,
|
|
1739
1754
|
connectionLabel: health.label,
|
|
@@ -1748,6 +1763,8 @@ export async function apply(ctx, config) {
|
|
|
1748
1763
|
command: s.command,
|
|
1749
1764
|
args: s.args,
|
|
1750
1765
|
credentialBindings: s.credentialBindings,
|
|
1766
|
+
oauthResource: s.oauthResource,
|
|
1767
|
+
oauthTokenEnv: s.oauthTokenEnv,
|
|
1751
1768
|
transport: s.transport,
|
|
1752
1769
|
serverName: s.serverName,
|
|
1753
1770
|
})),
|
package/lib/mcp-provision.js
CHANGED
|
@@ -26,12 +26,17 @@ export function authHeaders(record, grants) {
|
|
|
26
26
|
|
|
27
27
|
export function buildEntryConfig(record, grants, { failOnStartupError = false } = {}) {
|
|
28
28
|
if (record.transport === 'stdio') {
|
|
29
|
+
const env = { ...(record.env ?? {}) };
|
|
30
|
+
if (record.auth?.mode === 'oauth' && record.oauthTokenEnv) {
|
|
31
|
+
const grant = record.auth.grantKey ? grants.get(record.auth.grantKey) : null;
|
|
32
|
+
if (grant?.accessToken) env[record.oauthTokenEnv] = `Bearer ${grant.accessToken}`;
|
|
33
|
+
}
|
|
29
34
|
return {
|
|
30
35
|
transport: 'stdio',
|
|
31
36
|
serverName: record.serverName,
|
|
32
37
|
command: record.command,
|
|
33
38
|
args: record.args ?? [],
|
|
34
|
-
env
|
|
39
|
+
env,
|
|
35
40
|
cwd: record.cwd || process.cwd(),
|
|
36
41
|
failOnStartupError,
|
|
37
42
|
};
|
package/lib/schema.js
CHANGED
|
@@ -13,6 +13,8 @@ export const serverRefSchema = z.object({
|
|
|
13
13
|
args: z.array(z.string()).optional(),
|
|
14
14
|
env: z.record(z.string()).optional(),
|
|
15
15
|
credentialBindings: z.record(z.string()).optional(),
|
|
16
|
+
oauthResource: z.string().min(1).optional(),
|
|
17
|
+
oauthTokenEnv: z.string().min(1).optional(),
|
|
16
18
|
cwd: z.string().optional(),
|
|
17
19
|
serverName: z.string().min(1),
|
|
18
20
|
transport: z.enum(['streamable-http', 'sse', 'stdio']).optional(),
|
|
@@ -110,6 +112,7 @@ export const connectionRecordSchema = z.object({
|
|
|
110
112
|
command: z.string().min(1).optional(),
|
|
111
113
|
args: z.array(z.string()).optional(),
|
|
112
114
|
env: z.record(z.string()).optional(),
|
|
115
|
+
oauthTokenEnv: z.string().min(1).optional(),
|
|
113
116
|
cwd: z.string().optional(),
|
|
114
117
|
serverName: z.string().min(1),
|
|
115
118
|
headers: z.record(z.string()).optional(),
|
|
@@ -238,6 +241,8 @@ export function normalizeConnectorDescriptor(raw) {
|
|
|
238
241
|
args: transport === 'stdio' ? (s.args ?? []) : undefined,
|
|
239
242
|
env: transport === 'stdio' ? (s.env ?? {}) : undefined,
|
|
240
243
|
credentialBindings: transport === 'stdio' ? (s.credentialBindings ?? {}) : undefined,
|
|
244
|
+
oauthResource: transport === 'stdio' ? s.oauthResource : undefined,
|
|
245
|
+
oauthTokenEnv: transport === 'stdio' ? s.oauthTokenEnv : undefined,
|
|
241
246
|
cwd: transport === 'stdio' ? (s.cwd ?? '') : undefined,
|
|
242
247
|
serverName: s.serverName,
|
|
243
248
|
transport,
|
|
@@ -334,8 +339,9 @@ export function normalizeConnectionRecord(raw) {
|
|
|
334
339
|
command: transport === 'stdio' ? data.command : undefined,
|
|
335
340
|
args: transport === 'stdio' ? (data.args ?? []) : undefined,
|
|
336
341
|
env: transport === 'stdio' ? (data.env ?? {}) : undefined,
|
|
342
|
+
oauthTokenEnv: transport === 'stdio' && data.auth?.mode === 'oauth' ? data.oauthTokenEnv : undefined,
|
|
337
343
|
cwd: transport === 'stdio' ? (data.cwd ?? '') : undefined,
|
|
338
344
|
headers: transport === 'streamable-http' ? (data.headers ?? {}) : {},
|
|
339
|
-
auth: transport === 'stdio' ? undefined : data.auth,
|
|
345
|
+
auth: transport === 'stdio' && data.auth?.mode !== 'oauth' ? undefined : data.auth,
|
|
340
346
|
};
|
|
341
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
4
4
|
"description": "DeepSeek Harness MCP Connector and MCP Server marketplace with over one hundred MCP connectors, continuously updated. Discover, authorize, and manage connections in one place; supports OAuth 2.0 PKCE, API keys, stdio/HTTP, mcpServers JSON import, and tool and prompt discovery. Maintained by Qichacha/QCC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"args": { "type": "array", "items": { "type": "string" } },
|
|
58
58
|
"env": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
59
59
|
"credentialBindings": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
60
|
+
"oauthResource": { "type": "string", "minLength": 1 },
|
|
61
|
+
"oauthTokenEnv": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
60
62
|
"cwd": { "type": "string" },
|
|
61
63
|
"serverName": { "type": "string", "minLength": 1 },
|
|
62
64
|
"transport": { "enum": ["streamable-http", "stdio"] },
|
package/ui/index.html
CHANGED
|
@@ -521,7 +521,7 @@
|
|
|
521
521
|
reauthorize: '需重新授权', recovering: '自动重试中', connectionError: '连接异常', partialError: '部分异常', disabled: '已停用',
|
|
522
522
|
send: '发送', fillSend: '填写并发送', connectToUse: '连接后使用', try: '💬 去试试',
|
|
523
523
|
connectTry: '🔗 连接后试用', configureConnect: '🔑 录入凭据并连接',
|
|
524
|
-
reconfigureCredential: '🔑 重新配置凭据', loading: '加载中…',
|
|
524
|
+
reconfigureCredential: '🔑 重新配置凭据', completeSetup: '补充本地解析', upgradeOauth: '升级一键授权', loading: '加载中…',
|
|
525
525
|
};
|
|
526
526
|
const t = (key) => LABELS[key] || key;
|
|
527
527
|
const MCP_JSON_EXAMPLE = JSON.stringify({
|
|
@@ -1059,13 +1059,24 @@
|
|
|
1059
1059
|
function actionHtml(d) {
|
|
1060
1060
|
const id = esc(d.id);
|
|
1061
1061
|
const configured = d.connected?.length > 0;
|
|
1062
|
+
const incomplete = configured && d.connectionComplete === false;
|
|
1062
1063
|
const state = d.connectionState || (configured ? 'configured' : 'disconnected');
|
|
1064
|
+
if (incomplete && ['bearer', 'api-key'].includes(d.authMode)) {
|
|
1065
|
+
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.openConfig('${id}')">${t('completeSetup')}</button>`;
|
|
1066
|
+
}
|
|
1067
|
+
if (incomplete && d.authMode === 'oauth2-pkce') {
|
|
1068
|
+
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.connect('${id}')">${t('upgradeOauth')}</button>`;
|
|
1069
|
+
}
|
|
1063
1070
|
if (configured) {
|
|
1064
1071
|
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>`;
|
|
1065
1072
|
if (state === 'unknown') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('unknown')}</button>`;
|
|
1066
1073
|
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>`;
|
|
1067
1074
|
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>`;
|
|
1068
|
-
if (state === 'reauth')
|
|
1075
|
+
if (state === 'reauth') {
|
|
1076
|
+
const credentialAuth = ['bearer', 'api-key'].includes(d.authMode);
|
|
1077
|
+
const action = credentialAuth ? `window.__mcp.openConfig('${id}')` : `window.__mcp.showDetail('${id}')`;
|
|
1078
|
+
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();${action}">${credentialAuth ? t('reconfigureCredential') : t('reauthorize')}</button>`;
|
|
1079
|
+
}
|
|
1069
1080
|
if (state === 'recovering') 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('recovering')}</button>`;
|
|
1070
1081
|
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>`;
|
|
1071
1082
|
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>`;
|
|
@@ -1190,15 +1201,18 @@
|
|
|
1190
1201
|
? `<label for="cfg-token">${esc(credentialName)}(必填)</label><input id="cfg-token" type="password" autocomplete="off" placeholder="${esc(credentialPlaceholder)}" /><div class="field-help">${esc(credentialDescription)}</div>`
|
|
1191
1202
|
: `<div class="form-row"><div><label for="cfg-key-header">API Key Header</label><input id="cfg-key-header" value="${esc(preset.apiKeyHeader || 'X-Api-Key')}" /></div><div><label for="cfg-key-value">${esc(credentialName)}(必填)</label><input id="cfg-key-value" type="password" autocomplete="off" placeholder="${esc(credentialPlaceholder)}" /></div></div><div class="field-help">${esc(credentialDescription)}</div>`;
|
|
1192
1203
|
const serverPreview = servers.map((server) => {
|
|
1204
|
+
const modeLabel = server.transport === 'stdio' ? '本地 Agent · 本机文件' : '远程 Agent · 在线链接文档';
|
|
1193
1205
|
const endpoint = server.transport === 'stdio'
|
|
1194
1206
|
? `stdio · ${server.command || ''} ${(server.args || []).join(' ')}`.trim()
|
|
1195
1207
|
: server.url;
|
|
1196
|
-
return `<div>${esc(server.serverName)} · ${esc(endpoint)}</div>`;
|
|
1208
|
+
return `<div><strong>${esc(modeLabel)}</strong><br>${esc(server.serverName)} · ${esc(endpoint)}</div>`;
|
|
1197
1209
|
}).join('');
|
|
1210
|
+
const mixedDocumentModes = usesStdio && servers.some((server) => server.transport !== 'stdio');
|
|
1198
1211
|
openModal(`配置 ${preset.name}`, `
|
|
1199
1212
|
<input id="cfg-connector-id" type="hidden" value="${esc(preset.id)}" />
|
|
1200
1213
|
<input id="cfg-auth" type="hidden" value="${esc(authMode)}" />
|
|
1201
1214
|
<div class="form-note"><strong>一次配置 ${servers.length} 个 MCP Server</strong>以下 Server 共用同一份 ${esc(credentialName)};凭据仅保存在本机,不会写入市场目录。</div>
|
|
1215
|
+
${mixedDocumentModes ? '<div class="form-note"><strong>在线链接 + 本机文件同时启用</strong>远程入口解析公网可访问的 URL;本地入口读取对话中上传、粘贴或指定路径的文件并自动提交解析,需 Node.js 20+。</div>' : ''}
|
|
1202
1216
|
<div class="server-preview">${serverPreview}</div>
|
|
1203
1217
|
${scopeFieldHtml()}
|
|
1204
1218
|
${credentialFields}
|
|
@@ -1591,14 +1605,17 @@
|
|
|
1591
1605
|
const d = marketItems[connectorId];
|
|
1592
1606
|
if (!d) return;
|
|
1593
1607
|
detailConnector = d;
|
|
1594
|
-
|
|
1595
|
-
|
|
1608
|
+
const incomplete = d.connected?.length > 0 && d.connectionComplete === false;
|
|
1609
|
+
detailFailureState = incomplete ? 'incomplete' : (d.connectionState || '');
|
|
1610
|
+
detailNeedsReconfigure = incomplete || ['reauth', 'recovering', 'unavailable'].includes(detailFailureState);
|
|
1596
1611
|
const loadId = ++detailLoadId;
|
|
1597
1612
|
$('#detail-icon').innerHTML = isImageIcon(d.icon)
|
|
1598
1613
|
? `<img src="${esc(d.icon)}" alt="" onerror="this.parentElement.innerHTML='🔌';" />`
|
|
1599
1614
|
: `<span style="font-size:24px;">${esc(d.icon || '')}</span>`;
|
|
1600
1615
|
$('#detail-name').textContent = d.name || '';
|
|
1601
|
-
const initialStateLabel =
|
|
1616
|
+
const initialStateLabel = incomplete
|
|
1617
|
+
? `待补充 ${d.missingServers?.length || 1} 个服务`
|
|
1618
|
+
: (d.connectionLabel || (d.connected?.length ? '已配置' : '未连接'));
|
|
1602
1619
|
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${initialStateLabel} ${d.connected?.length || 0} 个服务` : `${initialStateLabel} ${d.connected?.length || 0} 个服务`;
|
|
1603
1620
|
$('#detail-desc').textContent = d.description || d.summary || '';
|
|
1604
1621
|
detailPrompts = (d.prompts || []).filter((prompt) => prompt?.text || prompt?.title);
|
|
@@ -1616,7 +1633,7 @@
|
|
|
1616
1633
|
$('#detail-disconnect-btn').style.display = d.connected?.length > 0 ? 'block' : 'none';
|
|
1617
1634
|
const needsCredential = ['bearer', 'api-key'].includes(d.authMode);
|
|
1618
1635
|
$('#detail-try-btn').textContent = detailNeedsReconfigure
|
|
1619
|
-
? d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
|
|
1636
|
+
? detailFailureState === 'incomplete' ? (d.authMode === 'oauth2-pkce' ? t('upgradeOauth') : t('completeSetup')) : d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
|
|
1620
1637
|
: d.connected?.length > 0 ? t('try') : needsCredential ? t('configureConnect') : t('connectTry');
|
|
1621
1638
|
if (!d.connected?.length && !d.toolsSnapshot?.length) {
|
|
1622
1639
|
$('#tools-summary').textContent = '连接后查看工具';
|
|
@@ -1657,10 +1674,13 @@
|
|
|
1657
1674
|
renderPromptPage();
|
|
1658
1675
|
return;
|
|
1659
1676
|
}
|
|
1660
|
-
detailFailureState = toolsResult.detail?.connectionState || 'healthy';
|
|
1661
|
-
detailNeedsReconfigure =
|
|
1662
|
-
|
|
1663
|
-
|
|
1677
|
+
detailFailureState = incomplete ? 'incomplete' : (toolsResult.detail?.connectionState || 'healthy');
|
|
1678
|
+
detailNeedsReconfigure = incomplete;
|
|
1679
|
+
const connectedLabel = incomplete
|
|
1680
|
+
? `待补充 ${d.missingServers?.length || 1} 个服务`
|
|
1681
|
+
: `${detailFailureState === 'degraded' ? '部分服务可用' : '已连接'} ${d.connected?.length || 0} 个服务`;
|
|
1682
|
+
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${connectedLabel}` : connectedLabel;
|
|
1683
|
+
$('#detail-try-btn').textContent = incomplete ? (d.authMode === 'oauth2-pkce' ? t('upgradeOauth') : t('completeSetup')) : t('try');
|
|
1664
1684
|
renderPromptPage();
|
|
1665
1685
|
detailToolServers = toolsResult.detail?.servers || [];
|
|
1666
1686
|
updateToolsSummary();
|
|
@@ -1748,10 +1768,25 @@
|
|
|
1748
1768
|
});
|
|
1749
1769
|
$('#detail-try-btn').addEventListener('click', async () => {
|
|
1750
1770
|
if (detailNeedsReconfigure) {
|
|
1771
|
+
if (detailFailureState === 'incomplete' && ['bearer', 'api-key'].includes(detailConnector?.authMode)) {
|
|
1772
|
+
openAddConnection('manual', detailConnector);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1751
1775
|
if (detailFailureState === 'reauth' && ['bearer', 'api-key'].includes(detailConnector?.authMode)) {
|
|
1752
1776
|
openAddConnection('manual', detailConnector);
|
|
1753
1777
|
return;
|
|
1754
1778
|
}
|
|
1779
|
+
if (detailFailureState === 'incomplete' && detailConnector?.authMode === 'oauth2-pkce') {
|
|
1780
|
+
const id = detailConnector.id;
|
|
1781
|
+
const r = await call('connect', { connectorId: id }, { timeoutMs: 310_000 });
|
|
1782
|
+
showToast(r.message, r.ok);
|
|
1783
|
+
if (r.ok) {
|
|
1784
|
+
markCommunityCtaEligible();
|
|
1785
|
+
await call('healthCheck', { connectorId: id });
|
|
1786
|
+
await refreshConnectionUi(id);
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1755
1790
|
if (detailFailureState === 'reauth' && detailConnector?.authMode === 'oauth2-pkce') {
|
|
1756
1791
|
const id = detailConnector.id;
|
|
1757
1792
|
const r = await call('connect', { connectorId: id }, { timeoutMs: 310_000 });
|