@yaoyuanchao/dingtalk 1.3.6 → 1.3.8
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/package.json +1 -1
- package/src/card-api.ts +573 -0
- package/src/config-schema.ts +19 -3
- package/src/monitor.ts +74 -0
package/package.json
CHANGED
package/src/card-api.ts
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DingTalk Interactive Card API
|
|
3
|
+
*
|
|
4
|
+
* 钉钉互动卡片 API 封装,支持:
|
|
5
|
+
* - 创建卡片实例
|
|
6
|
+
* - 投放卡片到会话
|
|
7
|
+
* - 普通更新卡片
|
|
8
|
+
* - 流式更新卡片(AI 打字机效果)
|
|
9
|
+
*
|
|
10
|
+
* 参考文档:
|
|
11
|
+
* - https://open.dingtalk.com/document/orgapp/api-streamingupdate
|
|
12
|
+
* - https://github.com/open-dingtalk/dingtalk-card-examples
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getDingTalkAccessToken } from "./api.js";
|
|
16
|
+
|
|
17
|
+
const DINGTALK_API_BASE = "https://api.dingtalk.com/v1.0";
|
|
18
|
+
|
|
19
|
+
/** HTTP POST with JSON body */
|
|
20
|
+
async function jsonPost(
|
|
21
|
+
url: string,
|
|
22
|
+
body: unknown,
|
|
23
|
+
headers?: Record<string, string>,
|
|
24
|
+
): Promise<any> {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
...headers,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(text);
|
|
37
|
+
} catch {
|
|
38
|
+
return { raw: text, status: response.status };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** HTTP PUT with JSON body */
|
|
43
|
+
async function jsonPut(
|
|
44
|
+
url: string,
|
|
45
|
+
body: unknown,
|
|
46
|
+
headers?: Record<string, string>,
|
|
47
|
+
): Promise<any> {
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method: "PUT",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
...headers,
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(body),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
} catch {
|
|
61
|
+
return { raw: text, status: response.status };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Card instance creation parameters */
|
|
66
|
+
export interface CreateCardInstanceParams {
|
|
67
|
+
clientId: string;
|
|
68
|
+
clientSecret: string;
|
|
69
|
+
cardTemplateId: string;
|
|
70
|
+
outTrackId: string;
|
|
71
|
+
cardData?: {
|
|
72
|
+
cardParamMap?: Record<string, string>;
|
|
73
|
+
cardMediaIdParamMap?: Record<string, string>;
|
|
74
|
+
};
|
|
75
|
+
robotCode?: string;
|
|
76
|
+
callbackType?: "STREAM" | "HTTP";
|
|
77
|
+
userIdType?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Card delivery parameters */
|
|
81
|
+
export interface DeliverCardParams {
|
|
82
|
+
clientId: string;
|
|
83
|
+
clientSecret: string;
|
|
84
|
+
outTrackId: string;
|
|
85
|
+
openSpaceId: string;
|
|
86
|
+
robotCode?: string;
|
|
87
|
+
/** For group delivery */
|
|
88
|
+
imGroupOpenDeliverModel?: {
|
|
89
|
+
robotCode: string;
|
|
90
|
+
atUserIds?: Record<string, string>;
|
|
91
|
+
};
|
|
92
|
+
/** For single chat delivery */
|
|
93
|
+
imRobotOpenDeliverModel?: {
|
|
94
|
+
spaceType: "IM_ROBOT";
|
|
95
|
+
robotCode: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Card update parameters */
|
|
100
|
+
export interface UpdateCardParams {
|
|
101
|
+
clientId: string;
|
|
102
|
+
clientSecret: string;
|
|
103
|
+
outTrackId: string;
|
|
104
|
+
cardData: {
|
|
105
|
+
cardParamMap?: Record<string, string>;
|
|
106
|
+
cardMediaIdParamMap?: Record<string, string>;
|
|
107
|
+
};
|
|
108
|
+
userIdType?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Streaming update parameters (AI typewriter effect) */
|
|
112
|
+
export interface StreamingUpdateParams {
|
|
113
|
+
clientId: string;
|
|
114
|
+
clientSecret: string;
|
|
115
|
+
outTrackId: string;
|
|
116
|
+
/** Unique identifier for this streaming session */
|
|
117
|
+
key: string;
|
|
118
|
+
/** Content to append */
|
|
119
|
+
content: string;
|
|
120
|
+
/** Whether this is the final update */
|
|
121
|
+
isFull?: boolean;
|
|
122
|
+
/** GUID for idempotency */
|
|
123
|
+
guid?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** API response with error info */
|
|
127
|
+
export interface CardApiResponse {
|
|
128
|
+
success: boolean;
|
|
129
|
+
result?: any;
|
|
130
|
+
errcode?: number;
|
|
131
|
+
errmsg?: string;
|
|
132
|
+
code?: string;
|
|
133
|
+
message?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a card instance
|
|
138
|
+
*
|
|
139
|
+
* API: POST /v1.0/card/instances
|
|
140
|
+
*/
|
|
141
|
+
export async function createCardInstance(
|
|
142
|
+
params: CreateCardInstanceParams,
|
|
143
|
+
): Promise<CardApiResponse> {
|
|
144
|
+
try {
|
|
145
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
146
|
+
|
|
147
|
+
const body: Record<string, any> = {
|
|
148
|
+
cardTemplateId: params.cardTemplateId,
|
|
149
|
+
outTrackId: params.outTrackId,
|
|
150
|
+
cardData: params.cardData || { cardParamMap: {} },
|
|
151
|
+
callbackType: params.callbackType || "STREAM",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (params.robotCode) {
|
|
155
|
+
body.robotCode = params.robotCode;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (params.userIdType !== undefined) {
|
|
159
|
+
body.userIdType = params.userIdType;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log("[dingtalk-card] createCardInstance request:", JSON.stringify(body, null, 2));
|
|
163
|
+
|
|
164
|
+
const res = await jsonPost(
|
|
165
|
+
`${DINGTALK_API_BASE}/card/instances`,
|
|
166
|
+
body,
|
|
167
|
+
{ "x-acs-dingtalk-access-token": token },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
console.log("[dingtalk-card] createCardInstance response:", JSON.stringify(res, null, 2));
|
|
171
|
+
|
|
172
|
+
if (res.code || res.errcode) {
|
|
173
|
+
console.warn("[dingtalk-card] Failed to create card instance:", {
|
|
174
|
+
success: false,
|
|
175
|
+
errcode: res.errcode,
|
|
176
|
+
errmsg: res.errmsg,
|
|
177
|
+
code: res.code,
|
|
178
|
+
message: res.message,
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
errcode: res.errcode,
|
|
183
|
+
errmsg: res.errmsg,
|
|
184
|
+
code: res.code,
|
|
185
|
+
message: res.message,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { success: true, result: res };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
message: String(err),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Add space to card instance
|
|
200
|
+
*
|
|
201
|
+
* API: POST /v1.0/card/instances/spaces
|
|
202
|
+
*/
|
|
203
|
+
export async function createCardSpace(
|
|
204
|
+
clientId: string,
|
|
205
|
+
clientSecret: string,
|
|
206
|
+
outTrackId: string,
|
|
207
|
+
openSpaceId: string,
|
|
208
|
+
): Promise<CardApiResponse> {
|
|
209
|
+
try {
|
|
210
|
+
const token = await getDingTalkAccessToken(clientId, clientSecret);
|
|
211
|
+
|
|
212
|
+
const res = await jsonPost(
|
|
213
|
+
`${DINGTALK_API_BASE}/card/instances/spaces`,
|
|
214
|
+
{ outTrackId, openSpaceId },
|
|
215
|
+
{ "x-acs-dingtalk-access-token": token },
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (res.code || res.errcode) {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
errcode: res.errcode,
|
|
222
|
+
errmsg: res.errmsg,
|
|
223
|
+
code: res.code,
|
|
224
|
+
message: res.message,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { success: true, result: res };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
message: String(err),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Deliver card to conversation
|
|
239
|
+
*
|
|
240
|
+
* API: POST /v1.0/card/instances/deliver
|
|
241
|
+
*/
|
|
242
|
+
export async function deliverCard(
|
|
243
|
+
params: DeliverCardParams,
|
|
244
|
+
): Promise<CardApiResponse> {
|
|
245
|
+
try {
|
|
246
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
247
|
+
|
|
248
|
+
const body: Record<string, any> = {
|
|
249
|
+
outTrackId: params.outTrackId,
|
|
250
|
+
openSpaceId: params.openSpaceId,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (params.imGroupOpenDeliverModel) {
|
|
254
|
+
body.imGroupOpenDeliverModel = params.imGroupOpenDeliverModel;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (params.imRobotOpenDeliverModel) {
|
|
258
|
+
body.imRobotOpenDeliverModel = params.imRobotOpenDeliverModel;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const res = await jsonPost(
|
|
262
|
+
`${DINGTALK_API_BASE}/card/instances/deliver`,
|
|
263
|
+
body,
|
|
264
|
+
{ "x-acs-dingtalk-access-token": token },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (res.code || res.errcode) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
errcode: res.errcode,
|
|
271
|
+
errmsg: res.errmsg,
|
|
272
|
+
code: res.code,
|
|
273
|
+
message: res.message,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { success: true, result: res };
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
message: String(err),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update card data
|
|
288
|
+
*
|
|
289
|
+
* API: PUT /v1.0/card/instances
|
|
290
|
+
*/
|
|
291
|
+
export async function updateCard(
|
|
292
|
+
params: UpdateCardParams,
|
|
293
|
+
): Promise<CardApiResponse> {
|
|
294
|
+
try {
|
|
295
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
296
|
+
|
|
297
|
+
const body: Record<string, any> = {
|
|
298
|
+
outTrackId: params.outTrackId,
|
|
299
|
+
cardData: params.cardData,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (params.userIdType !== undefined) {
|
|
303
|
+
body.userIdType = params.userIdType;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const res = await jsonPut(
|
|
307
|
+
`${DINGTALK_API_BASE}/card/instances`,
|
|
308
|
+
body,
|
|
309
|
+
{ "x-acs-dingtalk-access-token": token },
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (res.code || res.errcode) {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
errcode: res.errcode,
|
|
316
|
+
errmsg: res.errmsg,
|
|
317
|
+
code: res.code,
|
|
318
|
+
message: res.message,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { success: true, result: res };
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
message: String(err),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Streaming update for AI card (typewriter effect)
|
|
333
|
+
*
|
|
334
|
+
* API: PUT /v1.0/card/streaming
|
|
335
|
+
*
|
|
336
|
+
* Note: Requires Card.Streaming.Write permission
|
|
337
|
+
*/
|
|
338
|
+
export async function updateCardStreaming(
|
|
339
|
+
params: StreamingUpdateParams,
|
|
340
|
+
): Promise<CardApiResponse> {
|
|
341
|
+
try {
|
|
342
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
343
|
+
|
|
344
|
+
const body: Record<string, any> = {
|
|
345
|
+
outTrackId: params.outTrackId,
|
|
346
|
+
key: params.key,
|
|
347
|
+
content: params.content,
|
|
348
|
+
isFull: params.isFull ?? false,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if (params.guid) {
|
|
352
|
+
body.guid = params.guid;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const res = await jsonPut(
|
|
356
|
+
`${DINGTALK_API_BASE}/card/streaming`,
|
|
357
|
+
body,
|
|
358
|
+
{ "x-acs-dingtalk-access-token": token },
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (res.code || res.errcode) {
|
|
362
|
+
return {
|
|
363
|
+
success: false,
|
|
364
|
+
errcode: res.errcode,
|
|
365
|
+
errmsg: res.errmsg,
|
|
366
|
+
code: res.code,
|
|
367
|
+
message: res.message,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { success: true, result: res };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
message: String(err),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build openSpaceId for different scenarios
|
|
382
|
+
*/
|
|
383
|
+
export function buildOpenSpaceId(
|
|
384
|
+
type: "IM_GROUP" | "IM_ROBOT",
|
|
385
|
+
id: string,
|
|
386
|
+
): string {
|
|
387
|
+
// Format: dtv1.card//IM_GROUP.{openConversationId}
|
|
388
|
+
// or: dtv1.card//IM_ROBOT.{senderStaffId}
|
|
389
|
+
return `dtv1.card//${type}.${id}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Generate unique outTrackId
|
|
394
|
+
*/
|
|
395
|
+
export function generateOutTrackId(): string {
|
|
396
|
+
const timestamp = Date.now();
|
|
397
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
398
|
+
return `card_${timestamp}_${random}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* High-level: Send a card message to a conversation
|
|
403
|
+
*
|
|
404
|
+
* This combines createCardInstance + createCardSpace + deliverCard
|
|
405
|
+
*/
|
|
406
|
+
export async function sendCardMessage(params: {
|
|
407
|
+
clientId: string;
|
|
408
|
+
clientSecret: string;
|
|
409
|
+
robotCode: string;
|
|
410
|
+
cardTemplateId: string;
|
|
411
|
+
cardData: Record<string, string>;
|
|
412
|
+
/** For group chat */
|
|
413
|
+
openConversationId?: string;
|
|
414
|
+
/** For single chat (DM) */
|
|
415
|
+
senderStaffId?: string;
|
|
416
|
+
}): Promise<CardApiResponse & { outTrackId?: string }> {
|
|
417
|
+
const outTrackId = generateOutTrackId();
|
|
418
|
+
|
|
419
|
+
// Determine space type and ID
|
|
420
|
+
const isGroup = !!params.openConversationId;
|
|
421
|
+
const spaceType = isGroup ? "IM_GROUP" : "IM_ROBOT";
|
|
422
|
+
const spaceTargetId = isGroup ? params.openConversationId! : params.senderStaffId!;
|
|
423
|
+
const openSpaceId = buildOpenSpaceId(spaceType, spaceTargetId);
|
|
424
|
+
|
|
425
|
+
// Step 1: Create card instance
|
|
426
|
+
const createResult = await createCardInstance({
|
|
427
|
+
clientId: params.clientId,
|
|
428
|
+
clientSecret: params.clientSecret,
|
|
429
|
+
cardTemplateId: params.cardTemplateId,
|
|
430
|
+
outTrackId,
|
|
431
|
+
cardData: { cardParamMap: params.cardData },
|
|
432
|
+
robotCode: params.robotCode,
|
|
433
|
+
callbackType: "STREAM",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (!createResult.success) {
|
|
437
|
+
console.warn("[dingtalk-card] Failed to create card instance:", createResult);
|
|
438
|
+
return createResult;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Step 2: Add space
|
|
442
|
+
const spaceResult = await createCardSpace(
|
|
443
|
+
params.clientId,
|
|
444
|
+
params.clientSecret,
|
|
445
|
+
outTrackId,
|
|
446
|
+
openSpaceId,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (!spaceResult.success) {
|
|
450
|
+
console.warn("[dingtalk-card] Failed to create card space:", spaceResult);
|
|
451
|
+
return spaceResult;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Step 3: Deliver card
|
|
455
|
+
const deliverParams: DeliverCardParams = {
|
|
456
|
+
clientId: params.clientId,
|
|
457
|
+
clientSecret: params.clientSecret,
|
|
458
|
+
outTrackId,
|
|
459
|
+
openSpaceId,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
if (isGroup) {
|
|
463
|
+
deliverParams.imGroupOpenDeliverModel = {
|
|
464
|
+
robotCode: params.robotCode,
|
|
465
|
+
};
|
|
466
|
+
} else {
|
|
467
|
+
deliverParams.imRobotOpenDeliverModel = {
|
|
468
|
+
spaceType: "IM_ROBOT",
|
|
469
|
+
robotCode: params.robotCode,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const deliverResult = await deliverCard(deliverParams);
|
|
474
|
+
|
|
475
|
+
if (!deliverResult.success) {
|
|
476
|
+
console.warn("[dingtalk-card] Failed to deliver card:", deliverResult);
|
|
477
|
+
return deliverResult;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { success: true, outTrackId, result: deliverResult.result };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* High-level: Send an AI card with streaming support
|
|
485
|
+
*
|
|
486
|
+
* Returns a controller object for streaming updates
|
|
487
|
+
*/
|
|
488
|
+
export async function sendStreamingAICard(params: {
|
|
489
|
+
clientId: string;
|
|
490
|
+
clientSecret: string;
|
|
491
|
+
robotCode: string;
|
|
492
|
+
cardTemplateId: string;
|
|
493
|
+
/** Initial card data (use flowStatus="0" for loading state) */
|
|
494
|
+
initialData?: Record<string, string>;
|
|
495
|
+
/** Key for the streaming content variable in the template */
|
|
496
|
+
streamingKey?: string;
|
|
497
|
+
openConversationId?: string;
|
|
498
|
+
senderStaffId?: string;
|
|
499
|
+
}): Promise<{
|
|
500
|
+
success: boolean;
|
|
501
|
+
outTrackId?: string;
|
|
502
|
+
error?: string;
|
|
503
|
+
/** Update streaming content */
|
|
504
|
+
update: (content: string) => Promise<CardApiResponse>;
|
|
505
|
+
/** Mark streaming as complete */
|
|
506
|
+
finish: (finalContent: string) => Promise<CardApiResponse>;
|
|
507
|
+
}> {
|
|
508
|
+
const streamingKey = params.streamingKey || "content";
|
|
509
|
+
|
|
510
|
+
// Send initial card with loading state
|
|
511
|
+
const initialCardData: Record<string, string> = {
|
|
512
|
+
flowStatus: "0", // 0 = streaming in progress
|
|
513
|
+
...params.initialData,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const sendResult = await sendCardMessage({
|
|
517
|
+
clientId: params.clientId,
|
|
518
|
+
clientSecret: params.clientSecret,
|
|
519
|
+
robotCode: params.robotCode,
|
|
520
|
+
cardTemplateId: params.cardTemplateId,
|
|
521
|
+
cardData: initialCardData,
|
|
522
|
+
openConversationId: params.openConversationId,
|
|
523
|
+
senderStaffId: params.senderStaffId,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
if (!sendResult.success || !sendResult.outTrackId) {
|
|
527
|
+
return {
|
|
528
|
+
success: false,
|
|
529
|
+
error: sendResult.message || sendResult.errmsg || "Failed to send card",
|
|
530
|
+
update: async () => ({ success: false, message: "Card not initialized" }),
|
|
531
|
+
finish: async () => ({ success: false, message: "Card not initialized" }),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const outTrackId = sendResult.outTrackId;
|
|
536
|
+
|
|
537
|
+
// Return controller
|
|
538
|
+
return {
|
|
539
|
+
success: true,
|
|
540
|
+
outTrackId,
|
|
541
|
+
update: async (content: string) => {
|
|
542
|
+
return updateCardStreaming({
|
|
543
|
+
clientId: params.clientId,
|
|
544
|
+
clientSecret: params.clientSecret,
|
|
545
|
+
outTrackId,
|
|
546
|
+
key: streamingKey,
|
|
547
|
+
content,
|
|
548
|
+
isFull: false,
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
finish: async (finalContent: string) => {
|
|
552
|
+
// First update with final content
|
|
553
|
+
await updateCardStreaming({
|
|
554
|
+
clientId: params.clientId,
|
|
555
|
+
clientSecret: params.clientSecret,
|
|
556
|
+
outTrackId,
|
|
557
|
+
key: streamingKey,
|
|
558
|
+
content: finalContent,
|
|
559
|
+
isFull: true,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Then update flowStatus to complete
|
|
563
|
+
return updateCard({
|
|
564
|
+
clientId: params.clientId,
|
|
565
|
+
clientSecret: params.clientSecret,
|
|
566
|
+
outTrackId,
|
|
567
|
+
cardData: {
|
|
568
|
+
cardParamMap: { flowStatus: "1" }, // 1 = complete
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
package/src/config-schema.ts
CHANGED
|
@@ -9,8 +9,8 @@ export const groupPolicySchema = z.enum(['disabled', 'allowlist', 'open'], {
|
|
|
9
9
|
description: 'Group chat access control policy',
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
export const messageFormatSchema = z.enum(['text', 'markdown', 'richtext', 'auto'], {
|
|
13
|
-
description: 'Message format for bot responses (richtext is an alias for markdown, auto detects markdown features)',
|
|
12
|
+
export const messageFormatSchema = z.enum(['text', 'markdown', 'richtext', 'auto', 'card'], {
|
|
13
|
+
description: 'Message format for bot responses (richtext is an alias for markdown, auto detects markdown features, card uses interactive cards)',
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
// DingTalk 配置 Schema
|
|
@@ -61,9 +61,25 @@ export const dingTalkConfigSchema = z.object({
|
|
|
61
61
|
' - text: Plain text (recommended, supports tables)\n' +
|
|
62
62
|
' - markdown: DingTalk markdown (limited support, no tables)\n' +
|
|
63
63
|
' - richtext: Alias for markdown (deprecated, use markdown instead)\n' +
|
|
64
|
-
' - auto: Auto-detect markdown features in response'
|
|
64
|
+
' - auto: Auto-detect markdown features in response\n' +
|
|
65
|
+
' - card: Interactive card (requires cardTemplateId)'
|
|
65
66
|
),
|
|
66
67
|
|
|
68
|
+
// 卡片配置(当 messageFormat 为 card 时使用)
|
|
69
|
+
card: z.object({
|
|
70
|
+
templateId: z.string().min(1)
|
|
71
|
+
.describe('Card template ID from DingTalk card platform (e.g., "xxx.schema")'),
|
|
72
|
+
title: z.string().optional()
|
|
73
|
+
.describe('Card title (optional, if template has a title variable)'),
|
|
74
|
+
streamingEnabled: z.boolean().default(false)
|
|
75
|
+
.describe('Enable streaming update for AI typewriter effect (costs more API quota)'),
|
|
76
|
+
streamingKey: z.string().default('content')
|
|
77
|
+
.describe('Variable key for streaming content in the card template'),
|
|
78
|
+
fallbackToMarkdown: z.boolean().default(true)
|
|
79
|
+
.describe('Fall back to markdown when card delivery fails'),
|
|
80
|
+
}).optional()
|
|
81
|
+
.describe('Interactive card configuration (required when messageFormat is "card")'),
|
|
82
|
+
|
|
67
83
|
// 思考反馈
|
|
68
84
|
showThinking: z.boolean().default(false)
|
|
69
85
|
.describe('Send "正在思考..." feedback before AI responds'),
|
package/src/monitor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DingTalkRobotMessage, ResolvedDingTalkAccount, ExtractedMessage } from "./types.js";
|
|
2
2
|
import { sendViaSessionWebhook, sendMarkdownViaSessionWebhook, sendDingTalkRestMessage, batchGetUserInfo, downloadPicture, downloadMediaFile, cleanupOldMedia } from "./api.js";
|
|
3
|
+
import { sendCardMessage, sendStreamingAICard } from "./card-api.js";
|
|
3
4
|
import { getDingTalkRuntime } from "./runtime.js";
|
|
4
5
|
|
|
5
6
|
export interface DingTalkMonitorContext {
|
|
@@ -749,16 +750,89 @@ function resolveDeliverText(payload: any, log?: any): string | undefined {
|
|
|
749
750
|
return text || undefined;
|
|
750
751
|
}
|
|
751
752
|
|
|
753
|
+
/**
|
|
754
|
+
* Deliver reply using interactive card
|
|
755
|
+
* Returns true if card was sent successfully, false otherwise
|
|
756
|
+
*/
|
|
757
|
+
async function deliverCardReply(
|
|
758
|
+
target: any,
|
|
759
|
+
text: string,
|
|
760
|
+
cardConfig: { templateId: string; title?: string; streamingEnabled?: boolean; streamingKey?: string },
|
|
761
|
+
log?: any,
|
|
762
|
+
): Promise<boolean> {
|
|
763
|
+
try {
|
|
764
|
+
const { clientId, clientSecret, robotCode } = target.account;
|
|
765
|
+
|
|
766
|
+
if (!clientId || !clientSecret) {
|
|
767
|
+
log?.info?.("[dingtalk-card] Missing credentials for card delivery");
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const robotCodeValue = robotCode || clientId;
|
|
772
|
+
|
|
773
|
+
// Prepare card data - use 'content' as the main variable for markdown content
|
|
774
|
+
const isStreaming = cardConfig.streamingEnabled;
|
|
775
|
+
const cardData: Record<string, string> = {
|
|
776
|
+
content: text,
|
|
777
|
+
// flowStatus: "0" = streaming, "1" = complete
|
|
778
|
+
flowStatus: isStreaming ? "0" : "1",
|
|
779
|
+
// title: use configured title, or "完成" for non-streaming mode
|
|
780
|
+
title: cardConfig.title || (isStreaming ? "" : "完成"),
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
log?.info?.("[dingtalk-card] Sending card message, templateId=" + cardConfig.templateId);
|
|
784
|
+
|
|
785
|
+
const result = await sendCardMessage({
|
|
786
|
+
clientId,
|
|
787
|
+
clientSecret,
|
|
788
|
+
robotCode: robotCodeValue,
|
|
789
|
+
cardTemplateId: cardConfig.templateId,
|
|
790
|
+
cardData,
|
|
791
|
+
openConversationId: target.isDm ? undefined : target.conversationId,
|
|
792
|
+
senderStaffId: target.isDm ? target.senderId : undefined,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
if (result.success) {
|
|
796
|
+
log?.info?.("[dingtalk-card] Card sent successfully, outTrackId=" + result.outTrackId);
|
|
797
|
+
return true;
|
|
798
|
+
} else {
|
|
799
|
+
log?.info?.("[dingtalk-card] Card send failed: " + (result.message || result.errmsg || JSON.stringify(result)));
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
log?.info?.("[dingtalk-card] Card delivery error: " + (err instanceof Error ? err.message : String(err)));
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
752
808
|
async function deliverReply(target: any, text: string, log?: any): Promise<void> {
|
|
753
809
|
const now = Date.now();
|
|
754
810
|
const chunkLimit = 2000;
|
|
755
811
|
const messageFormat = target.account.config.messageFormat ?? "text";
|
|
812
|
+
const cardConfig = target.account.config.card;
|
|
813
|
+
|
|
814
|
+
// Handle card format
|
|
815
|
+
if (messageFormat === 'card' && cardConfig?.templateId) {
|
|
816
|
+
const cardSent = await deliverCardReply(target, text, cardConfig, log);
|
|
817
|
+
if (cardSent) return;
|
|
818
|
+
|
|
819
|
+
// Fallback to markdown if card failed and fallback is enabled
|
|
820
|
+
if (cardConfig.fallbackToMarkdown !== false) {
|
|
821
|
+
log?.info?.("[dingtalk] Card delivery failed, falling back to markdown");
|
|
822
|
+
} else {
|
|
823
|
+
log?.info?.("[dingtalk] Card delivery failed, no fallback configured");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
756
827
|
|
|
757
828
|
// Determine if this message should use markdown format
|
|
758
829
|
let isMarkdown: boolean;
|
|
759
830
|
if (messageFormat === 'auto') {
|
|
760
831
|
isMarkdown = detectMarkdownContent(text);
|
|
761
832
|
log?.info?.("[dingtalk] Auto-detected format: " + (isMarkdown ? "markdown" : "text"));
|
|
833
|
+
} else if (messageFormat === 'card') {
|
|
834
|
+
// Card failed, fallback to markdown
|
|
835
|
+
isMarkdown = true;
|
|
762
836
|
} else {
|
|
763
837
|
// Support both "markdown" and "richtext" (they're equivalent for DingTalk)
|
|
764
838
|
isMarkdown = messageFormat === "markdown" || messageFormat === "richtext";
|