@wu529778790/open-im 1.5.1-beta.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 +12 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/dingtalk/client.d.ts +18 -0
- package/dist/dingtalk/client.js +353 -1
- package/dist/dingtalk/event-handler.js +23 -6
- package/dist/dingtalk/message-sender.d.ts +9 -1
- package/dist/dingtalk/message-sender.js +278 -17
- package/dist/dingtalk/message-sender.test.d.ts +1 -0
- package/dist/dingtalk/message-sender.test.js +131 -0
- package/dist/setup.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,8 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
126
126
|
"enabled": false,
|
|
127
127
|
"allowedUserIds": [],
|
|
128
128
|
"clientId": "YOUR_DINGTALK_CLIENT_ID",
|
|
129
|
-
"clientSecret": "YOUR_DINGTALK_CLIENT_SECRET"
|
|
129
|
+
"clientSecret": "YOUR_DINGTALK_CLIENT_SECRET",
|
|
130
|
+
"cardTemplateId": "YOUR_DINGTALK_AI_CARD_TEMPLATE_ID"
|
|
130
131
|
},
|
|
131
132
|
"wechat": {
|
|
132
133
|
"enabled": false,
|
|
@@ -159,6 +160,7 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
159
160
|
| `FEISHU_ALLOWED_USER_IDS` | 飞书白名单 |
|
|
160
161
|
| `DINGTALK_CLIENT_ID` | 钉钉应用 Client ID / AppKey |
|
|
161
162
|
| `DINGTALK_CLIENT_SECRET` | 钉钉应用 Client Secret / AppSecret |
|
|
163
|
+
| `DINGTALK_CARD_TEMPLATE_ID` | 钉钉 AI 卡片模板 ID,配置后启用单条流式回复 |
|
|
162
164
|
| `DINGTALK_ALLOWED_USER_IDS` | 钉钉白名单 |
|
|
163
165
|
| `WEWORK_CORP_ID` | 企业微信 Bot ID |
|
|
164
166
|
| `WEWORK_SECRET` | 企业微信 Secret |
|
|
@@ -180,7 +182,13 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
180
182
|
- 企业微信:从 [企业微信管理后台](https://work.weixin.qq.com/) 获取 Bot ID 和 Secret
|
|
181
183
|
- 微信:测试中,支持标准模式和 AGP/Qclaw 相关配置
|
|
182
184
|
|
|
183
|
-
说明:钉钉当前采用“Stream Mode 收消息 + OpenAPI
|
|
185
|
+
说明:钉钉当前采用“Stream Mode 收消息 + OpenAPI 发送消息”的混合模式。
|
|
186
|
+
|
|
187
|
+
- 会话内普通文本回复默认走 `sessionWebhook`
|
|
188
|
+
- 若配置了 `cardTemplateId`,会尝试 AI 助理 `prepare/update/finish` 流式卡片;失败则 fallback 为普通文本(自定义机器人/普通群场景下互动卡片 API 报 param.error,暂不支持单条流式更新)
|
|
189
|
+
- 启动/关闭通知会发给最近一次已互动的钉钉会话;如果服务冷启动后还没有任何钉钉会话互动过,则没有可用目标可发
|
|
190
|
+
|
|
191
|
+
钉钉 AI 卡片模板:已适配官方「搜索结果卡片」模板,使用变量 `lastMessage`、`content`、`resources`、`users`、`flowStatus`。若使用该模板,无需修改模板即可实现流式更新。
|
|
184
192
|
|
|
185
193
|
## IM 内命令
|
|
186
194
|
|
|
@@ -217,6 +225,8 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
217
225
|
|
|
218
226
|
**钉钉无法回复**:确认应用已启用机器人 Stream Mode,并检查 `DINGTALK_CLIENT_ID`、`DINGTALK_CLIENT_SECRET` 或 `platforms.dingtalk` 配置是否正确。
|
|
219
227
|
|
|
228
|
+
**钉钉没有流式更新**:prepare 失败时 fallback 为普通文本回复。自定义机器人/普通群场景下,AI 助理和互动卡片 API 均不可用,仅支持单条文本回复。
|
|
229
|
+
|
|
220
230
|
**Cursor 报 `Authentication required`**:先执行 `agent login`,或在 `env` 中设置 `CURSOR_API_KEY`。
|
|
221
231
|
|
|
222
232
|
**Codex 报 `stream disconnected` / `error sending request`**:无法访问 `chatgpt.com`,请配置 `tools.codex.proxy` 或环境变量 `CODEX_PROXY`。
|
package/dist/config.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Config {
|
|
|
19
19
|
weworkWsUrl?: string;
|
|
20
20
|
dingtalkClientId?: string;
|
|
21
21
|
dingtalkClientSecret?: string;
|
|
22
|
+
dingtalkCardTemplateId?: string;
|
|
22
23
|
allowedUserIds: string[];
|
|
23
24
|
telegramAllowedUserIds: string[];
|
|
24
25
|
feishuAllowedUserIds: string[];
|
|
@@ -69,6 +70,7 @@ export interface Config {
|
|
|
69
70
|
dingtalk?: {
|
|
70
71
|
enabled: boolean;
|
|
71
72
|
allowedUserIds: string[];
|
|
73
|
+
cardTemplateId?: string;
|
|
72
74
|
};
|
|
73
75
|
};
|
|
74
76
|
}
|
package/dist/config.js
CHANGED
|
@@ -248,6 +248,8 @@ export function loadConfig() {
|
|
|
248
248
|
fileDingtalk?.clientId;
|
|
249
249
|
const dingtalkClientSecret = process.env.DINGTALK_CLIENT_SECRET ??
|
|
250
250
|
fileDingtalk?.clientSecret;
|
|
251
|
+
const dingtalkCardTemplateId = process.env.DINGTALK_CARD_TEMPLATE_ID ??
|
|
252
|
+
fileDingtalk?.cardTemplateId;
|
|
251
253
|
// 2. 计算启用平台
|
|
252
254
|
const enabledPlatforms = [];
|
|
253
255
|
const telegramEnabledFlag = fileTelegram?.enabled;
|
|
@@ -578,10 +580,12 @@ export function loadConfig() {
|
|
|
578
580
|
? {
|
|
579
581
|
enabled: true,
|
|
580
582
|
allowedUserIds: dingtalkAllowedUserIds,
|
|
583
|
+
cardTemplateId: dingtalkCardTemplateId,
|
|
581
584
|
}
|
|
582
585
|
: {
|
|
583
586
|
enabled: false,
|
|
584
587
|
allowedUserIds: dingtalkAllowedUserIds,
|
|
588
|
+
cardTemplateId: dingtalkCardTemplateId,
|
|
585
589
|
},
|
|
586
590
|
};
|
|
587
591
|
return {
|
|
@@ -602,6 +606,7 @@ export function loadConfig() {
|
|
|
602
606
|
weworkWsUrl: weworkWsUrl,
|
|
603
607
|
dingtalkClientId: dingtalkClientId ?? '',
|
|
604
608
|
dingtalkClientSecret: dingtalkClientSecret ?? '',
|
|
609
|
+
dingtalkCardTemplateId: dingtalkCardTemplateId ?? '',
|
|
605
610
|
allowedUserIds,
|
|
606
611
|
telegramAllowedUserIds,
|
|
607
612
|
feishuAllowedUserIds,
|
package/dist/constants.d.ts
CHANGED
|
@@ -15,3 +15,4 @@ export declare const MAX_FEISHU_MESSAGE_LENGTH = 4000;
|
|
|
15
15
|
/** CardKit 流式内容最大长度(卡片上限约 30KB,留余量) */
|
|
16
16
|
export declare const MAX_STREAMING_CONTENT_LENGTH = 25000;
|
|
17
17
|
export declare const MAX_WEWORK_MESSAGE_LENGTH = 2048;
|
|
18
|
+
export declare const MAX_DINGTALK_MESSAGE_LENGTH = 2048;
|
package/dist/constants.js
CHANGED
|
@@ -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,3 +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>;
|
|
18
|
+
export declare function prepareStreamingCard(target: string | DingTalkStreamingTarget, templateId: string, cardData: Record<string, unknown>): Promise<string>;
|
|
19
|
+
export declare function updateStreamingCard(conversationToken: string, templateId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
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');
|
|
@@ -58,6 +60,12 @@ export async function sendMarkdown(chatId, title, text) {
|
|
|
58
60
|
},
|
|
59
61
|
});
|
|
60
62
|
}
|
|
63
|
+
function buildAiCardContent(templateId, cardData) {
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
templateId,
|
|
66
|
+
cardData,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
61
69
|
function getRobotCode(target) {
|
|
62
70
|
if (!target.robotCode) {
|
|
63
71
|
throw new Error('DingTalk proactive target is missing robotCode');
|
|
@@ -109,9 +117,72 @@ async function callOpenApi(path, body) {
|
|
|
109
117
|
: text;
|
|
110
118
|
throw new Error(`DingTalk OpenAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
111
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
|
+
}
|
|
112
152
|
function normalizeConversationType(type) {
|
|
113
153
|
return type?.trim().toLowerCase();
|
|
114
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
|
+
}
|
|
115
186
|
function buildProactiveAttempts(target, content) {
|
|
116
187
|
const robotCode = getRobotCode(target);
|
|
117
188
|
const payload = buildTextPayload(content);
|
|
@@ -208,6 +279,7 @@ export function stopDingTalk() {
|
|
|
208
279
|
}
|
|
209
280
|
finally {
|
|
210
281
|
sessionWebhookByChat.clear();
|
|
282
|
+
unionIdByUserId.clear();
|
|
211
283
|
client = null;
|
|
212
284
|
messageHandler = null;
|
|
213
285
|
log.info('DingTalk client stopped');
|
|
@@ -230,10 +302,290 @@ export async function sendProactiveText(target, content) {
|
|
|
230
302
|
}
|
|
231
303
|
catch (err) {
|
|
232
304
|
lastError = err;
|
|
233
|
-
|
|
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
|
+
}
|
|
234
312
|
}
|
|
235
313
|
}
|
|
236
314
|
throw lastError instanceof Error
|
|
237
315
|
? lastError
|
|
238
316
|
: new Error(`DingTalk proactive send failed for chat ${target.chatId}`);
|
|
239
317
|
}
|
|
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
|
+
}
|
|
406
|
+
const token = result.result?.conversationToken;
|
|
407
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
408
|
+
throw new Error(`DingTalk prepare did not return conversationToken: ${JSON.stringify(result)}`);
|
|
409
|
+
}
|
|
410
|
+
return token;
|
|
411
|
+
}
|
|
412
|
+
export async function updateStreamingCard(conversationToken, templateId, cardData) {
|
|
413
|
+
const result = await callOpenApi('/v1.0/aiInteraction/update', {
|
|
414
|
+
conversationToken,
|
|
415
|
+
contentType: 'ai_card',
|
|
416
|
+
content: buildAiCardContent(templateId, cardData),
|
|
417
|
+
});
|
|
418
|
+
const success = result.result?.success;
|
|
419
|
+
if (success === false) {
|
|
420
|
+
throw new Error(`DingTalk update returned success=false: ${JSON.stringify(result)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export async function finishStreamingCard(conversationToken) {
|
|
424
|
+
const result = await callOpenApi('/v1.0/aiInteraction/finish', {
|
|
425
|
+
conversationToken,
|
|
426
|
+
});
|
|
427
|
+
const success = result.result?.success;
|
|
428
|
+
if (success === false) {
|
|
429
|
+
throw new Error(`DingTalk finish returned success=false: ${JSON.stringify(result)}`);
|
|
430
|
+
}
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AccessControl } from '../access/access-control.js';
|
|
2
2
|
import { RequestQueue } from '../queue/request-queue.js';
|
|
3
|
-
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendPermissionCard, sendModeCard, sendDirectorySelection, } from './message-sender.js';
|
|
3
|
+
import { configureDingTalkMessageSender, sendThinkingMessage, updateMessage, sendFinalMessages, sendErrorMessage, sendTextReply, startTypingLoop, sendPermissionCard, sendModeCard, sendDirectorySelection, } from './message-sender.js';
|
|
4
4
|
import { ackMessage, registerSessionWebhook } from './client.js';
|
|
5
5
|
import { registerPermissionSender } from '../hook/permission-server.js';
|
|
6
6
|
import { CommandHandler } from '../commands/handler.js';
|
|
@@ -22,6 +22,16 @@ function parseRobotMessage(data) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
export function setupDingTalkHandlers(config, sessionManager) {
|
|
25
|
+
configureDingTalkMessageSender({
|
|
26
|
+
cardTemplateId: config.dingtalkCardTemplateId,
|
|
27
|
+
robotCodeFallback: config.dingtalkClientId,
|
|
28
|
+
});
|
|
29
|
+
if (config.dingtalkCardTemplateId) {
|
|
30
|
+
log.info('DingTalk AI card streaming enabled');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
log.info('DingTalk AI card streaming disabled: no cardTemplateId configured');
|
|
34
|
+
}
|
|
25
35
|
const accessControl = new AccessControl(config.dingtalkAllowedUserIds);
|
|
26
36
|
const requestQueue = new RequestQueue();
|
|
27
37
|
const runningTasks = new Map();
|
|
@@ -34,7 +44,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
34
44
|
getRunningTasksSize: () => runningTasks.size,
|
|
35
45
|
});
|
|
36
46
|
registerPermissionSender('dingtalk', { sendTextReply, sendPermissionCard });
|
|
37
|
-
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
47
|
+
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, dingtalkTarget) {
|
|
38
48
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
39
49
|
const toolAdapter = getAdapter(config.aiCommand);
|
|
40
50
|
if (!toolAdapter) {
|
|
@@ -46,7 +56,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
46
56
|
: undefined;
|
|
47
57
|
log.info(`[AI_REQUEST] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
48
58
|
const toolId = config.aiCommand;
|
|
49
|
-
const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
|
|
59
|
+
const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, dingtalkTarget);
|
|
50
60
|
const stopTyping = startTypingLoop(chatId);
|
|
51
61
|
const taskKey = `${userId}:${msgId}`;
|
|
52
62
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'dingtalk', taskKey }, prompt, toolAdapter, {
|
|
@@ -58,7 +68,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
58
68
|
await sendFinalMessages(chatId, msgId, content, note ?? '', toolId);
|
|
59
69
|
},
|
|
60
70
|
sendError: async (error) => {
|
|
61
|
-
await
|
|
71
|
+
await sendErrorMessage(chatId, msgId, error, toolId);
|
|
62
72
|
},
|
|
63
73
|
extraCleanup: () => {
|
|
64
74
|
stopTyping();
|
|
@@ -103,7 +113,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
103
113
|
chatId,
|
|
104
114
|
userId,
|
|
105
115
|
conversationType: robotMessage.conversationType,
|
|
106
|
-
robotCode: robotMessage.robotCode,
|
|
116
|
+
robotCode: robotMessage.robotCode || config.dingtalkClientId,
|
|
107
117
|
});
|
|
108
118
|
setChatUser(chatId, userId, 'dingtalk');
|
|
109
119
|
try {
|
|
@@ -118,8 +128,15 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
118
128
|
}
|
|
119
129
|
const workDir = sessionManager.getWorkDir(userId);
|
|
120
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
|
+
};
|
|
121
138
|
const enqueueResult = requestQueue.enqueue(userId, convId, text, async (prompt) => {
|
|
122
|
-
await handleAIRequest(userId, chatId, prompt, workDir, convId);
|
|
139
|
+
await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, undefined, dingtalkTarget);
|
|
123
140
|
});
|
|
124
141
|
if (enqueueResult === 'rejected') {
|
|
125
142
|
await sendTextReply(chatId, '请求队列已满,请稍后再试。');
|
|
@@ -1,12 +1,20 @@
|
|
|
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 {
|
|
6
|
+
cardTemplateId?: string;
|
|
7
|
+
robotCodeFallback?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function configureDingTalkMessageSender(settings: SenderSettings): void;
|
|
10
|
+
export declare function sendThinkingMessage(chatId: string, _replyToMessageId?: string, toolId?: string, target?: DingTalkStreamingTarget): Promise<string>;
|
|
5
11
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
6
12
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
13
|
+
export declare function sendErrorMessage(chatId: string, messageId: string, error: string, toolId?: string): Promise<void>;
|
|
7
14
|
export declare function sendTextReply(chatId: string, text: string, _threadCtx?: ThreadContext | string): Promise<void>;
|
|
8
15
|
export declare function sendProactiveTextReply(target: string | DingTalkActiveTarget, text: string): Promise<void>;
|
|
9
16
|
export declare function sendPermissionCard(chatId: string, requestId: string, toolName: string, toolInput: string): Promise<void>;
|
|
10
17
|
export declare function sendModeCard(chatId: string, _userId: string, currentMode: string): Promise<void>;
|
|
11
18
|
export declare function sendDirectorySelection(chatId: string, currentDir: string, userId: string): Promise<void>;
|
|
12
19
|
export declare function startTypingLoop(_chatId: string): () => void;
|
|
20
|
+
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
|
-
import { sendText, sendProactiveText } 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';
|
|
7
|
-
import {
|
|
7
|
+
import { MAX_DINGTALK_MESSAGE_LENGTH } from '../constants.js';
|
|
8
8
|
const log = createLogger('DingTalkSender');
|
|
9
9
|
const STATUS_ICONS = {
|
|
10
10
|
thinking: '🔵',
|
|
@@ -12,9 +12,26 @@ const STATUS_ICONS = {
|
|
|
12
12
|
done: '✅',
|
|
13
13
|
error: '❌',
|
|
14
14
|
};
|
|
15
|
+
const FLOW_STATUS = {
|
|
16
|
+
thinking: 1,
|
|
17
|
+
streaming: 2,
|
|
18
|
+
done: 3,
|
|
19
|
+
error: 5,
|
|
20
|
+
};
|
|
21
|
+
let senderSettings = {};
|
|
22
|
+
const streamStates = new Map();
|
|
15
23
|
function generateMessageId() {
|
|
16
24
|
return `${Date.now()}-${randomBytes(6).toString('hex')}`;
|
|
17
25
|
}
|
|
26
|
+
export function configureDingTalkMessageSender(settings) {
|
|
27
|
+
senderSettings = {
|
|
28
|
+
cardTemplateId: settings.cardTemplateId?.trim(),
|
|
29
|
+
robotCodeFallback: settings.robotCodeFallback?.trim(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function getCardTemplateId() {
|
|
33
|
+
return senderSettings.cardTemplateId?.trim() || undefined;
|
|
34
|
+
}
|
|
18
35
|
function formatMessage(content, status, note, toolId = 'claude') {
|
|
19
36
|
const icon = STATUS_ICONS[status];
|
|
20
37
|
const toolName = getAIToolDisplayName(toolId);
|
|
@@ -30,6 +47,61 @@ function formatMessage(content, status, note, toolId = 'claude') {
|
|
|
30
47
|
text += `\n\n─────────\n${note}`;
|
|
31
48
|
return text;
|
|
32
49
|
}
|
|
50
|
+
function getToolTitle(toolId, status) {
|
|
51
|
+
const toolName = getAIToolDisplayName(toolId);
|
|
52
|
+
if (status === 'done')
|
|
53
|
+
return toolName;
|
|
54
|
+
if (status === 'thinking')
|
|
55
|
+
return `${toolName} - 思考中`;
|
|
56
|
+
if (status === 'streaming')
|
|
57
|
+
return `${toolName} - 执行中`;
|
|
58
|
+
return `${toolName} - 错误`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 适配钉钉官方「搜索结果卡片」模板变量结构
|
|
62
|
+
* 变量:lastMessage, content, resources, users, flowStatus
|
|
63
|
+
*/
|
|
64
|
+
function buildCardData(content, status, note, toolId = 'claude') {
|
|
65
|
+
const toolName = getAIToolDisplayName(toolId);
|
|
66
|
+
const safeContent = content.trim() || (status === 'thinking' ? '正在思考,请稍候...' : status === 'error' ? '执行失败' : '...');
|
|
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
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
lastMessage,
|
|
83
|
+
content: safeContent,
|
|
84
|
+
resources,
|
|
85
|
+
users: [],
|
|
86
|
+
flowStatus: FLOW_STATUS[status],
|
|
87
|
+
// 保留兼容字段
|
|
88
|
+
note: safeNote,
|
|
89
|
+
status,
|
|
90
|
+
toolName,
|
|
91
|
+
title: getToolTitle(toolId, status),
|
|
92
|
+
displayText: formatMessage(safeContent, status, safeNote, toolId),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function tryFinishCard(conversationToken) {
|
|
96
|
+
if (!conversationToken)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
await finishStreamingCard(conversationToken);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
log.warn('Failed to finish DingTalk streaming card:', err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
33
105
|
async function sendTextWithRetry(chatId, text, retries = 1) {
|
|
34
106
|
let lastError;
|
|
35
107
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -47,26 +119,165 @@ async function sendTextWithRetry(chatId, text, retries = 1) {
|
|
|
47
119
|
}
|
|
48
120
|
throw lastError;
|
|
49
121
|
}
|
|
50
|
-
export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'claude') {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
122
|
+
export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'claude', target) {
|
|
123
|
+
const messageId = generateMessageId();
|
|
124
|
+
const templateId = getCardTemplateId();
|
|
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
|
+
}
|
|
146
|
+
}
|
|
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
|
+
}
|
|
176
|
+
}
|
|
177
|
+
streamStates.set(messageId, { chatId, mode: 'text', toolId, target });
|
|
178
|
+
log.info('DingTalk 流式卡片不可用,将使用普通文本回复');
|
|
179
|
+
return messageId;
|
|
57
180
|
}
|
|
58
181
|
export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
|
|
59
|
-
// 钉钉第一版不发送中间更新,避免用户看到多条状态消息。
|
|
60
182
|
void chatId;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
183
|
+
const state = streamStates.get(messageId);
|
|
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
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
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;
|
|
206
|
+
}
|
|
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;
|
|
215
|
+
}
|
|
66
216
|
}
|
|
67
217
|
export async function sendFinalMessages(chatId, messageId, fullContent, note, toolId = 'claude') {
|
|
68
|
-
|
|
69
|
-
const
|
|
218
|
+
const parts = splitLongContent(fullContent, MAX_DINGTALK_MESSAGE_LENGTH);
|
|
219
|
+
const templateId = getCardTemplateId();
|
|
220
|
+
const state = streamStates.get(messageId);
|
|
221
|
+
if (templateId && state?.mode === 'card' && state.conversationToken) {
|
|
222
|
+
let updatedCard = false;
|
|
223
|
+
try {
|
|
224
|
+
const cardNote = parts.length > 1 ? `内容较长,后续将继续发送 (${1}/${parts.length})` : note;
|
|
225
|
+
await updateStreamingCard(state.conversationToken, templateId, buildCardData(parts[0], 'done', cardNote, toolId));
|
|
226
|
+
updatedCard = true;
|
|
227
|
+
try {
|
|
228
|
+
await finishStreamingCard(state.conversationToken);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
log.warn('Failed to finish DingTalk streaming card after final update:', err);
|
|
232
|
+
}
|
|
233
|
+
streamStates.delete(messageId);
|
|
234
|
+
for (let i = 1; i < parts.length; i++) {
|
|
235
|
+
const partNote = i === parts.length - 1 ? note : `继续输出 (${i + 1}/${parts.length})`;
|
|
236
|
+
await sendTextWithRetry(chatId, formatMessage(parts[i], 'done', partNote, toolId));
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
if (updatedCard) {
|
|
242
|
+
log.warn('Final DingTalk card update already succeeded; skip text fallback:', err);
|
|
243
|
+
streamStates.delete(messageId);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
log.warn('Failed to finalize DingTalk streaming card, falling back to text:', err);
|
|
247
|
+
await tryFinishCard(state.conversationToken);
|
|
248
|
+
}
|
|
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
|
+
}
|
|
280
|
+
streamStates.delete(messageId);
|
|
70
281
|
for (let i = 0; i < parts.length; i++) {
|
|
71
282
|
const partNote = parts.length > 1
|
|
72
283
|
? `${i === parts.length - 1 ? note + '\n' : ''}(续 ${i + 1}/${parts.length})`.trim()
|
|
@@ -74,6 +285,56 @@ export async function sendFinalMessages(chatId, messageId, fullContent, note, to
|
|
|
74
285
|
await sendTextWithRetry(chatId, formatMessage(parts[i], 'done', partNote, toolId));
|
|
75
286
|
}
|
|
76
287
|
}
|
|
288
|
+
export async function sendErrorMessage(chatId, messageId, error, toolId = 'claude') {
|
|
289
|
+
const templateId = getCardTemplateId();
|
|
290
|
+
const state = streamStates.get(messageId);
|
|
291
|
+
if (templateId && state?.mode === 'card' && state.conversationToken) {
|
|
292
|
+
let updatedCard = false;
|
|
293
|
+
try {
|
|
294
|
+
await updateStreamingCard(state.conversationToken, templateId, buildCardData(`错误:${error}`, 'error', '执行失败', toolId));
|
|
295
|
+
updatedCard = true;
|
|
296
|
+
try {
|
|
297
|
+
await finishStreamingCard(state.conversationToken);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
log.warn('Failed to finish DingTalk error card after update:', err);
|
|
301
|
+
}
|
|
302
|
+
streamStates.delete(messageId);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
if (updatedCard) {
|
|
307
|
+
log.warn('DingTalk error card update already succeeded; skip text fallback:', err);
|
|
308
|
+
streamStates.delete(messageId);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
log.warn('Failed to send DingTalk error card, falling back to text:', err);
|
|
312
|
+
await tryFinishCard(state.conversationToken);
|
|
313
|
+
}
|
|
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
|
+
}
|
|
335
|
+
streamStates.delete(messageId);
|
|
336
|
+
await sendTextWithRetry(chatId, formatMessage(`错误:${error}`, 'error', '执行失败', toolId));
|
|
337
|
+
}
|
|
77
338
|
export async function sendTextReply(chatId, text, _threadCtx) {
|
|
78
339
|
await sendTextWithRetry(chatId, text);
|
|
79
340
|
log.info(`Text reply sent to DingTalk chat ${chatId}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const sendTextMock = vi.fn();
|
|
3
|
+
const sendProactiveTextMock = vi.fn();
|
|
4
|
+
const prepareStreamingCardMock = vi.fn();
|
|
5
|
+
const updateStreamingCardMock = vi.fn();
|
|
6
|
+
const finishStreamingCardMock = vi.fn();
|
|
7
|
+
vi.mock('./client.js', () => ({
|
|
8
|
+
sendText: sendTextMock,
|
|
9
|
+
sendProactiveText: sendProactiveTextMock,
|
|
10
|
+
prepareStreamingCard: prepareStreamingCardMock,
|
|
11
|
+
updateStreamingCard: updateStreamingCardMock,
|
|
12
|
+
finishStreamingCard: finishStreamingCardMock,
|
|
13
|
+
}));
|
|
14
|
+
describe('DingTalk message sender', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
sendTextMock.mockReset();
|
|
18
|
+
sendProactiveTextMock.mockReset();
|
|
19
|
+
prepareStreamingCardMock.mockReset();
|
|
20
|
+
updateStreamingCardMock.mockReset();
|
|
21
|
+
finishStreamingCardMock.mockReset();
|
|
22
|
+
});
|
|
23
|
+
it('uses AI card streaming when template is configured', async () => {
|
|
24
|
+
prepareStreamingCardMock.mockResolvedValue('ctx-1');
|
|
25
|
+
const sender = await import('./message-sender.js');
|
|
26
|
+
sender.configureDingTalkMessageSender({ cardTemplateId: 'tpl-1' });
|
|
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
|
+
});
|
|
33
|
+
await sender.updateMessage('cid-1', messageId, '处理中', 'streaming', '执行中', 'codex');
|
|
34
|
+
await sender.sendFinalMessages('cid-1', messageId, '最终结果', '耗时 1s', 'codex');
|
|
35
|
+
expect(prepareStreamingCardMock).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(prepareStreamingCardMock).toHaveBeenCalledWith({
|
|
37
|
+
chatId: 'cid-1',
|
|
38
|
+
conversationType: '0',
|
|
39
|
+
senderStaffId: 'staff-1',
|
|
40
|
+
senderId: 'sender-1',
|
|
41
|
+
}, 'tpl-1', expect.objectContaining({
|
|
42
|
+
status: 'thinking',
|
|
43
|
+
flowStatus: 1,
|
|
44
|
+
toolName: 'Codex',
|
|
45
|
+
}));
|
|
46
|
+
expect(updateStreamingCardMock).toHaveBeenCalledTimes(2);
|
|
47
|
+
expect(updateStreamingCardMock).toHaveBeenNthCalledWith(1, 'ctx-1', 'tpl-1', expect.objectContaining({
|
|
48
|
+
content: '处理中',
|
|
49
|
+
note: '执行中',
|
|
50
|
+
status: 'streaming',
|
|
51
|
+
flowStatus: 2,
|
|
52
|
+
}));
|
|
53
|
+
expect(updateStreamingCardMock).toHaveBeenNthCalledWith(2, 'ctx-1', 'tpl-1', expect.objectContaining({
|
|
54
|
+
content: '最终结果',
|
|
55
|
+
note: '耗时 1s',
|
|
56
|
+
status: 'done',
|
|
57
|
+
flowStatus: 3,
|
|
58
|
+
}));
|
|
59
|
+
expect(finishStreamingCardMock).toHaveBeenCalledWith('ctx-1');
|
|
60
|
+
expect(sendTextMock).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
it('falls back to plain text when no card template is configured', async () => {
|
|
63
|
+
const sender = await import('./message-sender.js');
|
|
64
|
+
sender.configureDingTalkMessageSender({ cardTemplateId: '' });
|
|
65
|
+
const messageId = await sender.sendThinkingMessage('cid-2', undefined, 'claude');
|
|
66
|
+
await sender.sendFinalMessages('cid-2', messageId, '普通文本结果', '完成', 'claude');
|
|
67
|
+
expect(prepareStreamingCardMock).not.toHaveBeenCalled();
|
|
68
|
+
expect(updateStreamingCardMock).not.toHaveBeenCalled();
|
|
69
|
+
expect(finishStreamingCardMock).not.toHaveBeenCalled();
|
|
70
|
+
expect(sendTextMock).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(sendTextMock.mock.calls[0][1]).toContain('普通文本结果');
|
|
72
|
+
});
|
|
73
|
+
it('falls back to plain text when prepare fails', async () => {
|
|
74
|
+
prepareStreamingCardMock.mockRejectedValue(new Error('prepare failed'));
|
|
75
|
+
const sender = await import('./message-sender.js');
|
|
76
|
+
sender.configureDingTalkMessageSender({ cardTemplateId: 'tpl-2' });
|
|
77
|
+
const messageId = await sender.sendThinkingMessage('cid-3', undefined, 'claude');
|
|
78
|
+
await sender.sendFinalMessages('cid-3', messageId, '降级结果', '完成', 'claude');
|
|
79
|
+
expect(prepareStreamingCardMock).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(updateStreamingCardMock).not.toHaveBeenCalled();
|
|
81
|
+
expect(finishStreamingCardMock).not.toHaveBeenCalled();
|
|
82
|
+
expect(sendTextMock).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(sendTextMock.mock.calls[0][1]).toContain('降级结果');
|
|
84
|
+
});
|
|
85
|
+
it('updates card to error state and finishes it on failure', async () => {
|
|
86
|
+
prepareStreamingCardMock.mockResolvedValue('ctx-error');
|
|
87
|
+
const sender = await import('./message-sender.js');
|
|
88
|
+
sender.configureDingTalkMessageSender({ cardTemplateId: 'tpl-error' });
|
|
89
|
+
const messageId = await sender.sendThinkingMessage('cid-4', undefined, 'cursor');
|
|
90
|
+
await sender.sendErrorMessage('cid-4', messageId, '爆了', 'cursor');
|
|
91
|
+
expect(updateStreamingCardMock).toHaveBeenCalledWith('ctx-error', 'tpl-error', expect.objectContaining({
|
|
92
|
+
content: '错误:爆了',
|
|
93
|
+
note: '执行失败',
|
|
94
|
+
status: 'error',
|
|
95
|
+
flowStatus: 5,
|
|
96
|
+
toolName: 'Cursor',
|
|
97
|
+
}));
|
|
98
|
+
expect(finishStreamingCardMock).toHaveBeenCalledWith('ctx-error');
|
|
99
|
+
});
|
|
100
|
+
it('does not duplicate plain text when final card update succeeds but finish fails', async () => {
|
|
101
|
+
prepareStreamingCardMock.mockResolvedValue('ctx-finish');
|
|
102
|
+
finishStreamingCardMock.mockRejectedValue(new Error('finish failed'));
|
|
103
|
+
const sender = await import('./message-sender.js');
|
|
104
|
+
sender.configureDingTalkMessageSender({ cardTemplateId: 'tpl-finish' });
|
|
105
|
+
const messageId = await sender.sendThinkingMessage('cid-5', undefined, 'claude');
|
|
106
|
+
await sender.sendFinalMessages('cid-5', messageId, '最终结果', '完成', 'claude');
|
|
107
|
+
expect(updateStreamingCardMock).toHaveBeenCalledWith('ctx-finish', 'tpl-finish', expect.objectContaining({
|
|
108
|
+
content: '最终结果',
|
|
109
|
+
status: 'done',
|
|
110
|
+
}));
|
|
111
|
+
expect(finishStreamingCardMock).toHaveBeenCalledWith('ctx-finish');
|
|
112
|
+
expect(sendTextMock).not.toHaveBeenCalled();
|
|
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
|
+
});
|
|
131
|
+
});
|
package/dist/setup.js
CHANGED
|
@@ -102,6 +102,7 @@ function printManualInstructions(configPath) {
|
|
|
102
102
|
"enabled": false,
|
|
103
103
|
"clientId": "你的钉钉 Client ID(可选)",
|
|
104
104
|
"clientSecret": "你的钉钉 Client Secret(可选)",
|
|
105
|
+
"cardTemplateId": "你的钉钉 AI 卡片模板 ID(可选,配置后启用单条流式)",
|
|
105
106
|
"allowedUserIds": ["允许访问的钉钉用户 ID(可选)"]
|
|
106
107
|
},
|
|
107
108
|
"wechat": {
|
|
@@ -485,14 +486,22 @@ export async function runInteractiveSetup() {
|
|
|
485
486
|
initial: existing?.platforms?.dingtalk?.clientSecret ?? "",
|
|
486
487
|
validate: (v) => (v.trim() ? true : "Client Secret 不能为空"),
|
|
487
488
|
},
|
|
489
|
+
{
|
|
490
|
+
type: "text",
|
|
491
|
+
name: "cardTemplateId",
|
|
492
|
+
message: "钉钉 AI 卡片模板 ID(可选,配置后启用单条流式)",
|
|
493
|
+
initial: existing?.platforms?.dingtalk?.cardTemplateId ?? "",
|
|
494
|
+
},
|
|
488
495
|
], { onCancel });
|
|
489
496
|
const dtClientId = dingtalkResp.clientId?.trim() || existing?.platforms?.dingtalk?.clientId;
|
|
490
497
|
const dtClientSecret = dingtalkResp.clientSecret?.trim() || existing?.platforms?.dingtalk?.clientSecret;
|
|
498
|
+
const dtCardTemplateId = dingtalkResp.cardTemplateId?.trim() || existing?.platforms?.dingtalk?.cardTemplateId;
|
|
491
499
|
if (dtClientId && dtClientSecret) {
|
|
492
500
|
config.platforms.dingtalk = {
|
|
493
501
|
enabled: true,
|
|
494
502
|
clientId: dtClientId,
|
|
495
503
|
clientSecret: dtClientSecret,
|
|
504
|
+
cardTemplateId: dtCardTemplateId,
|
|
496
505
|
};
|
|
497
506
|
}
|
|
498
507
|
else if (platform === "dingtalk") {
|
|
@@ -827,6 +836,7 @@ export async function runInteractiveSetup() {
|
|
|
827
836
|
enabled: true,
|
|
828
837
|
clientId: config.platforms.dingtalk?.clientId,
|
|
829
838
|
clientSecret: config.platforms.dingtalk?.clientSecret,
|
|
839
|
+
cardTemplateId: config.platforms.dingtalk?.cardTemplateId,
|
|
830
840
|
allowedUserIds: dingtalkIds,
|
|
831
841
|
};
|
|
832
842
|
}
|