dsh-mcp-connector 0.2.46 → 0.2.48
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 +27 -1
- package/README.en.md +8 -3
- package/README.md +8 -3
- package/docs/CLI-PROVIDERS.md +56 -0
- package/docs/STDIO-SUPPORT.md +4 -4
- package/docs/screenshots/05-unified-tool-search.png +0 -0
- package/docs/screenshots/06-tool-parameters.png +0 -0
- package/docs/screenshots/07-connection-diagnostics.png +0 -0
- package/docs/screenshots/README.md +23 -30
- package/docs/screenshots/assets.json +33 -4
- package/lib/cli-providers.js +436 -0
- package/package.json +6 -3
- package/screenshots.json +4 -1
- package/scripts/cli-bridge.mjs +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.48] - 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 新增受控 CLI→MCP Provider 框架,首批把钉钉官方 `dws` 的通讯录、日程、待办、文档、OA 待审批和日志查询映射为 11 个只读 MCP 工具。
|
|
12
|
+
- 连接时通过官方 `dws auth status` 验证 OAuth 就绪状态,未登录、无权限或 Token 无法刷新时不误报“已连接”。
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- CLI 只能执行代码审核的命令与参数,不经 Shell;写入动词、未声明参数和控制字符均拒绝。子进程超时、输出大小受限,成功及失败输出中的 Token/Secret 都在返回 Host 前脱敏。
|
|
17
|
+
|
|
18
|
+
## [0.2.47] - 2026-09-14
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- 新增三张当前工具工作台宣传截图,覆盖统一工具搜索、参数详情与连接诊断;更新 Storefront 清单、来源版本与完整性哈希。
|
|
23
|
+
- 将演示 GIF 更新为 43 秒六场景流程,并在中英文 README 中补齐当前能力截图。
|
|
24
|
+
- 更新宣传总账、媒体发布登记和外部分发状态;记录 `dshmarket@1.46.1` 的五组搜索复验结果及 D+7/D+14 复盘日期。
|
|
25
|
+
|
|
26
|
+
### Verification
|
|
27
|
+
|
|
28
|
+
- 250 项自动测试通过;7 张 Storefront 截图及 43 秒演示通过哈希、尺寸、采集约束和无凭据检查;75 个发布文件通过白名单与敏感信息扫描。
|
|
29
|
+
- npm/GitHub 营销元数据线上检查通过;外部目录非收敛项继续由 Issue #69 跟踪。
|
|
30
|
+
|
|
7
31
|
## [0.2.46] - 2026-09-13
|
|
8
32
|
|
|
9
33
|
### Changed
|
|
@@ -704,7 +728,9 @@
|
|
|
704
728
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
705
729
|
- iframe 消息校验同源和消息来源。
|
|
706
730
|
|
|
707
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
731
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.48...HEAD
|
|
732
|
+
[0.2.48]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.47...v0.2.48
|
|
733
|
+
[0.2.47]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.46...v0.2.47
|
|
708
734
|
[0.2.46]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.45...v0.2.46
|
|
709
735
|
[0.2.45]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.44...v0.2.45
|
|
710
736
|
[0.2.44]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.43...v0.2.44
|
package/README.en.md
CHANGED
|
@@ -36,7 +36,7 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
36
36
|
|
|
37
37
|
Fully restart DeepSeek Harness Desktop or `dsh web` after installation or upgrade, then open **MCP Connector** in the primary sidebar, or open it directly from **Settings → Plugins → Plugin Configuration → MCP Connector**.
|
|
38
38
|
|
|
39
|
-

