dsh-mcp-connector 0.2.44 → 0.2.46
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 +36 -1
- package/README.en.md +24 -16
- package/README.md +24 -16
- package/docs/USER-GUIDE.md +28 -0
- package/lib/discovery-policy.js +21 -0
- package/lib/index.js +116 -11
- package/lib/mcp-http.js +4 -2
- package/lib/mcp-validation.js +2 -1
- package/lib/tool-catalog-cache.js +11 -7
- package/lib/web.js +3 -0
- package/package.json +3 -3
- package/scripts/ui-harness.mjs +30 -2
- package/ui/index.html +127 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.46] - 2026-09-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- 重写中英文 README 首屏,围绕接入 MCP Server、跨连接工具查找和连接排障统一产品表达,并用可核验的能力表替换竞品比较文案。
|
|
12
|
+
- 扩充统一工具工作台用户指南,补齐来源定位、参数阅读、发现/缓存语义、连接检查与逐连接重新发现流程。
|
|
13
|
+
- 同步 npm、GitHub About、README 与外部目录的双语描述,新增 npm 250 字符商店描述上限及用户结果关键词自动校验。
|
|
14
|
+
|
|
15
|
+
### Verification
|
|
16
|
+
|
|
17
|
+
- 248 项自动测试通过;4 张现有 storefront 截图通过完整性和无凭据检查;72 个发布文件通过白名单与敏感信息扫描。
|
|
18
|
+
|
|
19
|
+
## [0.2.45] - 2026-09-12
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- 新增“工具”页:跨连接统一搜索、精确工具名优先、连接/服务/发现状态筛选及分页;搜索与筛选仅读取缓存,不访问远端服务。
|
|
24
|
+
- 结果展示连接与服务来源、最后成功缓存时间及最近发现状态;故障连接保留缓存,并提供单连接“重新发现工具”和“检查连接”入口及已有诊断建议。
|
|
25
|
+
- 参数详情增加类型、必填、枚举、约束和可展开嵌套结构;复杂或超大结构提示摘要限制,同时保留原始安全缓存 Schema。
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- 后台最多并发两条连接;健康连接五分钟到期,普通故障从三十秒指数退避至十五分钟,429 遵守 Retry-After,鉴权失败暂停自动重试。
|
|
30
|
+
- 手动故障处理合并同连接并发请求、设置五秒冷却,不能绕过限流等待。未选择 Workspace 的工具页仅展示全局连接。
|
|
31
|
+
|
|
32
|
+
### Verification
|
|
33
|
+
|
|
34
|
+
- 248 项自动测试通过,覆盖工作区隔离、精确搜索、分页、只读搜索零远端请求、单连接诊断及 429 等待;72 个发布文件通过白名单与敏感内容扫描。
|
|
35
|
+
- 本机无凭据模拟页面验证多服务汇总、搜索筛选、参数详情及恢复后状态同步;未执行目标工具,未更改真实 web profile。
|
|
36
|
+
- 确定性调度模型中,六条健康连接持续查看十分钟从 126 次降至 18 次发现任务;此为策略模型验证,不是生产性能基准。
|
|
37
|
+
|
|
7
38
|
## [0.2.44] - 2026-09-12
|
|
8
39
|
|
|
9
40
|
### Added
|
|
@@ -673,7 +704,11 @@
|
|
|
673
704
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
674
705
|
- iframe 消息校验同源和消息来源。
|
|
675
706
|
|
|
676
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
707
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.46...HEAD
|
|
708
|
+
[0.2.46]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.45...v0.2.46
|
|
709
|
+
[0.2.45]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.44...v0.2.45
|
|
710
|
+
[0.2.44]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.43...v0.2.44
|
|
711
|
+
[0.2.43]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.42...v0.2.43
|
|
677
712
|
[0.2.42]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.41...v0.2.42
|
|
678
713
|
[0.2.41]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.40...v0.2.41
|
|
679
714
|
[0.2.40]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.39...v0.2.40
|
package/README.en.md
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Connect, Find, and Troubleshoot MCP Servers in DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 100+ MCP connectors for DeepSeek Harness, one place to connect MCP servers, search tools across connections, and troubleshoot connections.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Connect MCP servers from a continuously updated catalog. Search tools across enabled connections in the current scope, inspect readable parameters, source metadata, and the last successful cache time, then check a connection or rediscover its tools when discovery fails. Supports OAuth 2.0 PKCE, API keys, Streamable HTTP/stdio, and `mcpServers` JSON import.
|
|
6
6
|
|
|
7
7
|
> Here, “skill extension” means extending an agent through MCP tools and prompts; this package does not present itself as a standalone DSH Skill.
|
|
8
8
|
|
|
9
9
|
[简体中文](README.md)
|
|
10
10
|
|
|
11
|
+
### Tool discovery and troubleshooting
|
|
12
|
+
|
|
13
|
+
Search cached tools across enabled connections visible to the current workspace in the Tools tab. Filter by connection, service and latest discovery status, then open parameter details for types, required fields and nested structures. Without a selected workspace, only global connections appear.
|
|
14
|
+
|
|
15
|
+
Results identify their source and last successful cache time. A cached tool is not a guarantee that the service is currently callable. Expand connection diagnostics to check the connection or rediscover tools. Healthy discovery becomes due after five minutes; while the page remains connected, background scheduling checks due tasks. Failures back off, and authentication failures pause automatic retries.
|
|
16
|
+
|
|
17
|
+
Tool browsing does not execute target tools. Parameter summaries may omit complex rules; the original-schema view contains the sanitized, size-limited cached schema, not an untouched server response. See the [Chinese tool-workspace guide](docs/USER-GUIDE.md#51-工具工作台统一查找工具).
|
|
18
|
+
|
|
11
19
|
[Chinese user guide](docs/USER-GUIDE.md) · [Connector onboarding](https://github.com/duhu2000/dsh-mcp-connector-registry/blob/main/docs/ONBOARDING.md) · [First contribution](docs/FIRST-CONTRIBUTION.md) · [Contributing](CONTRIBUTING.md) · [Issues](https://github.com/duhu2000/dsh-mcp-connector/issues)
|
|
12
20
|
|
|
13
21
|
[](https://github.com/duhu2000/dsh-mcp-connector/actions/workflows/ci.yml)
|
|
@@ -32,19 +40,18 @@ Fully restart DeepSeek Harness Desktop or `dsh web` after installation or upgrad
|
|
|
32
40
|
|
|
33
41
|
If the plugin helps you connect an MCP server faster, consider [starring the repository](https://github.com/duhu2000/dsh-mcp-connector/stargazers), [submitting a connector](https://github.com/duhu2000/dsh-mcp-connector-registry/blob/main/docs/ONBOARDING.md), or [contributing a fix](CONTRIBUTING.md).
|
|
34
42
|
|
|
35
|
-
##
|
|
43
|
+
## What MCP Connector provides
|
|
36
44
|
|
|
37
|
-
| Capability |
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
| Plugin version discovery and safe updates | Usually not included | ✅ |
|
|
45
|
+
| Capability | What it gives you |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Continuously updated catalog | Browse featured connectors and nine business categories; refresh after a Registry update to fetch new cards |
|
|
48
|
+
| Multiple connection paths | Connect with OAuth 2.0 PKCE, API keys, Streamable HTTP/stdio, or `mcpServers` JSON |
|
|
49
|
+
| Cross-connection tool search | Search the last-success cache of enabled connections in the current scope, then filter by connection, service, or state |
|
|
50
|
+
| Readable parameters and sources | Inspect types, required fields, enums, nested summaries, sources, cache time, and a sanitized schema |
|
|
51
|
+
| Connection troubleshooting | Use observed stages and stable error codes to check a connection or rediscover tools while retaining an available last-success cache |
|
|
52
|
+
| Scope and governance | Manage project/global visibility plus Connection, Server, and Tool rules |
|
|
53
|
+
| Safer lifecycle management | Keep credentials local, refresh grants, export redacted backups, restore snapshots, and preserve the previous connection on failure |
|
|
54
|
+
| Plugin updates | Detect newer versions and, when the host exposes a compatible Update Provider, show progress, failures, and rollback results |
|
|
48
55
|
|
|
49
56
|
## Features
|
|
50
57
|
|
|
@@ -105,6 +112,7 @@ Run the same command again to upgrade. Fully quit and restart DeepSeek Harness D
|
|
|
105
112
|
2. Choose a connector, confirm **Current project** or **All projects (global)**, and complete authorization or configuration.
|
|
106
113
|
3. Open its details to inspect tools or send an example prompt to a new conversation draft.
|
|
107
114
|
4. Use Installed or the conversation tools to enable, disable, inspect, or disconnect a connection.
|
|
115
|
+
5. After connecting, open **Tools** to search by tool name or description and filter by connection or service. Provider authorization, fees, and actual execution remain under the provider and DSH Host.
|
|
108
116
|
|
|
109
117
|
Connected tools are exposed to the model with the `mcp__<serverName>__*` prefix.
|
|
110
118
|
|
|
@@ -178,7 +186,7 @@ npm run dev:ui
|
|
|
178
186
|
|
|
179
187
|
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.
|
|
180
188
|
|
|
181
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
189
|
+
The current public version is [`dsh-mcp-connector@0.2.46`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.46](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.46).
|
|
182
190
|
|
|
183
191
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
184
192
|
|
package/README.md
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
# MCP
|
|
1
|
+
# MCP连接器:在 DeepSeek Harness 接入、查找和排障 MCP Server
|
|
2
2
|
|
|
3
|
-
> DeepSeek Harness
|
|
3
|
+
> DeepSeek Harness 的超百个 MCP连接器,一个入口完成 MCP Server 接入、跨连接工具查找与连接排障。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
从持续更新的连接器目录接入 MCP Server;跨当前范围内已启用连接查找工具,查看易读参数、来源和最后成功缓存时间,并在发现异常时检查连接或重新发现工具。支持 OAuth 2.0 PKCE、API Key、Streamable HTTP/stdio 与 `mcpServers` JSON 导入。
|
|
6
6
|
|
|
7
7
|
> 注:“技能扩展”指通过 MCP 工具和 Prompt 扩展智能体能力,本包不会伪装成独立 DSH Skill。
|
|
8
8
|
|
|
9
9
|
[English](README.en.md)
|
|
10
10
|
|
|
11
|
+
### 工具查找与故障处理
|
|
12
|
+
|
|
13
|
+
在“工具”页统一查找当前工作区可见、已启用连接的工具。输入工具名或描述,再按连接、服务和最近发现状态筛选;点击“参数详情”查看类型、必填项与嵌套结构。未选择工作区时,仅展示全局连接。
|
|
14
|
+
|
|
15
|
+
结果标注来源和最后成功缓存时间。发现失败时仍可查看已有缓存,但不代表服务当前可调用。展开“连接状态与故障处理”,按建议使用“检查连接”或“重新发现工具”。健康连接五分钟后到期,页面保持连接时后台检查到期任务;失败时退避,鉴权失败暂停自动重试。
|
|
16
|
+
|
|
17
|
+
详见[工具工作台教程](docs/USER-GUIDE.md#51-工具工作台统一查找工具)。工具页用于发现与诊断,不执行目标工具;参数摘要与原始安全缓存均有裁剪边界。
|
|
18
|
+
|
|
11
19
|
[用户手册](docs/USER-GUIDE.md) · [第三方连接器上架指南](https://github.com/duhu2000/dsh-mcp-connector-registry/blob/main/docs/ONBOARDING.md) · [首次贡献](docs/FIRST-CONTRIBUTION.md) · [参与贡献](CONTRIBUTING.md) · [问题反馈](https://github.com/duhu2000/dsh-mcp-connector/issues)
|
|
12
20
|
|
|
13
21
|
[](https://github.com/duhu2000/dsh-mcp-connector/actions/workflows/ci.yml)
|
|
@@ -32,19 +40,18 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
32
40
|
|
|
33
41
|
如果它帮你更快地接入 MCP Server,欢迎 [GitHub 点个 Star](https://github.com/duhu2000/dsh-mcp-connector/stargazers)、[提交新的连接器](https://github.com/duhu2000/dsh-mcp-connector-registry/blob/main/docs/ONBOARDING.md)或[参与贡献](CONTRIBUTING.md)。
|
|
34
42
|
|
|
35
|
-
##
|
|
43
|
+
## MCP连接器能做什么
|
|
36
44
|
|
|
37
|
-
| 能力 |
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
| 插件版本检测与安全更新 | 通常无 | ✅ |
|
|
45
|
+
| 能力 | 用户得到什么 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| 持续更新的连接器目录 | 浏览精选与 9 类业务连接器;Registry 更新后刷新即可获取新卡片 |
|
|
48
|
+
| 多种接入方式 | 使用 OAuth 2.0 PKCE、API Key、Streamable HTTP/stdio 或 `mcpServers` JSON 接入 |
|
|
49
|
+
| 跨连接工具查找 | 在当前范围内已启用连接的最后成功缓存中按名称或描述搜索,并按连接、服务和状态筛选 |
|
|
50
|
+
| 易读参数与来源 | 查看类型、必填、枚举、嵌套摘要、来源、缓存时间及安全裁剪后的 Schema |
|
|
51
|
+
| 连接排障 | 根据明确的阶段和错误码检查连接或重新发现工具,同时保留可用的最后成功缓存 |
|
|
52
|
+
| 作用域与治理 | 管理 project/global 可见范围以及 Connection、Server、Tool 三层规则 |
|
|
53
|
+
| 安全生命周期 | 支持凭据本机存储、授权刷新、脱敏备份、快照回滚及失败时保留原连接 |
|
|
54
|
+
| 插件更新 | 检测新版本,并在宿主提供兼容 Update Provider 时显示进度、失败原因和回滚结果 |
|
|
48
55
|
|
|
49
56
|
## 功能
|
|
50
57
|
|
|
@@ -106,6 +113,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-mcp-connector/m
|
|
|
106
113
|
2. 在市场中选择连接器,确认“当前项目”或“所有项目(全局)”,再完成授权或配置。
|
|
107
114
|
3. 打开卡片详情,可点击示例 Prompt 的发送按钮,在当前工作区创建/复用空白会话并写入草稿。
|
|
108
115
|
4. 在“已安装”或对话工具中查看、停用、恢复或断开连接。
|
|
116
|
+
5. 连接后打开“工具”页,按工具名或描述查找能力,使用连接/服务筛选定位来源;授权、费用及正式调用仍由相应服务与 DSH Host 管理。
|
|
109
117
|
|
|
110
118
|
连接成功后,工具按 `mcp__<serverName>__*` 前缀提供给模型。
|
|
111
119
|
|
|
@@ -173,7 +181,7 @@ npm run dev:ui
|
|
|
173
181
|
|
|
174
182
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
175
183
|
|
|
176
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
184
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.46`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.46](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.46)。
|
|
177
185
|
|
|
178
186
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
179
187
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -106,6 +106,34 @@ OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据
|
|
|
106
106
|
|
|
107
107
|
对话中可用 `mcp_connector_tool_search` 按名称、标题或描述搜索缓存,再用 `mcp_connector_tool_detail` 精确读取某个工具的参数 schema。两者只用于发现,不代理或执行 MCP 调用。
|
|
108
108
|
|
|
109
|
+
### 5.1 工具工作台:统一查找工具
|
|
110
|
+
|
|
111
|
+
1. 先连接所需 MCP 服务,打开顶部“工具”页;工具发现会在后台进行,无需先打开每个连接的详情。
|
|
112
|
+
2. 输入工具名或描述中的关键词。精确工具名优先;同名工具仍可能来自不同连接,请核对来源。
|
|
113
|
+
3. 使用“连接”“服务”“发现状态”筛选缩小范围,点击“清除筛选”恢复。结果按页展示。
|
|
114
|
+
4. 工具页搜索的是已启用且当前范围可见连接的成功缓存,不是市场中所有未安装连接器的工具。未选择 Workspace 时仅显示全局连接。
|
|
115
|
+
5. 没有结果时先清除筛选,并确认连接已启用、范围正确且至少成功发现过一次;首次发现可能仍在等待。
|
|
116
|
+
|
|
117
|
+
### 5.2 查看参数与来源
|
|
118
|
+
|
|
119
|
+
每条结果展示连接器、连接、服务、最近发现状态和最后成功缓存时间。点击“参数详情”查看类型、必填、说明、枚举和嵌套结构。
|
|
120
|
+
|
|
121
|
+
复杂引用、组合规则或过大结构会提示摘要限制;可展开“原始 Schema(安全缓存)”继续查看。这里的原始 Schema 仍是安全裁剪后的缓存,不含被移除的默认值或示例,也不保证保留超限字段。此页不执行工具。
|
|
122
|
+
|
|
123
|
+
### 5.3 发现状态和最后成功缓存
|
|
124
|
+
|
|
125
|
+
“尚未发现”表示还没有当前发现状态;“Host 状态未知”表示宿主状态无法确认;“最近发现成功/失败”仅代表最近一次发现,不保证随后调用的结果。
|
|
126
|
+
|
|
127
|
+
失败时已有的最后成功工具缓存仍可查看;超过 24 小时会提示陈旧。没有成功记录则无法提供缓存,成功空列表也不代表故障。连接改配、停用或断开可能使原结果不再可见。
|
|
128
|
+
|
|
129
|
+
### 5.4 检查连接与重新发现
|
|
130
|
+
|
|
131
|
+
展开“连接状态与故障处理”,先阅读阶段与建议。“检查连接”执行该连接的健康检查;“重新发现工具”重新获取该连接的工具元数据。两者不是目标工具试运行,也不保证自动修复。
|
|
132
|
+
|
|
133
|
+
同连接进行中的操作会合并,频繁操作有冷却;工具页操作不能绕过有效限流等待。鉴权失败请到“已安装”页更新凭据或重新授权。
|
|
134
|
+
|
|
135
|
+
健康后台发现五分钟后到期;页面保持 SSE 连接时按调度检查到期项,普通故障指数退避,限流结合 `Retry-After` 等待,鉴权失败暂停自动重试。不要把“刷新工具视图”误认为强制访问所有服务,它只刷新缓存视图。
|
|
136
|
+
|
|
109
137
|
## 6. 添加自定义连接
|
|
110
138
|
|
|
111
139
|
点击“+ 添加连接”后有三种方式。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Pure scheduling policy. No credentials, timers or network operations.
|
|
2
|
+
export function nextDiscovery(previous, outcome, now = Date.now()) {
|
|
3
|
+
const failures = outcome.ok ? 0 : Math.min((previous?.failures || 0) + 1, 10);
|
|
4
|
+
if (outcome.errorKind === 'auth') return { failures, nextAt: null, reason: 'auth' };
|
|
5
|
+
const rateLimited = outcome.errorKind === 'rate-limit';
|
|
6
|
+
const delay = outcome.ok ? 300_000 : Math.min((rateLimited ? 60_000 : 30_000) * 2 ** (failures - 1), 900_000);
|
|
7
|
+
const retry = Number.isFinite(outcome.retryAfterMs) ? Math.max(0, outcome.retryAfterMs) : 0;
|
|
8
|
+
return { failures, nextAt: now + Math.max(delay, retry), reason: outcome.ok ? 'success' : rateLimited ? 'rate-limit' : 'retry' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function discoveryDue(plan, now = Date.now()) {
|
|
12
|
+
return !plan || (plan.nextAt !== null && plan.nextAt <= now);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseRetryAfter(value, now = Date.now()) {
|
|
16
|
+
if (!value) return 0;
|
|
17
|
+
const seconds = Number(value);
|
|
18
|
+
const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(value) - now;
|
|
19
|
+
// Bound untrusted headers; malformed/negative values fall back to local backoff.
|
|
20
|
+
return Number.isFinite(delay) ? Math.max(0, Math.min(delay, 86_400_000)) : 0;
|
|
21
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -69,8 +69,10 @@ import {
|
|
|
69
69
|
scopeTargets,
|
|
70
70
|
} from './connection-scopes.js';
|
|
71
71
|
import { installConnectorSettings } from './settings.js';
|
|
72
|
+
import { nextDiscovery, discoveryDue } from './discovery-policy.js';
|
|
72
73
|
import {
|
|
73
74
|
connectionToolCacheSignature,
|
|
75
|
+
browseToolCatalog,
|
|
74
76
|
createToolCatalogRecord,
|
|
75
77
|
findToolDetails,
|
|
76
78
|
searchToolCatalog,
|
|
@@ -170,6 +172,10 @@ export async function apply(ctx, config) {
|
|
|
170
172
|
};
|
|
171
173
|
const statusEvents = new StatusEventHub();
|
|
172
174
|
const toolRequests = new Map();
|
|
175
|
+
const discoveryPlans = new Map();
|
|
176
|
+
const connectionChecks = new Map();
|
|
177
|
+
const explorerActions = new Map();
|
|
178
|
+
const explorerActionTimes = new Map();
|
|
173
179
|
|
|
174
180
|
const hostGovernance = createHostGovernanceController(ctx, {
|
|
175
181
|
getRules: () => governancePolicies.snapshot().rules,
|
|
@@ -1008,6 +1014,8 @@ export async function apply(ctx, config) {
|
|
|
1008
1014
|
}
|
|
1009
1015
|
|
|
1010
1016
|
async function deleteLastSuccessfulTools(key) {
|
|
1017
|
+
discoveryPlans.delete(key);
|
|
1018
|
+
connectionChecks.delete(key);
|
|
1011
1019
|
state.toolViews.delete(key);
|
|
1012
1020
|
state.toolCatalogCache.delete(key);
|
|
1013
1021
|
if (config.persistSecrets) {
|
|
@@ -1038,6 +1046,38 @@ export async function apply(ctx, config) {
|
|
|
1038
1046
|
return entries;
|
|
1039
1047
|
}
|
|
1040
1048
|
|
|
1049
|
+
function explorerRecords(workspaceId) {
|
|
1050
|
+
// Unlike the legacy management overview, no Workspace means global-only.
|
|
1051
|
+
return [...state.connections.values()].filter((record) => record.enabled !== false
|
|
1052
|
+
&& connectionVisibleInWorkspace(connectionScopes.binding(record.key), workspaceId));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function explorerConnection(record) {
|
|
1056
|
+
const view = state.toolViews.get(record.key);
|
|
1057
|
+
const server = view?.record === record ? view.server : null;
|
|
1058
|
+
const cache = cachedToolsForRecord(record);
|
|
1059
|
+
const plan = discoveryPlans.get(record.key);
|
|
1060
|
+
const currentPlan = plan?.record === record ? plan : null;
|
|
1061
|
+
const check = connectionChecks.get(record.key);
|
|
1062
|
+
const diagnostic = check?.record === record && (!view || check.at >= view.at)
|
|
1063
|
+
? check.summary.diagnostic
|
|
1064
|
+
: server ? buildHealthSummary({ connectorId: record.connectorId, records: [record], results: [{
|
|
1065
|
+
ok: server.ok || server.unknown, kind: server.unknown ? 'managed' : server.ok ? 'connected' : server.errorKind || 'network',
|
|
1066
|
+
code: server.diagnosticCode, serverName: record.serverName, serverKey: record.serverKey, message: server.error,
|
|
1067
|
+
}], checkedAt: view.at }).diagnostic : null;
|
|
1068
|
+
const status = !server ? 'pending' : server.unknown ? 'unknown' : server.ok ? 'success' : 'failed';
|
|
1069
|
+
return {
|
|
1070
|
+
connectionKey: record.key, connectionName: record.name, connectorId: record.connectorId,
|
|
1071
|
+
connectorName: state.merged.find((item) => item.id === record.connectorId)?.name || record.name,
|
|
1072
|
+
serverName: record.serverName, transport: record.transport, status,
|
|
1073
|
+
checkedAt: view?.record === record ? view.at : null,
|
|
1074
|
+
observedAt: cache?.observedAt || null, stale: cache?.stale || false,
|
|
1075
|
+
toolCount: cache?.entry.tools.length || 0, cacheTruncated: cache?.entry.truncated === true,
|
|
1076
|
+
diagnostic, nextDiscoveryAt: currentPlan?.nextAt ?? null,
|
|
1077
|
+
retryReason: currentPlan?.reason || null,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1041
1081
|
function readinessResults(records, validationResults = []) {
|
|
1042
1082
|
return records.map((record, index) => {
|
|
1043
1083
|
const validated = validationResults[index];
|
|
@@ -2311,13 +2351,17 @@ export async function apply(ctx, config) {
|
|
|
2311
2351
|
};
|
|
2312
2352
|
},
|
|
2313
2353
|
|
|
2314
|
-
async healthCheck(connectorId) {
|
|
2354
|
+
async healthCheck(connectorId, workspaceId, connectionKey) {
|
|
2355
|
+
if (connectionKey && !explorerRecords(workspaceId).some((record) => record.key === connectionKey && record.connectorId === connectorId)) {
|
|
2356
|
+
return { ok: false, message: '连接不存在、已停用或当前工作区不可见' };
|
|
2357
|
+
}
|
|
2315
2358
|
const targetIds = connectorId
|
|
2316
2359
|
? [String(connectorId)]
|
|
2317
2360
|
: [...new Set([...state.connections.values()].map((record) => record.connectorId).filter(Boolean))];
|
|
2318
2361
|
const summaries = [];
|
|
2319
2362
|
for (const id of targetIds) {
|
|
2320
|
-
const records = [...state.connections.values()].filter((record) => record.connectorId === id
|
|
2363
|
+
const records = [...state.connections.values()].filter((record) => record.connectorId === id
|
|
2364
|
+
&& (!connectionKey || record.key === connectionKey));
|
|
2321
2365
|
const enabledRecords = records.filter((record) => record.enabled !== false);
|
|
2322
2366
|
if (records.length === 0 || enabledRecords.length === 0) {
|
|
2323
2367
|
const summary = healthSummary(id, records, [], Date.now());
|
|
@@ -2369,7 +2413,19 @@ export async function apply(ctx, config) {
|
|
|
2369
2413
|
});
|
|
2370
2414
|
checkedResults = validation.detail.results;
|
|
2371
2415
|
}
|
|
2372
|
-
|
|
2416
|
+
const summary = connectionKey ? healthSummary(id, records, [...immediateResults, ...checkedResults], Date.now())
|
|
2417
|
+
: cacheHealth(id, records, [...immediateResults, ...checkedResults]);
|
|
2418
|
+
if (connectionKey && state.connections.get(connectionKey) === records[0]) {
|
|
2419
|
+
connectionChecks.set(connectionKey, { record: records[0], summary, at: Date.now() });
|
|
2420
|
+
const outcome = [...immediateResults, ...checkedResults][0];
|
|
2421
|
+
if (outcome && !outcome.ok) {
|
|
2422
|
+
const prior = discoveryPlans.get(connectionKey);
|
|
2423
|
+
discoveryPlans.set(connectionKey, { record: records[0], ...nextDiscovery(prior?.record === records[0] ? prior : null, { ok: false, errorKind: outcome.kind, retryAfterMs: outcome.retryAfterMs }) });
|
|
2424
|
+
} else if (discoveryPlans.get(connectionKey)?.reason === 'auth') {
|
|
2425
|
+
discoveryPlans.delete(connectionKey); scheduleDiscovery();
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
summaries.push(summary);
|
|
2373
2429
|
}
|
|
2374
2430
|
const healthy = summaries.filter((item) => item.connectionState === 'healthy').length;
|
|
2375
2431
|
const unknown = summaries.filter((item) => item.connectionState === 'unknown').length;
|
|
@@ -2654,6 +2710,48 @@ export async function apply(ctx, config) {
|
|
|
2654
2710
|
return { ok: true, message: `已将 "${connector.name}" ${published ? '上架' : '下架'}` };
|
|
2655
2711
|
},
|
|
2656
2712
|
|
|
2713
|
+
async toolExplorer(input = {}) {
|
|
2714
|
+
const records = explorerRecords(input.workspaceId);
|
|
2715
|
+
const connections = records.map(explorerConnection);
|
|
2716
|
+
const selected = records.filter((record) => (!input.connectionKey || record.key === input.connectionKey)
|
|
2717
|
+
&& (!input.serverName || record.serverName === input.serverName)
|
|
2718
|
+
&& (!input.status || explorerConnection(record).status === input.status));
|
|
2719
|
+
const entries = selected.map((record) => cachedToolsForRecord(record)?.entry).filter(Boolean);
|
|
2720
|
+
const result = browseToolCatalog(entries, { query: input.query, offset: input.offset, limit: input.limit });
|
|
2721
|
+
const byKey = new Map(connections.map((item) => [item.connectionKey, item]));
|
|
2722
|
+
return { ok: true, message: `找到 ${result.total} 个工具`, detail: {
|
|
2723
|
+
...result, items: result.items.map((item) => ({ ...item, connection: byKey.get(item.connectionKey) })),
|
|
2724
|
+
connections, selectedConnections: selected.map(explorerConnection), discoveryOnly: true,
|
|
2725
|
+
scopeLabel: input.workspaceId ? '当前工作区可见连接' : '全局连接(未选择工作区)',
|
|
2726
|
+
} };
|
|
2727
|
+
},
|
|
2728
|
+
|
|
2729
|
+
async toolExplorerDetail(input = {}) {
|
|
2730
|
+
if (!explorerRecords(input.workspaceId).some((record) => record.key === input.connectionKey)) {
|
|
2731
|
+
return { ok: false, message: '连接不存在、已停用或当前工作区不可见' };
|
|
2732
|
+
}
|
|
2733
|
+
return api.toolDetail(input);
|
|
2734
|
+
},
|
|
2735
|
+
|
|
2736
|
+
async toolExplorerAction(input = {}) {
|
|
2737
|
+
const record = explorerRecords(input.workspaceId).find((item) => item.key === input.connectionKey);
|
|
2738
|
+
if (!record || !['discover', 'check'].includes(input.action)) return { ok: false, message: '操作无效或连接当前不可见' };
|
|
2739
|
+
const pending = explorerActions.get(record);
|
|
2740
|
+
if (pending) return pending;
|
|
2741
|
+
const plan = discoveryPlans.get(record.key);
|
|
2742
|
+
if (plan?.record === record && plan.reason === 'rate-limit' && !discoveryDue(plan)) {
|
|
2743
|
+
return { ok: false, message: '服务正在限流,请等待下一次允许重试时间', detail: { retryAt: plan.nextAt } };
|
|
2744
|
+
}
|
|
2745
|
+
if (Date.now() - (explorerActionTimes.get(record.key) || 0) < 5_000) return { ok: false, message: '操作过于频繁,请稍后重试' };
|
|
2746
|
+
explorerActionTimes.set(record.key, Date.now());
|
|
2747
|
+
const request = input.action === 'discover'
|
|
2748
|
+
? api.toolsList(record.connectorId, input.workspaceId, false, record.key)
|
|
2749
|
+
: api.healthCheck(record.connectorId, input.workspaceId, record.key);
|
|
2750
|
+
explorerActions.set(record, request);
|
|
2751
|
+
try { return await request; }
|
|
2752
|
+
finally { explorerActions.delete(record); statusEvents.publish(input.action === 'discover' ? 'tools' : 'status'); }
|
|
2753
|
+
},
|
|
2754
|
+
|
|
2657
2755
|
async toolSearch(input = {}) {
|
|
2658
2756
|
try {
|
|
2659
2757
|
const connectorId = String(input.connectorId ?? '').trim() || undefined;
|
|
@@ -2678,7 +2776,6 @@ export async function apply(ctx, config) {
|
|
|
2678
2776
|
const view = state.toolViews.get(record.key);
|
|
2679
2777
|
return view?.record === record && !view.server.ok;
|
|
2680
2778
|
}).length;
|
|
2681
|
-
if (pendingServers > 0) scheduleDiscovery();
|
|
2682
2779
|
return {
|
|
2683
2780
|
ok: true,
|
|
2684
2781
|
message: `最后成功工具缓存中找到 ${items.length} 个匹配项${staleCount ? `,其中 ${staleCount} 个已超过 24 小时` : ''}${pendingServers ? `;${pendingServers} 个服务正在后台发现` : ''}${failedServers ? `;${failedServers} 个服务最近读取失败` : ''}`,
|
|
@@ -2894,6 +2991,7 @@ export async function apply(ctx, config) {
|
|
|
2894
2991
|
ok: false,
|
|
2895
2992
|
error: error.message,
|
|
2896
2993
|
errorKind: error.kind,
|
|
2994
|
+
retryAfterMs: error.retryAfterMs,
|
|
2897
2995
|
policy: serverPolicy(record),
|
|
2898
2996
|
...cachedFields,
|
|
2899
2997
|
});
|
|
@@ -2909,7 +3007,10 @@ export async function apply(ctx, config) {
|
|
|
2909
3007
|
}
|
|
2910
3008
|
records.forEach((record, index) => {
|
|
2911
3009
|
if (state.connections.get(record.key) === record && record.enabled !== false) {
|
|
2912
|
-
|
|
3010
|
+
const at = Date.now();
|
|
3011
|
+
state.toolViews.set(record.key, { record, server: servers[index], at });
|
|
3012
|
+
const prior = discoveryPlans.get(record.key);
|
|
3013
|
+
discoveryPlans.set(record.key, { record, ...nextDiscovery(prior?.record === record ? prior : null, servers[index], at) });
|
|
2913
3014
|
}
|
|
2914
3015
|
});
|
|
2915
3016
|
const totalTools = servers.reduce((sum, s) => sum + (s.tools?.length || 0), 0);
|
|
@@ -3057,15 +3158,19 @@ export async function apply(ctx, config) {
|
|
|
3057
3158
|
discoveryRunning = true;
|
|
3058
3159
|
discoveryPending = false;
|
|
3059
3160
|
try {
|
|
3060
|
-
const
|
|
3161
|
+
const records = [...state.connections.values()].filter((record) => {
|
|
3162
|
+
const plan = discoveryPlans.get(record.key);
|
|
3163
|
+
return record.enabled !== false && discoveryDue(plan?.record === record ? plan : null);
|
|
3164
|
+
});
|
|
3061
3165
|
let cursor = 0;
|
|
3062
|
-
await Promise.all(Array.from({ length: Math.min(2,
|
|
3063
|
-
while (cursor <
|
|
3064
|
-
const
|
|
3065
|
-
|
|
3166
|
+
await Promise.all(Array.from({ length: Math.min(2, records.length) }, async () => {
|
|
3167
|
+
while (cursor < records.length && !discoveryDisposed) {
|
|
3168
|
+
const record = records[cursor++];
|
|
3169
|
+
if (state.connections.get(record.key) !== record || record.enabled === false) continue;
|
|
3170
|
+
await api.toolsList(record.connectorId, undefined, false, record.key).catch(() => {});
|
|
3171
|
+
if (!discoveryDisposed) statusEvents.publish('tools');
|
|
3066
3172
|
}
|
|
3067
3173
|
}));
|
|
3068
|
-
if (!discoveryDisposed) statusEvents.publish('tools');
|
|
3069
3174
|
} finally {
|
|
3070
3175
|
discoveryRunning = false;
|
|
3071
3176
|
if (discoveryPending) scheduleDiscovery();
|
package/lib/mcp-http.js
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { MCP_PROTOCOL_VERSION } from './constants.js';
|
|
9
9
|
import { authHeaders } from './mcp-provision.js';
|
|
10
|
+
import { parseRetryAfter } from './discovery-policy.js';
|
|
10
11
|
|
|
11
12
|
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
12
13
|
const MAX_TOOL_PAGES = 100;
|
|
13
14
|
const MAX_TOOLS = 10_000;
|
|
14
15
|
|
|
15
16
|
export class McpHttpError extends Error {
|
|
16
|
-
constructor(message, { kind = 'protocol', status } = {}) {
|
|
17
|
+
constructor(message, { kind = 'protocol', status, retryAfterMs } = {}) {
|
|
17
18
|
super(message);
|
|
18
19
|
this.name = 'McpHttpError';
|
|
19
20
|
this.kind = kind;
|
|
20
21
|
this.status = status;
|
|
22
|
+
this.retryAfterMs = retryAfterMs;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -64,7 +66,7 @@ function responseError(response, payload, text) {
|
|
|
64
66
|
return new McpHttpError(`凭据无效、已过期或权限不足(HTTP ${response.status})`, { kind: 'auth', status: response.status });
|
|
65
67
|
}
|
|
66
68
|
if (response.status === 429) {
|
|
67
|
-
return new McpHttpError('服务请求过于频繁(HTTP 429),请稍后重试', { kind: 'rate-limit', status: response.status });
|
|
69
|
+
return new McpHttpError('服务请求过于频繁(HTTP 429),请稍后重试', { kind: 'rate-limit', status: response.status, retryAfterMs: parseRetryAfter(response.headers.get('retry-after')) });
|
|
68
70
|
}
|
|
69
71
|
const suffix = rawMessage ? `:${rawMessage}` : '';
|
|
70
72
|
return new McpHttpError(`MCP 请求失败(HTTP ${response.status})${suffix}`, { kind: 'http', status: response.status });
|
package/lib/mcp-validation.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { MCP_PROTOCOL_VERSION } from './constants.js';
|
|
6
6
|
import { authHeaders } from './mcp-provision.js';
|
|
7
|
+
import { parseRetryAfter } from './discovery-policy.js';
|
|
7
8
|
|
|
8
9
|
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
9
10
|
|
|
@@ -142,7 +143,7 @@ export async function validateConnectionRecord(record, { timeoutMs = 15_000, fet
|
|
|
142
143
|
return failed(record, 'auth', `Key/Token 无效、已过期或当前账号没有该 Server 权限(HTTP ${response.status})`, { httpStatus: response.status });
|
|
143
144
|
}
|
|
144
145
|
if (response.status === 429) {
|
|
145
|
-
return failed(record, 'rate-limit', '服务请求过于频繁(HTTP 429),请稍后重试', { httpStatus: response.status });
|
|
146
|
+
return failed(record, 'rate-limit', '服务请求过于频繁(HTTP 429),请稍后重试', { httpStatus: response.status, retryAfterMs: parseRetryAfter(response.headers.get('retry-after')) });
|
|
146
147
|
}
|
|
147
148
|
if (!response.ok) {
|
|
148
149
|
const message = response.status >= 500
|
|
@@ -167,14 +167,19 @@ function validLimit(limit) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export function searchToolCatalog(entries, { query, connectorId, limit = 20 } = {}, now = Date.now(), ttlMs = TOOL_CACHE_TTL_MS) {
|
|
170
|
+
if (!normalizeToolSearchText(query)) throw new Error('工具搜索关键词不能为空');
|
|
171
|
+
return browseToolCatalog(entries, { query, connectorId, limit }, now, ttlMs).items;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function browseToolCatalog(entries, { query, connectorId, serverName, limit = 20, offset = 0 } = {}, now = Date.now(), ttlMs = TOOL_CACHE_TTL_MS) {
|
|
170
175
|
const normalizedQuery = normalizeToolSearchText(query);
|
|
171
|
-
if (!normalizedQuery) throw new Error('工具搜索关键词不能为空');
|
|
172
176
|
const results = [];
|
|
173
177
|
for (const entry of entries ?? []) {
|
|
174
178
|
if (connectorId && entry.connectorId !== connectorId) continue;
|
|
179
|
+
if (serverName && entry.serverName !== serverName) continue;
|
|
175
180
|
const cache = toolCatalogCacheView(entry, now, ttlMs);
|
|
176
181
|
for (const tool of entry.tools ?? []) {
|
|
177
|
-
const score = toolSearchScore(tool, normalizedQuery);
|
|
182
|
+
const score = normalizedQuery ? toolSearchScore(tool, normalizedQuery) : 0;
|
|
178
183
|
if (score < 0) continue;
|
|
179
184
|
results.push({
|
|
180
185
|
connectorId: entry.connectorId,
|
|
@@ -191,12 +196,11 @@ export function searchToolCatalog(entries, { query, connectorId, limit = 20 } =
|
|
|
191
196
|
});
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
|
-
|
|
195
|
-
.sort((left, right) => right.score - left.score
|
|
199
|
+
results.sort((left, right) => right.score - left.score
|
|
196
200
|
|| left.name.localeCompare(right.name)
|
|
197
|
-
|| left.serverName.localeCompare(right.serverName))
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
|| left.serverName.localeCompare(right.serverName) || left.connectionKey.localeCompare(right.connectionKey));
|
|
202
|
+
const start = Number.isFinite(Number(offset)) ? Math.max(0, Math.trunc(Number(offset))) : 0;
|
|
203
|
+
return { total: results.length, items: results.slice(start, start + validLimit(limit)).map(({ score, ...item }) => item) };
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
export function findToolDetails(entries, { toolName, connectorId, serverName } = {}, now = Date.now(), ttlMs = TOOL_CACHE_TTL_MS) {
|
package/lib/web.js
CHANGED
|
@@ -50,6 +50,9 @@ const API_WHITELIST = new Map([
|
|
|
50
50
|
['refreshCatalog', (api) => api.refreshCatalog()],
|
|
51
51
|
['toolsList', (api, p) => api.toolsList(p?.connectorId, p?.workspaceId, p?.cachedOnly === true, p?.connectionKey)],
|
|
52
52
|
['toolSearch', (api, p) => api.toolSearch(p ?? {})],
|
|
53
|
+
['toolExplorer', (api, p) => api.toolExplorer(p ?? {})],
|
|
54
|
+
['toolExplorerDetail', (api, p) => api.toolExplorerDetail(p ?? {})],
|
|
55
|
+
['toolExplorerAction', (api, p) => api.toolExplorerAction(p ?? {})],
|
|
53
56
|
['toolDetail', (api, p) => api.toolDetail(p ?? {})],
|
|
54
57
|
]);
|
|
55
58
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "DeepSeek Harness MCP Connector
|
|
3
|
+
"version": "0.2.46",
|
|
4
|
+
"description": "DeepSeek Harness MCP Connector: connect servers, search tools across active connections, and troubleshoot discovery. Includes a continuously updated catalog; supports OAuth 2.0 PKCE, API keys, Streamable HTTP/stdio, and mcpServers JSON import.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"test": "node --test",
|
|
44
|
-
"lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/settings.js && node --check lib/version-status.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/diagnostics.js && node --check lib/connection-backup.js && node --check lib/connection-editor.js && node --check lib/connection-scopes.js && node --check lib/governance.js && node --check lib/tool-catalog-cache.js && node --check lib/status-events.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/grant-journal.js && node --check lib/oauth.js && node --check lib/grant-lifecycle.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/check-distribution-sync.mjs && node --check scripts/check-readme-version.mjs && node --check scripts/check-marketing-metadata.mjs && node --check scripts/check-storefront-screenshots.mjs && node --check scripts/sync-registry-stats.mjs && node --check scripts/verify-pack.mjs",
|
|
44
|
+
"lint": "node --check lib/discovery-policy.js && node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/settings.js && node --check lib/version-status.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/diagnostics.js && node --check lib/connection-backup.js && node --check lib/connection-editor.js && node --check lib/connection-scopes.js && node --check lib/governance.js && node --check lib/tool-catalog-cache.js && node --check lib/status-events.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/grant-journal.js && node --check lib/oauth.js && node --check lib/grant-lifecycle.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/check-distribution-sync.mjs && node --check scripts/check-readme-version.mjs && node --check scripts/check-marketing-metadata.mjs && node --check scripts/check-storefront-screenshots.mjs && node --check scripts/sync-registry-stats.mjs && node --check scripts/verify-pack.mjs",
|
|
45
45
|
"registry:build": "node scripts/build-registry.mjs",
|
|
46
46
|
"registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
|
|
47
47
|
"dev:ui": "node scripts/ui-harness.mjs",
|
package/scripts/ui-harness.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { normalizeConnectorDescriptor } from '../lib/schema.js';
|
|
|
8
8
|
import { auditDescriptor, auditRawDescriptor, mergeCatalog } from '../lib/catalog.js';
|
|
9
9
|
import { normalizeGovernanceMutation, publicToolName, resolveGovernancePolicy } from '../lib/governance.js';
|
|
10
10
|
import { ConnectionScopeService, bindingForConnection, scopeLabel } from '../lib/connection-scopes.js';
|
|
11
|
+
import { browseToolCatalog, createToolCatalogRecord } from '../lib/tool-catalog-cache.js';
|
|
11
12
|
|
|
12
13
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
14
|
const packageJson = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
@@ -43,6 +44,8 @@ const bundledAssets = new Map([
|
|
|
43
44
|
['wind-logo.png', 'image/png'],
|
|
44
45
|
]);
|
|
45
46
|
const eventClients = new Set();
|
|
47
|
+
const mockExplorerRecovered = new Set();
|
|
48
|
+
const mockObservedAt = Date.now();
|
|
46
49
|
let eventSequence = 0;
|
|
47
50
|
|
|
48
51
|
function publishStatus(type) {
|
|
@@ -359,6 +362,28 @@ const server = createServer(async (req, res) => {
|
|
|
359
362
|
json(res, { ok: false, message: `Mock 作用域回滚失败: ${error.message}` }); return;
|
|
360
363
|
}
|
|
361
364
|
}
|
|
365
|
+
if (method === 'toolExplorer') {
|
|
366
|
+
const connections = catalog.filter((item) => connected.has(item.id)).flatMap((item) => item.servers.map((server, index) => {
|
|
367
|
+
const key = `${item.id}:${server.serverKey}`;
|
|
368
|
+
const failed = index === 1 && !mockExplorerRecovered.has(key);
|
|
369
|
+
return { connectionKey: key, connectorId: item.id, connectorName: item.name, connectionName: `${item.name} · ${server.serverKey}`,
|
|
370
|
+
serverName: server.serverName, status: failed ? 'failed' : 'success', checkedAt: mockObservedAt, observedAt: mockObservedAt,
|
|
371
|
+
toolCount: 125, diagnostic: failed ? { stageLabel: '网络与传输', message: '模拟连接超时(非真实服务故障)', action: '检查网络后重新发现工具' } : null };
|
|
372
|
+
}));
|
|
373
|
+
const selectedConnections = connections.filter((c) => (!params.connectionKey || c.connectionKey === params.connectionKey)
|
|
374
|
+
&& (!params.serverName || c.serverName === params.serverName) && (!params.status || c.status === params.status));
|
|
375
|
+
const entries = selectedConnections.map((c) => createToolCatalogRecord({ key: c.connectionKey, connectorId: c.connectorId, serverName: c.serverName, transport: 'stdio' },
|
|
376
|
+
Array.from({ length: 125 }, (_, i) => ({ name: `mock_tool_${String(i + 1).padStart(3, '0')}`, title: `Mock 工具 ${i + 1}`, description: '本机无凭据测试数据,不执行真实工具。' })), mockObservedAt));
|
|
377
|
+
const result = browseToolCatalog(entries, params);
|
|
378
|
+
json(res, { ok: true, message: 'Mock 工具浏览', detail: { ...result,
|
|
379
|
+
items: result.items.map((item) => ({ ...item, connection: connections.find((c) => c.connectionKey === item.connectionKey) })),
|
|
380
|
+
connections, selectedConnections, scopeLabel: '本机无凭据模拟连接' } }); return;
|
|
381
|
+
}
|
|
382
|
+
if (method === 'toolExplorerAction') {
|
|
383
|
+
mockExplorerRecovered.add(params.connectionKey);
|
|
384
|
+
publishStatus('tools');
|
|
385
|
+
json(res, { ok: true, message: '模拟操作完成:未访问真实服务' }); return;
|
|
386
|
+
}
|
|
362
387
|
if (method === 'toolsList') {
|
|
363
388
|
const connector = catalog.find((item) => item.id === params.connectorId);
|
|
364
389
|
const server = connector?.servers?.[0] ?? { serverKey: 'mock', serverName: 'mock-server' };
|
|
@@ -430,14 +455,17 @@ const server = createServer(async (req, res) => {
|
|
|
430
455
|
}).filter((tool) => `${tool.name} ${tool.title} ${tool.description}`.toLowerCase().includes(query)).slice(0, params.limit || 20);
|
|
431
456
|
json(res, { ok: true, message: `Mock 找到 ${items.length} 个工具`, detail: { items, source: 'last-success-cache', discoveryOnly: true } }); return;
|
|
432
457
|
}
|
|
433
|
-
if (method === 'toolDetail') {
|
|
458
|
+
if (method === 'toolDetail' || method === 'toolExplorerDetail') {
|
|
434
459
|
json(res, { ok: true, message: `Mock 工具详情:${params.toolName}`, detail: { tool: {
|
|
435
460
|
connectorId: params.connectorId,
|
|
436
461
|
serverName: params.serverName,
|
|
437
462
|
name: params.toolName,
|
|
438
463
|
title: params.toolName,
|
|
439
464
|
description: '本地无凭据 UI 验收数据;不会执行真实 MCP 工具。',
|
|
440
|
-
inputSchema: { type: 'object', properties: { query: { type: 'string', description: '公开检索词'
|
|
465
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string', description: '公开检索词', minLength: 1 },
|
|
466
|
+
mode: { type: 'string', enum: ['basic', 'full'], description: '返回详细程度' },
|
|
467
|
+
filters: { type: 'object', properties: { regions: { type: 'array', items: { type: 'string' } } } },
|
|
468
|
+
target: { oneOf: [{ type: 'string' }, { type: 'number' }] } }, required: ['query'] },
|
|
441
469
|
observedAt: Date.now(),
|
|
442
470
|
stale: false,
|
|
443
471
|
schemaTruncated: false,
|
package/ui/index.html
CHANGED
|
@@ -331,6 +331,16 @@
|
|
|
331
331
|
.tool-deprecated { color: var(--warn); background: var(--warn-bg); border-radius: 999px; padding: 1px 6px; font-size: 10px; line-height: 1.5; flex: 0 0 auto; }
|
|
332
332
|
.tool-description { color: var(--text-2); font-size: 12px; line-height: 1.55; margin-top: 4px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
333
333
|
.tool-description.empty { color: var(--muted); font-style: italic; }
|
|
334
|
+
.explorer-filters { display:flex; flex-wrap:wrap; gap:10px; align-items:end; }
|
|
335
|
+
.explorer-filters label { display:flex; flex:1 1 150px; flex-direction:column; gap:5px; font-size:12px; }
|
|
336
|
+
.explorer-filters select { width:100%; min-width:0; padding:8px; border:1px solid var(--line); border-radius:8px; background:var(--bg); color:var(--text); }
|
|
337
|
+
.explorer-card { border:1px solid var(--line); padding:14px; border-radius:10px; margin:10px 0; overflow-wrap:anywhere; }
|
|
338
|
+
.explorer-card p { margin:8px 0; white-space:pre-wrap; }
|
|
339
|
+
.parameter-scroll { overflow-x:auto; }
|
|
340
|
+
.parameter-table { width:100%; border-collapse:collapse; font-size:12px; }
|
|
341
|
+
.parameter-table th, .parameter-table td { border-bottom:1px solid var(--line); padding:8px; text-align:left; vertical-align:top; overflow-wrap:anywhere; }
|
|
342
|
+
.parameter-table th { white-space:nowrap; }
|
|
343
|
+
.parameter-table details { margin-top:6px; }
|
|
334
344
|
.tools-loading, .tools-error { font-size: 13px; color: var(--muted); padding: 8px; }
|
|
335
345
|
.tools-error { color: var(--bad); }
|
|
336
346
|
.tools-empty { color: var(--muted); font-size: 12px; text-align: center; padding: 24px 8px; }
|
|
@@ -429,6 +439,7 @@
|
|
|
429
439
|
<div class="tabs" id="tabs">
|
|
430
440
|
<button data-view="market" class="active">市场 <span class="tab-count" id="market-tab-count"></span></button>
|
|
431
441
|
<button data-view="installed">已安装 <span class="tab-count" id="installed-tab-count"></span></button>
|
|
442
|
+
<button data-view="tools">工具</button>
|
|
432
443
|
</div>
|
|
433
444
|
<div class="header-spacer"></div>
|
|
434
445
|
<div class="search-box">
|
|
@@ -546,6 +557,13 @@
|
|
|
546
557
|
let view = 'market';
|
|
547
558
|
let marketItems = {};
|
|
548
559
|
let searchQuery = '';
|
|
560
|
+
let explorerFilter = { connectionKey: '', serverName: '', status: '' };
|
|
561
|
+
let explorerOffset = 0;
|
|
562
|
+
let explorerSequence = 0;
|
|
563
|
+
let explorerDetailSequence = 0;
|
|
564
|
+
let explorerData = null;
|
|
565
|
+
let searchTimer = null;
|
|
566
|
+
const explorerBusy = new Set();
|
|
549
567
|
let marketCategory = '';
|
|
550
568
|
const PROMPT_PAGE_SIZE = 4;
|
|
551
569
|
let detailPrompts = [];
|
|
@@ -649,7 +667,14 @@
|
|
|
649
667
|
currentWorkspace = workspace && typeof workspace.workspaceId === 'string'
|
|
650
668
|
? { workspaceId: workspace.workspaceId, title: String(workspace.title || workspace.workspaceId) }
|
|
651
669
|
: null;
|
|
652
|
-
if ((currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId)
|
|
670
|
+
if ((currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId) {
|
|
671
|
+
explorerSequence++; explorerDetailSequence++;
|
|
672
|
+
explorerData = null; explorerOffset = 0;
|
|
673
|
+
explorerFilter = { connectionKey: '', serverName: '', status: '' };
|
|
674
|
+
closeModal(); closeDetail();
|
|
675
|
+
if (view === 'tools') main.innerHTML = '';
|
|
676
|
+
refresh();
|
|
677
|
+
}
|
|
653
678
|
return;
|
|
654
679
|
}
|
|
655
680
|
if (event.data?.type === PROMPT_RESULT_TYPE) {
|
|
@@ -965,15 +990,42 @@
|
|
|
965
990
|
return;
|
|
966
991
|
}
|
|
967
992
|
const tool = result.detail?.tool || {};
|
|
993
|
+
showToolParameters(tool, toolName, serverName);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function schemaSummary(schema, depth = 0, budget = { remaining: 150 }) {
|
|
997
|
+
if (depth > 5 || --budget.remaining < 0) return '<p class="hint">结构较大,请查看原始 Schema。</p>';
|
|
998
|
+
if (typeof schema === 'boolean') return `<p>${schema ? '允许任意输入' : '不允许任何输入'}</p>`;
|
|
999
|
+
if (!schema || typeof schema !== 'object') return '<p class="hint">暂无参数定义。</p>';
|
|
1000
|
+
const complex = ['$ref', 'oneOf', 'anyOf', 'allOf', 'if', 'then', 'else', 'not'].some((key) => key in schema);
|
|
1001
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1002
|
+
const allEntries = Object.entries(schema.properties || {});
|
|
1003
|
+
const entries = allEntries.slice(0, 100);
|
|
1004
|
+
const rows = entries.map(([key, raw]) => {
|
|
1005
|
+
if (--budget.remaining < 0) return '';
|
|
1006
|
+
const field = raw && typeof raw === 'object' ? raw : {};
|
|
1007
|
+
const type = typeof raw === 'boolean' ? (raw ? '任意类型' : '禁止输入') : Array.isArray(field.type) ? field.type.join(' / ') : field.type || (field.properties ? 'object' : '未声明');
|
|
1008
|
+
const constraints = ['enum', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'pattern'].filter((name) => field[name] !== undefined)
|
|
1009
|
+
.map((name) => `${name}: ${JSON.stringify(field[name])}`).join(';');
|
|
1010
|
+
const nested = field.properties || field.items || ['$ref', 'oneOf', 'anyOf', 'allOf'].some((name) => name in field);
|
|
1011
|
+
return `<tr><td><code>${esc(key)}</code></td><td>${esc(type)}</td><td>${required.has(key) ? '必填' : '可选'}</td><td>${esc(field.description || '暂无说明')}${constraints ? `<div class="hint">${esc(constraints)}</div>` : ''}${nested ? `<details><summary>展开结构</summary>${schemaSummary(field, depth + 1, budget)}</details>` : ''}</td></tr>`;
|
|
1012
|
+
}).join('');
|
|
1013
|
+
const items = (allEntries.length > 100 || budget.remaining < 0 ? '<p class="hint">摘要已截断,请查看原始 Schema 中的其余字段。</p>' : '') + (schema.items !== undefined ? `<details><summary>数组元素</summary>${schemaSummary(schema.items, depth + 1, budget)}</details>` : '');
|
|
1014
|
+
return `${complex ? '<p class="hint">包含引用或组合约束,摘要不代表完整规则,请同时查看原始 Schema。</p>' : ''}${rows ? `<div class="parameter-scroll"><table class="parameter-table"><thead><tr><th>参数</th><th>类型</th><th>要求</th><th>说明与约束</th></tr></thead><tbody>${rows}</tbody></table></div>` : `<p class="hint">类型:${esc(Array.isArray(schema.type) ? schema.type.join(' / ') : schema.type || '未声明')};无可展开的具名参数,请查看原始 Schema。</p>`}${items}`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function showToolParameters(tool, toolName, serverName) {
|
|
968
1018
|
const observed = tool.observedAt ? new Date(tool.observedAt).toLocaleString('zh-CN', { hour12: false }) : '未知';
|
|
969
1019
|
const schema = tool.inputSchema ? JSON.stringify(tool.inputSchema, null, 2) : '未缓存输入参数 schema';
|
|
970
1020
|
openModal(`工具参数 · ${tool.name || toolName}`, `
|
|
971
1021
|
<p class="hint">${esc(tool.serverName || serverName)} · 最后成功 ${esc(observed)}${tool.stale ? ' · 缓存已超过 24 小时' : ''}${tool.schemaTruncated ? ' · schema 已安全裁剪' : ''}</p>
|
|
972
1022
|
<p>${esc(tool.description || '暂无描述')}</p>
|
|
973
|
-
|
|
1023
|
+
${schemaSummary(tool.inputSchema)}
|
|
1024
|
+
<details><summary>原始 Schema(安全缓存)</summary><pre class="tool-schema">${esc(schema)}</pre></details>
|
|
974
1025
|
<div class="m-actions"><button class="btn ghost" type="button" onclick="window.__mcp.closeModal()">关闭</button></div>`);
|
|
975
1026
|
}
|
|
976
1027
|
function closeModal() {
|
|
1028
|
+
explorerDetailSequence++;
|
|
977
1029
|
$('#modalMask').classList.remove('show');
|
|
978
1030
|
$('#modalMask').setAttribute('aria-hidden', 'true');
|
|
979
1031
|
pendingPrompt = null;
|
|
@@ -1153,6 +1205,68 @@
|
|
|
1153
1205
|
return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn" type="button" onclick="event.stopPropagation();window.__mcp.openConfig('${id}')">${t('configure')}</button>`;
|
|
1154
1206
|
}
|
|
1155
1207
|
|
|
1208
|
+
const EXPLORER_STATUS = { pending: '尚未发现', unknown: 'Host 状态未知', success: '最近发现成功', failed: '最近发现失败' };
|
|
1209
|
+
const formatObserved = (value) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚无记录';
|
|
1210
|
+
|
|
1211
|
+
async function loadToolExplorer() {
|
|
1212
|
+
const sequence = ++explorerSequence;
|
|
1213
|
+
const workspaceId = currentWorkspace?.workspaceId;
|
|
1214
|
+
syncMarketNav();
|
|
1215
|
+
if (!$('#tool-explorer')) {
|
|
1216
|
+
main.innerHTML = `<section id="tool-explorer"><p class="hint">搜索当前范围内已启用连接的工具缓存;不会执行工具。最近发现成功不代表每次调用均可成功。</p><div class="explorer-filters"><label>连接<select id="explorer-connection" aria-label="按连接筛选"></select></label><label>服务<select id="explorer-server" aria-label="按服务筛选"></select></label><label>发现状态<select id="explorer-status" aria-label="按发现状态筛选"><option value="">全部状态</option>${Object.entries(EXPLORER_STATUS).map(([key, label]) => `<option value="${key}">${label}</option>`).join('')}</select></label><button class="btn ghost" data-explorer-clear>清除筛选</button></div><p id="explorer-summary" role="status">正在读取缓存…</p><div id="explorer-connections"></div><div id="explorer-results"></div><div id="explorer-pages" class="m-actions"></div></section>`;
|
|
1217
|
+
}
|
|
1218
|
+
const response = await call('toolExplorer', { ...workspaceParams(), ...explorerFilter, query: searchQuery, offset: explorerOffset, limit: 20 });
|
|
1219
|
+
if (sequence !== explorerSequence || view !== 'tools' || workspaceId !== currentWorkspace?.workspaceId) return;
|
|
1220
|
+
if (!response.ok) { $('#explorer-summary').textContent = response.message; return; }
|
|
1221
|
+
explorerData = response.detail;
|
|
1222
|
+
const { connections = [], selectedConnections = [], items = [], total = 0 } = explorerData;
|
|
1223
|
+
if (explorerOffset >= total && explorerOffset > 0) { explorerOffset = Math.max(0, Math.floor((total - 1) / 20) * 20); return loadToolExplorer(); }
|
|
1224
|
+
$('#explorer-connection').innerHTML = '<option value="">全部连接</option>' + connections.map((c) => `<option value="${esc(c.connectionKey)}">${esc(c.connectionName)} · ${esc(c.serverName)}</option>`).join('');
|
|
1225
|
+
const services = [...new Set(connections.filter((c) => !explorerFilter.connectionKey || c.connectionKey === explorerFilter.connectionKey).map((c) => c.serverName))];
|
|
1226
|
+
$('#explorer-server').innerHTML = '<option value="">全部服务</option>' + services.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join('');
|
|
1227
|
+
$('#explorer-connection').value = explorerFilter.connectionKey;
|
|
1228
|
+
$('#explorer-server').value = explorerFilter.serverName;
|
|
1229
|
+
$('#explorer-status').value = explorerFilter.status;
|
|
1230
|
+
$('#explorer-summary').textContent = `${explorerData.scopeLabel} · ${connections.length} 条连接 · 匹配 ${total} 个工具 · 显示 ${items.length} 个`;
|
|
1231
|
+
$('#explorer-connections').innerHTML = `<details ${explorerFilter.connectionKey || !total ? 'open' : ''}><summary>连接状态与故障处理(${selectedConnections.length})</summary>${selectedConnections.map((c) => `<div class="explorer-card"><strong>${esc(c.connectionName)}</strong><div>${esc(c.serverName)} · ${esc(EXPLORER_STATUS[c.status] || c.status)} · 缓存 ${c.toolCount} 个工具</div><div class="hint">最近发现:${esc(formatObserved(c.checkedAt))};最后成功:${esc(formatObserved(c.observedAt))}${c.stale ? ' · 缓存超过 24 小时' : ''}${c.cacheTruncated ? ' · 缓存清单已截断' : ''}</div>${c.diagnostic ? `<div>${esc(c.diagnostic.stageLabel)}:${esc(c.diagnostic.message)}<br>建议:${esc(c.diagnostic.action)}</div>` : ''}${c.retryReason === 'auth' ? '<p>鉴权失败已暂停后台重试,请到已安装页更新凭据或重新授权。</p>' : c.nextDiscoveryAt ? `<p class="hint">下次后台发现不早于:${esc(formatObserved(c.nextDiscoveryAt))}(页面保持连接时)</p>` : ''}<div class="m-actions"><button class="btn ghost" data-explorer-action="discover" data-key="${esc(c.connectionKey)}" ${explorerBusy.has(c.connectionKey) ? 'disabled' : ''}>重新发现工具</button><button class="btn ghost" data-explorer-action="check" data-key="${esc(c.connectionKey)}" ${explorerBusy.has(c.connectionKey) ? 'disabled' : ''}>检查连接</button></div></div>`).join('') || '<p>当前筛选下没有连接。</p>'}</details>`;
|
|
1232
|
+
$('#explorer-results').innerHTML = items.map((item, index) => `<article class="explorer-card"><strong>${esc(item.name)}</strong>${item.title !== item.name ? `<div>${esc(item.title)}</div>` : ''}<p>${esc(item.description || '暂无描述')}</p><div class="hint">来源:${esc(item.connection.connectorName)} / ${esc(item.connection.connectionName)} / ${esc(item.serverName)}<br>${esc(EXPLORER_STATUS[item.connection.status])} · 最后成功缓存 ${esc(formatObserved(item.observedAt))}${item.stale ? ' · 已超过 24 小时' : ''}</div><button class="btn ghost" data-explorer-detail="${index}">参数详情</button></article>`).join('') || `<p class="empty">${connections.length ? '没有匹配工具。请调整关键词或筛选;也可展开连接状态检查是否尚未发现或读取失败。' : '当前范围没有已启用连接。请先添加连接,或选择对应工作区。'}</p>`;
|
|
1233
|
+
$('#explorer-pages').innerHTML = `<button class="btn ghost" data-explorer-page="prev" ${explorerOffset === 0 ? 'disabled' : ''}>上一页</button><span>${Math.floor(explorerOffset / 20) + 1} / ${Math.max(1, Math.ceil(total / 20))}</span><button class="btn ghost" data-explorer-page="next" ${explorerOffset + 20 >= total ? 'disabled' : ''}>下一页</button>`;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
main.addEventListener('change', (event) => {
|
|
1237
|
+
const fields = { 'explorer-connection': 'connectionKey', 'explorer-server': 'serverName', 'explorer-status': 'status' };
|
|
1238
|
+
const field = fields[event.target.id];
|
|
1239
|
+
if (!field) return;
|
|
1240
|
+
explorerFilter[field] = event.target.value;
|
|
1241
|
+
if (field === 'connectionKey') explorerFilter.serverName = '';
|
|
1242
|
+
explorerOffset = 0; void loadToolExplorer();
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
main.addEventListener('click', async (event) => {
|
|
1246
|
+
const button = event.target.closest('button');
|
|
1247
|
+
if (!button || view !== 'tools') return;
|
|
1248
|
+
if (button.hasAttribute('data-explorer-clear')) {
|
|
1249
|
+
explorerFilter = { connectionKey: '', serverName: '', status: '' }; explorerOffset = 0; await loadToolExplorer();
|
|
1250
|
+
} else if (button.dataset.explorerPage) {
|
|
1251
|
+
explorerOffset = Math.max(0, explorerOffset + (button.dataset.explorerPage === 'next' ? 20 : -20)); await loadToolExplorer();
|
|
1252
|
+
} else if (button.dataset.explorerDetail !== undefined) {
|
|
1253
|
+
const item = explorerData?.items[Number(button.dataset.explorerDetail)];
|
|
1254
|
+
if (!item) return;
|
|
1255
|
+
const requestId = ++explorerDetailSequence;
|
|
1256
|
+
const result = await call('toolExplorerDetail', { connectionKey: item.connectionKey, serverName: item.serverName, toolName: item.name, ...workspaceParams() });
|
|
1257
|
+
if (requestId !== explorerDetailSequence || view !== 'tools') return;
|
|
1258
|
+
if (result.ok) showToolParameters(result.detail.tool, item.name, item.serverName);
|
|
1259
|
+
else showToast(result.message, false);
|
|
1260
|
+
} else if (button.dataset.explorerAction) {
|
|
1261
|
+
const key = button.dataset.key;
|
|
1262
|
+
if (explorerBusy.has(key)) return;
|
|
1263
|
+
explorerBusy.add(key); button.disabled = true;
|
|
1264
|
+
const result = await call('toolExplorerAction', { connectionKey: key, action: button.dataset.explorerAction, ...workspaceParams() });
|
|
1265
|
+
explorerBusy.delete(key); showToast(result.message, result.ok);
|
|
1266
|
+
if (view === 'tools') await loadToolExplorer();
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1156
1270
|
async function loadInstalled() {
|
|
1157
1271
|
syncMarketNav();
|
|
1158
1272
|
main.innerHTML = '<div class="empty">加载中…</div>';
|
|
@@ -2034,7 +2148,8 @@
|
|
|
2034
2148
|
statusRefreshRunning = true;
|
|
2035
2149
|
statusRefreshPending = false;
|
|
2036
2150
|
try {
|
|
2037
|
-
if (view === '
|
|
2151
|
+
if (view === 'tools') await loadToolExplorer();
|
|
2152
|
+
else if (view === 'market') await loadMarket({ checkHealth: false, preserveScroll: true });
|
|
2038
2153
|
else await loadInstalled();
|
|
2039
2154
|
await refreshOpenToolDetail();
|
|
2040
2155
|
} finally {
|
|
@@ -2073,8 +2188,11 @@
|
|
|
2073
2188
|
statusEventSource = null;
|
|
2074
2189
|
});
|
|
2075
2190
|
|
|
2076
|
-
function refresh() { return view === 'market' ? loadMarket() : loadInstalled(); }
|
|
2191
|
+
function refresh() { return view === 'tools' ? loadToolExplorer() : view === 'market' ? loadMarket() : loadInstalled(); }
|
|
2077
2192
|
function syncTabs() {
|
|
2193
|
+
searchInput.placeholder = view === 'tools' ? '搜索工具名称或描述…' : '搜索连接器…';
|
|
2194
|
+
$('#refresh').textContent = view === 'tools' ? '刷新工具视图' : '刷新连接器目录';
|
|
2195
|
+
$('#refresh').title = $('#refresh').textContent;
|
|
2078
2196
|
document.querySelectorAll('#tabs button').forEach((button) => button.classList.toggle('active', button.dataset.view === view));
|
|
2079
2197
|
syncMarketNav();
|
|
2080
2198
|
}
|
|
@@ -2097,6 +2215,7 @@
|
|
|
2097
2215
|
}
|
|
2098
2216
|
});
|
|
2099
2217
|
$('#refresh').addEventListener('click', async () => {
|
|
2218
|
+
if (view === 'tools') { await loadToolExplorer(); return; }
|
|
2100
2219
|
const r = await call('refreshCatalog', {});
|
|
2101
2220
|
showToast(r.message, r.ok);
|
|
2102
2221
|
if (r.ok) {
|
|
@@ -2110,12 +2229,16 @@
|
|
|
2110
2229
|
const btn = e.target.closest('button[data-view]');
|
|
2111
2230
|
if (!btn) return;
|
|
2112
2231
|
view = btn.dataset.view;
|
|
2232
|
+
explorerSequence++; explorerDetailSequence++;
|
|
2113
2233
|
syncTabs();
|
|
2114
2234
|
refresh();
|
|
2115
2235
|
});
|
|
2116
2236
|
|
|
2117
2237
|
searchInput.addEventListener('input', (e) => {
|
|
2118
2238
|
searchQuery = e.target.value.trim();
|
|
2239
|
+
explorerSequence++; explorerOffset = 0;
|
|
2240
|
+
clearTimeout(searchTimer);
|
|
2241
|
+
if (view === 'tools') { searchTimer = setTimeout(() => { if (view === 'tools') void loadToolExplorer(); }, 200); return; }
|
|
2119
2242
|
expandedMarketSections.clear();
|
|
2120
2243
|
refresh();
|
|
2121
2244
|
});
|