@wu529778790/open-im 1.5.1 → 1.5.2-beta.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/README.md +3 -11
- package/dist/dingtalk/client.d.ts +16 -1
- package/dist/dingtalk/client.js +321 -7
- package/dist/dingtalk/event-handler.js +12 -4
- package/dist/dingtalk/message-sender.d.ts +3 -1
- package/dist/dingtalk/message-sender.js +157 -21
- package/dist/dingtalk/message-sender.test.js +29 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,18 +185,10 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
185
185
|
说明:钉钉当前采用“Stream Mode 收消息 + OpenAPI 发送消息”的混合模式。
|
|
186
186
|
|
|
187
187
|
- 会话内普通文本回复默认走 `sessionWebhook`
|
|
188
|
-
-
|
|
188
|
+
- 若配置了 `cardTemplateId`,会尝试 AI 助理 `prepare/update/finish` 流式卡片;失败则 fallback 为普通文本(自定义机器人/普通群场景下互动卡片 API 报 param.error,暂不支持单条流式更新)
|
|
189
189
|
- 启动/关闭通知会发给最近一次已互动的钉钉会话;如果服务冷启动后还没有任何钉钉会话互动过,则没有可用目标可发
|
|
190
190
|
|
|
191
|
-
钉钉 AI
|
|
192
|
-
|
|
193
|
-
- `title`
|
|
194
|
-
- `content`
|
|
195
|
-
- `note`
|
|
196
|
-
- `toolName`
|
|
197
|
-
- `status`
|
|
198
|
-
- `flowStatus`
|
|
199
|
-
- `displayText`
|
|
191
|
+
钉钉 AI 卡片模板:已适配官方「搜索结果卡片」模板,使用变量 `lastMessage`、`content`、`resources`、`users`、`flowStatus`。若使用该模板,无需修改模板即可实现流式更新。
|
|
200
192
|
|
|
201
193
|
## IM 内命令
|
|
202
194
|
|
|
@@ -233,7 +225,7 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
233
225
|
|
|
234
226
|
**钉钉无法回复**:确认应用已启用机器人 Stream Mode,并检查 `DINGTALK_CLIENT_ID`、`DINGTALK_CLIENT_SECRET` 或 `platforms.dingtalk` 配置是否正确。
|
|
235
227
|
|
|
236
|
-
|
|
228
|
+
**钉钉没有流式更新**:prepare 失败时 fallback 为普通文本回复。自定义机器人/普通群场景下,AI 助理和互动卡片 API 均不可用,仅支持单条文本回复。
|
|
237
229
|
|
|
238
230
|
**Cursor 报 `Authentication required`**:先执行 `agent login`,或在 `env` 中设置 `CURSOR_API_KEY`。
|
|
239
231
|
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type DWClientDownStream } from 'dingtalk-stream';
|
|
2
2
|
import type { Config } from '../config.js';
|
|
3
3
|
import type { DingTalkActiveTarget } from '../shared/active-chats.js';
|
|
4
|
+
export interface DingTalkStreamingTarget {
|
|
5
|
+
chatId: string;
|
|
6
|
+
conversationType?: string;
|
|
7
|
+
senderStaffId?: string;
|
|
8
|
+
senderId?: string;
|
|
9
|
+
robotCode?: string;
|
|
10
|
+
}
|
|
4
11
|
export declare function registerSessionWebhook(chatId: string, sessionWebhook: string): void;
|
|
5
12
|
export declare function sendText(chatId: string, content: string): Promise<unknown>;
|
|
6
13
|
export declare function sendMarkdown(chatId: string, title: string, text: string): Promise<unknown>;
|
|
@@ -8,6 +15,14 @@ export declare function ackMessage(messageId: string, result?: unknown): void;
|
|
|
8
15
|
export declare function initDingTalk(cfg: Config, eventHandler: (data: DWClientDownStream) => Promise<void>): Promise<void>;
|
|
9
16
|
export declare function stopDingTalk(): void;
|
|
10
17
|
export declare function sendProactiveText(target: string | DingTalkActiveTarget, content: string): Promise<void>;
|
|
11
|
-
export declare function prepareStreamingCard(
|
|
18
|
+
export declare function prepareStreamingCard(target: string | DingTalkStreamingTarget, templateId: string, cardData: Record<string, unknown>): Promise<string>;
|
|
12
19
|
export declare function updateStreamingCard(conversationToken: string, templateId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
13
20
|
export declare function finishStreamingCard(conversationToken: string): Promise<void>;
|
|
21
|
+
/** 创建并投放卡片(卡片平台 API,支持普通群流式更新) */
|
|
22
|
+
export declare function createAndDeliverCard(target: DingTalkStreamingTarget, templateId: string, outTrackId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
23
|
+
/** 更新卡片实例(用于流式更新) */
|
|
24
|
+
export declare function updateCardInstance(outTrackId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
25
|
+
/** 互动卡片普通版:发送(用于 prepare 失败时的 fallback 流式) */
|
|
26
|
+
export declare function sendRobotInteractiveCard(target: DingTalkStreamingTarget, cardBizId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
27
|
+
/** 互动卡片普通版:更新(单条消息流式更新) */
|
|
28
|
+
export declare function updateRobotInteractiveCard(cardBizId: string, cardData: Record<string, unknown>): Promise<void>;
|
package/dist/dingtalk/client.js
CHANGED
|
@@ -2,10 +2,12 @@ import { DWClient, TOPIC_ROBOT } from 'dingtalk-stream';
|
|
|
2
2
|
import { createLogger } from '../logger.js';
|
|
3
3
|
const log = createLogger('DingTalk');
|
|
4
4
|
const DINGTALK_OPENAPI_BASE = 'https://api.dingtalk.com';
|
|
5
|
+
const DINGTALK_OAPI_BASE = 'https://oapi.dingtalk.com';
|
|
5
6
|
const TEXT_MSG_KEY = 'sampleText';
|
|
6
7
|
let client = null;
|
|
7
8
|
let messageHandler = null;
|
|
8
9
|
const sessionWebhookByChat = new Map();
|
|
10
|
+
const unionIdByUserId = new Map();
|
|
9
11
|
function getClient() {
|
|
10
12
|
if (!client) {
|
|
11
13
|
throw new Error('DingTalk client not initialized');
|
|
@@ -115,9 +117,72 @@ async function callOpenApi(path, body) {
|
|
|
115
117
|
: text;
|
|
116
118
|
throw new Error(`DingTalk OpenAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
117
119
|
}
|
|
120
|
+
async function callOapi(path, body) {
|
|
121
|
+
const accessToken = await getClient().getAccessToken();
|
|
122
|
+
const res = await fetch(`${DINGTALK_OAPI_BASE}${path}?access_token=${encodeURIComponent(String(accessToken))}`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: {
|
|
125
|
+
'content-type': 'application/json',
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
signal: AbortSignal.timeout(30000),
|
|
129
|
+
});
|
|
130
|
+
const text = await res.text();
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
throw new Error(`DingTalk OAPI failed: ${res.status} ${text}`);
|
|
133
|
+
}
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error(`DingTalk OAPI returned non-JSON response: ${text}`);
|
|
140
|
+
}
|
|
141
|
+
const errorCode = parsed.errcode;
|
|
142
|
+
if (errorCode === 0 || errorCode === '0' || errorCode === undefined) {
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
const errorMessage = typeof parsed.errmsg === 'string'
|
|
146
|
+
? parsed.errmsg
|
|
147
|
+
: typeof parsed.message === 'string'
|
|
148
|
+
? parsed.message
|
|
149
|
+
: text;
|
|
150
|
+
throw new Error(`DingTalk OAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
151
|
+
}
|
|
118
152
|
function normalizeConversationType(type) {
|
|
119
153
|
return type?.trim().toLowerCase();
|
|
120
154
|
}
|
|
155
|
+
function isSingleConversation(type) {
|
|
156
|
+
const normalizedType = normalizeConversationType(type);
|
|
157
|
+
return (normalizedType === '0' ||
|
|
158
|
+
normalizedType === 'single' ||
|
|
159
|
+
normalizedType === 'singlechat' ||
|
|
160
|
+
normalizedType === 'oto');
|
|
161
|
+
}
|
|
162
|
+
function isGroupConversation(type) {
|
|
163
|
+
const normalizedType = normalizeConversationType(type);
|
|
164
|
+
return (normalizedType === '1' ||
|
|
165
|
+
normalizedType === '2' ||
|
|
166
|
+
normalizedType === 'group' ||
|
|
167
|
+
normalizedType === 'groupchat');
|
|
168
|
+
}
|
|
169
|
+
async function resolveUnionIdByUserId(userId) {
|
|
170
|
+
if (!userId)
|
|
171
|
+
return undefined;
|
|
172
|
+
const cached = unionIdByUserId.get(userId);
|
|
173
|
+
if (cached)
|
|
174
|
+
return cached;
|
|
175
|
+
const result = await callOapi('/topapi/v2/user/get', {
|
|
176
|
+
userid: userId,
|
|
177
|
+
language: 'zh_CN',
|
|
178
|
+
});
|
|
179
|
+
const unionId = result.result?.unionid;
|
|
180
|
+
if (typeof unionId === 'string' && unionId.length > 0) {
|
|
181
|
+
unionIdByUserId.set(userId, unionId);
|
|
182
|
+
return unionId;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
121
186
|
function buildProactiveAttempts(target, content) {
|
|
122
187
|
const robotCode = getRobotCode(target);
|
|
123
188
|
const payload = buildTextPayload(content);
|
|
@@ -214,6 +279,7 @@ export function stopDingTalk() {
|
|
|
214
279
|
}
|
|
215
280
|
finally {
|
|
216
281
|
sessionWebhookByChat.clear();
|
|
282
|
+
unionIdByUserId.clear();
|
|
217
283
|
client = null;
|
|
218
284
|
messageHandler = null;
|
|
219
285
|
log.info('DingTalk client stopped');
|
|
@@ -236,19 +302,107 @@ export async function sendProactiveText(target, content) {
|
|
|
236
302
|
}
|
|
237
303
|
catch (err) {
|
|
238
304
|
lastError = err;
|
|
239
|
-
|
|
305
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
306
|
+
if (msg.includes('robot') || msg.includes('resource.not.found')) {
|
|
307
|
+
log.debug(`DingTalk proactive ${attempt.label} send failed:`, err);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
log.warn(`DingTalk proactive ${attempt.label} send failed:`, err);
|
|
311
|
+
}
|
|
240
312
|
}
|
|
241
313
|
}
|
|
242
314
|
throw lastError instanceof Error
|
|
243
315
|
? lastError
|
|
244
316
|
: new Error(`DingTalk proactive send failed for chat ${target.chatId}`);
|
|
245
317
|
}
|
|
246
|
-
export async function prepareStreamingCard(
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
318
|
+
export async function prepareStreamingCard(target, templateId, cardData) {
|
|
319
|
+
const normalizedTarget = typeof target === 'string' ? { chatId: target } : target;
|
|
320
|
+
const contentType = 'ai_card';
|
|
321
|
+
const content = buildAiCardContent(templateId, cardData);
|
|
322
|
+
const attempts = [];
|
|
323
|
+
log.debug(`DingTalk prepare: conversationType=${normalizedTarget.conversationType ?? 'undefined'}, senderStaffId=${normalizedTarget.senderStaffId ?? 'undefined'}`);
|
|
324
|
+
if (isSingleConversation(normalizedTarget.conversationType)) {
|
|
325
|
+
let unionId;
|
|
326
|
+
try {
|
|
327
|
+
unionId = await resolveUnionIdByUserId(normalizedTarget.senderStaffId);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
log.debug('Failed to resolve DingTalk unionId from senderStaffId:', err);
|
|
331
|
+
}
|
|
332
|
+
if (unionId) {
|
|
333
|
+
attempts.push({
|
|
334
|
+
label: 'single-unionid',
|
|
335
|
+
body: { unionId, contentType, content },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (normalizedTarget.chatId) {
|
|
339
|
+
attempts.push({
|
|
340
|
+
label: 'single-chatid',
|
|
341
|
+
body: { openConversationId: normalizedTarget.chatId, contentType, content },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (isGroupConversation(normalizedTarget.conversationType)) {
|
|
346
|
+
// 群聊时也优先尝试 unionId:部分场景下 conversationType 可能误报,或单聊被识别为群聊
|
|
347
|
+
let unionId;
|
|
348
|
+
try {
|
|
349
|
+
unionId = await resolveUnionIdByUserId(normalizedTarget.senderStaffId);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
log.debug('Failed to resolve DingTalk unionId for group (fallback):', err);
|
|
353
|
+
}
|
|
354
|
+
if (unionId) {
|
|
355
|
+
attempts.push({
|
|
356
|
+
label: 'group-unionid',
|
|
357
|
+
body: { unionId, contentType, content },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (normalizedTarget.chatId) {
|
|
361
|
+
attempts.push({
|
|
362
|
+
label: 'group-chatid',
|
|
363
|
+
body: { openConversationId: normalizedTarget.chatId, contentType, content },
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
let unionId;
|
|
369
|
+
try {
|
|
370
|
+
unionId = await resolveUnionIdByUserId(normalizedTarget.senderStaffId);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
log.debug('Failed to resolve DingTalk unionId for unknown conversation type:', err);
|
|
374
|
+
}
|
|
375
|
+
if (unionId) {
|
|
376
|
+
attempts.push({
|
|
377
|
+
label: 'unknown-unionid',
|
|
378
|
+
body: { unionId, contentType, content },
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (normalizedTarget.chatId) {
|
|
382
|
+
attempts.push({
|
|
383
|
+
label: 'unknown-chatid',
|
|
384
|
+
body: { openConversationId: normalizedTarget.chatId, contentType, content },
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (attempts.length === 0) {
|
|
389
|
+
throw new Error('DingTalk prepare target is incomplete');
|
|
390
|
+
}
|
|
391
|
+
let result;
|
|
392
|
+
let lastError;
|
|
393
|
+
for (const attempt of attempts) {
|
|
394
|
+
try {
|
|
395
|
+
result = await callOpenApi('/v1.0/aiInteraction/prepare', attempt.body);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
lastError = err;
|
|
400
|
+
log.debug(`DingTalk prepare attempt failed (${attempt.label}):`, err);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!result) {
|
|
404
|
+
throw lastError instanceof Error ? lastError : new Error('DingTalk prepare failed');
|
|
405
|
+
}
|
|
252
406
|
const token = result.result?.conversationToken;
|
|
253
407
|
if (typeof token !== 'string' || token.length === 0) {
|
|
254
408
|
throw new Error(`DingTalk prepare did not return conversationToken: ${JSON.stringify(result)}`);
|
|
@@ -275,3 +429,163 @@ export async function finishStreamingCard(conversationToken) {
|
|
|
275
429
|
throw new Error(`DingTalk finish returned success=false: ${JSON.stringify(result)}`);
|
|
276
430
|
}
|
|
277
431
|
}
|
|
432
|
+
/** 创建并投放卡片(卡片平台 API,支持普通群流式更新) */
|
|
433
|
+
export async function createAndDeliverCard(target, templateId, outTrackId, cardData) {
|
|
434
|
+
const { chatId, robotCode, conversationType, senderStaffId } = target;
|
|
435
|
+
if (!robotCode) {
|
|
436
|
+
throw new Error('DingTalk robotCode required for createAndDeliver');
|
|
437
|
+
}
|
|
438
|
+
const isSingle = isSingleConversation(conversationType);
|
|
439
|
+
const cardParamMap = buildCardParamMap(cardData);
|
|
440
|
+
if (!cardParamMap.content && !cardParamMap.lastMessage)
|
|
441
|
+
cardParamMap.content = '...';
|
|
442
|
+
const lastMsg = String(cardData.lastMessage ?? cardData.displayText ?? cardData.content ?? cardData.title ?? 'AI').slice(0, 50);
|
|
443
|
+
const body = {
|
|
444
|
+
userId: senderStaffId ?? 'system',
|
|
445
|
+
cardTemplateId: templateId,
|
|
446
|
+
outTrackId,
|
|
447
|
+
cardData: { cardParamMap },
|
|
448
|
+
};
|
|
449
|
+
if (isSingle && senderStaffId) {
|
|
450
|
+
body.openSpaceId = `dtv1.card//im_robot.${senderStaffId}`;
|
|
451
|
+
body.imRobotOpenSpaceModel = {
|
|
452
|
+
lastMessageI18n: { zh_CN: lastMsg },
|
|
453
|
+
searchSupport: { searchIcon: '', searchTypeName: '消息', searchDesc: '' },
|
|
454
|
+
notification: { alertContent: lastMsg },
|
|
455
|
+
};
|
|
456
|
+
body.imRobotOpenDeliverModel = { spaceType: 'IM_ROBOT' };
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
body.openSpaceId = `dtv1.card//im_group.${chatId}`;
|
|
460
|
+
body.imGroupOpenSpaceModel = {
|
|
461
|
+
lastMessageI18n: { zh_CN: lastMsg },
|
|
462
|
+
searchSupport: { searchIcon: '', searchTypeName: '消息', searchDesc: '' },
|
|
463
|
+
notification: { alertContent: lastMsg },
|
|
464
|
+
};
|
|
465
|
+
body.imGroupOpenDeliverModel = {
|
|
466
|
+
robotCode,
|
|
467
|
+
atUserIds: {},
|
|
468
|
+
recipients: [],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
await callOpenApiWithMethod('POST', '/v1.0/card/instances/createAndDeliver', body);
|
|
472
|
+
}
|
|
473
|
+
/** 将 cardData 转为 cardParamMap(对象/数组需 JSON 序列化) */
|
|
474
|
+
function buildCardParamMap(cardData) {
|
|
475
|
+
const cardParamMap = {};
|
|
476
|
+
for (const [k, v] of Object.entries(cardData)) {
|
|
477
|
+
if (v === undefined || v === null)
|
|
478
|
+
continue;
|
|
479
|
+
if (typeof v === 'object') {
|
|
480
|
+
cardParamMap[k] = JSON.stringify(v);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
cardParamMap[k] = String(v);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return cardParamMap;
|
|
487
|
+
}
|
|
488
|
+
/** 更新卡片实例(用于流式更新) */
|
|
489
|
+
export async function updateCardInstance(outTrackId, cardData) {
|
|
490
|
+
const cardParamMap = buildCardParamMap(cardData);
|
|
491
|
+
await callOpenApiWithMethod('PUT', '/v1.0/card/instances', {
|
|
492
|
+
outTrackId,
|
|
493
|
+
cardData: { cardParamMap },
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/** StandardCard 模板结构(与钉钉官方 Go 打字机示例完全一致:text=标题,markdown=内容) */
|
|
497
|
+
function buildStandardCardData(cardData) {
|
|
498
|
+
const title = String(cardData.title ?? 'AI');
|
|
499
|
+
const content = String(cardData.content ?? cardData.displayText ?? '').trim() || '...';
|
|
500
|
+
const schema = {
|
|
501
|
+
config: { autoLayout: true, enableForward: true },
|
|
502
|
+
header: {
|
|
503
|
+
title: { type: 'text', text: title },
|
|
504
|
+
logo: '@lALPDfJ6V_FPDmvNAfTNAfQ',
|
|
505
|
+
},
|
|
506
|
+
contents: [
|
|
507
|
+
{ type: 'text', text: title, id: 'text_1693929551595' },
|
|
508
|
+
{ type: 'divider', id: 'divider_1693929551595' },
|
|
509
|
+
{ type: 'markdown', text: content, id: 'markdown_1693929674245' },
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
return JSON.stringify(schema);
|
|
513
|
+
}
|
|
514
|
+
/** 互动卡片普通版:发送(用于 prepare 失败时的 fallback 流式) */
|
|
515
|
+
export async function sendRobotInteractiveCard(target, cardBizId, cardData) {
|
|
516
|
+
const { chatId, robotCode, conversationType } = target;
|
|
517
|
+
if (!robotCode) {
|
|
518
|
+
throw new Error('DingTalk robotCode required for interactive card');
|
|
519
|
+
}
|
|
520
|
+
const cardDataStr = buildStandardCardData(cardData);
|
|
521
|
+
const isSingle = isSingleConversation(conversationType);
|
|
522
|
+
const body = {
|
|
523
|
+
cardTemplateId: 'StandardCard',
|
|
524
|
+
cardBizId,
|
|
525
|
+
outTrackId: cardBizId,
|
|
526
|
+
robotCode,
|
|
527
|
+
cardData: cardDataStr,
|
|
528
|
+
};
|
|
529
|
+
if (isSingle && target.senderStaffId) {
|
|
530
|
+
body.singleChatReceiver = JSON.stringify({ userid: target.senderStaffId });
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
body.openConversationId = chatId;
|
|
534
|
+
}
|
|
535
|
+
log.debug(`DingTalk sendRobotInteractiveCard: isSingle=${isSingle}, robotCode=${robotCode?.slice(0, 8)}..., chatIdLen=${chatId?.length}`);
|
|
536
|
+
try {
|
|
537
|
+
await callOpenApiWithMethod('POST', '/v1.0/im/v1.0/robot/interactiveCards/send', body);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
541
|
+
if (msg.includes('param.error') || msg.includes('参数无效')) {
|
|
542
|
+
log.warn('DingTalk robot interactive card param.error - request body (no secrets):', JSON.stringify({ ...body, robotCode: body.robotCode ? '[REDACTED]' : undefined }, null, 2));
|
|
543
|
+
}
|
|
544
|
+
throw err;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** 互动卡片普通版:更新(单条消息流式更新) */
|
|
548
|
+
export async function updateRobotInteractiveCard(cardBizId, cardData) {
|
|
549
|
+
const cardDataStr = buildStandardCardData(cardData);
|
|
550
|
+
const body = { cardBizId, cardData: cardDataStr };
|
|
551
|
+
await callOpenApiWithMethod('PUT', '/v1.0/im/robots/interactiveCards', body);
|
|
552
|
+
}
|
|
553
|
+
async function callOpenApiWithMethod(method, path, body) {
|
|
554
|
+
const accessToken = await getClient().getAccessToken();
|
|
555
|
+
const res = await fetch(`${DINGTALK_OPENAPI_BASE}${path}`, {
|
|
556
|
+
method,
|
|
557
|
+
headers: {
|
|
558
|
+
'content-type': 'application/json',
|
|
559
|
+
'x-acs-dingtalk-access-token': String(accessToken),
|
|
560
|
+
},
|
|
561
|
+
body: JSON.stringify(body),
|
|
562
|
+
signal: AbortSignal.timeout(30000),
|
|
563
|
+
});
|
|
564
|
+
const text = await res.text();
|
|
565
|
+
if (!res.ok) {
|
|
566
|
+
throw new Error(`DingTalk OpenAPI failed: ${res.status} ${text}`);
|
|
567
|
+
}
|
|
568
|
+
let parsed;
|
|
569
|
+
try {
|
|
570
|
+
parsed = JSON.parse(text);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return text;
|
|
574
|
+
}
|
|
575
|
+
const errorCode = parsed.errorcode ?? parsed.errcode;
|
|
576
|
+
const success = parsed.success;
|
|
577
|
+
if (errorCode === 0 ||
|
|
578
|
+
errorCode === '0' ||
|
|
579
|
+
success === true ||
|
|
580
|
+
(errorCode === undefined && success === undefined)) {
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
const errorMessage = typeof parsed.errmsg === 'string'
|
|
584
|
+
? parsed.errmsg
|
|
585
|
+
: typeof parsed.errormsg === 'string'
|
|
586
|
+
? parsed.errormsg
|
|
587
|
+
: typeof parsed.message === 'string'
|
|
588
|
+
? parsed.message
|
|
589
|
+
: text;
|
|
590
|
+
throw new Error(`DingTalk OpenAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
591
|
+
}
|
|
@@ -24,6 +24,7 @@ function parseRobotMessage(data) {
|
|
|
24
24
|
export function setupDingTalkHandlers(config, sessionManager) {
|
|
25
25
|
configureDingTalkMessageSender({
|
|
26
26
|
cardTemplateId: config.dingtalkCardTemplateId,
|
|
27
|
+
robotCodeFallback: config.dingtalkClientId,
|
|
27
28
|
});
|
|
28
29
|
if (config.dingtalkCardTemplateId) {
|
|
29
30
|
log.info('DingTalk AI card streaming enabled');
|
|
@@ -43,7 +44,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
43
44
|
getRunningTasksSize: () => runningTasks.size,
|
|
44
45
|
});
|
|
45
46
|
registerPermissionSender('dingtalk', { sendTextReply, sendPermissionCard });
|
|
46
|
-
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
47
|
+
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, dingtalkTarget) {
|
|
47
48
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
48
49
|
const toolAdapter = getAdapter(config.aiCommand);
|
|
49
50
|
if (!toolAdapter) {
|
|
@@ -55,7 +56,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
55
56
|
: undefined;
|
|
56
57
|
log.info(`[AI_REQUEST] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
57
58
|
const toolId = config.aiCommand;
|
|
58
|
-
const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
|
|
59
|
+
const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, dingtalkTarget);
|
|
59
60
|
const stopTyping = startTypingLoop(chatId);
|
|
60
61
|
const taskKey = `${userId}:${msgId}`;
|
|
61
62
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'dingtalk', taskKey }, prompt, toolAdapter, {
|
|
@@ -112,7 +113,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
112
113
|
chatId,
|
|
113
114
|
userId,
|
|
114
115
|
conversationType: robotMessage.conversationType,
|
|
115
|
-
robotCode: robotMessage.robotCode,
|
|
116
|
+
robotCode: robotMessage.robotCode || config.dingtalkClientId,
|
|
116
117
|
});
|
|
117
118
|
setChatUser(chatId, userId, 'dingtalk');
|
|
118
119
|
try {
|
|
@@ -127,8 +128,15 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
127
128
|
}
|
|
128
129
|
const workDir = sessionManager.getWorkDir(userId);
|
|
129
130
|
const convId = sessionManager.getConvId(userId);
|
|
131
|
+
const dingtalkTarget = {
|
|
132
|
+
chatId,
|
|
133
|
+
conversationType: robotMessage.conversationType,
|
|
134
|
+
senderStaffId: robotMessage.senderStaffId,
|
|
135
|
+
senderId: robotMessage.senderId,
|
|
136
|
+
robotCode: robotMessage.robotCode || config.dingtalkClientId,
|
|
137
|
+
};
|
|
130
138
|
const enqueueResult = requestQueue.enqueue(userId, convId, text, async (prompt) => {
|
|
131
|
-
await handleAIRequest(userId, chatId, prompt, workDir, convId);
|
|
139
|
+
await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, undefined, dingtalkTarget);
|
|
132
140
|
});
|
|
133
141
|
if (enqueueResult === 'rejected') {
|
|
134
142
|
await sendTextReply(chatId, '请求队列已满,请稍后再试。');
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { DingTalkStreamingTarget } from './client.js';
|
|
1
2
|
import type { ThreadContext } from '../shared/types.js';
|
|
2
3
|
import type { DingTalkActiveTarget } from '../shared/active-chats.js';
|
|
3
4
|
export type MessageStatus = 'thinking' | 'streaming' | 'done' | 'error';
|
|
4
5
|
interface SenderSettings {
|
|
5
6
|
cardTemplateId?: string;
|
|
7
|
+
robotCodeFallback?: string;
|
|
6
8
|
}
|
|
7
9
|
export declare function configureDingTalkMessageSender(settings: SenderSettings): void;
|
|
8
|
-
export declare function sendThinkingMessage(chatId: string, _replyToMessageId?: string, toolId?: string): Promise<string>;
|
|
10
|
+
export declare function sendThinkingMessage(chatId: string, _replyToMessageId?: string, toolId?: string, target?: DingTalkStreamingTarget): Promise<string>;
|
|
9
11
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
10
12
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
11
13
|
export declare function sendErrorMessage(chatId: string, messageId: string, error: string, toolId?: string): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
|
-
import { sendText, sendProactiveText, prepareStreamingCard, updateStreamingCard, finishStreamingCard, } from './client.js';
|
|
3
|
+
import { sendText, sendProactiveText, prepareStreamingCard, updateStreamingCard, finishStreamingCard, createAndDeliverCard, updateCardInstance, sendRobotInteractiveCard, updateRobotInteractiveCard, } from './client.js';
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
5
|
import { splitLongContent, getAIToolDisplayName } from '../shared/utils.js';
|
|
6
6
|
import { listDirectories, buildDirectoryKeyboard } from '../commands/handler.js';
|
|
@@ -26,6 +26,7 @@ function generateMessageId() {
|
|
|
26
26
|
export function configureDingTalkMessageSender(settings) {
|
|
27
27
|
senderSettings = {
|
|
28
28
|
cardTemplateId: settings.cardTemplateId?.trim(),
|
|
29
|
+
robotCodeFallback: settings.robotCodeFallback?.trim(),
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
function getCardTemplateId() {
|
|
@@ -56,20 +57,39 @@ function getToolTitle(toolId, status) {
|
|
|
56
57
|
return `${toolName} - 执行中`;
|
|
57
58
|
return `${toolName} - 错误`;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* 适配钉钉官方「搜索结果卡片」模板变量结构
|
|
62
|
+
* 变量:lastMessage, content, resources, users, flowStatus
|
|
63
|
+
*/
|
|
59
64
|
function buildCardData(content, status, note, toolId = 'claude') {
|
|
60
65
|
const toolName = getAIToolDisplayName(toolId);
|
|
61
66
|
const safeContent = content.trim() || (status === 'thinking' ? '正在思考,请稍候...' : status === 'error' ? '执行失败' : '...');
|
|
62
67
|
const safeNote = note?.trim() || '';
|
|
68
|
+
// lastMessage: 卡片摘要,用于会话列表预览
|
|
69
|
+
const lastMessage = safeContent.length > 50 ? `${safeContent.slice(0, 47)}...` : safeContent || getToolTitle(toolId, status);
|
|
70
|
+
// resources: 对象数组,note 作为来源列表(如 "1. xxx\n2. yyy" 按行解析)
|
|
71
|
+
const resources = [];
|
|
72
|
+
if (safeNote) {
|
|
73
|
+
for (const line of safeNote.split('\n')) {
|
|
74
|
+
const t = line.replace(/^\d+\.\s*/, '').trim();
|
|
75
|
+
if (t)
|
|
76
|
+
resources.push({ title: t });
|
|
77
|
+
}
|
|
78
|
+
if (resources.length === 0)
|
|
79
|
+
resources.push({ title: safeNote });
|
|
80
|
+
}
|
|
63
81
|
return {
|
|
64
|
-
|
|
82
|
+
lastMessage,
|
|
65
83
|
content: safeContent,
|
|
84
|
+
resources,
|
|
85
|
+
users: [],
|
|
86
|
+
flowStatus: FLOW_STATUS[status],
|
|
87
|
+
// 保留兼容字段
|
|
66
88
|
note: safeNote,
|
|
67
|
-
toolName,
|
|
68
89
|
status,
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
toolName,
|
|
91
|
+
title: getToolTitle(toolId, status),
|
|
71
92
|
displayText: formatMessage(safeContent, status, safeNote, toolId),
|
|
72
|
-
updatedAt: new Date().toISOString(),
|
|
73
93
|
};
|
|
74
94
|
}
|
|
75
95
|
async function tryFinishCard(conversationToken) {
|
|
@@ -99,33 +119,99 @@ async function sendTextWithRetry(chatId, text, retries = 1) {
|
|
|
99
119
|
}
|
|
100
120
|
throw lastError;
|
|
101
121
|
}
|
|
102
|
-
export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'claude') {
|
|
122
|
+
export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'claude', target) {
|
|
103
123
|
const messageId = generateMessageId();
|
|
104
124
|
const templateId = getCardTemplateId();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
const robotCode = target?.robotCode || senderSettings.robotCodeFallback;
|
|
126
|
+
// 1. 优先尝试互动卡片普通版(机器人适用,无需 AI 助理权限)
|
|
127
|
+
if (robotCode) {
|
|
128
|
+
try {
|
|
129
|
+
const effectiveTarget = target
|
|
130
|
+
? { ...target, robotCode }
|
|
131
|
+
: { chatId, robotCode };
|
|
132
|
+
const cardBizId = messageId;
|
|
133
|
+
await sendRobotInteractiveCard(effectiveTarget, cardBizId, buildCardData('', 'thinking', '请稍候', toolId));
|
|
134
|
+
streamStates.set(messageId, {
|
|
135
|
+
chatId,
|
|
136
|
+
mode: 'interactiveCard',
|
|
137
|
+
cardBizId,
|
|
138
|
+
toolId,
|
|
139
|
+
target,
|
|
140
|
+
});
|
|
141
|
+
return messageId;
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
log.debug('DingTalk 互动卡片普通版失败,尝试其他方式:', err);
|
|
145
|
+
}
|
|
110
146
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
147
|
+
// 2. 尝试 AI 助理 prepare(需 AI 助理会话)或 createAndDeliver(高级版)
|
|
148
|
+
if (templateId) {
|
|
149
|
+
try {
|
|
150
|
+
const conversationToken = await prepareStreamingCard(target ?? chatId, templateId, buildCardData('', 'thinking', '请稍候', toolId));
|
|
151
|
+
streamStates.set(messageId, { chatId, mode: 'card', conversationToken, toolId, target });
|
|
152
|
+
return messageId;
|
|
153
|
+
}
|
|
154
|
+
catch (prepareErr) {
|
|
155
|
+
log.debug('DingTalk prepare failed, trying createAndDeliver:', prepareErr);
|
|
156
|
+
if (robotCode) {
|
|
157
|
+
try {
|
|
158
|
+
const effectiveTarget = target
|
|
159
|
+
? { ...target, robotCode }
|
|
160
|
+
: { chatId, robotCode };
|
|
161
|
+
await createAndDeliverCard(effectiveTarget, templateId, messageId, buildCardData('', 'thinking', '请稍候', toolId));
|
|
162
|
+
streamStates.set(messageId, {
|
|
163
|
+
chatId,
|
|
164
|
+
mode: 'cardInstance',
|
|
165
|
+
outTrackId: messageId,
|
|
166
|
+
toolId,
|
|
167
|
+
target,
|
|
168
|
+
});
|
|
169
|
+
return messageId;
|
|
170
|
+
}
|
|
171
|
+
catch (cardErr) {
|
|
172
|
+
log.debug('DingTalk createAndDeliver failed:', cardErr);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
114
176
|
}
|
|
177
|
+
streamStates.set(messageId, { chatId, mode: 'text', toolId, target });
|
|
178
|
+
log.info('DingTalk 流式卡片不可用,将使用普通文本回复');
|
|
115
179
|
return messageId;
|
|
116
180
|
}
|
|
117
181
|
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
118
182
|
void chatId;
|
|
119
|
-
const templateId = getCardTemplateId();
|
|
120
183
|
const state = streamStates.get(messageId);
|
|
121
|
-
if (!
|
|
184
|
+
if (!state)
|
|
185
|
+
return;
|
|
186
|
+
if (state.mode === 'card' && state.conversationToken) {
|
|
187
|
+
const templateId = getCardTemplateId();
|
|
188
|
+
if (!templateId)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
await updateStreamingCard(state.conversationToken, templateId, buildCardData(content, status, note, toolId));
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
log.warn('Failed to update DingTalk streaming card:', err);
|
|
195
|
+
}
|
|
122
196
|
return;
|
|
123
197
|
}
|
|
124
|
-
|
|
125
|
-
|
|
198
|
+
if (state.mode === 'cardInstance' && state.outTrackId) {
|
|
199
|
+
try {
|
|
200
|
+
await updateCardInstance(state.outTrackId, buildCardData(content, status, note, toolId));
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
log.warn('Failed to update DingTalk card instance:', err);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
126
206
|
}
|
|
127
|
-
|
|
128
|
-
|
|
207
|
+
if (state.mode === 'interactiveCard' && state.cardBizId) {
|
|
208
|
+
try {
|
|
209
|
+
await updateRobotInteractiveCard(state.cardBizId, buildCardData(content, status, note, toolId));
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
log.warn('Failed to update DingTalk interactive card:', err);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
129
215
|
}
|
|
130
216
|
}
|
|
131
217
|
export async function sendFinalMessages(chatId, messageId, fullContent, note, toolId = 'claude') {
|
|
@@ -161,6 +247,36 @@ export async function sendFinalMessages(chatId, messageId, fullContent, note, to
|
|
|
161
247
|
await tryFinishCard(state.conversationToken);
|
|
162
248
|
}
|
|
163
249
|
}
|
|
250
|
+
if (templateId && state?.mode === 'cardInstance' && state.outTrackId) {
|
|
251
|
+
try {
|
|
252
|
+
const cardNote = parts.length > 1 ? `内容较长,后续将继续发送 (${1}/${parts.length})` : note;
|
|
253
|
+
await updateCardInstance(state.outTrackId, buildCardData(parts[0], 'done', cardNote, toolId));
|
|
254
|
+
streamStates.delete(messageId);
|
|
255
|
+
for (let i = 1; i < parts.length; i++) {
|
|
256
|
+
const partNote = i === parts.length - 1 ? note : `继续输出 (${i + 1}/${parts.length})`;
|
|
257
|
+
await sendTextWithRetry(chatId, formatMessage(parts[i], 'done', partNote, toolId));
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
log.warn('Failed to finalize DingTalk card instance, falling back to text:', err);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (state?.mode === 'interactiveCard' && state.cardBizId) {
|
|
266
|
+
try {
|
|
267
|
+
const cardNote = parts.length > 1 ? `内容较长,后续将继续发送 (${1}/${parts.length})` : note;
|
|
268
|
+
await updateRobotInteractiveCard(state.cardBizId, buildCardData(parts[0], 'done', cardNote, toolId));
|
|
269
|
+
streamStates.delete(messageId);
|
|
270
|
+
for (let i = 1; i < parts.length; i++) {
|
|
271
|
+
const partNote = i === parts.length - 1 ? note : `继续输出 (${i + 1}/${parts.length})`;
|
|
272
|
+
await sendTextWithRetry(chatId, formatMessage(parts[i], 'done', partNote, toolId));
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
log.warn('Failed to finalize DingTalk interactive card, falling back to text:', err);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
164
280
|
streamStates.delete(messageId);
|
|
165
281
|
for (let i = 0; i < parts.length; i++) {
|
|
166
282
|
const partNote = parts.length > 1
|
|
@@ -196,6 +312,26 @@ export async function sendErrorMessage(chatId, messageId, error, toolId = 'claud
|
|
|
196
312
|
await tryFinishCard(state.conversationToken);
|
|
197
313
|
}
|
|
198
314
|
}
|
|
315
|
+
if (templateId && state?.mode === 'cardInstance' && state.outTrackId) {
|
|
316
|
+
try {
|
|
317
|
+
await updateCardInstance(state.outTrackId, buildCardData(`错误:${error}`, 'error', '执行失败', toolId));
|
|
318
|
+
streamStates.delete(messageId);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
log.warn('Failed to update DingTalk error card instance, falling back to text:', err);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (state?.mode === 'interactiveCard' && state.cardBizId) {
|
|
326
|
+
try {
|
|
327
|
+
await updateRobotInteractiveCard(state.cardBizId, buildCardData(`错误:${error}`, 'error', '执行失败', toolId));
|
|
328
|
+
streamStates.delete(messageId);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
log.warn('Failed to update DingTalk error interactive card, falling back to text:', err);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
199
335
|
streamStates.delete(messageId);
|
|
200
336
|
await sendTextWithRetry(chatId, formatMessage(`错误:${error}`, 'error', '执行失败', toolId));
|
|
201
337
|
}
|
|
@@ -24,11 +24,21 @@ describe('DingTalk message sender', () => {
|
|
|
24
24
|
prepareStreamingCardMock.mockResolvedValue('ctx-1');
|
|
25
25
|
const sender = await import('./message-sender.js');
|
|
26
26
|
sender.configureDingTalkMessageSender({ cardTemplateId: 'tpl-1' });
|
|
27
|
-
const messageId = await sender.sendThinkingMessage('cid-1', undefined, 'codex'
|
|
27
|
+
const messageId = await sender.sendThinkingMessage('cid-1', undefined, 'codex', {
|
|
28
|
+
chatId: 'cid-1',
|
|
29
|
+
conversationType: '0',
|
|
30
|
+
senderStaffId: 'staff-1',
|
|
31
|
+
senderId: 'sender-1',
|
|
32
|
+
});
|
|
28
33
|
await sender.updateMessage('cid-1', messageId, '处理中', 'streaming', '执行中', 'codex');
|
|
29
34
|
await sender.sendFinalMessages('cid-1', messageId, '最终结果', '耗时 1s', 'codex');
|
|
30
35
|
expect(prepareStreamingCardMock).toHaveBeenCalledTimes(1);
|
|
31
|
-
expect(prepareStreamingCardMock).toHaveBeenCalledWith(
|
|
36
|
+
expect(prepareStreamingCardMock).toHaveBeenCalledWith({
|
|
37
|
+
chatId: 'cid-1',
|
|
38
|
+
conversationType: '0',
|
|
39
|
+
senderStaffId: 'staff-1',
|
|
40
|
+
senderId: 'sender-1',
|
|
41
|
+
}, 'tpl-1', expect.objectContaining({
|
|
32
42
|
status: 'thinking',
|
|
33
43
|
flowStatus: 1,
|
|
34
44
|
toolName: 'Codex',
|
|
@@ -101,4 +111,21 @@ describe('DingTalk message sender', () => {
|
|
|
101
111
|
expect(finishStreamingCardMock).toHaveBeenCalledWith('ctx-finish');
|
|
102
112
|
expect(sendTextMock).not.toHaveBeenCalled();
|
|
103
113
|
});
|
|
114
|
+
it('passes single chat metadata to prepare call', async () => {
|
|
115
|
+
prepareStreamingCardMock.mockResolvedValue('ctx-meta');
|
|
116
|
+
const sender = await import('./message-sender.js');
|
|
117
|
+
sender.configureDingTalkMessageSender({ cardTemplateId: 'tpl-meta' });
|
|
118
|
+
await sender.sendThinkingMessage('cid-meta', undefined, 'claude', {
|
|
119
|
+
chatId: 'cid-meta',
|
|
120
|
+
conversationType: '0',
|
|
121
|
+
senderStaffId: '015038621332843498',
|
|
122
|
+
senderId: '$:LWCP_v1:$abc',
|
|
123
|
+
});
|
|
124
|
+
expect(prepareStreamingCardMock).toHaveBeenCalledWith({
|
|
125
|
+
chatId: 'cid-meta',
|
|
126
|
+
conversationType: '0',
|
|
127
|
+
senderStaffId: '015038621332843498',
|
|
128
|
+
senderId: '$:LWCP_v1:$abc',
|
|
129
|
+
}, 'tpl-meta', expect.any(Object));
|
|
130
|
+
});
|
|
104
131
|
});
|