@huo15/dingtalk-connector-pro 1.0.4 → 1.0.7
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/README.en.md +106 -384
- package/README.md +14 -18
- package/dist/index.js +17 -0
- package/dist/openclaw.plugin.json +498 -0
- package/dist/package.json +91 -0
- package/dist/src/channel.js +415 -0
- package/dist/src/config/accounts.js +182 -0
- package/dist/src/config/schema.js +135 -0
- package/dist/src/core/connection.js +561 -0
- package/dist/src/core/message-handler.js +1422 -0
- package/dist/src/core/provider.js +59 -0
- package/dist/src/core/state.js +49 -0
- package/dist/src/directory.js +53 -0
- package/dist/src/docs.js +209 -0
- package/dist/src/gateway-methods.js +360 -0
- package/dist/src/onboarding.js +337 -0
- package/dist/src/policy.js +15 -0
- package/dist/src/probe.js +144 -0
- package/dist/src/reply-dispatcher.js +435 -0
- package/dist/src/runtime.js +26 -0
- package/dist/src/sdk/helpers.js +237 -0
- package/dist/src/sdk/types.js +13 -0
- package/dist/src/secret-input.js +13 -0
- package/dist/src/services/media/audio.js +40 -0
- package/dist/src/services/media/chunk-upload.js +211 -0
- package/dist/src/services/media/common.js +120 -0
- package/dist/src/services/media/file.js +54 -0
- package/dist/src/services/media/image.js +59 -0
- package/dist/src/services/media/index.js +9 -0
- package/dist/src/services/media/video.js +133 -0
- package/dist/src/services/media.js +889 -0
- package/dist/src/services/messaging/card.js +234 -0
- package/dist/src/services/messaging/index.js +8 -0
- package/dist/src/services/messaging/send.js +85 -0
- package/dist/src/services/messaging.js +680 -0
- package/dist/src/targets.js +38 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/agent.js +55 -0
- package/dist/src/utils/async.js +40 -0
- package/dist/src/utils/constants.js +24 -0
- package/dist/src/utils/http-client.js +33 -0
- package/dist/src/utils/index.js +7 -0
- package/dist/src/utils/logger.js +76 -0
- package/dist/src/utils/session.js +95 -0
- package/dist/src/utils/token.js +71 -0
- package/dist/src/utils/utils-legacy.js +393 -0
- package/index.ts +3 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +16 -5
- package/src/channel.js +415 -0
- package/src/channel.ts +12 -12
- package/src/config/accounts.js +182 -0
- package/src/config/accounts.ts +2 -2
- package/src/config/schema.js +135 -0
- package/src/config/schema.ts +2 -2
- package/src/core/connection.js +561 -0
- package/src/core/connection.ts +2 -2
- package/src/core/message-handler.js +1422 -0
- package/src/core/message-handler.ts +12 -12
- package/src/core/provider.js +59 -0
- package/src/core/provider.ts +4 -4
- package/src/core/state.js +49 -0
- package/src/directory.js +53 -0
- package/src/directory.ts +2 -2
- package/src/docs.js +209 -0
- package/src/docs.ts +3 -3
- package/src/gateway-methods.js +360 -0
- package/src/gateway-methods.ts +5 -5
- package/src/onboarding.js +337 -0
- package/src/onboarding.ts +4 -4
- package/src/policy.js +15 -0
- package/src/policy.ts +1 -1
- package/src/probe.js +144 -0
- package/src/probe.ts +2 -2
- package/src/reply-dispatcher.js +435 -0
- package/src/reply-dispatcher.ts +9 -9
- package/src/runtime.js +26 -0
- package/src/sdk/helpers.js +237 -0
- package/src/sdk/helpers.ts +1 -1
- package/src/sdk/types.js +13 -0
- package/src/secret-input.js +13 -0
- package/src/secret-input.ts +1 -1
- package/src/services/media/audio.js +40 -0
- package/src/services/media/audio.ts +2 -2
- package/src/services/media/chunk-upload.js +211 -0
- package/src/services/media/chunk-upload.ts +2 -2
- package/src/services/media/common.js +120 -0
- package/src/services/media/common.ts +3 -3
- package/src/services/media/file.js +54 -0
- package/src/services/media/file.ts +2 -2
- package/src/services/media/image.js +59 -0
- package/src/services/media/image.ts +2 -2
- package/src/services/media/index.js +9 -0
- package/src/services/media/index.ts +6 -6
- package/src/services/media/video.js +133 -0
- package/src/services/media/video.ts +2 -2
- package/src/services/media.js +889 -0
- package/src/services/media.ts +12 -12
- package/src/services/messaging/card.js +234 -0
- package/src/services/messaging/card.ts +3 -3
- package/src/services/messaging/index.js +8 -0
- package/src/services/messaging/index.ts +3 -3
- package/src/services/messaging/send.js +85 -0
- package/src/services/messaging/send.ts +3 -3
- package/src/services/messaging.js +680 -0
- package/src/services/messaging.ts +8 -8
- package/src/targets.js +38 -0
- package/src/targets.ts +1 -1
- package/src/types/index.js +1 -0
- package/src/types/index.ts +1 -1
- package/src/utils/agent.js +55 -0
- package/src/utils/async.js +40 -0
- package/src/utils/constants.js +24 -0
- package/src/utils/http-client.js +33 -0
- package/src/utils/http-client.ts +1 -1
- package/src/utils/index.js +7 -0
- package/src/utils/index.ts +4 -4
- package/src/utils/logger.js +76 -0
- package/src/utils/session.js +95 -0
- package/src/utils/session.ts +1 -1
- package/src/utils/token.js +71 -0
- package/src/utils/token.ts +2 -2
- package/src/utils/utils-legacy.js +393 -0
- package/src/utils/utils-legacy.ts +8 -8
- package/CHANGELOG.md +0 -485
- package/SKILL.md +0 -40
- package/_meta.json +0 -4
- package/docs/AGENT_ROUTING.md +0 -335
- package/docs/DEAP_AGENT_GUIDE.en.md +0 -115
- package/docs/DEAP_AGENT_GUIDE.md +0 -115
- package/docs/images/dingtalk.svg +0 -1
- package/docs/images/image-1.png +0 -0
- package/docs/images/image-2.png +0 -0
- package/docs/images/image-3.png +0 -0
- package/docs/images/image-4.png +0 -0
- package/docs/images/image-5.png +0 -0
- package/docs/images/image-6.png +0 -0
- package/docs/images/image-7.png +0 -0
- package/install-beta.sh +0 -438
- package/install-npm.sh +0 -167
- package/src/hooks/init.ts +0 -16
- package/tsconfig.json +0 -20
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Methods 注册
|
|
3
|
+
*
|
|
4
|
+
* 提供钉钉插件的 RPC 接口,允许外部系统、AI Agent 和其他插件调用钉钉功能
|
|
5
|
+
*/
|
|
6
|
+
import { resolveDingtalkAccount } from "./config/accounts.js";
|
|
7
|
+
import { DingtalkDocsClient } from "./docs.js";
|
|
8
|
+
import { sendProactive } from "./services/messaging.js";
|
|
9
|
+
import { getUnionId } from "./utils/utils-legacy.js";
|
|
10
|
+
/**
|
|
11
|
+
* 注册所有 Gateway Methods
|
|
12
|
+
*/
|
|
13
|
+
export function registerGatewayMethods(api) {
|
|
14
|
+
const log = api.logger;
|
|
15
|
+
// ============ 消息发送类 ============
|
|
16
|
+
/**
|
|
17
|
+
* 主动发送单聊消息
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* await gateway.call('dingtalk-connector.sendToUser', {
|
|
22
|
+
* userId: 'user123',
|
|
23
|
+
* content: '任务已完成!',
|
|
24
|
+
* useAICard: true
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
api.registerGatewayMethod('dingtalk-connector.sendToUser', async ({ context, params, respond }) => {
|
|
29
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
30
|
+
const cfg = loadConfig();
|
|
31
|
+
try {
|
|
32
|
+
const { userId, userIds, content, msgType, title, useAICard, fallbackToNormal, accountId } = params || {};
|
|
33
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
34
|
+
if (!account.config?.clientId) {
|
|
35
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
36
|
+
}
|
|
37
|
+
const targetUserIds = userIds || (userId ? [userId] : []);
|
|
38
|
+
if (targetUserIds.length === 0) {
|
|
39
|
+
return respond(false, { error: 'userId or userIds is required' });
|
|
40
|
+
}
|
|
41
|
+
if (!content) {
|
|
42
|
+
return respond(false, { error: 'content is required' });
|
|
43
|
+
}
|
|
44
|
+
// 构建目标
|
|
45
|
+
const target = targetUserIds.length === 1
|
|
46
|
+
? { userId: targetUserIds[0] }
|
|
47
|
+
: { userIds: targetUserIds };
|
|
48
|
+
const result = await sendProactive(account.config, target, content, {
|
|
49
|
+
msgType,
|
|
50
|
+
title,
|
|
51
|
+
log,
|
|
52
|
+
useAICard: useAICard !== false,
|
|
53
|
+
fallbackToNormal: fallbackToNormal !== false,
|
|
54
|
+
});
|
|
55
|
+
respond(result.ok, result);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
log?.error?.(`[Gateway][sendToUser] 错误: ${err.message}`);
|
|
59
|
+
respond(false, { error: err.message });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* 主动发送群聊消息
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* await gateway.call('dingtalk-connector.sendToGroup', {
|
|
68
|
+
* openConversationId: 'cid123',
|
|
69
|
+
* content: '构建失败,请检查日志',
|
|
70
|
+
* useAICard: true
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
api.registerGatewayMethod('dingtalk-connector.sendToGroup', async ({ context, params, respond }) => {
|
|
75
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
76
|
+
const cfg = loadConfig();
|
|
77
|
+
try {
|
|
78
|
+
const { openConversationId, content, msgType, title, useAICard, fallbackToNormal, accountId } = params || {};
|
|
79
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
80
|
+
if (!account.config?.clientId) {
|
|
81
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
82
|
+
}
|
|
83
|
+
if (!openConversationId) {
|
|
84
|
+
return respond(false, { error: 'openConversationId is required' });
|
|
85
|
+
}
|
|
86
|
+
if (!content) {
|
|
87
|
+
return respond(false, { error: 'content is required' });
|
|
88
|
+
}
|
|
89
|
+
const result = await sendProactive(account.config, { openConversationId }, content, {
|
|
90
|
+
msgType,
|
|
91
|
+
title,
|
|
92
|
+
log,
|
|
93
|
+
useAICard: useAICard !== false,
|
|
94
|
+
fallbackToNormal: fallbackToNormal !== false,
|
|
95
|
+
});
|
|
96
|
+
respond(result.ok, result);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log?.error?.(`[Gateway][sendToGroup] 错误: ${err.message}`);
|
|
100
|
+
console.error(err);
|
|
101
|
+
respond(false, { error: err.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
api.registerGatewayMethod('dingtalk-connector.send', async ({ context, params, respond }) => {
|
|
105
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
106
|
+
const cfg = loadConfig();
|
|
107
|
+
try {
|
|
108
|
+
const { target, content, message, msgType, title, useAICard, fallbackToNormal, accountId } = params || {};
|
|
109
|
+
const actualContent = content || message;
|
|
110
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
111
|
+
log?.info?.(`[Gateway][send] 收到请求: target=${target}, contentLen=${actualContent?.length}`);
|
|
112
|
+
if (!account.config?.clientId) {
|
|
113
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
114
|
+
}
|
|
115
|
+
if (!target) {
|
|
116
|
+
return respond(false, { error: 'target is required (format: user:<userId> or group:<openConversationId>)' });
|
|
117
|
+
}
|
|
118
|
+
if (!actualContent) {
|
|
119
|
+
return respond(false, { error: 'content is required' });
|
|
120
|
+
}
|
|
121
|
+
const targetStr = String(target);
|
|
122
|
+
let sendTarget;
|
|
123
|
+
if (targetStr.startsWith('user:')) {
|
|
124
|
+
sendTarget = { userId: targetStr.slice(5) };
|
|
125
|
+
}
|
|
126
|
+
else if (targetStr.startsWith('group:')) {
|
|
127
|
+
sendTarget = { openConversationId: targetStr.slice(6) };
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// 默认当作 userId
|
|
131
|
+
sendTarget = { userId: targetStr };
|
|
132
|
+
}
|
|
133
|
+
const result = await sendProactive(account.config, sendTarget, actualContent, {
|
|
134
|
+
msgType,
|
|
135
|
+
title,
|
|
136
|
+
log,
|
|
137
|
+
useAICard: useAICard !== false,
|
|
138
|
+
fallbackToNormal: fallbackToNormal !== false,
|
|
139
|
+
});
|
|
140
|
+
respond(result.ok, result);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
log?.error?.(`[Gateway][send] 错误: ${err.message}`);
|
|
144
|
+
respond(false, { error: err.message });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// ============ 文档操作类 ============
|
|
148
|
+
api.registerGatewayMethod('dingtalk-connector.docs.read', async ({ context, params, respond }) => {
|
|
149
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
150
|
+
const cfg = loadConfig();
|
|
151
|
+
try {
|
|
152
|
+
const { docId, operatorId: rawOperatorId, accountId } = params || {};
|
|
153
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
154
|
+
if (!account.config?.clientId) {
|
|
155
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
156
|
+
}
|
|
157
|
+
if (!docId) {
|
|
158
|
+
return respond(false, { error: 'docId is required' });
|
|
159
|
+
}
|
|
160
|
+
if (!rawOperatorId) {
|
|
161
|
+
return respond(false, { error: 'operatorId (unionId or staffId) is required' });
|
|
162
|
+
}
|
|
163
|
+
// 如果 operatorId 不像 unionId,尝试转换
|
|
164
|
+
let operatorId = rawOperatorId;
|
|
165
|
+
if (!rawOperatorId.includes('$')) {
|
|
166
|
+
const resolved = await getUnionId(rawOperatorId, account.config, log);
|
|
167
|
+
if (resolved)
|
|
168
|
+
operatorId = resolved;
|
|
169
|
+
}
|
|
170
|
+
const client = new DingtalkDocsClient(account.config, log);
|
|
171
|
+
const content = await client.readDoc(docId, operatorId);
|
|
172
|
+
if (content !== null) {
|
|
173
|
+
respond(true, { content });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
respond(false, { error: 'Failed to read document node' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
log?.error?.(`[Gateway][docs.read] 错误: ${err.message}`);
|
|
181
|
+
respond(false, { error: err.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* 创建钉钉文档
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const result = await gateway.call('dingtalk-connector.docs.create', {
|
|
190
|
+
* spaceId: 'workspace123',
|
|
191
|
+
* title: '会议纪要',
|
|
192
|
+
* content: '今天讨论了...'
|
|
193
|
+
* });
|
|
194
|
+
* console.log('文档ID:', result.docId);
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
api.registerGatewayMethod('dingtalk-connector.docs.create', async ({ context, params, respond }) => {
|
|
198
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
199
|
+
const cfg = loadConfig();
|
|
200
|
+
try {
|
|
201
|
+
const { spaceId, title, content, accountId } = params || {};
|
|
202
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
203
|
+
if (!account.config?.clientId) {
|
|
204
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
205
|
+
}
|
|
206
|
+
if (!spaceId || !title) {
|
|
207
|
+
return respond(false, { error: 'spaceId and title are required' });
|
|
208
|
+
}
|
|
209
|
+
const client = new DingtalkDocsClient(account.config, log);
|
|
210
|
+
const doc = await client.createDoc(spaceId, title, content);
|
|
211
|
+
if (doc) {
|
|
212
|
+
respond(true, doc);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
respond(false, { error: 'Failed to create document' });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
log?.error?.(`[Gateway][docs.create] 错误: ${err.message}`);
|
|
220
|
+
respond(false, { error: err.message });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
/**
|
|
224
|
+
* 向钉钉文档追加内容
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* await gateway.call('dingtalk-connector.docs.append', {
|
|
229
|
+
* docId: 'doc123',
|
|
230
|
+
* content: '补充内容...'
|
|
231
|
+
* });
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
api.registerGatewayMethod('dingtalk-connector.docs.append', async ({ context, params, respond }) => {
|
|
235
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
236
|
+
const cfg = loadConfig();
|
|
237
|
+
try {
|
|
238
|
+
const { docId, content, accountId } = params || {};
|
|
239
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
240
|
+
if (!account.config?.clientId) {
|
|
241
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
242
|
+
}
|
|
243
|
+
if (!docId || !content) {
|
|
244
|
+
return respond(false, { error: 'docId and content are required' });
|
|
245
|
+
}
|
|
246
|
+
const client = new DingtalkDocsClient(account.config, log);
|
|
247
|
+
const ok = await client.appendToDoc(docId, content);
|
|
248
|
+
respond(ok, ok ? { success: true } : { error: 'Failed to append to document' });
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
log?.error?.(`[Gateway][docs.append] 错误: ${err.message}`);
|
|
252
|
+
respond(false, { error: err.message });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* 搜索钉钉文档
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* const result = await gateway.call('dingtalk-connector.docs.search', {
|
|
261
|
+
* keyword: '项目规范',
|
|
262
|
+
* spaceId: 'workspace123' // 可选
|
|
263
|
+
* });
|
|
264
|
+
* console.log('找到文档:', result.docs);
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
api.registerGatewayMethod('dingtalk-connector.docs.search', async ({ context, params, respond }) => {
|
|
268
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
269
|
+
const cfg = loadConfig();
|
|
270
|
+
try {
|
|
271
|
+
const { keyword, spaceId, accountId } = params || {};
|
|
272
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
273
|
+
if (!account.config?.clientId) {
|
|
274
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
275
|
+
}
|
|
276
|
+
if (!keyword) {
|
|
277
|
+
return respond(false, { error: 'keyword is required' });
|
|
278
|
+
}
|
|
279
|
+
const client = new DingtalkDocsClient(account.config, log);
|
|
280
|
+
const docs = await client.searchDocs(keyword, spaceId);
|
|
281
|
+
respond(true, { docs });
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
log?.error?.(`[Gateway][docs.search] 错误: ${err.message}`);
|
|
285
|
+
respond(false, { error: err.message });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
/**
|
|
289
|
+
* 列出空间下的文档
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* const result = await gateway.call('dingtalk-connector.docs.list', {
|
|
294
|
+
* spaceId: 'workspace123',
|
|
295
|
+
* parentId: 'folder456' // 可选,不传则列出根目录
|
|
296
|
+
* });
|
|
297
|
+
* console.log('文档列表:', result.docs);
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
api.registerGatewayMethod('dingtalk-connector.docs.list', async ({ context, params, respond }) => {
|
|
301
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
302
|
+
const cfg = loadConfig();
|
|
303
|
+
try {
|
|
304
|
+
const { spaceId, parentId, accountId } = params || {};
|
|
305
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
306
|
+
if (!account.config?.clientId) {
|
|
307
|
+
return respond(false, { error: 'DingTalk not configured' });
|
|
308
|
+
}
|
|
309
|
+
if (!spaceId) {
|
|
310
|
+
return respond(false, { error: 'spaceId is required' });
|
|
311
|
+
}
|
|
312
|
+
const client = new DingtalkDocsClient(account.config, log);
|
|
313
|
+
const docs = await client.listDocs(spaceId, parentId);
|
|
314
|
+
respond(true, { docs });
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
log?.error?.(`[Gateway][docs.list] 错误: ${err.message}`);
|
|
318
|
+
respond(false, { error: err.message });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
// ============ 状态检查类 ============
|
|
322
|
+
api.registerGatewayMethod('dingtalk-connector.status', async ({ context, params, respond }) => {
|
|
323
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
324
|
+
const cfg = loadConfig();
|
|
325
|
+
try {
|
|
326
|
+
const accountId = params?.accountId;
|
|
327
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
328
|
+
const hasClientId = !!account.config?.clientId;
|
|
329
|
+
const hasClientSecret = !!account.config?.clientSecret;
|
|
330
|
+
respond(true, {
|
|
331
|
+
configured: hasClientId && hasClientSecret,
|
|
332
|
+
enabled: account.enabled,
|
|
333
|
+
accountId: account.accountId,
|
|
334
|
+
clientId: hasClientId ? String(account.config.clientId).substring(0, 8) + '...' : undefined,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
log?.error?.(`[Gateway][status] 错误: ${err.message}`);
|
|
339
|
+
respond(false, { error: err.message });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
api.registerGatewayMethod('dingtalk-connector.probe', async ({ context, respond }) => {
|
|
343
|
+
const { loadConfig } = await import('openclaw/plugin-sdk/config-runtime');
|
|
344
|
+
const cfg = loadConfig();
|
|
345
|
+
try {
|
|
346
|
+
const account = resolveDingtalkAccount({ cfg });
|
|
347
|
+
if (!account.config?.clientId || !account.config?.clientSecret) {
|
|
348
|
+
return respond(false, { error: 'Not configured' });
|
|
349
|
+
}
|
|
350
|
+
// 尝试获取 access token 来验证连接
|
|
351
|
+
const { getAccessToken } = await import('./utils/utils-legacy.js');
|
|
352
|
+
await getAccessToken(account.config);
|
|
353
|
+
respond(true, { ok: true, details: { clientId: account.config.clientId } });
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
log?.error?.(`[Gateway][probe] 错误: ${err.message}`);
|
|
357
|
+
respond(false, { ok: false, error: err.message });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink, hasConfiguredSecretInput, } from "./sdk/helpers.js";
|
|
2
|
+
import { promptSingleChannelSecretInput } from "openclaw/plugin-sdk/setup";
|
|
3
|
+
import { resolveDingtalkAccount, resolveDingtalkCredentials } from "./config/accounts.js";
|
|
4
|
+
import { probeDingtalk } from "./probe.js";
|
|
5
|
+
const channel = "dingtalk-connector";
|
|
6
|
+
function normalizeString(value) {
|
|
7
|
+
if (typeof value === "number") {
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
if (typeof value !== "string") {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed || undefined;
|
|
15
|
+
}
|
|
16
|
+
function setDingtalkDmPolicy(cfg, dmPolicy) {
|
|
17
|
+
const allowFrom = dmPolicy === "open"
|
|
18
|
+
? addWildcardAllowFrom(cfg.channels?.["dingtalk-connector"]?.allowFrom)?.map((entry) => String(entry))
|
|
19
|
+
: undefined;
|
|
20
|
+
return {
|
|
21
|
+
...cfg,
|
|
22
|
+
channels: {
|
|
23
|
+
...cfg.channels,
|
|
24
|
+
"dingtalk-connector": {
|
|
25
|
+
...cfg.channels?.["dingtalk-connector"],
|
|
26
|
+
dmPolicy,
|
|
27
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function setDingtalkAllowFrom(cfg, allowFrom) {
|
|
33
|
+
return {
|
|
34
|
+
...cfg,
|
|
35
|
+
channels: {
|
|
36
|
+
...cfg.channels,
|
|
37
|
+
"dingtalk-connector": {
|
|
38
|
+
...cfg.channels?.["dingtalk-connector"],
|
|
39
|
+
allowFrom,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function parseAllowFromInput(raw) {
|
|
45
|
+
return raw
|
|
46
|
+
.split(/[\n,;]+/g)
|
|
47
|
+
.map((entry) => entry.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
async function promptDingtalkAllowFrom(params) {
|
|
51
|
+
const existing = params.cfg.channels?.["dingtalk-connector"]?.allowFrom ?? [];
|
|
52
|
+
await params.prompter.note([
|
|
53
|
+
"Allowlist DingTalk DMs by user ID.",
|
|
54
|
+
"You can find user ID in DingTalk admin console or via API.",
|
|
55
|
+
"Examples:",
|
|
56
|
+
"- user123456",
|
|
57
|
+
"- user789012",
|
|
58
|
+
].join("\n"), "DingTalk allowlist");
|
|
59
|
+
while (true) {
|
|
60
|
+
const entry = await params.prompter.text({
|
|
61
|
+
message: "DingTalk allowFrom (user IDs)",
|
|
62
|
+
placeholder: "user123456, user789012",
|
|
63
|
+
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
64
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
65
|
+
});
|
|
66
|
+
const parts = parseAllowFromInput(String(entry));
|
|
67
|
+
if (parts.length === 0) {
|
|
68
|
+
await params.prompter.note("Enter at least one user.", "DingTalk allowlist");
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const unique = [
|
|
72
|
+
...new Set([
|
|
73
|
+
...existing.map((v) => String(v).trim()).filter(Boolean),
|
|
74
|
+
...parts,
|
|
75
|
+
]),
|
|
76
|
+
];
|
|
77
|
+
return setDingtalkAllowFrom(params.cfg, unique);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function noteDingtalkCredentialHelp(prompter) {
|
|
81
|
+
await prompter.note([
|
|
82
|
+
"1) Go to DingTalk Open Platform (open-dev.dingtalk.com)",
|
|
83
|
+
"2) Create an enterprise internal app",
|
|
84
|
+
"3) Get Client ID and Client Secret from Credentials page",
|
|
85
|
+
"4) Enable required permissions: im:message, im:chat",
|
|
86
|
+
"5) Publish the app or add it to a test group",
|
|
87
|
+
"Tip: you can also set DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET env vars.",
|
|
88
|
+
`Docs: ${formatDocsLink("/channels/dingtalk-connector", "dingtalk-connector")}`,
|
|
89
|
+
].join("\n"), "DingTalk credentials");
|
|
90
|
+
}
|
|
91
|
+
async function promptDingtalkClientId(params) {
|
|
92
|
+
const clientId = String(await params.prompter.text({
|
|
93
|
+
message: "Enter DingTalk Client ID",
|
|
94
|
+
initialValue: params.initialValue,
|
|
95
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
96
|
+
})).trim();
|
|
97
|
+
return clientId;
|
|
98
|
+
}
|
|
99
|
+
function setDingtalkGroupPolicy(cfg, groupPolicy) {
|
|
100
|
+
return {
|
|
101
|
+
...cfg,
|
|
102
|
+
channels: {
|
|
103
|
+
...cfg.channels,
|
|
104
|
+
"dingtalk-connector": {
|
|
105
|
+
...cfg.channels?.["dingtalk-connector"],
|
|
106
|
+
enabled: true,
|
|
107
|
+
groupPolicy,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function setDingtalkGroupAllowFrom(cfg, groupAllowFrom) {
|
|
113
|
+
return {
|
|
114
|
+
...cfg,
|
|
115
|
+
channels: {
|
|
116
|
+
...cfg.channels,
|
|
117
|
+
"dingtalk-connector": {
|
|
118
|
+
...cfg.channels?.["dingtalk-connector"],
|
|
119
|
+
groupAllowFrom,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const dmPolicy = {
|
|
125
|
+
label: "DingTalk",
|
|
126
|
+
channel,
|
|
127
|
+
policyKey: "channels.dingtalk-connector.dmPolicy",
|
|
128
|
+
allowFromKey: "channels.dingtalk-connector.allowFrom",
|
|
129
|
+
getCurrent: (cfg) => cfg.channels?.["dingtalk-connector"]?.dmPolicy ?? "open",
|
|
130
|
+
setPolicy: (cfg, policy) => setDingtalkDmPolicy(cfg, policy),
|
|
131
|
+
promptAllowFrom: promptDingtalkAllowFrom,
|
|
132
|
+
};
|
|
133
|
+
export const dingtalkOnboardingAdapter = {
|
|
134
|
+
channel,
|
|
135
|
+
getStatus: async ({ cfg }) => {
|
|
136
|
+
// Use resolveDingtalkAccount to correctly support pure multi-account configs
|
|
137
|
+
// where credentials are only under accounts.<id>, not at the top level.
|
|
138
|
+
const defaultAccount = resolveDingtalkAccount({ cfg });
|
|
139
|
+
const configured = defaultAccount.configured;
|
|
140
|
+
let probeResult = null;
|
|
141
|
+
if (configured && defaultAccount.clientId && defaultAccount.clientSecret) {
|
|
142
|
+
try {
|
|
143
|
+
probeResult = await probeDingtalk({
|
|
144
|
+
clientId: defaultAccount.clientId,
|
|
145
|
+
clientSecret: defaultAccount.clientSecret,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Ignore probe errors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const statusLines = [];
|
|
153
|
+
if (!configured) {
|
|
154
|
+
statusLines.push("DingTalk: needs app credentials");
|
|
155
|
+
}
|
|
156
|
+
else if (probeResult?.ok) {
|
|
157
|
+
statusLines.push(`DingTalk: connected as ${probeResult.botName ?? "bot"}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
statusLines.push("DingTalk: configured (connection not verified)");
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
channel,
|
|
164
|
+
configured,
|
|
165
|
+
statusLines,
|
|
166
|
+
selectionHint: configured ? "configured" : "needs app creds",
|
|
167
|
+
quickstartScore: configured ? 2 : 0,
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
configure: async ({ cfg, prompter }) => {
|
|
171
|
+
const dingtalkCfg = cfg.channels?.["dingtalk-connector"];
|
|
172
|
+
const resolved = resolveDingtalkCredentials(dingtalkCfg, {
|
|
173
|
+
allowUnresolvedSecretRef: true,
|
|
174
|
+
});
|
|
175
|
+
const hasConfigSecret = hasConfiguredSecretInput(dingtalkCfg?.clientSecret);
|
|
176
|
+
const hasConfigCreds = Boolean(typeof dingtalkCfg?.clientId === "string" && dingtalkCfg.clientId.trim() && hasConfigSecret);
|
|
177
|
+
let canUseEnv = Boolean(!hasConfigCreds && process.env.DINGTALK_CLIENT_ID?.trim() && process.env.DINGTALK_CLIENT_SECRET?.trim());
|
|
178
|
+
let next = cfg;
|
|
179
|
+
let clientId = null;
|
|
180
|
+
let clientSecret = null;
|
|
181
|
+
let clientSecretProbeValue = null;
|
|
182
|
+
if (!resolved) {
|
|
183
|
+
await noteDingtalkCredentialHelp(prompter);
|
|
184
|
+
}
|
|
185
|
+
// Check if we can use environment variables
|
|
186
|
+
if (canUseEnv) {
|
|
187
|
+
const useEnv = await prompter.confirm({
|
|
188
|
+
message: "DINGTALK_CLIENT_ID + DINGTALK_CLIENT_SECRET detected. Use env vars?",
|
|
189
|
+
initialValue: true,
|
|
190
|
+
});
|
|
191
|
+
if (useEnv) {
|
|
192
|
+
next = {
|
|
193
|
+
...next,
|
|
194
|
+
channels: {
|
|
195
|
+
...next.channels,
|
|
196
|
+
"dingtalk-connector": { ...next.channels?.["dingtalk-connector"], enabled: true },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
// Environment variables will be used, skip manual input
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// User chose not to use env vars, proceed to manual input
|
|
203
|
+
canUseEnv = false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// If not using env vars, prompt for credentials
|
|
207
|
+
if (!canUseEnv) {
|
|
208
|
+
// Check if we should keep existing configuration
|
|
209
|
+
if (resolved && hasConfigSecret) {
|
|
210
|
+
const keepExisting = await prompter.confirm({
|
|
211
|
+
message: "DingTalk credentials already configured. Keep them?",
|
|
212
|
+
initialValue: true,
|
|
213
|
+
});
|
|
214
|
+
if (!keepExisting) {
|
|
215
|
+
// User wants to reconfigure, proceed to input
|
|
216
|
+
// Step 1: Prompt for Client ID first
|
|
217
|
+
clientId = await promptDingtalkClientId({
|
|
218
|
+
prompter,
|
|
219
|
+
initialValue: normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
220
|
+
});
|
|
221
|
+
// Step 2: Then prompt for Client Secret
|
|
222
|
+
const clientSecretResult = await promptSingleChannelSecretInput({
|
|
223
|
+
cfg: next,
|
|
224
|
+
prompter,
|
|
225
|
+
providerHint: "dingtalk",
|
|
226
|
+
credentialLabel: "Client Secret",
|
|
227
|
+
accountConfigured: false, // Force new input
|
|
228
|
+
canUseEnv: false, // Already handled above
|
|
229
|
+
hasConfigToken: false, // Force new input
|
|
230
|
+
envPrompt: "", // Not used
|
|
231
|
+
keepPrompt: "", // Not used
|
|
232
|
+
inputPrompt: "Enter DingTalk Client Secret",
|
|
233
|
+
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
234
|
+
});
|
|
235
|
+
if (clientSecretResult.action === "set") {
|
|
236
|
+
clientSecret = clientSecretResult.value;
|
|
237
|
+
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// If keepExisting is true, we don't modify anything
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// No existing config, prompt for new credentials
|
|
244
|
+
// Step 1: Prompt for Client ID first
|
|
245
|
+
clientId = await promptDingtalkClientId({
|
|
246
|
+
prompter,
|
|
247
|
+
initialValue: normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
248
|
+
});
|
|
249
|
+
// Step 2: Then prompt for Client Secret
|
|
250
|
+
const clientSecretResult = await promptSingleChannelSecretInput({
|
|
251
|
+
cfg: next,
|
|
252
|
+
prompter,
|
|
253
|
+
providerHint: "dingtalk",
|
|
254
|
+
credentialLabel: "Client Secret",
|
|
255
|
+
accountConfigured: false,
|
|
256
|
+
canUseEnv: false,
|
|
257
|
+
hasConfigToken: false,
|
|
258
|
+
envPrompt: "",
|
|
259
|
+
keepPrompt: "",
|
|
260
|
+
inputPrompt: "Enter DingTalk Client Secret",
|
|
261
|
+
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
262
|
+
});
|
|
263
|
+
if (clientSecretResult.action === "set") {
|
|
264
|
+
clientSecret = clientSecretResult.value;
|
|
265
|
+
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (clientId && clientSecret) {
|
|
270
|
+
next = {
|
|
271
|
+
...next,
|
|
272
|
+
channels: {
|
|
273
|
+
...next.channels,
|
|
274
|
+
"dingtalk-connector": {
|
|
275
|
+
...next.channels?.["dingtalk-connector"],
|
|
276
|
+
enabled: true,
|
|
277
|
+
clientId,
|
|
278
|
+
clientSecret,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
// Test connection
|
|
283
|
+
try {
|
|
284
|
+
const probe = await probeDingtalk({
|
|
285
|
+
clientId,
|
|
286
|
+
clientSecret: clientSecretProbeValue ?? undefined,
|
|
287
|
+
});
|
|
288
|
+
if (probe.ok) {
|
|
289
|
+
await prompter.note(`Connected as ${probe.botName ?? "bot"}`, "DingTalk connection test");
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
await prompter.note(`Connection failed: ${probe.error ?? "unknown error"}`, "DingTalk connection test");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "DingTalk connection test");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Group policy
|
|
300
|
+
const groupPolicy = await prompter.select({
|
|
301
|
+
message: "Group chat policy",
|
|
302
|
+
options: [
|
|
303
|
+
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
304
|
+
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
305
|
+
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
306
|
+
],
|
|
307
|
+
initialValue: next.channels?.["dingtalk-connector"]?.groupPolicy ?? "open",
|
|
308
|
+
});
|
|
309
|
+
if (groupPolicy) {
|
|
310
|
+
next = setDingtalkGroupPolicy(next, groupPolicy);
|
|
311
|
+
}
|
|
312
|
+
// Group allowlist if needed
|
|
313
|
+
if (groupPolicy === "allowlist") {
|
|
314
|
+
const existing = next.channels?.["dingtalk-connector"]?.groupAllowFrom ?? [];
|
|
315
|
+
const entry = await prompter.text({
|
|
316
|
+
message: "Group chat allowlist (conversation IDs)",
|
|
317
|
+
placeholder: "cidxxxx, cidyyyy",
|
|
318
|
+
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
319
|
+
});
|
|
320
|
+
if (entry) {
|
|
321
|
+
const parts = parseAllowFromInput(String(entry));
|
|
322
|
+
if (parts.length > 0) {
|
|
323
|
+
next = setDingtalkGroupAllowFrom(next, parts);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
328
|
+
},
|
|
329
|
+
dmPolicy,
|
|
330
|
+
disable: (cfg) => ({
|
|
331
|
+
...cfg,
|
|
332
|
+
channels: {
|
|
333
|
+
...cfg.channels,
|
|
334
|
+
"dingtalk-connector": { ...cfg.channels?.["dingtalk-connector"], enabled: false },
|
|
335
|
+
},
|
|
336
|
+
}),
|
|
337
|
+
};
|