|
|
40
40
|
|
|
41
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).
|
|
42
42
|
|
|
@@ -87,8 +87,12 @@ As of 2026-09-11, the public Registry publishes 106 connector descriptors. After
|
|
|
87
87
|
|  |  |
|
|
88
88
|
| Tool discovery, descriptions, and scrolling | JSON import |
|
|
89
89
|
|  |  |
|
|
90
|
+
| Unified tool search and sources | Parameter details and safe schema |
|
|
91
|
+
|  |  |
|
|
92
|
+
| Connection diagnostics and last-success cache | |
|
|
93
|
+
|  | |
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
Assets `01`–`04` preserve the historical `v0.2.37` marketplace snapshot. Assets `05`–`07` and the 43-second walkthrough come from the current `v0.2.46` credential-free UI harness. The harness mirrors the real 800px product panel, producing two cards per desktop row. It shows only public marketplace metadata, example prompts, and explicitly marked mock tool descriptions—never credentials, local paths, or query results. See [`docs/screenshots/README.md`](docs/screenshots/README.md) for provenance.
|
|
92
96
|
|
|
93
97
|
## Installation
|
|
94
98
|
|
|
@@ -186,7 +190,7 @@ npm run dev:ui
|
|
|
186
190
|
|
|
187
191
|
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.
|
|
188
192
|
|
|
189
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
193
|
+
The current public version is [`dsh-mcp-connector@0.2.48`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.48](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.48).
|
|
190
194
|
|
|
191
195
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
192
196
|
|
|
@@ -198,6 +202,7 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](d
|
|
|
198
202
|
- External URLs use HTTPS by default, and HTTP is allowed for loopback development. A user-created connection may explicitly opt in to plaintext HTTP for literal RFC1918 IPv4 or RFC4193 IPv6 ULA addresses; hostnames, public HTTP, link-local addresses, and cloud metadata addresses remain blocked.
|
|
199
203
|
- Remote descriptors and catalogs are limited to 2 MiB, Web API requests to 1 MiB, and imported JSON is scanned for credential fields before normalization.
|
|
200
204
|
- Streamable HTTP and stdio are supported end to end. Legacy `sse` entries are normalized to Streamable HTTP. The connector passes stdio `command/args/env/cwd` to `@deepseek-ai/dsh-mcp-client` instead of reimplementing process transport.
|
|
205
|
+
- Official CLIs that are not MCP servers can be exposed through a separate [controlled CLI Provider](docs/CLI-PROVIDERS.md). Only code-reviewed commands and arguments are available; the initial DingTalk `dws` provider is read-only.
|
|
201
206
|
- stdio starts a local process. Import or connect only trusted commands and packages. Catalog descriptors may declare `credentialFields` and `credentialBindings`, but may never contain actual token/secret values; user input is injected only into the local Host process environment.
|
|
202
207
|
- OAuth DCR client secrets share the same local-only boundary as access and refresh tokens and are omitted from catalog/status responses and logs.
|
|
203
208
|
- OAuth failures identify resource discovery, server discovery, client registration, browser callback, or token exchange. A DCR HTTP 403 is reported as provider-side client admission rejection rather than a missing user authorization.
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
36
36
|
|
|
37
37
|
安装或升级后完全重启 DeepSeek Harness Desktop 或 `dsh web`,然后打开左侧「🧩 MCP连接器」;也可从“设置 → 插件 → 插件配置 → MCP连接器”直接打开。
|
|
38
38
|
|
|
39
|
-

|
|
40
40
|
|
|
41
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)。
|
|
42
42
|
|
|
@@ -88,8 +88,12 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
88
88
|
|  |  |
|
|
89
89
|
| 工具发现、描述与独立滚动 | JSON 导入 |
|
|
90
90
|
|  |  |
|
|
91
|
+
| 统一工具搜索与来源 | 参数详情与安全 Schema |
|
|
92
|
+
|  |  |
|
|
93
|
+
| 连接诊断与最后成功缓存 | |
|
|
94
|
+
|  | |
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
`01`–`04` 保留 `v0.2.37` 的历史市场快照,`05`–`07` 与 43 秒演示来自 `v0.2.46` 当前无凭据 UI harness。桌面端一行 2 张卡片;只展示公开市场元数据、示例 Prompt 和明确标识的 Mock 工具说明,不包含凭据、本机路径或查询结果。详见 [`docs/screenshots/README.md`](docs/screenshots/README.md)。
|
|
93
97
|
|
|
94
98
|
## 安装
|
|
95
99
|
|
|
@@ -181,7 +185,7 @@ npm run dev:ui
|
|
|
181
185
|
|
|
182
186
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
183
187
|
|
|
184
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
188
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.48`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.48](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.48)。
|
|
185
189
|
|
|
186
190
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
187
191
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
|
@@ -197,6 +201,7 @@ stdio 传输的架构、透传边界与安全约束见 [docs/STDIO-SUPPORT.md](d
|
|
|
197
201
|
- 外部 URL 默认仅允许 HTTPS,HTTP 默认仅允许回环地址。用户自建连接可在明确风险确认后放行 RFC1918 IPv4 / RFC4193 IPv6 ULA 字面量;域名、公网 HTTP、链路本地与云元数据地址仍拒绝。
|
|
198
202
|
- 远程目录/描述响应限制 2 MiB,Web API 请求限制 1 MiB;原始 JSON 在归一化前扫描凭据字段。
|
|
199
203
|
- 完整覆盖 Streamable HTTP 与 stdio;旧 `sse` 配置在导入/恢复时归一为 Streamable HTTP。stdio 的 `command/args/env/cwd` 原样交给 `@deepseek-ai/dsh-mcp-client`,插件本身不重复实现进程传输。
|
|
204
|
+
- 对本身不是 MCP Server 的官方 CLI,使用独立的[受控 CLI Provider](docs/CLI-PROVIDERS.md)桥接为 stdio MCP;只允许代码登记的命令与参数,首个钉钉 `dws` Provider 仅开放只读能力。
|
|
200
205
|
- stdio 会启动本机进程:仅导入或连接可信命令/软件包。市场目录只能用 `credentialFields` + `credentialBindings` 声明输入与 env 映射,不得携带真实 token/secret;用户填写值只写入本机连接记录并交给 Host。
|
|
201
206
|
- OAuth DCR 的 `client_secret` 与 Access/Refresh Token 采用相同的本机存储边界,不会进入市场 API、状态输出或日志。
|
|
202
207
|
- OAuth 连接失败会标明资源发现、服务发现、客户端注册、授权回调或 Token 换取阶段;DCR HTTP 403 表示服务商拒绝未准入客户端,不是用户未点击授权。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# 受控 CLI Provider
|
|
2
|
+
|
|
3
|
+
MCP 连接器原生支持 Streamable HTTP 和 stdio MCP。对于钉钉 `dws` 这类面向人类与 Agent、但本身不是 MCP Server 的官方 CLI,插件通过独立的 CLI→MCP 桥接进程暴露经过审核的结构化工具。
|
|
4
|
+
|
|
5
|
+
## 安全边界
|
|
6
|
+
|
|
7
|
+
- Provider 在代码中显式登记,市场卡片和工具参数均不能指定任意可执行文件。
|
|
8
|
+
- 子进程通过 `spawn(command, args, { shell: false })` 启动,不解析 Shell 表达式。
|
|
9
|
+
- 首个 `dingtalk-dws` Provider 仅登记只读命令;创建、发送、审批、拒绝、删除、更新和上传均不暴露。
|
|
10
|
+
- 每个工具只接受 JSON Schema 声明过的字段,参数作为独立 argv 传递。
|
|
11
|
+
- stdout/stderr 均受大小限制,执行有超时;错误中的 Token 和 Secret 在返回 Host 前脱敏。
|
|
12
|
+
- MCP 客户端枚举工具前执行官方 `dws auth status` 预检;未登录、权限不足或 Token 无法刷新时连接直接失败,不会误显示“已连接”。
|
|
13
|
+
- 登录、Token 刷新、企业管理员授权、租户隔离和 API 审计由官方 CLI 与开放平台负责。
|
|
14
|
+
|
|
15
|
+
## 钉钉前置条件
|
|
16
|
+
|
|
17
|
+
1. 市场卡片固定使用已审核的 `dingtalk-workspace-cli@1.0.61`,无需全局安装 `dws`。
|
|
18
|
+
2. 首次连接前,在普通终端执行 `npx -y dingtalk-workspace-cli@1.0.61 auth login`;无浏览器环境加 `--device`。
|
|
19
|
+
3. 企业管理员需在钉钉开发者平台开启 CLI 访问。
|
|
20
|
+
4. 多组织用户可在工具参数里传稳定的 `corpId:userId` profile;不传时使用当前 profile。
|
|
21
|
+
|
|
22
|
+
授权信息保存在钉钉官方 CLI 的用户配置和系统 Keychain 中,不写入 Connector 配置。后续升级 `dws` 需先复核官方命令、权限和输出 Schema,然后通过 Registry PR 更换固定版本。
|
|
23
|
+
|
|
24
|
+
启动 MCP 桥接:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"dingtalk-workspace": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": [
|
|
32
|
+
"-y",
|
|
33
|
+
"--legacy-peer-deps",
|
|
34
|
+
"--package",
|
|
35
|
+
"dsh-mcp-connector@0.2.48",
|
|
36
|
+
"--package",
|
|
37
|
+
"dingtalk-workspace-cli@1.0.61",
|
|
38
|
+
"dsh-mcp-cli-bridge",
|
|
39
|
+
"--provider",
|
|
40
|
+
"dingtalk-dws"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 首批只读能力
|
|
48
|
+
|
|
49
|
+
- 当前用户、通讯录搜索与用户详情
|
|
50
|
+
- 日程列表与日程详情
|
|
51
|
+
- 待办列表与待办详情
|
|
52
|
+
- 文档搜索
|
|
53
|
+
- 待处理 OA 审批列表
|
|
54
|
+
- 收到的日志和日志模板
|
|
55
|
+
|
|
56
|
+
后续增加 Provider 或命令时,必须同时提供:官方来源、固定命令路径、输入 Schema、风险级别、只读/写入分类、脱敏策略和单元测试。写入命令不得混入只读 Provider。
|
package/docs/STDIO-SUPPORT.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
## 一、背景与目标
|
|
14
14
|
|
|
15
|
-
### 1.1
|
|
15
|
+
### 1.1 实施前的现状缺口(历史记录)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
以下内容记录实施前的缺口;当前版本已经支持 stdio。实施前 `dsh-mcp-connector` 的传输方式(`transport`)仅支持:
|
|
18
18
|
|
|
19
19
|
- `streamable-http`
|
|
20
20
|
- `sse`
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
|
|
24
24
|
| 传输方式 | WorkBuddy | TraeWork | Qwen Code | 我们 |
|
|
25
25
|
|---------|:--------:|:--------:|:--------:|:----:|
|
|
26
|
-
| stdio(本地进程) | ✅ | ✅ | ✅ |
|
|
26
|
+
| stdio(本地进程) | ✅ | ✅ | ✅ | ✅(当前已完成) |
|
|
27
27
|
| HTTP / streamable-http | ❓ | ✅ | ✅ | ✅ |
|
|
28
28
|
| SSE | ❓ | ❓ | ✅ | ✅ |
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**历史影响**:缺失 stdio 时无法接入大量社区 stdio 型 MCP server(GitHub、filesystem、playwright、memory 等 npm/pip 包);该缺口现已关闭。
|
|
31
31
|
|
|
32
32
|
### 1.2 补齐目标
|
|
33
33
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
# UI 素材说明
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
仓库根目录的 [`screenshots.json`](../../screenshots.json) 按市场总览、连接器详情、工具发现、
|
|
15
|
-
JSON 导入的顺序声明这 4 张图片,供 awesome-dsh-plugin 和 DSH Market 的 App Store 风格详情页
|
|
16
|
-
直接采集。根清单保持外部消费者兼容的路径数组;[`assets.json`](assets.json) 记录
|
|
17
|
-
1280×720 视口、800px 产品面板、2 列、无凭据状态,以及每张截图和 `demo.gif` 的
|
|
18
|
-
SHA-256、尺寸与 GIF 时长。
|
|
19
|
-
`npm run storefront:check` 会校验路径、顺序、格式、文件存在性、哈希、尺寸和采集约束;
|
|
20
|
-
`npm run check` 已包含该门禁。
|
|
3
|
+
本目录保存 `dsh-mcp-connector` 的公开界面截图。所有素材均来自无凭据 UI harness:外层浏览器视口为 1280×720,harness 复刻 `lib/client.js` 中定义的 800px 产品面板,因此桌面端稳定为一行 2 张卡片。画面只展示公开目录元数据、示例 Prompt 和明确标识为 Mock 的工具数据,不包含 OAuth Token、API Key、本机路径、用户会话或查询结果。
|
|
4
|
+
|
|
5
|
+
素材分为两组,避免把历史截图误写成当前版本:
|
|
6
|
+
|
|
7
|
+
- `01`–`04` 是 2026-09-06 从 `v0.2.37` 采集的历史市场、详情、发现和导入快照;当时公开 Registry 为 105 条,与 4 张随包唯一卡片去重后显示 109 张。
|
|
8
|
+
- `05`–`07` 和 `../demo.gif` 是 2026-09-14 从 `v0.2.46` 当前 UI harness 采集的工具工作台、参数和诊断素材。harness 中的目录数量、连接数量和工具数量属于测试夹具,不代表生产市场统计。
|
|
9
|
+
|
|
10
|
+
旧素材曾直接以 1280px 打开内部 iframe,绕过真实产品的 800px 宿主面板,因此触发 `auto-fill/minmax(300px)` 的 3 列布局;这是采集 harness/视口过期,不是产品 CSS 漂移。当前全部登记素材都从浏览器中的真实页面状态直接截取,没有后期重排卡片或修改连接状态。
|
|
11
|
+
|
|
12
|
+
仓库根目录的 [`screenshots.json`](../../screenshots.json) 按市场总览、连接器详情、工具发现、JSON 导入、统一工具搜索、参数详情和连接诊断的顺序声明 7 张图片,供 awesome-dsh-plugin 和 DSH Market 的 App Store 风格详情页采集。根清单保持外部消费者兼容的路径数组;[`assets.json`](assets.json) 记录采集版本、视口、产品面板、无凭据状态,以及每张截图和 `demo.gif` 的 SHA-256、尺寸与 GIF 时长。
|
|
21
13
|
|
|
22
14
|
| 文件 | 展示内容 |
|
|
23
15
|
|---|---|
|
|
24
|
-
| `01-market-overview.jpg` | 109 张合并市场卡片、桌面两列、推荐章节、9
|
|
25
|
-
| `02-connector-detail.jpg` |
|
|
26
|
-
| `03-tool-discovery.jpg` | Context7 无凭据 Mock 健康状态、Mock 工具数量/描述、搜索和独立滚动区 |
|
|
27
|
-
| `04-json-import.jpg` | Streamable HTTP / stdio JSON 导入与本机凭据提示 |
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
16
|
+
| `01-market-overview.jpg` | `v0.2.37` 历史市场快照:109 张合并市场卡片、桌面两列、推荐章节、9 分类和固定分类栏 |
|
|
17
|
+
| `02-connector-detail.jpg` | `v0.2.37` 智能文档解析详情:一次 OAuth 授权同时连接远程 URL 解析与本地 `qcc-document-mcp` 文件解析 |
|
|
18
|
+
| `03-tool-discovery.jpg` | `v0.2.37` Context7 无凭据 Mock 健康状态、Mock 工具数量/描述、搜索和独立滚动区 |
|
|
19
|
+
| `04-json-import.jpg` | `v0.2.37` Streamable HTTP / stdio JSON 导入与本机凭据提示 |
|
|
20
|
+
| `05-unified-tool-search.png` | `v0.2.46` 统一工具搜索:跨连接结果、来源、发现失败和最后成功缓存 |
|
|
21
|
+
| `06-tool-parameters.png` | `v0.2.46` 参数详情:必填、类型、枚举、嵌套结构和原始安全 Schema |
|
|
22
|
+
| `07-connection-diagnostics.png` | `v0.2.46` 连接诊断:成功缓存、可控失败原因、建议、“检查连接”和“重新发现工具” |
|
|
23
|
+
|
|
24
|
+
`../demo.gif` 由 6 个真实浏览器状态顺序编码:市场、连接器详情、工具详情、统一搜索、参数 Schema、连接诊断。时长 43 秒,分辨率 960×540;编码只做等比例缩放和 GIF 调色,不改变布局或状态。
|
|
25
|
+
|
|
26
|
+
`npm run storefront:check` 校验路径、顺序、格式、文件存在性、哈希、尺寸和采集约束;`npm run check` 已包含该门禁。更新 UI 后继续使用 1280×720 浏览器视口、800px 产品面板和无凭据初始状态采集,并检查桌面两列、企查查卡片不出现“需重新授权”,且画面不包含凭据、本机路径、真实会话、查询结果或其他不应公开的内容。
|
|
27
|
+
|
|
28
|
+
截图顶部“无凭据 Mock”标识和工具名中的 `mock_` 前缀用于明确区分验收数据与真实授权。真实用户的已安装数量、授权状态和服务健康度会因本机环境而不同。
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"capture": {
|
|
4
|
-
"capturedOn": "2026-09-
|
|
4
|
+
"capturedOn": "2026-09-14",
|
|
5
|
+
"sourceVersion": "0.2.46",
|
|
5
6
|
"browserViewport": {
|
|
6
7
|
"width": 1280,
|
|
7
8
|
"height": 720
|
|
8
9
|
},
|
|
9
10
|
"productPanelWidth": 800,
|
|
10
11
|
"desktopCardColumns": 2,
|
|
11
|
-
"state": "credential-free-mock"
|
|
12
|
+
"state": "credential-free-mock",
|
|
13
|
+
"preservedLegacyScreenshots": 4,
|
|
14
|
+
"notes": "01-04 preserve the 2026-09-06 v0.2.37 storefront snapshot; 05-07 and demo.gif were captured from the current v0.2.46 UI harness on 2026-09-14."
|
|
12
15
|
},
|
|
13
16
|
"screenshots": [
|
|
14
17
|
{
|
|
@@ -34,13 +37,39 @@
|
|
|
34
37
|
"sha256": "de0a87cc7f247fad40ac37188f5a1d026f4087571e98d64595e80f8bcd9410be",
|
|
35
38
|
"width": 1280,
|
|
36
39
|
"height": 720
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"path": "docs/screenshots/05-unified-tool-search.png",
|
|
43
|
+
"sha256": "a75119afeccdb666e1496917ab1cac50e368882412fe5ce16cfdb268c2a6595b",
|
|
44
|
+
"width": 1280,
|
|
45
|
+
"height": 720,
|
|
46
|
+
"capturedOn": "2026-09-14",
|
|
47
|
+
"sourceVersion": "0.2.46"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"path": "docs/screenshots/06-tool-parameters.png",
|
|
51
|
+
"sha256": "199a960e6c07c84ff2e3b99acf64536b6875f2ebf08b5a86a1df971cc120da70",
|
|
52
|
+
"width": 1280,
|
|
53
|
+
"height": 720,
|
|
54
|
+
"capturedOn": "2026-09-14",
|
|
55
|
+
"sourceVersion": "0.2.46"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "docs/screenshots/07-connection-diagnostics.png",
|
|
59
|
+
"sha256": "5ca0e53cf560c68a06239c29651f9d9e49ee184c43d43432ccdc4139299e811b",
|
|
60
|
+
"width": 1280,
|
|
61
|
+
"height": 720,
|
|
62
|
+
"capturedOn": "2026-09-14",
|
|
63
|
+
"sourceVersion": "0.2.46"
|
|
37
64
|
}
|
|
38
65
|
],
|
|
39
66
|
"demo": {
|
|
40
67
|
"path": "docs/demo.gif",
|
|
41
|
-
"sha256": "
|
|
68
|
+
"sha256": "69250ef0d6730d2c708c1fea6a3c8041d495008a40f53a02a894238601f165ec",
|
|
42
69
|
"width": 960,
|
|
43
70
|
"height": 540,
|
|
44
|
-
"durationSeconds":
|
|
71
|
+
"durationSeconds": 43,
|
|
72
|
+
"capturedOn": "2026-09-14",
|
|
73
|
+
"sourceVersion": "0.2.46"
|
|
45
74
|
}
|
|
46
75
|
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
+
const WRITE_VERBS = new Set([
|
|
6
|
+
'add', 'approve', 'create', 'delete', 'disable', 'done', 'enable', 'insert',
|
|
7
|
+
'invite', 'move', 'recall', 'reject', 'remove', 'rename', 'reply', 'revoke',
|
|
8
|
+
'send', 'switch', 'update', 'upload',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function stringField(flag, options = {}) {
|
|
12
|
+
return { flag, type: 'string', ...options };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function integerField(flag, options = {}) {
|
|
16
|
+
return { flag, type: 'integer', ...options };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function booleanField(flag, options = {}) {
|
|
20
|
+
return { flag, type: 'boolean', ...options };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stringArrayField(flag, options = {}) {
|
|
24
|
+
return { flag, type: 'string-array', ...options };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const profileField = stringField('--profile', {
|
|
28
|
+
global: true,
|
|
29
|
+
description: '可选的钉钉账号 profile(建议使用 corpId:userId)。不填写时使用 dws 当前 profile。',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function readTool(name, description, command, fields = {}) {
|
|
33
|
+
return { name, description, command, fields, readOnly: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dingtalkDwsTools = [
|
|
37
|
+
readTool(
|
|
38
|
+
'dingtalk_get_current_user',
|
|
39
|
+
'读取当前钉钉 OAuth 身份的用户、组织和部门信息。',
|
|
40
|
+
['contact', 'user', 'get-self'],
|
|
41
|
+
{ profile: profileField },
|
|
42
|
+
),
|
|
43
|
+
readTool(
|
|
44
|
+
'dingtalk_search_users',
|
|
45
|
+
'按关键词只读搜索钉钉企业通讯录。多人同名时应展示候选,不得自动选择。',
|
|
46
|
+
['contact', 'user', 'search'],
|
|
47
|
+
{
|
|
48
|
+
profile: profileField,
|
|
49
|
+
query: stringField('--query', { required: true, description: '姓名、职位或通讯录关键词。' }),
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
readTool(
|
|
53
|
+
'dingtalk_get_users',
|
|
54
|
+
'按一个或多个 userId 批量读取钉钉用户资料。',
|
|
55
|
+
['contact', 'user', 'get'],
|
|
56
|
+
{
|
|
57
|
+
profile: profileField,
|
|
58
|
+
ids: stringArrayField('--ids', { required: true, description: '钉钉 userId 列表。' }),
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
readTool(
|
|
62
|
+
'dingtalk_list_calendar_events',
|
|
63
|
+
'只读列出当前用户在指定时间范围内的钉钉日程;不传时间时由 dws 使用默认范围。',
|
|
64
|
+
['calendar', 'event', 'list'],
|
|
65
|
+
{
|
|
66
|
+
profile: profileField,
|
|
67
|
+
start: stringField('--start', { description: 'ISO 8601 开始时间。' }),
|
|
68
|
+
end: stringField('--end', { description: 'ISO 8601 结束时间。' }),
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
readTool(
|
|
72
|
+
'dingtalk_get_calendar_event',
|
|
73
|
+
'按日程 ID 只读获取钉钉日程详情。',
|
|
74
|
+
['calendar', 'event', 'get'],
|
|
75
|
+
{
|
|
76
|
+
profile: profileField,
|
|
77
|
+
id: stringField('--id', { required: true, description: '日程 ID。' }),
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
readTool(
|
|
81
|
+
'dingtalk_list_todos',
|
|
82
|
+
'只读列出当前用户的钉钉待办。',
|
|
83
|
+
['todo', 'task', 'list'],
|
|
84
|
+
{
|
|
85
|
+
profile: profileField,
|
|
86
|
+
page: integerField('--page', { minimum: 1, maximum: 10000, description: '页码。' }),
|
|
87
|
+
size: integerField('--size', { minimum: 1, maximum: 100, description: '每页数量。' }),
|
|
88
|
+
completed: booleanField('--status', { description: 'true 仅已完成,false 仅未完成;不传则不过滤。' }),
|
|
89
|
+
},
|
|
90
|
+
),
|
|
91
|
+
readTool(
|
|
92
|
+
'dingtalk_get_todo',
|
|
93
|
+
'按待办 ID 只读获取钉钉待办详情。',
|
|
94
|
+
['todo', 'task', 'get'],
|
|
95
|
+
{
|
|
96
|
+
profile: profileField,
|
|
97
|
+
taskId: stringField('--task-id', { required: true, description: '待办任务 ID。' }),
|
|
98
|
+
},
|
|
99
|
+
),
|
|
100
|
+
readTool(
|
|
101
|
+
'dingtalk_search_documents',
|
|
102
|
+
'只读搜索当前用户有权访问的钉钉文件和文档。',
|
|
103
|
+
['drive', 'search'],
|
|
104
|
+
{
|
|
105
|
+
profile: profileField,
|
|
106
|
+
query: stringField('--query', { required: true, description: '文件或文档标题、内容关键词。' }),
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
readTool(
|
|
110
|
+
'dingtalk_list_pending_approvals',
|
|
111
|
+
'只读列出指定时间范围内当前用户待处理的钉钉 OA 审批实例;不会执行同意或拒绝。',
|
|
112
|
+
['oa', 'approval', 'list-pending'],
|
|
113
|
+
{
|
|
114
|
+
profile: profileField,
|
|
115
|
+
start: stringField('--start', { required: true, description: 'ISO 8601 开始时间。' }),
|
|
116
|
+
end: stringField('--end', { required: true, description: 'ISO 8601 结束时间。' }),
|
|
117
|
+
page: integerField('--page', { minimum: 1, maximum: 10000, description: '页码。' }),
|
|
118
|
+
limit: integerField('--limit', { minimum: 1, maximum: 100, description: '每页数量。' }),
|
|
119
|
+
},
|
|
120
|
+
),
|
|
121
|
+
readTool(
|
|
122
|
+
'dingtalk_list_reports',
|
|
123
|
+
'只读列出指定时间范围内当前用户收到的钉钉日志。',
|
|
124
|
+
['report', 'inbox', 'list'],
|
|
125
|
+
{
|
|
126
|
+
profile: profileField,
|
|
127
|
+
start: stringField('--start', { required: true, description: 'ISO 8601 开始时间。' }),
|
|
128
|
+
end: stringField('--end', { required: true, description: 'ISO 8601 结束时间。' }),
|
|
129
|
+
cursor: integerField('--cursor', { minimum: 0, description: '分页游标。' }),
|
|
130
|
+
size: integerField('--size', { minimum: 1, maximum: 20, description: '返回数量。' }),
|
|
131
|
+
},
|
|
132
|
+
),
|
|
133
|
+
readTool(
|
|
134
|
+
'dingtalk_list_report_templates',
|
|
135
|
+
'只读列出当前用户可用的钉钉日志模板。',
|
|
136
|
+
['report', 'template', 'list'],
|
|
137
|
+
{ profile: profileField },
|
|
138
|
+
),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
export const CLI_PROVIDERS = Object.freeze({
|
|
142
|
+
'dingtalk-dws': Object.freeze({
|
|
143
|
+
id: 'dingtalk-dws',
|
|
144
|
+
name: '钉钉工作台 CLI',
|
|
145
|
+
executable: 'dws',
|
|
146
|
+
homepage: 'https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli',
|
|
147
|
+
preflight: Object.freeze(['auth', 'status']),
|
|
148
|
+
tools: Object.freeze(dingtalkDwsTools),
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function inputSchema(tool) {
|
|
153
|
+
const properties = {};
|
|
154
|
+
const required = [];
|
|
155
|
+
for (const [name, field] of Object.entries(tool.fields)) {
|
|
156
|
+
const schema = { description: field.description };
|
|
157
|
+
if (field.type === 'integer') {
|
|
158
|
+
schema.type = 'integer';
|
|
159
|
+
if (field.minimum !== undefined) schema.minimum = field.minimum;
|
|
160
|
+
if (field.maximum !== undefined) schema.maximum = field.maximum;
|
|
161
|
+
} else if (field.type === 'boolean') {
|
|
162
|
+
schema.type = 'boolean';
|
|
163
|
+
} else if (field.type === 'string-array') {
|
|
164
|
+
schema.type = 'array';
|
|
165
|
+
schema.items = { type: 'string', minLength: 1 };
|
|
166
|
+
schema.minItems = 1;
|
|
167
|
+
schema.maxItems = 100;
|
|
168
|
+
} else {
|
|
169
|
+
schema.type = 'string';
|
|
170
|
+
schema.minLength = 1;
|
|
171
|
+
}
|
|
172
|
+
properties[name] = schema;
|
|
173
|
+
if (field.required) required.push(name);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties,
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
...(required.length ? { required } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function listCliProviderTools(providerId) {
|
|
184
|
+
const provider = CLI_PROVIDERS[providerId];
|
|
185
|
+
if (!provider) throw new Error(`unknown CLI provider: ${providerId}`);
|
|
186
|
+
return provider.tools.map((tool) => ({
|
|
187
|
+
name: tool.name,
|
|
188
|
+
description: tool.description,
|
|
189
|
+
inputSchema: inputSchema(tool),
|
|
190
|
+
annotations: {
|
|
191
|
+
readOnlyHint: true,
|
|
192
|
+
destructiveHint: false,
|
|
193
|
+
idempotentHint: true,
|
|
194
|
+
openWorldHint: true,
|
|
195
|
+
},
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function assertSafeExecutable(executable) {
|
|
200
|
+
const value = String(executable ?? '').trim();
|
|
201
|
+
if (!value || /[\0\r\n]/.test(value) || /\s/.test(value)) {
|
|
202
|
+
throw new Error('CLI executable must be one command name or an absolute path without whitespace');
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateField(name, value, field) {
|
|
208
|
+
if (value === undefined || value === null || value === '') {
|
|
209
|
+
if (field.required) throw new Error(`missing required argument: ${name}`);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (field.type === 'string') {
|
|
213
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
|
|
214
|
+
if (/[\0\r\n]/.test(value)) throw new Error(`${name} contains forbidden control characters`);
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
if (field.type === 'integer') {
|
|
218
|
+
if (!Number.isInteger(value)) throw new Error(`${name} must be an integer`);
|
|
219
|
+
if (field.minimum !== undefined && value < field.minimum) throw new Error(`${name} must be >= ${field.minimum}`);
|
|
220
|
+
if (field.maximum !== undefined && value > field.maximum) throw new Error(`${name} must be <= ${field.maximum}`);
|
|
221
|
+
return String(value);
|
|
222
|
+
}
|
|
223
|
+
if (field.type === 'boolean') {
|
|
224
|
+
if (typeof value !== 'boolean') throw new Error(`${name} must be a boolean`);
|
|
225
|
+
return String(value);
|
|
226
|
+
}
|
|
227
|
+
if (field.type === 'string-array') {
|
|
228
|
+
if (!Array.isArray(value) || !value.length || value.length > 100) {
|
|
229
|
+
throw new Error(`${name} must be an array containing 1-100 strings`);
|
|
230
|
+
}
|
|
231
|
+
const values = value.map((item) => {
|
|
232
|
+
if (typeof item !== 'string' || !item.trim() || /[\0\r\n,]/.test(item)) {
|
|
233
|
+
throw new Error(`${name} contains an invalid item`);
|
|
234
|
+
}
|
|
235
|
+
return item;
|
|
236
|
+
});
|
|
237
|
+
return values.join(',');
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`unsupported field type for ${name}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildCliInvocation(providerId, toolName, input = {}, options = {}) {
|
|
243
|
+
const provider = CLI_PROVIDERS[providerId];
|
|
244
|
+
if (!provider) throw new Error(`unknown CLI provider: ${providerId}`);
|
|
245
|
+
const tool = provider.tools.find((candidate) => candidate.name === toolName);
|
|
246
|
+
if (!tool) throw new Error(`unknown tool for ${providerId}: ${toolName}`);
|
|
247
|
+
if (!tool.readOnly || tool.command.some((part) => WRITE_VERBS.has(part))) {
|
|
248
|
+
throw new Error(`CLI provider tool is not approved for read-only execution: ${toolName}`);
|
|
249
|
+
}
|
|
250
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
251
|
+
throw new Error('tool arguments must be an object');
|
|
252
|
+
}
|
|
253
|
+
const unknown = Object.keys(input).filter((key) => !Object.hasOwn(tool.fields, key));
|
|
254
|
+
if (unknown.length) throw new Error(`unknown tool arguments: ${unknown.join(', ')}`);
|
|
255
|
+
|
|
256
|
+
const globalArgs = [];
|
|
257
|
+
const commandArgs = [];
|
|
258
|
+
for (const [name, field] of Object.entries(tool.fields)) {
|
|
259
|
+
const value = validateField(name, input[name], field);
|
|
260
|
+
if (value === undefined) continue;
|
|
261
|
+
const target = field.global ? globalArgs : commandArgs;
|
|
262
|
+
target.push(field.flag, value);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
command: assertSafeExecutable(options.executable ?? process.env.DSH_MCP_DINGTALK_DWS_BIN ?? provider.executable),
|
|
266
|
+
args: [...globalArgs, ...tool.command, ...commandArgs, '--format', 'json'],
|
|
267
|
+
provider,
|
|
268
|
+
tool,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function buildCliPreflightInvocation(providerId, options = {}) {
|
|
273
|
+
const provider = CLI_PROVIDERS[providerId];
|
|
274
|
+
if (!provider) throw new Error(`unknown CLI provider: ${providerId}`);
|
|
275
|
+
if (!Array.isArray(provider.preflight) || !provider.preflight.length) return null;
|
|
276
|
+
if (provider.preflight.some((part) => WRITE_VERBS.has(part))) {
|
|
277
|
+
throw new Error(`CLI provider preflight is not approved for read-only execution: ${providerId}`);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
command: assertSafeExecutable(options.executable ?? process.env.DSH_MCP_DINGTALK_DWS_BIN ?? provider.executable),
|
|
281
|
+
args: [...provider.preflight, '--format', 'json'],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function redactCliError(value) {
|
|
286
|
+
return String(value ?? '')
|
|
287
|
+
.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s"']+/gi, '$1[REDACTED]')
|
|
288
|
+
.replace(/(["']?(?:(?:access|refresh)[_-]?token|client[_-]?secret|app[_-]?secret)["']?\s*[=:]\s*["']?)[^\s,"'}]+/gi, '$1[REDACTED]')
|
|
289
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, 'Bearer [REDACTED]')
|
|
290
|
+
.trim();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function runCliProcess(command, args, options = {}) {
|
|
294
|
+
const timeoutMs = Number.isInteger(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
295
|
+
const maxOutputBytes = Number.isInteger(options.maxOutputBytes) ? options.maxOutputBytes : MAX_OUTPUT_BYTES;
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const child = spawn(command, args, {
|
|
298
|
+
shell: false,
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
301
|
+
env: options.env ?? process.env,
|
|
302
|
+
});
|
|
303
|
+
const stdout = [];
|
|
304
|
+
const stderr = [];
|
|
305
|
+
let stdoutBytes = 0;
|
|
306
|
+
let stderrBytes = 0;
|
|
307
|
+
let settled = false;
|
|
308
|
+
const fail = (error) => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
reject(error);
|
|
313
|
+
};
|
|
314
|
+
const timer = setTimeout(() => {
|
|
315
|
+
child.kill('SIGTERM');
|
|
316
|
+
fail(new Error(`CLI command timed out after ${timeoutMs}ms`));
|
|
317
|
+
}, Math.max(1000, Math.min(timeoutMs, 300_000)));
|
|
318
|
+
|
|
319
|
+
child.once('error', (error) => {
|
|
320
|
+
if (error?.code === 'ENOENT') {
|
|
321
|
+
fail(new Error(`未找到官方 CLI 命令“${command}”;请先完成安装和 OAuth 登录,再重启 DSH`));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
fail(error);
|
|
325
|
+
});
|
|
326
|
+
child.stdout.on('data', (chunk) => {
|
|
327
|
+
stdoutBytes += chunk.length;
|
|
328
|
+
if (stdoutBytes > maxOutputBytes) {
|
|
329
|
+
child.kill('SIGTERM');
|
|
330
|
+
fail(new Error(`CLI stdout exceeded ${maxOutputBytes} bytes`));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
stdout.push(chunk);
|
|
334
|
+
});
|
|
335
|
+
child.stderr.on('data', (chunk) => {
|
|
336
|
+
stderrBytes += chunk.length;
|
|
337
|
+
if (stderrBytes <= maxOutputBytes) stderr.push(chunk);
|
|
338
|
+
});
|
|
339
|
+
child.once('close', (code, signal) => {
|
|
340
|
+
if (settled) return;
|
|
341
|
+
settled = true;
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
const output = Buffer.concat(stdout).toString('utf8').trim();
|
|
344
|
+
const errorOutput = redactCliError(Buffer.concat(stderr).toString('utf8'));
|
|
345
|
+
if (code !== 0) {
|
|
346
|
+
const hint = {
|
|
347
|
+
2: '身份认证或授权失败;请运行 dws auth login,并确认企业管理员已开启 CLI 访问',
|
|
348
|
+
3: '参数未通过当前固定 dws 版本校验;请核对工具参数或升级 MCP 连接器目录',
|
|
349
|
+
4: '钉钉要求补充权限授权;请按官方授权提示完成后重试',
|
|
350
|
+
6: 'dws 命令或服务发现失败;请检查网络、登录状态与 MCP 连接器目录版本',
|
|
351
|
+
}[code] ?? '';
|
|
352
|
+
const details = [hint, errorOutput, signal ? `signal=${signal}` : ''].filter(Boolean).join(';');
|
|
353
|
+
reject(new Error(`CLI command failed with exit code ${code}${details ? `: ${details}` : ''}`));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
resolve(output);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function executeCliProviderTool(providerId, toolName, input = {}, options = {}) {
|
|
362
|
+
const invocation = buildCliInvocation(providerId, toolName, input, options);
|
|
363
|
+
const runner = options.runner ?? runCliProcess;
|
|
364
|
+
const output = await runner(invocation.command, invocation.args, options);
|
|
365
|
+
let normalized = redactCliError(output || '{"ok":true}');
|
|
366
|
+
try {
|
|
367
|
+
normalized = JSON.stringify(JSON.parse(normalized), null, 2);
|
|
368
|
+
} catch {
|
|
369
|
+
// Keep non-JSON success output readable; dws normally emits JSON with --format json.
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
content: [{ type: 'text', text: normalized }],
|
|
373
|
+
isError: false,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function ensureCliProviderReady(providerId, options = {}) {
|
|
378
|
+
const invocation = buildCliPreflightInvocation(providerId, options);
|
|
379
|
+
if (!invocation) return;
|
|
380
|
+
const runner = options.runner ?? runCliProcess;
|
|
381
|
+
await runner(invocation.command, invocation.args, options);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function handleCliBridgeRequest(message, options = {}) {
|
|
385
|
+
const providerId = options.providerId ?? 'dingtalk-dws';
|
|
386
|
+
if (!message || typeof message !== 'object') throw new Error('invalid JSON-RPC message');
|
|
387
|
+
if (!Object.hasOwn(message, 'id')) return null;
|
|
388
|
+
const base = { jsonrpc: '2.0', id: message.id };
|
|
389
|
+
try {
|
|
390
|
+
if (message.method === 'initialize') {
|
|
391
|
+
return {
|
|
392
|
+
...base,
|
|
393
|
+
result: {
|
|
394
|
+
protocolVersion: message.params?.protocolVersion ?? '2025-06-18',
|
|
395
|
+
capabilities: { tools: { listChanged: false } },
|
|
396
|
+
serverInfo: { name: `dsh-mcp-cli-bridge/${providerId}`, version: '1.0.0' },
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (message.method === 'ping') return { ...base, result: {} };
|
|
401
|
+
if (message.method === 'tools/list') {
|
|
402
|
+
try {
|
|
403
|
+
const preflight = options.preflight ?? ensureCliProviderReady;
|
|
404
|
+
await preflight(providerId, options);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
...base,
|
|
408
|
+
error: {
|
|
409
|
+
code: -32001,
|
|
410
|
+
message: `CLI Provider 未就绪:${redactCliError(error?.message ?? error)}`,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return { ...base, result: { tools: listCliProviderTools(providerId) } };
|
|
415
|
+
}
|
|
416
|
+
if (message.method === 'tools/call') {
|
|
417
|
+
const name = message.params?.name;
|
|
418
|
+
const args = message.params?.arguments ?? {};
|
|
419
|
+
try {
|
|
420
|
+
const execute = options.execute ?? executeCliProviderTool;
|
|
421
|
+
return { ...base, result: await execute(providerId, name, args, options) };
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return {
|
|
424
|
+
...base,
|
|
425
|
+
result: {
|
|
426
|
+
content: [{ type: 'text', text: redactCliError(error?.message ?? error) }],
|
|
427
|
+
isError: true,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return { ...base, error: { code: -32601, message: `Method not found: ${message.method}` } };
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return { ...base, error: { code: -32603, message: redactCliError(error?.message ?? error) } };
|
|
435
|
+
}
|
|
436
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
4
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",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"./package.json": "./package.json"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"dsh-mcp-connector-probe": "scripts/probe-connector.mjs"
|
|
13
|
+
"dsh-mcp-connector-probe": "scripts/probe-connector.mjs",
|
|
14
|
+
"dsh-mcp-cli-bridge": "scripts/cli-bridge.mjs"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"lib",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"docs/TOOL-GOVERNANCE.md",
|
|
26
27
|
"docs/FIRST-CONTRIBUTION.md",
|
|
27
28
|
"docs/STDIO-SUPPORT.md",
|
|
29
|
+
"docs/CLI-PROVIDERS.md",
|
|
28
30
|
"docs/USER-GUIDE.md",
|
|
29
31
|
"docs/screenshots",
|
|
30
32
|
"cordis.patch.yml",
|
|
@@ -36,12 +38,13 @@
|
|
|
36
38
|
"LICENSE",
|
|
37
39
|
"install.sh",
|
|
38
40
|
"scripts/probe-connector.mjs",
|
|
41
|
+
"scripts/cli-bridge.mjs",
|
|
39
42
|
"scripts/build-registry.mjs",
|
|
40
43
|
"scripts/ui-harness.mjs"
|
|
41
44
|
],
|
|
42
45
|
"scripts": {
|
|
43
46
|
"test": "node --test",
|
|
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",
|
|
47
|
+
"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/cli-providers.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/cli-bridge.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
48
|
"registry:build": "node scripts/build-registry.mjs",
|
|
46
49
|
"registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
|
|
47
50
|
"dev:ui": "node scripts/ui-harness.mjs",
|
package/screenshots.json
CHANGED
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
"docs/screenshots/01-market-overview.jpg",
|
|
3
3
|
"docs/screenshots/02-connector-detail.jpg",
|
|
4
4
|
"docs/screenshots/03-tool-discovery.jpg",
|
|
5
|
-
"docs/screenshots/04-json-import.jpg"
|
|
5
|
+
"docs/screenshots/04-json-import.jpg",
|
|
6
|
+
"docs/screenshots/05-unified-tool-search.png",
|
|
7
|
+
"docs/screenshots/06-tool-parameters.png",
|
|
8
|
+
"docs/screenshots/07-connection-diagnostics.png"
|
|
6
9
|
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { handleCliBridgeRequest } from '../lib/cli-providers.js';
|
|
3
|
+
|
|
4
|
+
function providerFromArgs(argv) {
|
|
5
|
+
const index = argv.indexOf('--provider');
|
|
6
|
+
if (index < 0) return 'dingtalk-dws';
|
|
7
|
+
const value = argv[index + 1];
|
|
8
|
+
if (!value || value.startsWith('-')) throw new Error('--provider requires a value');
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const providerId = providerFromArgs(process.argv.slice(2));
|
|
13
|
+
let buffer = '';
|
|
14
|
+
|
|
15
|
+
function send(message) {
|
|
16
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.stdin.setEncoding('utf8');
|
|
20
|
+
process.stdin.on('data', (chunk) => {
|
|
21
|
+
buffer += chunk;
|
|
22
|
+
while (buffer.includes('\n')) {
|
|
23
|
+
const index = buffer.indexOf('\n');
|
|
24
|
+
const line = buffer.slice(0, index).trim();
|
|
25
|
+
buffer = buffer.slice(index + 1);
|
|
26
|
+
if (!line) continue;
|
|
27
|
+
Promise.resolve()
|
|
28
|
+
.then(() => handleCliBridgeRequest(JSON.parse(line), { providerId }))
|
|
29
|
+
.then((response) => { if (response) send(response); })
|
|
30
|
+
.catch((error) => send({
|
|
31
|
+
jsonrpc: '2.0',
|
|
32
|
+
id: null,
|
|
33
|
+
error: { code: -32700, message: String(error?.message ?? error) },
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
});
|