@xuanmiss-npm/dingtalk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +48 -0
- package/src/channel.ts +386 -0
- package/src/client-registry.ts +27 -0
- package/src/config.ts +185 -0
- package/src/monitor.ts +318 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +429 -0
- package/tsconfig.json +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 萧轩miss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @xuanmiss/dingtalk
|
|
2
|
+
|
|
3
|
+
OpenClaw 钉钉 (DingTalk) 渠道插件 - 支持 Stream 模式接入。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
### 从 npm 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @xuanmiss/dingtalk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 从本地源码安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openclaw plugins install ./extensions/dingtalk
|
|
17
|
+
# 或使用 link 模式(不复制文件,适合开发)
|
|
18
|
+
openclaw plugins install -l ./extensions/dingtalk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 前置条件
|
|
22
|
+
|
|
23
|
+
1. 在 [钉钉开放平台](https://open.dingtalk.com/) 创建一个企业内部应用/机器人
|
|
24
|
+
2. 获取以下凭证:
|
|
25
|
+
- **Client ID** (AppKey)
|
|
26
|
+
- **Client Secret** (AppSecret)
|
|
27
|
+
- **Robot Code** (机器人 Code,可选,默认使用 Client ID)
|
|
28
|
+
|
|
29
|
+
## 配置
|
|
30
|
+
|
|
31
|
+
### 使用环境变量(推荐用于默认账户)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export DINGTALK_CLIENT_ID="your-client-id"
|
|
35
|
+
export DINGTALK_CLIENT_SECRET="your-client-secret"
|
|
36
|
+
export DINGTALK_ROBOT_CODE="your-robot-code" # 可选
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 使用配置文件
|
|
40
|
+
|
|
41
|
+
在 OpenClaw 配置文件中添加:
|
|
42
|
+
|
|
43
|
+
```json5
|
|
44
|
+
{
|
|
45
|
+
channels: {
|
|
46
|
+
dingtalk: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
clientId: "your-client-id",
|
|
49
|
+
clientSecret: "your-client-secret",
|
|
50
|
+
robotCode: "your-robot-code", // 可选
|
|
51
|
+
dmPolicy: "pairing", // "open" | "allowlist" | "pairing"
|
|
52
|
+
groupPolicy: "allowlist", // "open" | "allowlist"
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 消息策略
|
|
59
|
+
|
|
60
|
+
### DM(私聊)策略
|
|
61
|
+
|
|
62
|
+
- `pairing`(默认):未知发送者需要通过配对码验证
|
|
63
|
+
- `allowlist`:仅允许 `allowFrom` 列表中的用户
|
|
64
|
+
- `open`:允许所有用户
|
|
65
|
+
|
|
66
|
+
### Group(群聊)策略
|
|
67
|
+
|
|
68
|
+
- `allowlist`(默认):需要 @机器人 才会响应
|
|
69
|
+
- `open`:响应所有消息(需要 @机器人)
|
|
70
|
+
|
|
71
|
+
## 多账户配置
|
|
72
|
+
|
|
73
|
+
支持配置多个钉钉机器人账户:
|
|
74
|
+
|
|
75
|
+
```json5
|
|
76
|
+
{
|
|
77
|
+
channels: {
|
|
78
|
+
dingtalk: {
|
|
79
|
+
accounts: {
|
|
80
|
+
default: {
|
|
81
|
+
name: "主机器人",
|
|
82
|
+
clientId: "client-id-1",
|
|
83
|
+
clientSecret: "secret-1",
|
|
84
|
+
},
|
|
85
|
+
alerts: {
|
|
86
|
+
name: "告警机器人",
|
|
87
|
+
clientId: "client-id-2",
|
|
88
|
+
clientSecret: "secret-2",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 配对验证
|
|
97
|
+
|
|
98
|
+
默认情况下,新用户需要通过配对验证:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# 查看待验证的配对请求
|
|
102
|
+
openclaw pairing list dingtalk
|
|
103
|
+
|
|
104
|
+
# 批准配对请求
|
|
105
|
+
openclaw pairing approve dingtalk <CODE>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 发送消息
|
|
109
|
+
|
|
110
|
+
通过 CLI 发送消息:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# 发送到用户
|
|
114
|
+
openclaw message send dingtalk user:<userId> "Hello!"
|
|
115
|
+
|
|
116
|
+
# 发送到群聊
|
|
117
|
+
openclaw message send dingtalk <conversationId> "Hello group!"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 故障排查
|
|
121
|
+
|
|
122
|
+
### 常见问题
|
|
123
|
+
|
|
124
|
+
1. **无法连接**
|
|
125
|
+
- 检查 Client ID 和 Client Secret 是否正确
|
|
126
|
+
- 确认机器人已在钉钉开放平台启用
|
|
127
|
+
|
|
128
|
+
2. **群聊无响应**
|
|
129
|
+
- 确保机器人已被添加到群聊
|
|
130
|
+
- 确认消息中 @了机器人
|
|
131
|
+
- 检查 `groupPolicy` 配置
|
|
132
|
+
|
|
133
|
+
3. **私聊无响应**
|
|
134
|
+
- 检查 `dmPolicy` 配置
|
|
135
|
+
- 如果是 `pairing` 模式,确认用户已完成配对验证
|
|
136
|
+
|
|
137
|
+
## 相关链接
|
|
138
|
+
|
|
139
|
+
- [钉钉开放平台](https://open.dingtalk.com/)
|
|
140
|
+
- [OpenClaw 文档](https://openclaw.dev/)
|
|
141
|
+
- [GitHub 仓库](https://github.com/xuanmiss/openclaw-dingtalk)
|
|
142
|
+
- [问题反馈](https://github.com/xuanmiss/openclaw-dingtalk/issues)
|
|
143
|
+
|
|
144
|
+
## 许可证
|
|
145
|
+
|
|
146
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { dingtalkPlugin } from "./src/channel.js";
|
|
4
|
+
import { setDingtalkRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "dingtalk",
|
|
8
|
+
name: "DingTalk",
|
|
9
|
+
description: "DingTalk (钉钉) channel plugin - Stream mode",
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api: OpenClawPluginApi) {
|
|
12
|
+
setDingtalkRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: dingtalkPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xuanmiss-npm/dingtalk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw DingTalk (钉钉) channel plugin - Stream mode",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "xuanmiss",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/xuanmiss/openclaw-dingtalk.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/xuanmiss/openclaw-dingtalk#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/xuanmiss/openclaw-dingtalk/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openclaw",
|
|
18
|
+
"dingtalk",
|
|
19
|
+
"钉钉",
|
|
20
|
+
"chatbot",
|
|
21
|
+
"plugin"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"dingtalk-stream": "^2.1.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"openclaw": "2026.1.29"
|
|
28
|
+
},
|
|
29
|
+
"openclaw": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts"
|
|
32
|
+
],
|
|
33
|
+
"channel": {
|
|
34
|
+
"id": "dingtalk",
|
|
35
|
+
"label": "DingTalk",
|
|
36
|
+
"selectionLabel": "DingTalk (钉钉 Stream)",
|
|
37
|
+
"docsPath": "/channels/dingtalk",
|
|
38
|
+
"docsLabel": "dingtalk",
|
|
39
|
+
"blurb": "企业级即时通讯平台钉钉,支持Stream模式接入。",
|
|
40
|
+
"order": 20
|
|
41
|
+
},
|
|
42
|
+
"install": {
|
|
43
|
+
"npmSpec": "@xuanmiss/dingtalk",
|
|
44
|
+
"localPath": "extensions/dingtalk",
|
|
45
|
+
"defaultChoice": "npm"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { ChannelPlugin, ChannelMessageActionAdapter } from "openclaw/plugin-sdk";
|
|
2
|
+
import {
|
|
3
|
+
buildChannelConfigSchema,
|
|
4
|
+
formatPairingApproveHint,
|
|
5
|
+
DmPolicySchema,
|
|
6
|
+
GroupPolicySchema,
|
|
7
|
+
} from "openclaw/plugin-sdk";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import {
|
|
10
|
+
resolveDingtalkAccount,
|
|
11
|
+
listDingtalkAccountIds,
|
|
12
|
+
resolveDefaultDingtalkAccountId,
|
|
13
|
+
DEFAULT_ACCOUNT_ID,
|
|
14
|
+
type ResolvedDingtalkAccount,
|
|
15
|
+
} from "./config.js";
|
|
16
|
+
import { sendMessageDingtalk } from "./send.js";
|
|
17
|
+
import { monitorDingtalkProvider, probeDingtalk, createOpenClawMessageHandler } from "./monitor.js";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// 配置Schema (使用 Zod)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const DingtalkGroupSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
requireMention: z.boolean().optional(),
|
|
26
|
+
allowFrom: z.array(z.string()).optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
const DingtalkAccountSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
enabled: z.boolean().optional(),
|
|
33
|
+
name: z.string().optional(),
|
|
34
|
+
clientId: z.string().optional(),
|
|
35
|
+
clientSecret: z.string().optional(),
|
|
36
|
+
robotCode: z.string().optional(),
|
|
37
|
+
dmPolicy: DmPolicySchema.optional(),
|
|
38
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
|
|
42
|
+
export const DingtalkConfigSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
enabled: z.boolean().optional(),
|
|
45
|
+
name: z.string().optional(),
|
|
46
|
+
clientId: z.string().optional(),
|
|
47
|
+
clientSecret: z.string().optional(),
|
|
48
|
+
robotCode: z.string().optional(),
|
|
49
|
+
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
50
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
51
|
+
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
52
|
+
groups: z.record(z.string(), DingtalkGroupSchema.optional()).optional(),
|
|
53
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
54
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
55
|
+
blockStreaming: z.boolean().optional(),
|
|
56
|
+
accounts: z.record(z.string(), DingtalkAccountSchema.optional()).optional(),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// 渠道元数据
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
const meta = {
|
|
66
|
+
id: "dingtalk" as const,
|
|
67
|
+
label: "DingTalk",
|
|
68
|
+
selectionLabel: "DingTalk (钉钉 Stream)",
|
|
69
|
+
detailLabel: "钉钉机器人",
|
|
70
|
+
docsPath: "/channels/dingtalk",
|
|
71
|
+
docsLabel: "dingtalk",
|
|
72
|
+
blurb: "企业级即时通讯平台,支持Stream模式接入。",
|
|
73
|
+
systemImage: "message.badge.filled.fill",
|
|
74
|
+
order: 20,
|
|
75
|
+
aliases: ["dd", "dingding", "ding"],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// 消息动作适配器
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
const dingtalkMessageActions: ChannelMessageActionAdapter = {
|
|
83
|
+
listActions: () => ["send"],
|
|
84
|
+
|
|
85
|
+
extractToolSend: ({ args }) => {
|
|
86
|
+
const to = args.to || args.target || args.conversationId || args.userId;
|
|
87
|
+
return to ? { to: String(to) } : null;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
handleAction: async (ctx) => {
|
|
91
|
+
if (ctx.action === "send") {
|
|
92
|
+
const message = String(ctx.params.message || ctx.params.text || "");
|
|
93
|
+
const to = String(ctx.params.to || ctx.params.conversationId || ctx.params.userId || "");
|
|
94
|
+
|
|
95
|
+
if (!to) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: "Error: target (to/conversationId/userId) required" }],
|
|
98
|
+
details: { error: "missing_target" },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!message) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: "Error: message required" }],
|
|
105
|
+
details: { error: "missing_message" },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const atUsersRaw = ctx.params.atUsers || ctx.params.atUserIds || ctx.params.mentions;
|
|
110
|
+
const atUsers = Array.isArray(atUsersRaw)
|
|
111
|
+
? atUsersRaw.map(String)
|
|
112
|
+
: typeof atUsersRaw === "string"
|
|
113
|
+
? atUsersRaw.split(",").map(s => s.trim())
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
116
|
+
const result = await sendMessageDingtalk(to, message, {
|
|
117
|
+
cfg: ctx.cfg,
|
|
118
|
+
accountId: ctx.accountId ?? undefined,
|
|
119
|
+
atUsers,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
124
|
+
details: result,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: `Unsupported action: ${ctx.action}` }],
|
|
130
|
+
details: { error: "unsupported_action", action: ctx.action },
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// 主渠道插件定义
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
|
|
140
|
+
id: "dingtalk",
|
|
141
|
+
meta,
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// 能力声明
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
capabilities: {
|
|
147
|
+
chatTypes: ["direct", "group"],
|
|
148
|
+
reactions: false, // 钉钉暂不支持reaction
|
|
149
|
+
threads: false, // 钉钉暂不支持线程
|
|
150
|
+
media: true, // 支持媒体消息
|
|
151
|
+
nativeCommands: false,
|
|
152
|
+
blockStreaming: true,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// 配置热重载
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
reload: { configPrefixes: ["channels.dingtalk"] },
|
|
159
|
+
configSchema: buildChannelConfigSchema(DingtalkConfigSchema),
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// 配置适配器
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
config: {
|
|
165
|
+
listAccountIds: (cfg) => listDingtalkAccountIds(cfg),
|
|
166
|
+
resolveAccount: (cfg, accountId) => resolveDingtalkAccount({ cfg, accountId }),
|
|
167
|
+
defaultAccountId: (cfg) => resolveDefaultDingtalkAccountId(cfg),
|
|
168
|
+
|
|
169
|
+
isConfigured: (account) => Boolean(account.clientId?.trim() && account.clientSecret?.trim()),
|
|
170
|
+
|
|
171
|
+
describeAccount: (account) => ({
|
|
172
|
+
accountId: account.accountId,
|
|
173
|
+
name: account.name,
|
|
174
|
+
enabled: account.enabled,
|
|
175
|
+
configured: Boolean(account.clientId?.trim() && account.clientSecret?.trim()),
|
|
176
|
+
tokenSource: account.tokenSource,
|
|
177
|
+
}),
|
|
178
|
+
|
|
179
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
180
|
+
(resolveDingtalkAccount({ cfg, accountId }).config.allowFrom ?? []).map(String),
|
|
181
|
+
|
|
182
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
183
|
+
allowFrom
|
|
184
|
+
.map((entry) => String(entry).trim())
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.map((entry) => entry.replace(/^dingtalk:/i, ""))
|
|
187
|
+
.map((entry) => entry.toLowerCase()),
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// 安全适配器
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
security: {
|
|
194
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
195
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
196
|
+
const dingtalkConfig = (cfg.channels as Record<string, unknown> | undefined)?.dingtalk as
|
|
197
|
+
| Record<string, unknown>
|
|
198
|
+
| undefined;
|
|
199
|
+
const useAccountPath = Boolean(
|
|
200
|
+
(dingtalkConfig?.accounts as Record<string, unknown> | undefined)?.[resolvedAccountId],
|
|
201
|
+
);
|
|
202
|
+
const basePath = useAccountPath
|
|
203
|
+
? `channels.dingtalk.accounts.${resolvedAccountId}.`
|
|
204
|
+
: "channels.dingtalk.";
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
policy: account.config.dmPolicy ?? "pairing",
|
|
208
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
209
|
+
policyPath: `${basePath}dmPolicy`,
|
|
210
|
+
allowFromPath: basePath,
|
|
211
|
+
approveHint: formatPairingApproveHint("dingtalk"),
|
|
212
|
+
normalizeEntry: (raw: string) => raw.replace(/^dingtalk:/i, ""),
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
collectWarnings: ({ account, cfg }) => {
|
|
217
|
+
const warnings: string[] = [];
|
|
218
|
+
const defaultGroupPolicy = (
|
|
219
|
+
cfg.channels as { defaults?: { groupPolicy?: string } } | undefined
|
|
220
|
+
)?.defaults?.groupPolicy;
|
|
221
|
+
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
|
|
222
|
+
|
|
223
|
+
if (groupPolicy === "open") {
|
|
224
|
+
warnings.push(
|
|
225
|
+
`- DingTalk groups: groupPolicy="open" allows any group member to trigger the bot (mention-gated). ` +
|
|
226
|
+
`Set channels.dingtalk.groupPolicy="allowlist" and configure channels.dingtalk.groups.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return warnings;
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// 消息适配器
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
messaging: {
|
|
238
|
+
normalizeTarget: (raw) => {
|
|
239
|
+
const trimmed = raw.trim();
|
|
240
|
+
if (!trimmed) return undefined;
|
|
241
|
+
// 支持 dingtalk:userId 格式
|
|
242
|
+
return trimmed.replace(/^dingtalk:/i, "");
|
|
243
|
+
},
|
|
244
|
+
targetResolver: {
|
|
245
|
+
looksLikeId: (raw) => {
|
|
246
|
+
const trimmed = raw.trim();
|
|
247
|
+
// 钉钉用户ID格式检测
|
|
248
|
+
return /^[a-zA-Z0-9]{10,}$/.test(trimmed) || trimmed.startsWith("cid");
|
|
249
|
+
},
|
|
250
|
+
hint: "<userId|conversationId>",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// 配对适配器
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
pairing: {
|
|
258
|
+
idLabel: "dingtalkUserId",
|
|
259
|
+
normalizeAllowEntry: (entry) => entry.replace(/^dingtalk:/i, ""),
|
|
260
|
+
notifyApproval: async ({ id }) => {
|
|
261
|
+
await sendMessageDingtalk(id, "✅ 你已被授权与 OpenClaw 对话!", {});
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// 消息动作
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
actions: dingtalkMessageActions,
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// 出站消息适配器
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
outbound: {
|
|
274
|
+
deliveryMode: "direct",
|
|
275
|
+
chunker: null,
|
|
276
|
+
textChunkLimit: 4000,
|
|
277
|
+
|
|
278
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
279
|
+
const result = await sendMessageDingtalk(to, text, {
|
|
280
|
+
cfg,
|
|
281
|
+
accountId: accountId ?? undefined,
|
|
282
|
+
});
|
|
283
|
+
return { channel: "dingtalk", ...result };
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
287
|
+
// 钉钉的媒体消息需要特殊处理
|
|
288
|
+
// 这里简化为发送文本+链接
|
|
289
|
+
const messageWithMedia = mediaUrl ? `${text}\n\n📎 ${mediaUrl}` : text;
|
|
290
|
+
const result = await sendMessageDingtalk(to, messageWithMedia, {
|
|
291
|
+
cfg,
|
|
292
|
+
accountId: accountId ?? undefined,
|
|
293
|
+
});
|
|
294
|
+
return { channel: "dingtalk", ...result };
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// 状态适配器
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
status: {
|
|
302
|
+
defaultRuntime: {
|
|
303
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
304
|
+
running: false,
|
|
305
|
+
lastStartAt: null,
|
|
306
|
+
lastStopAt: null,
|
|
307
|
+
lastError: null,
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
probeAccount: async ({ account, timeoutMs }) =>
|
|
311
|
+
probeDingtalk(account.clientId, account.clientSecret, timeoutMs),
|
|
312
|
+
|
|
313
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
314
|
+
accountId: account.accountId,
|
|
315
|
+
name: account.name,
|
|
316
|
+
enabled: account.enabled,
|
|
317
|
+
configured: Boolean(account.clientId?.trim() && account.clientSecret?.trim()),
|
|
318
|
+
tokenSource: account.tokenSource,
|
|
319
|
+
running: runtime?.running ?? false,
|
|
320
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
321
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
322
|
+
lastError: runtime?.lastError ?? null,
|
|
323
|
+
probe,
|
|
324
|
+
}),
|
|
325
|
+
|
|
326
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
327
|
+
configured: snapshot.configured ?? false,
|
|
328
|
+
tokenSource: snapshot.tokenSource ?? "none",
|
|
329
|
+
running: snapshot.running ?? false,
|
|
330
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
331
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
332
|
+
lastError: snapshot.lastError ?? null,
|
|
333
|
+
probe: snapshot.probe,
|
|
334
|
+
}),
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Gateway适配器
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
gateway: {
|
|
341
|
+
startAccount: async (ctx) => {
|
|
342
|
+
const account = ctx.account;
|
|
343
|
+
|
|
344
|
+
// 先探测验证凭证
|
|
345
|
+
let probeLabel = "";
|
|
346
|
+
try {
|
|
347
|
+
const probe = await probeDingtalk(account.clientId, account.clientSecret, 3000);
|
|
348
|
+
if (probe.ok) {
|
|
349
|
+
probeLabel = ` (${account.robotCode || account.clientId})`;
|
|
350
|
+
} else {
|
|
351
|
+
ctx.log?.warn?.(`[${account.accountId}] Probe failed: ${probe.error}`);
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
ctx.log?.debug?.(`[${account.accountId}] Probe error: ${String(err)}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
ctx.log?.info(`[${account.accountId}] Starting DingTalk Stream provider${probeLabel}`);
|
|
358
|
+
|
|
359
|
+
const onMessage = createOpenClawMessageHandler({
|
|
360
|
+
accountId: account.accountId,
|
|
361
|
+
config: ctx.cfg,
|
|
362
|
+
runtime: ctx.runtime,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return monitorDingtalkProvider({
|
|
366
|
+
clientId: account.clientId,
|
|
367
|
+
clientSecret: account.clientSecret,
|
|
368
|
+
robotCode: account.robotCode,
|
|
369
|
+
accountId: account.accountId,
|
|
370
|
+
config: ctx.cfg,
|
|
371
|
+
runtime: ctx.runtime,
|
|
372
|
+
abortSignal: ctx.abortSignal,
|
|
373
|
+
onMessage,
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// 目录适配器
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
directory: {
|
|
382
|
+
self: async () => null,
|
|
383
|
+
listPeers: async () => [],
|
|
384
|
+
listGroups: async () => [],
|
|
385
|
+
},
|
|
386
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DWClient } from "dingtalk-stream";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 存放活跃的钉钉客户端实例
|
|
5
|
+
*/
|
|
6
|
+
const activeClients = new Map<string, DWClient>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 注册客户端
|
|
10
|
+
*/
|
|
11
|
+
export function registerDingtalkClient(accountId: string, client: DWClient) {
|
|
12
|
+
activeClients.set(accountId, client);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 获取客户端
|
|
17
|
+
*/
|
|
18
|
+
export function getDingtalkClient(accountId: string): DWClient | undefined {
|
|
19
|
+
return activeClients.get(accountId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 移除客户端
|
|
24
|
+
*/
|
|
25
|
+
export function unregisterDingtalkClient(accountId: string) {
|
|
26
|
+
activeClients.delete(accountId);
|
|
27
|
+
}
|