dsh-mcp-connector 0.2.47 → 0.2.49
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 +25 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/CLI-PROVIDERS.md +58 -0
- package/docs/STDIO-SUPPORT.md +4 -4
- package/lib/cli-providers.js +459 -0
- package/lib/mcp-provision.js +3 -1
- package/package.json +6 -3
- package/scripts/cli-bridge.mjs +36 -0
- package/ui/index.html +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.49] - 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- 修复钉钉 CLI 状态查询返回 `success: true, authenticated: false` 时误显示已连接:枚举工具前严格检查授权布尔值,异常响应安全拒绝,并提示官方 OAuth 登录命令。
|
|
12
|
+
- 重建运行配置时精确升级旧市场钉钉桥接版本,保留用户自定义命令与凭据。
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- 卡片、详情和连接入口显示提供方、授权方式、运行位置和权限提示;连接失败在弹窗内保留原因。
|
|
17
|
+
- 增加 0.2.50 授权设计及 WPS / 腾讯会议接入验证清单,尚不代表完成真实租户验收。
|
|
18
|
+
|
|
19
|
+
## [0.2.48] - 2026-09-15
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- 新增受控 CLI→MCP Provider 框架,首批把钉钉官方 `dws` 的通讯录、日程、待办、文档、OA 待审批和日志查询映射为 11 个只读 MCP 工具。
|
|
24
|
+
- 连接时通过官方 `dws auth status` 验证 OAuth 就绪状态,未登录、无权限或 Token 无法刷新时不误报“已连接”。
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- CLI 只能执行代码审核的命令与参数,不经 Shell;写入动词、未声明参数和控制字符均拒绝。子进程超时、输出大小受限,成功及失败输出中的 Token/Secret 都在返回 Host 前脱敏。
|
|
29
|
+
|
|
7
30
|
## [0.2.47] - 2026-09-14
|
|
8
31
|
|
|
9
32
|
### Changed
|
|
@@ -717,7 +740,8 @@
|
|
|
717
740
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
718
741
|
- iframe 消息校验同源和消息来源。
|
|
719
742
|
|
|
720
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
743
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.48...HEAD
|
|
744
|
+
[0.2.48]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.47...v0.2.48
|
|
721
745
|
[0.2.47]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.46...v0.2.47
|
|
722
746
|
[0.2.46]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.45...v0.2.46
|
|
723
747
|
[0.2.45]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.44...v0.2.45
|
package/README.en.md
CHANGED
|
@@ -190,7 +190,7 @@ npm run dev:ui
|
|
|
190
190
|
|
|
191
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.
|
|
192
192
|
|
|
193
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
193
|
+
The current public version is [`dsh-mcp-connector@0.2.49`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.49](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.49).
|
|
194
194
|
|
|
195
195
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
196
196
|
|
|
@@ -202,6 +202,7 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](d
|
|
|
202
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.
|
|
203
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.
|
|
204
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.
|
|
205
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.
|
|
206
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.
|
|
207
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
|
@@ -185,7 +185,7 @@ npm run dev:ui
|
|
|
185
185
|
|
|
186
186
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
187
187
|
|
|
188
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
188
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.49`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.49](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.49)。
|
|
189
189
|
|
|
190
190
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
191
191
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
|
@@ -201,6 +201,7 @@ stdio 传输的架构、透传边界与安全约束见 [docs/STDIO-SUPPORT.md](d
|
|
|
201
201
|
- 外部 URL 默认仅允许 HTTPS,HTTP 默认仅允许回环地址。用户自建连接可在明确风险确认后放行 RFC1918 IPv4 / RFC4193 IPv6 ULA 字面量;域名、公网 HTTP、链路本地与云元数据地址仍拒绝。
|
|
202
202
|
- 远程目录/描述响应限制 2 MiB,Web API 请求限制 1 MiB;原始 JSON 在归一化前扫描凭据字段。
|
|
203
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 仅开放只读能力。
|
|
204
205
|
- stdio 会启动本机进程:仅导入或连接可信命令/软件包。市场目录只能用 `credentialFields` + `credentialBindings` 声明输入与 env 映射,不得携带真实 token/secret;用户填写值只写入本机连接记录并交给 Host。
|
|
205
206
|
- OAuth DCR 的 `client_secret` 与 Access/Refresh Token 采用相同的本机存储边界,不会进入市场 API、状态输出或日志。
|
|
206
207
|
- OAuth 连接失败会标明资源发现、服务发现、客户端注册、授权回调或 Token 换取阶段;DCR HTTP 403 表示服务商拒绝未准入客户端,不是用户未点击授权。
|
|
@@ -0,0 +1,58 @@
|
|
|
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` 预检;只有 JSON 明确返回 `success: true` 且 `authenticated: true` 才列出工具。退出码为 0 不代表已登录;未授权或响应异常时拒绝连接并提供登录提示。预检不代表全部业务权限已获批,具体工具仍由官方 CLI 校验权限。
|
|
13
|
+
- 首次登录请在运行 DSH 的同一电脑、同一系统用户下执行 `npx -y dingtalk-workspace-cli@1.0.61 auth login`(无浏览器环境加 `--device`),完成后重新连接。不会自动替用户登录或读取聊天记录。
|
|
14
|
+
- Authentication readiness requires both `success === true` and `authenticated === true`; a successful status query alone is insufficient. Invalid responses fail closed without exposing identity or tokens. Run the login command above as the same OS user running DSH, then reconnect. Per-tool permissions are still enforced by the official CLI.
|
|
15
|
+
- 登录、Token 刷新、企业管理员授权、租户隔离和 API 审计由官方 CLI 与开放平台负责。
|
|
16
|
+
|
|
17
|
+
## 钉钉前置条件
|
|
18
|
+
|
|
19
|
+
1. 市场卡片固定使用已审核的 `dingtalk-workspace-cli@1.0.61`,无需全局安装 `dws`。
|
|
20
|
+
2. 首次连接前,在普通终端执行 `npx -y dingtalk-workspace-cli@1.0.61 auth login`;无浏览器环境加 `--device`。
|
|
21
|
+
3. 企业管理员需在钉钉开发者平台开启 CLI 访问。
|
|
22
|
+
4. 多组织用户可在工具参数里传稳定的 `corpId:userId` profile;不传时使用当前 profile。
|
|
23
|
+
|
|
24
|
+
授权信息保存在钉钉官方 CLI 的用户配置和系统 Keychain 中,不写入 Connector 配置。后续升级 `dws` 需先复核官方命令、权限和输出 Schema,然后通过 Registry PR 更换固定版本。
|
|
25
|
+
|
|
26
|
+
启动 MCP 桥接:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"dingtalk-workspace": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": [
|
|
34
|
+
"-y",
|
|
35
|
+
"--legacy-peer-deps",
|
|
36
|
+
"--package",
|
|
37
|
+
"dsh-mcp-connector@0.2.48",
|
|
38
|
+
"--package",
|
|
39
|
+
"dingtalk-workspace-cli@1.0.61",
|
|
40
|
+
"dsh-mcp-cli-bridge",
|
|
41
|
+
"--provider",
|
|
42
|
+
"dingtalk-dws"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 首批只读能力
|
|
50
|
+
|
|
51
|
+
- 当前用户、通讯录搜索与用户详情
|
|
52
|
+
- 日程列表与日程详情
|
|
53
|
+
- 待办列表与待办详情
|
|
54
|
+
- 文档搜索
|
|
55
|
+
- 待处理 OA 审批列表
|
|
56
|
+
- 收到的日志和日志模板
|
|
57
|
+
|
|
58
|
+
后续增加 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
|
|
|
@@ -0,0 +1,459 @@
|
|
|
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
|
+
const output = await runner(invocation.command, invocation.args, options);
|
|
382
|
+
// Exit code 0 / success:true means the status query succeeded, not that
|
|
383
|
+
// the user is authenticated. Never expose the raw status (identity/tokens).
|
|
384
|
+
let status;
|
|
385
|
+
try {
|
|
386
|
+
status = JSON.parse(output);
|
|
387
|
+
} catch {
|
|
388
|
+
throw new Error('无法验证钉钉 OAuth 状态:官方 CLI 未返回有效 JSON;请检查 CLI 版本后重试');
|
|
389
|
+
}
|
|
390
|
+
if (!status || typeof status !== 'object' || Array.isArray(status)
|
|
391
|
+
|| status.success !== true || status.authenticated !== true) {
|
|
392
|
+
throw new Error('钉钉 OAuth 未就绪:请先在运行 DSH 的同一电脑、同一系统用户下执行 npx -y dingtalk-workspace-cli@1.0.61 auth login(无浏览器环境加 --device),完成授权后重新连接;如仍失败,请确认企业管理员已开启 CLI 访问');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Upgrade only the exact managed legacy invocation; preserve custom commands,
|
|
397
|
+
// environment, cwd and credentials. Applied on every provision, including restore.
|
|
398
|
+
export function cliBridgeArgs(record) {
|
|
399
|
+
const legacy = ['--yes', '--legacy-peer-deps', '--package', 'dsh-mcp-connector@0.2.48',
|
|
400
|
+
'--package', 'dingtalk-workspace-cli@1.0.61', 'dsh-mcp-cli-bridge', '--provider', 'dingtalk-dws'];
|
|
401
|
+
const args = record.args ?? [];
|
|
402
|
+
if (record.connectorId !== 'dingtalk' || record.command !== 'npx'
|
|
403
|
+
|| JSON.stringify(args) !== JSON.stringify(legacy)) return args;
|
|
404
|
+
return args.map((arg) => arg === 'dsh-mcp-connector@0.2.48' ? 'dsh-mcp-connector@0.2.49' : arg);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function handleCliBridgeRequest(message, options = {}) {
|
|
408
|
+
const providerId = options.providerId ?? 'dingtalk-dws';
|
|
409
|
+
if (!message || typeof message !== 'object') throw new Error('invalid JSON-RPC message');
|
|
410
|
+
if (!Object.hasOwn(message, 'id')) return null;
|
|
411
|
+
const base = { jsonrpc: '2.0', id: message.id };
|
|
412
|
+
try {
|
|
413
|
+
if (message.method === 'initialize') {
|
|
414
|
+
return {
|
|
415
|
+
...base,
|
|
416
|
+
result: {
|
|
417
|
+
protocolVersion: message.params?.protocolVersion ?? '2025-06-18',
|
|
418
|
+
capabilities: { tools: { listChanged: false } },
|
|
419
|
+
serverInfo: { name: `dsh-mcp-cli-bridge/${providerId}`, version: '1.0.0' },
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (message.method === 'ping') return { ...base, result: {} };
|
|
424
|
+
if (message.method === 'tools/list') {
|
|
425
|
+
try {
|
|
426
|
+
const preflight = options.preflight ?? ensureCliProviderReady;
|
|
427
|
+
await preflight(providerId, options);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return {
|
|
430
|
+
...base,
|
|
431
|
+
error: {
|
|
432
|
+
code: -32001,
|
|
433
|
+
message: `CLI Provider 未就绪:${redactCliError(error?.message ?? error)}`,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return { ...base, result: { tools: listCliProviderTools(providerId) } };
|
|
438
|
+
}
|
|
439
|
+
if (message.method === 'tools/call') {
|
|
440
|
+
const name = message.params?.name;
|
|
441
|
+
const args = message.params?.arguments ?? {};
|
|
442
|
+
try {
|
|
443
|
+
const execute = options.execute ?? executeCliProviderTool;
|
|
444
|
+
return { ...base, result: await execute(providerId, name, args, options) };
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return {
|
|
447
|
+
...base,
|
|
448
|
+
result: {
|
|
449
|
+
content: [{ type: 'text', text: redactCliError(error?.message ?? error) }],
|
|
450
|
+
isError: true,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return { ...base, error: { code: -32601, message: `Method not found: ${message.method}` } };
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return { ...base, error: { code: -32603, message: redactCliError(error?.message ?? error) } };
|
|
458
|
+
}
|
|
459
|
+
}
|
package/lib/mcp-provision.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* entryId = {entryPrefix}-{record.key};serverName 决定工具名前缀,生命周期内不变 → 幂等。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { cliBridgeArgs } from './cli-providers.js';
|
|
7
|
+
|
|
6
8
|
export function entryIdFor(config, record) {
|
|
7
9
|
return `${config.entryPrefix}-${record.key}`;
|
|
8
10
|
}
|
|
@@ -35,7 +37,7 @@ export function buildEntryConfig(record, grants, { failOnStartupError = false }
|
|
|
35
37
|
transport: 'stdio',
|
|
36
38
|
serverName: record.serverName,
|
|
37
39
|
command: record.command,
|
|
38
|
-
args: record
|
|
40
|
+
args: cliBridgeArgs(record),
|
|
39
41
|
env,
|
|
40
42
|
cwd: record.cwd || process.cwd(),
|
|
41
43
|
failOnStartupError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
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",
|
|
@@ -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
|
+
});
|
package/ui/index.html
CHANGED
|
@@ -1155,6 +1155,15 @@
|
|
|
1155
1155
|
}
|
|
1156
1156
|
}
|
|
1157
1157
|
|
|
1158
|
+
function connectionLabels(d) {
|
|
1159
|
+
const local = (d.servers || []).some((s) => s.transport === 'stdio');
|
|
1160
|
+
const auth = d.id === 'dingtalk' ? '官方 CLI OAuth'
|
|
1161
|
+
: ({ 'oauth2-pkce': 'OAuth 授权', bearer: 'Bearer 凭据', 'api-key': 'API Key' }[d.authMode] || '按服务要求授权');
|
|
1162
|
+
return [d.vendor ? `提供方:${d.vendor}` : '提供方:待核实', auth,
|
|
1163
|
+
local ? '运行于 DSH 主机' : '远程服务',
|
|
1164
|
+
d.id === 'dingtalk' ? '当前桥接仅提供只读工具' : '权限以授权页和工具策略为准'];
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1158
1167
|
function cardHtml(d) {
|
|
1159
1168
|
const icon = isImageIcon(d.icon)
|
|
1160
1169
|
? `<img src="${esc(d.icon)}" alt="" onerror="this.parentElement.innerHTML='🔌';" />`
|
|
@@ -1166,6 +1175,7 @@
|
|
|
1166
1175
|
<div class="content">
|
|
1167
1176
|
<div class="name">${esc(d.name)} ${probe}</div>
|
|
1168
1177
|
<div class="summary">${esc(d.summary || '')}</div>
|
|
1178
|
+
<div class="hint">${connectionLabels(d).map(esc).join(' · ')}</div>
|
|
1169
1179
|
</div>
|
|
1170
1180
|
<div class="actions">${actions}</div>
|
|
1171
1181
|
</div>`;
|
|
@@ -1408,6 +1418,7 @@
|
|
|
1408
1418
|
if (!connector) return;
|
|
1409
1419
|
openModal(`连接 ${connector.name}`, `
|
|
1410
1420
|
<input id="scope-connector-id" type="hidden" value="${esc(connectorId)}" />
|
|
1421
|
+
<div class="form-note">${connectionLabels(connector).map(esc).join(' · ')}${connector.credentialDescription ? `<p>${esc(connector.credentialDescription)}</p>` : ''}${connectorId === 'dingtalk' ? '<p>尚未登录?请在运行 DSH 的同一系统用户终端执行 <code>npx -y dingtalk-workspace-cli@1.0.61 auth login</code>,完成后点击确认连接。本版本不会自动发起登录;工具注册不代表已获得全部业务权限。</p>' : ''}</div>
|
|
1411
1422
|
<div class="form-note"><strong>选择连接影响范围</strong>连接配置与凭据仍只保存一份;项目范围会由 Host 在工具可见性和执行层强制隔离。</div>
|
|
1412
1423
|
${scopeFieldHtml()}
|
|
1413
1424
|
<div class="inline-error" id="modal-error" tabindex="-1"></div>
|
|
@@ -1571,6 +1582,7 @@
|
|
|
1571
1582
|
const r = await call('connect', { connectorId, ...scopeParams }, { timeoutMs });
|
|
1572
1583
|
setModalBusy(false);
|
|
1573
1584
|
showToast(r.message, r.ok);
|
|
1585
|
+
if (!r.ok) showModalError(r.message);
|
|
1574
1586
|
if (r.ok) {
|
|
1575
1587
|
markCommunityCtaEligible();
|
|
1576
1588
|
closeModal();
|
|
@@ -1881,6 +1893,7 @@
|
|
|
1881
1893
|
: (d.connectionLabel || (d.connected?.length ? '已配置' : '未连接'));
|
|
1882
1894
|
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${initialStateLabel} ${d.connected?.length || 0} 个服务` : `${initialStateLabel} ${d.connected?.length || 0} 个服务`;
|
|
1883
1895
|
$('#detail-desc').textContent = d.description || d.summary || '';
|
|
1896
|
+
$('#detail-desc').textContent += '\n' + connectionLabels(d).join(' · ');
|
|
1884
1897
|
detailPrompts = (d.prompts || []).filter((prompt) => prompt?.text || prompt?.title);
|
|
1885
1898
|
promptPage = 0;
|
|
1886
1899
|
renderPromptPage();
|