@huo15/dingtalk-connector-pro 1.0.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +479 -0
  4. package/README.md +209 -0
  5. package/docs/AGENT_ROUTING.md +335 -0
  6. package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
  7. package/docs/DEAP_AGENT_GUIDE.md +115 -0
  8. package/docs/images/dingtalk.svg +1 -0
  9. package/docs/images/image-1.png +0 -0
  10. package/docs/images/image-2.png +0 -0
  11. package/docs/images/image-3.png +0 -0
  12. package/docs/images/image-4.png +0 -0
  13. package/docs/images/image-5.png +0 -0
  14. package/docs/images/image-6.png +0 -0
  15. package/docs/images/image-7.png +0 -0
  16. package/index.ts +28 -0
  17. package/install-beta.sh +438 -0
  18. package/install-npm.sh +167 -0
  19. package/openclaw.plugin.json +498 -0
  20. package/package.json +80 -0
  21. package/src/channel.ts +463 -0
  22. package/src/config/accounts.ts +242 -0
  23. package/src/config/schema.ts +148 -0
  24. package/src/core/connection.ts +722 -0
  25. package/src/core/message-handler.ts +1700 -0
  26. package/src/core/provider.ts +111 -0
  27. package/src/core/state.ts +54 -0
  28. package/src/directory.ts +95 -0
  29. package/src/docs.ts +293 -0
  30. package/src/gateway-methods.ts +404 -0
  31. package/src/onboarding.ts +413 -0
  32. package/src/policy.ts +32 -0
  33. package/src/probe.ts +212 -0
  34. package/src/reply-dispatcher.ts +630 -0
  35. package/src/runtime.ts +32 -0
  36. package/src/sdk/helpers.ts +322 -0
  37. package/src/sdk/types.ts +513 -0
  38. package/src/secret-input.ts +19 -0
  39. package/src/services/media/audio.ts +54 -0
  40. package/src/services/media/chunk-upload.ts +296 -0
  41. package/src/services/media/common.ts +155 -0
  42. package/src/services/media/file.ts +70 -0
  43. package/src/services/media/image.ts +81 -0
  44. package/src/services/media/index.ts +10 -0
  45. package/src/services/media/video.ts +162 -0
  46. package/src/services/media.ts +1136 -0
  47. package/src/services/messaging/card.ts +342 -0
  48. package/src/services/messaging/index.ts +17 -0
  49. package/src/services/messaging/send.ts +141 -0
  50. package/src/services/messaging.ts +1013 -0
  51. package/src/targets.ts +45 -0
  52. package/src/types/index.ts +59 -0
  53. package/src/utils/agent.ts +63 -0
  54. package/src/utils/async.ts +51 -0
  55. package/src/utils/constants.ts +27 -0
  56. package/src/utils/http-client.ts +37 -0
  57. package/src/utils/index.ts +8 -0
  58. package/src/utils/logger.ts +78 -0
  59. package/src/utils/session.ts +147 -0
  60. package/src/utils/token.ts +93 -0
  61. package/src/utils/utils-legacy.ts +454 -0
  62. package/tsconfig.json +20 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * AI Card 流式响应模块
3
+ * 支持 AI Card 创建、流式更新、完成
4
+ */
5
+
6
+ import type { DingtalkConfig } from "../../types/index.ts";
7
+ import { DINGTALK_API, getAccessToken } from "../../utils/token.ts";
8
+ import { dingtalkHttp } from "../../utils/http-client.ts";
9
+
10
+ // ============ 常量 ============
11
+
12
+ const AI_CARD_TEMPLATE_ID = "02fcf2f4-5e02-4a85-b672-46d1f715543e.schema";
13
+
14
+ /** AI Card 状态 */
15
+ const AICardStatus = {
16
+ PROCESSING: "1",
17
+ INPUTING: "2",
18
+ FINISHED: "3",
19
+ EXECUTING: "4",
20
+ FAILED: "5",
21
+ } as const;
22
+
23
+ /** AI Card 实例接口 */
24
+ export interface AICardInstance {
25
+ cardInstanceId: string;
26
+ accessToken: string;
27
+ tokenExpireTime: number;
28
+ inputingStarted: boolean;
29
+ }
30
+
31
+ /** AI Card 投放目标类型 */
32
+ export type AICardTarget =
33
+ | { type: "user"; userId: string }
34
+ | { type: "group"; openConversationId: string };
35
+
36
+ // ============ Markdown 格式修正 ============
37
+
38
+ /**
39
+ * 确保 Markdown 表格前有空行,否则钉钉无法正确渲染表格
40
+ */
41
+ function ensureTableBlankLines(text: string): string {
42
+ const lines = text.split("\n");
43
+ const result: string[] = [];
44
+
45
+ const tableDividerRegex = /^\s*\|?\s*:?-+:?\s*(\|?\s*:?-+:?\s*)+\|?\s*$/;
46
+ const tableRowRegex = /^\s*\|?.*\|.*\|?\s*$/;
47
+
48
+ const isDivider = (line: string) =>
49
+ line &&
50
+ typeof line === "string" &&
51
+ line.includes("|") &&
52
+ tableDividerRegex.test(line);
53
+
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const currentLine = lines[i];
56
+ const nextLine = lines[i + 1] ?? "";
57
+
58
+ if (
59
+ tableRowRegex.test(currentLine) &&
60
+ isDivider(nextLine) &&
61
+ i > 0 &&
62
+ lines[i - 1].trim() !== "" &&
63
+ !tableRowRegex.test(lines[i - 1])
64
+ ) {
65
+ result.push("");
66
+ }
67
+
68
+ result.push(currentLine);
69
+ }
70
+ return result.join("\n");
71
+ }
72
+
73
+ // ============ AI Card 相关 ============
74
+
75
+ /**
76
+ * 构建卡片投放请求体
77
+ */
78
+ export function buildDeliverBody(
79
+ cardInstanceId: string,
80
+ target: AICardTarget,
81
+ robotCode: string,
82
+ ): any {
83
+ const base = { outTrackId: cardInstanceId, userIdType: 1 };
84
+
85
+ if (target.type === "group") {
86
+ return {
87
+ ...base,
88
+ openSpaceId: `dtv1.card//IM_GROUP.${target.openConversationId}`,
89
+ imGroupOpenDeliverModel: {
90
+ robotCode,
91
+ },
92
+ };
93
+ }
94
+
95
+ return {
96
+ ...base,
97
+ openSpaceId: `dtv1.card//IM_ROBOT.${target.userId}`,
98
+ imRobotOpenDeliverModel: {
99
+ spaceType: 'IM_ROBOT',
100
+ robotCode,
101
+ extension: {
102
+ dynamicSummary: 'true',
103
+ },
104
+ },
105
+ };
106
+ }
107
+
108
+ /**
109
+ * 通用 AI Card 创建函数
110
+ */
111
+ export async function createAICardForTarget(
112
+ config: DingtalkConfig,
113
+ target: AICardTarget,
114
+ log?: any,
115
+ ): Promise<AICardInstance | null> {
116
+ const targetDesc =
117
+ target.type === "group"
118
+ ? `群聊 ${target.openConversationId}`
119
+ : `用户 ${target.userId}`;
120
+
121
+ try {
122
+ const token = await getAccessToken(config);
123
+ const cardInstanceId = `card_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
124
+
125
+ log?.info?.(
126
+ `[DingTalk][AICard] 开始创建卡片:${targetDesc}, outTrackId=${cardInstanceId}`,
127
+ );
128
+
129
+ // 1. 创建卡片实例
130
+ const createBody = {
131
+ cardTemplateId: AI_CARD_TEMPLATE_ID,
132
+ outTrackId: cardInstanceId,
133
+ cardData: {
134
+ cardParamMap: {
135
+ config: JSON.stringify({ autoLayout: true }),
136
+ }
137
+ },
138
+ callbackType: "STREAM",
139
+ imGroupOpenSpaceModel: { supportForward: true },
140
+ imRobotOpenSpaceModel: { supportForward: true },
141
+ };
142
+
143
+ const createResp = await dingtalkHttp.post(
144
+ `${DINGTALK_API}/v1.0/card/instances`,
145
+ createBody,
146
+ {
147
+ headers: {
148
+ "x-acs-dingtalk-access-token": token,
149
+ "Content-Type": "application/json",
150
+ },
151
+ },
152
+ );
153
+
154
+ // 2. 投放卡片
155
+ const deliverBody = buildDeliverBody(
156
+ cardInstanceId,
157
+ target,
158
+ String(config.clientId ?? ""),
159
+ );
160
+
161
+ const deliverResp = await dingtalkHttp.post(
162
+ `${DINGTALK_API}/v1.0/card/instances/deliver`,
163
+ deliverBody,
164
+ {
165
+ headers: {
166
+ "x-acs-dingtalk-access-token": token,
167
+ "Content-Type": "application/json",
168
+ },
169
+ },
170
+ );
171
+
172
+ // 记录 token 过期时间(钉钉 token 有效期 2 小时)
173
+ const tokenExpireTime = Date.now() + 2 * 60 * 60 * 1000;
174
+
175
+ return { cardInstanceId, accessToken: token, tokenExpireTime, inputingStarted: false };
176
+ } catch (err: any) {
177
+ log?.error?.(
178
+ `[DingTalk][AICard] 创建卡片失败 (${targetDesc}): ${err.message}`,
179
+ );
180
+ if (err.response) {
181
+ log?.error?.(
182
+ `[DingTalk][AICard] 错误响应:status=${err.response.status}`,
183
+ );
184
+ }
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 确保 Token 有效(自动刷新过期的 Token)
191
+ */
192
+ async function ensureValidToken(
193
+ card: AICardInstance,
194
+ config: DingtalkConfig,
195
+ ): Promise<string> {
196
+ // 如果 token 即将过期(提前 5 分钟刷新)
197
+ if (Date.now() > card.tokenExpireTime - 5 * 60 * 1000) {
198
+ const newToken = await getAccessToken(config);
199
+ card.accessToken = newToken;
200
+ card.tokenExpireTime = Date.now() + 2 * 60 * 60 * 1000;
201
+ }
202
+ return card.accessToken;
203
+ }
204
+
205
+ /**
206
+ * 流式更新 AI Card 内容
207
+ */
208
+ export async function streamAICard(
209
+ card: AICardInstance,
210
+ content: string,
211
+ finished: boolean = false,
212
+ config?: DingtalkConfig,
213
+ log?: any,
214
+ ): Promise<void> {
215
+ // 确保 token 有效
216
+ if (config) {
217
+ await ensureValidToken(card, config);
218
+ }
219
+ if (!card.inputingStarted) {
220
+ const statusBody = {
221
+ outTrackId: card.cardInstanceId,
222
+ cardData: {
223
+ cardParamMap: {
224
+ flowStatus: AICardStatus.INPUTING,
225
+ msgContent: content,
226
+ staticMsgContent: "",
227
+ sys_full_json_obj: JSON.stringify({
228
+ order: ["msgContent"],
229
+ }),
230
+ config: JSON.stringify({ autoLayout: true }),
231
+ },
232
+ },
233
+ };
234
+ try {
235
+ const statusResp = await dingtalkHttp.put(
236
+ `${DINGTALK_API}/v1.0/card/instances`,
237
+ statusBody,
238
+ {
239
+ headers: {
240
+ "x-acs-dingtalk-access-token": card.accessToken,
241
+ "Content-Type": "application/json",
242
+ },
243
+ },
244
+ );
245
+ log?.info?.(
246
+ `[DingTalk][AICard] INPUTING 响应:status=${statusResp.status}`,
247
+ );
248
+ } catch (err: any) {
249
+ log?.error?.(`[DingTalk][AICard] INPUTING 切换失败:${err.message}`);
250
+ throw err;
251
+ }
252
+ card.inputingStarted = true;
253
+ }
254
+
255
+ const fixedContent = ensureTableBlankLines(content);
256
+ const body = {
257
+ outTrackId: card.cardInstanceId,
258
+ guid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
259
+ key: "msgContent",
260
+ content: fixedContent,
261
+ isFull: true,
262
+ isFinalize: finished,
263
+ isError: false,
264
+ };
265
+
266
+ log?.info?.(
267
+ `[DingTalk][AICard] PUT /v1.0/card/streaming contentLen=${content.length} isFinalize=${finished}`,
268
+ );
269
+ try {
270
+ const streamResp = await dingtalkHttp.put(
271
+ `${DINGTALK_API}/v1.0/card/streaming`,
272
+ body,
273
+ {
274
+ headers: {
275
+ "x-acs-dingtalk-access-token": card.accessToken,
276
+ "Content-Type": "application/json",
277
+ },
278
+ },
279
+ );
280
+ log?.info?.(
281
+ `[DingTalk][AICard] streaming 响应:status=${streamResp.status}`,
282
+ );
283
+ } catch (err: any) {
284
+ log?.error?.(`[DingTalk][AICard] streaming 更新失败:${err.message}`);
285
+ throw err;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * 完成 AI Card
291
+ */
292
+ export async function finishAICard(
293
+ card: AICardInstance,
294
+ content: string,
295
+ config?: DingtalkConfig,
296
+ log?: any,
297
+ ): Promise<void> {
298
+ // 确保 token 有效
299
+ if (config) {
300
+ await ensureValidToken(card, config);
301
+ }
302
+ const fixedContent = ensureTableBlankLines(content);
303
+ log?.info?.(
304
+ `[DingTalk][AICard] 开始 finish,最终内容长度=${fixedContent.length}`,
305
+ );
306
+
307
+ await streamAICard(card, fixedContent, true, log);
308
+
309
+ const body = {
310
+ outTrackId: card.cardInstanceId,
311
+ cardData: {
312
+ cardParamMap: {
313
+ flowStatus: AICardStatus.FINISHED,
314
+ msgContent: fixedContent,
315
+ staticMsgContent: "",
316
+ sys_full_json_obj: JSON.stringify({
317
+ order: ["msgContent"],
318
+ }),
319
+ config: JSON.stringify({ autoLayout: true }),
320
+ },
321
+ },
322
+ cardUpdateOptions: { updateCardDataByKey: true },
323
+ };
324
+
325
+ try {
326
+ const finishResp = await dingtalkHttp.put(
327
+ `${DINGTALK_API}/v1.0/card/instances`,
328
+ body,
329
+ {
330
+ headers: {
331
+ "x-acs-dingtalk-access-token": card.accessToken,
332
+ "Content-Type": "application/json",
333
+ },
334
+ },
335
+ );
336
+ log?.info?.(
337
+ `[DingTalk][AICard] FINISHED 响应:status=${finishResp.status}`,
338
+ );
339
+ } catch (err: any) {
340
+ log?.error?.(`[DingTalk][AICard] FINISHED 更新失败:${err.message}`);
341
+ }
342
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 消息发送模块统一导出
3
+ */
4
+
5
+ export * from './send.ts';
6
+ export * from './card.ts';
7
+
8
+ // 兼容旧实现(`src/services/messaging.ts`)中仍被外部调用的 API。
9
+ // 注意:这里只显式导出函数,避免与 `send.ts/card.ts` 的类型/常量命名冲突。
10
+ export {
11
+ sendMessage,
12
+ sendProactive,
13
+ sendToUser,
14
+ sendToGroup,
15
+ sendTextToDingTalk,
16
+ sendMediaToDingTalk,
17
+ } from '../messaging.ts';
@@ -0,0 +1,141 @@
1
+ /**
2
+ * 消息发送基础模块
3
+ * 支持 Markdown、文本、链接等消息类型
4
+ */
5
+
6
+ import type { DingtalkConfig } from '../../types/index.ts';
7
+ import { DINGTALK_API, getAccessToken } from '../../utils/token.ts';
8
+ import { dingtalkHttp } from '../../utils/http-client.ts';
9
+
10
+ /** 消息类型枚举 */
11
+ export type DingTalkMsgType = 'text' | 'markdown' | 'link' | 'actionCard' | 'image';
12
+
13
+ /** 主动发送消息的结果 */
14
+ export interface SendResult {
15
+ ok: boolean;
16
+ processQueryKey?: string;
17
+ cardInstanceId?: string;
18
+ error?: string;
19
+ usedAICard?: boolean;
20
+ }
21
+
22
+ /** 主动发送选项 */
23
+ export interface ProactiveSendOptions {
24
+ msgType?: DingTalkMsgType;
25
+ replyToId?: string;
26
+ title?: string;
27
+ log?: any;
28
+ useAICard?: boolean;
29
+ fallbackToNormal?: boolean;
30
+ }
31
+
32
+ /**
33
+ * 发送 Markdown 消息
34
+ */
35
+ export async function sendMarkdownMessage(
36
+ config: DingtalkConfig,
37
+ sessionWebhook: string,
38
+ title: string,
39
+ markdown: string,
40
+ options: any = {},
41
+ ): Promise<any> {
42
+ const token = await getAccessToken(config);
43
+ let text = markdown;
44
+ if (options.atUserId) text = `${text} @${options.atUserId}`;
45
+
46
+ const body: any = {
47
+ msgtype: 'markdown',
48
+ markdown: {
49
+ title,
50
+ text: text,
51
+ },
52
+ };
53
+
54
+ if (options.atUserId) {
55
+ body.at = {
56
+ userIds: [options.atUserId],
57
+ isAtAll: false,
58
+ };
59
+ }
60
+
61
+ const resp = await dingtalkHttp.post(sessionWebhook, body, {
62
+ headers: {
63
+ 'x-acs-dingtalk-access-token': token,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ });
67
+
68
+ return resp.data;
69
+ }
70
+
71
+ /**
72
+ * 发送文本消息
73
+ */
74
+ export async function sendTextMessage(
75
+ config: DingtalkConfig,
76
+ sessionWebhook: string,
77
+ content: string,
78
+ options: any = {},
79
+ ): Promise<any> {
80
+ const token = await getAccessToken(config);
81
+ let text = content;
82
+ if (options.atUserId) text = `${text} @${options.atUserId}`;
83
+
84
+ const body: any = {
85
+ msgtype: 'text',
86
+ text: {
87
+ content: text,
88
+ },
89
+ };
90
+
91
+ if (options.atUserId) {
92
+ body.at = {
93
+ userIds: [options.atUserId],
94
+ isAtAll: false,
95
+ };
96
+ }
97
+
98
+ const resp = await dingtalkHttp.post(sessionWebhook, body, {
99
+ headers: {
100
+ 'x-acs-dingtalk-access-token': token,
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ });
104
+
105
+ return resp.data;
106
+ }
107
+
108
+ /**
109
+ * 发送链接消息
110
+ */
111
+ export async function sendLinkMessage(
112
+ config: DingtalkConfig,
113
+ sessionWebhook: string,
114
+ params: {
115
+ title: string;
116
+ text: string;
117
+ picUrl?: string;
118
+ messageUrl: string;
119
+ },
120
+ ): Promise<any> {
121
+ const token = await getAccessToken(config);
122
+
123
+ const body = {
124
+ msgtype: 'link',
125
+ link: {
126
+ title: params.title,
127
+ text: params.text,
128
+ picUrl: params.picUrl,
129
+ messageUrl: params.messageUrl,
130
+ },
131
+ };
132
+
133
+ const resp = await dingtalkHttp.post(sessionWebhook, body, {
134
+ headers: {
135
+ 'x-acs-dingtalk-access-token': token,
136
+ 'Content-Type': 'application/json',
137
+ },
138
+ });
139
+
140
+ return resp.data;
141
+ }