@soimy/dingtalk 3.1.4 β 3.2.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 +155 -12
- package/package.json +2 -2
- package/src/card-service.ts +31 -51
- package/src/channel.ts +317 -94
- package/src/config-schema.ts +9 -1
- package/src/connection-manager.ts +14 -1
- package/src/inbound-handler.ts +221 -177
- package/src/media-utils.ts +517 -3
- package/src/onboarding.ts +47 -3
- package/src/send-service.ts +73 -27
- package/src/session-lock.ts +32 -0
- package/src/types.ts +15 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -2,14 +2,10 @@ import axios from "axios";
|
|
|
2
2
|
import { normalizeAllowFrom, isSenderAllowed, isSenderGroupAllowed } from "./access-control";
|
|
3
3
|
import { getAccessToken } from "./auth";
|
|
4
4
|
import {
|
|
5
|
-
cleanupCardCache,
|
|
6
5
|
createAICard,
|
|
7
6
|
finishAICard,
|
|
8
7
|
formatContentForCard,
|
|
9
|
-
getActiveCardIdByTarget,
|
|
10
|
-
getCardById,
|
|
11
8
|
isCardInTerminalState,
|
|
12
|
-
streamAICard,
|
|
13
9
|
} from "./card-service";
|
|
14
10
|
import { resolveGroupConfig } from "./config";
|
|
15
11
|
import { formatGroupMembers, noteGroupMember } from "./group-members-store";
|
|
@@ -24,6 +20,7 @@ import { getDingTalkRuntime } from "./runtime";
|
|
|
24
20
|
import { sendBySession, sendMessage } from "./send-service";
|
|
25
21
|
import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
|
|
26
22
|
import { AICardStatus } from "./types";
|
|
23
|
+
import { acquireSessionLock } from "./session-lock";
|
|
27
24
|
import { formatDingTalkErrorPayloadLog, maskSensitiveData } from "./utils";
|
|
28
25
|
|
|
29
26
|
const DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS = 24;
|
|
@@ -148,8 +145,11 @@ export async function downloadMedia(
|
|
|
148
145
|
const contentType = mediaResponse.headers["content-type"] || "application/octet-stream";
|
|
149
146
|
const buffer = Buffer.from(mediaResponse.data as ArrayBuffer);
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
const maxBytes =
|
|
149
|
+
config.mediaMaxMb && config.mediaMaxMb > 0 ? config.mediaMaxMb * 1024 * 1024 : undefined;
|
|
150
|
+
const saved = maxBytes
|
|
151
|
+
? await rt.channel.media.saveMediaBuffer(buffer, contentType, "inbound", maxBytes)
|
|
152
|
+
: await rt.channel.media.saveMediaBuffer(buffer, contentType, "inbound");
|
|
153
153
|
log?.debug?.(`[DingTalk] Media saved: ${saved.path}`);
|
|
154
154
|
return { path: saved.path, mimeType: saved.contentType ?? contentType };
|
|
155
155
|
} catch (err: any) {
|
|
@@ -185,9 +185,6 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
185
185
|
|
|
186
186
|
log?.debug?.("[DingTalk] Full Inbound Data:", JSON.stringify(maskSensitiveData(data)));
|
|
187
187
|
|
|
188
|
-
// Clean up old terminal cards opportunistically on inbound traffic.
|
|
189
|
-
cleanupCardCache();
|
|
190
|
-
|
|
191
188
|
// 1) Ignore self messages from bot.
|
|
192
189
|
if (data.senderId === data.chatbotUserId || data.senderStaffId === data.chatbotUserId) {
|
|
193
190
|
log?.debug?.("[DingTalk] Ignoring robot self-message");
|
|
@@ -326,6 +323,35 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
326
323
|
agentId: route.agentId,
|
|
327
324
|
});
|
|
328
325
|
|
|
326
|
+
const to = isDirect ? senderId : groupId;
|
|
327
|
+
|
|
328
|
+
// 3) Select response mode (card vs markdown).
|
|
329
|
+
// Card creation runs BEFORE media download so the user sees immediate visual
|
|
330
|
+
// feedback while large files are still being downloaded.
|
|
331
|
+
const useCardMode = dingtalkConfig.messageType === "card";
|
|
332
|
+
let currentAICard = undefined;
|
|
333
|
+
let lastCardContent = "";
|
|
334
|
+
|
|
335
|
+
if (useCardMode) {
|
|
336
|
+
try {
|
|
337
|
+
log?.debug?.(
|
|
338
|
+
`[DingTalk][AICard] conversationType=${data.conversationType}, conversationId=${to}`,
|
|
339
|
+
);
|
|
340
|
+
const aiCard = await createAICard(dingtalkConfig, to, log);
|
|
341
|
+
if (aiCard) {
|
|
342
|
+
currentAICard = aiCard;
|
|
343
|
+
} else {
|
|
344
|
+
log?.warn?.(
|
|
345
|
+
"[DingTalk] Failed to create AI card (returned null), fallback to text/markdown.",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
} catch (err: any) {
|
|
349
|
+
log?.warn?.(
|
|
350
|
+
`[DingTalk] Failed to create AI card: ${err.message}, fallback to text/markdown.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
329
355
|
let mediaPath: string | undefined;
|
|
330
356
|
let mediaType: string | undefined;
|
|
331
357
|
if (content.mediaPath && dingtalkConfig.robotCode) {
|
|
@@ -366,7 +392,6 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
366
392
|
envelope: envelopeOptions,
|
|
367
393
|
});
|
|
368
394
|
|
|
369
|
-
const to = isDirect ? senderId : groupId;
|
|
370
395
|
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
371
396
|
Body: body,
|
|
372
397
|
RawBody: content.text,
|
|
@@ -407,199 +432,218 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
407
432
|
|
|
408
433
|
log?.info?.(`[DingTalk] Inbound: from=${senderName} text="${content.text.slice(0, 50)}..."`);
|
|
409
434
|
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const existingCardId = getActiveCardIdByTarget(targetKey);
|
|
418
|
-
const existingCard = existingCardId ? getCardById(existingCardId) : undefined;
|
|
419
|
-
|
|
420
|
-
// Reuse active non-terminal card to keep one card per conversation.
|
|
421
|
-
if (existingCard && !isCardInTerminalState(existingCard.state)) {
|
|
422
|
-
currentAICard = existingCard;
|
|
423
|
-
log?.debug?.("[DingTalk] Reusing existing active AI card for this conversation.");
|
|
424
|
-
} else {
|
|
435
|
+
// Serialize dispatchReply + card finalize per session to prevent the runtime
|
|
436
|
+
// from receiving concurrent dispatch calls on the same session key, which
|
|
437
|
+
// causes empty replies for all but the first caller.
|
|
438
|
+
const releaseSessionLock = await acquireSessionLock(route.sessionKey);
|
|
439
|
+
try {
|
|
440
|
+
// 4) Optional "thinking..." feedback for non-card mode.
|
|
441
|
+
if (dingtalkConfig.showThinking !== false) {
|
|
425
442
|
try {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
|
|
443
|
+
const thinkingText = "π€ ζθδΈοΌθ―·η¨ε...";
|
|
444
|
+
if (useCardMode && currentAICard) {
|
|
445
|
+
log?.debug?.("[DingTalk] AI Card in thinking state, skipping thinking message send.");
|
|
429
446
|
} else {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
447
|
+
lastCardContent = thinkingText;
|
|
448
|
+
const sendResult = await sendMessage(dingtalkConfig, to, thinkingText, {
|
|
449
|
+
sessionWebhook,
|
|
450
|
+
atUserId: !isDirect ? senderId : null,
|
|
451
|
+
log,
|
|
452
|
+
card: currentAICard,
|
|
453
|
+
});
|
|
454
|
+
if (!sendResult.ok) {
|
|
455
|
+
throw new Error(sendResult.error || "Thinking message send failed");
|
|
456
|
+
}
|
|
433
457
|
}
|
|
434
458
|
} catch (err: any) {
|
|
435
|
-
log?.
|
|
436
|
-
|
|
437
|
-
|
|
459
|
+
log?.debug?.(`[DingTalk] Thinking message failed: ${err.message}`);
|
|
460
|
+
if (err?.response?.data !== undefined) {
|
|
461
|
+
log?.debug?.(formatDingTalkErrorPayloadLog("inbound.thinkingMessage", err.response.data));
|
|
462
|
+
}
|
|
438
463
|
}
|
|
439
464
|
}
|
|
440
|
-
}
|
|
441
465
|
|
|
442
|
-
|
|
443
|
-
if (dingtalkConfig.showThinking !== false) {
|
|
466
|
+
let queuedFinal: unknown;
|
|
444
467
|
try {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
468
|
+
const dispatchResult = await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
469
|
+
ctx,
|
|
470
|
+
cfg,
|
|
471
|
+
dispatcherOptions: {
|
|
472
|
+
responsePrefix: "",
|
|
473
|
+
deliver: async (payload: any, info?: { kind: string }) => {
|
|
474
|
+
try {
|
|
475
|
+
const textToSend = payload.markdown || payload.text;
|
|
476
|
+
if (!textToSend) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (typeof textToSend === "string" && isUnhandledStopReasonText(textToSend)) {
|
|
481
|
+
log?.warn?.(`[DingTalk] Suppressed stop reason from outbound chat content: ${textToSend}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (useCardMode && currentAICard && info?.kind === "final") {
|
|
486
|
+
lastCardContent = textToSend;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (useCardMode && currentAICard && info?.kind === "tool") {
|
|
491
|
+
if (isCardInTerminalState(currentAICard.state)) {
|
|
492
|
+
log?.debug?.(
|
|
493
|
+
`[DingTalk] Skipping tool stream update because card is terminal: state=${currentAICard.state}`,
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
log?.info?.(
|
|
499
|
+
`[DingTalk] Tool result received, streaming to AI Card: ${textToSend.slice(0, 100)}`,
|
|
500
|
+
);
|
|
501
|
+
const toolText = formatContentForCard(textToSend, "tool");
|
|
502
|
+
if (toolText) {
|
|
503
|
+
const sendResult = await sendMessage(dingtalkConfig, to, toolText, {
|
|
504
|
+
sessionWebhook,
|
|
505
|
+
atUserId: !isDirect ? senderId : null,
|
|
506
|
+
log,
|
|
507
|
+
card: currentAICard,
|
|
508
|
+
cardUpdateMode: "append",
|
|
509
|
+
});
|
|
510
|
+
if (!sendResult.ok) {
|
|
511
|
+
throw new Error(sendResult.error || "Tool stream send failed");
|
|
512
|
+
}
|
|
513
|
+
lastCardContent = currentAICard.lastStreamedContent || toolText;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
lastCardContent = textToSend;
|
|
519
|
+
const sendResult = await sendMessage(dingtalkConfig, to, textToSend, {
|
|
520
|
+
sessionWebhook,
|
|
521
|
+
atUserId: !isDirect ? senderId : null,
|
|
522
|
+
log,
|
|
523
|
+
card: currentAICard,
|
|
524
|
+
});
|
|
525
|
+
if (!sendResult.ok) {
|
|
526
|
+
throw new Error(sendResult.error || "Reply send failed");
|
|
527
|
+
}
|
|
528
|
+
} catch (err: any) {
|
|
529
|
+
log?.error?.(`[DingTalk] Reply failed: ${err.message}`);
|
|
530
|
+
if (err?.response?.data !== undefined) {
|
|
531
|
+
log?.error?.(formatDingTalkErrorPayloadLog("inbound.replyDeliver", err.response.data));
|
|
532
|
+
}
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
replyOptions: {
|
|
538
|
+
onReasoningStream: async (payload: any) => {
|
|
539
|
+
if (!useCardMode || !currentAICard) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
489
542
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
490
543
|
log?.debug?.(
|
|
491
|
-
`[DingTalk] Skipping
|
|
544
|
+
`[DingTalk] Skipping thinking stream update because card is terminal: state=${currentAICard.state}`,
|
|
492
545
|
);
|
|
493
546
|
return;
|
|
494
547
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
`[DingTalk] Tool result received, streaming to AI Card: ${textToSend.slice(0, 100)}`,
|
|
498
|
-
);
|
|
499
|
-
const toolText = formatContentForCard(textToSend, "tool");
|
|
500
|
-
if (toolText) {
|
|
501
|
-
await streamAICard(currentAICard, toolText, false, log);
|
|
548
|
+
const thinkingText = formatContentForCard(payload.text, "thinking");
|
|
549
|
+
if (!thinkingText) {
|
|
502
550
|
return;
|
|
503
551
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
552
|
+
try {
|
|
553
|
+
const sendResult = await sendMessage(dingtalkConfig, to, thinkingText, {
|
|
554
|
+
sessionWebhook,
|
|
555
|
+
atUserId: !isDirect ? senderId : null,
|
|
556
|
+
log,
|
|
557
|
+
card: currentAICard,
|
|
558
|
+
cardUpdateMode: "append",
|
|
559
|
+
});
|
|
560
|
+
if (!sendResult.ok) {
|
|
561
|
+
throw new Error(sendResult.error || "Thinking stream send failed");
|
|
562
|
+
}
|
|
563
|
+
} catch (err: any) {
|
|
564
|
+
log?.debug?.(`[DingTalk] Thinking stream update failed: ${err.message}`);
|
|
565
|
+
if (err?.response?.data !== undefined) {
|
|
566
|
+
log?.debug?.(formatDingTalkErrorPayloadLog("inbound.thinkingStream", err.response.data));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
queuedFinal = dispatchResult?.queuedFinal;
|
|
573
|
+
} catch (dispatchErr: any) {
|
|
574
|
+
if (useCardMode && currentAICard && !isCardInTerminalState(currentAICard.state)) {
|
|
575
|
+
try {
|
|
576
|
+
await finishAICard(currentAICard, "β ε€ηε€±θ΄₯", log);
|
|
577
|
+
} catch (cardCloseErr: any) {
|
|
578
|
+
log?.debug?.(`[DingTalk] Failed to finalize card after dispatch error: ${cardCloseErr.message}`);
|
|
579
|
+
currentAICard.state = AICardStatus.FAILED;
|
|
580
|
+
currentAICard.lastUpdated = Date.now();
|
|
527
581
|
}
|
|
582
|
+
}
|
|
583
|
+
throw dispatchErr;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 5) Finalize card stream if card mode is active.
|
|
587
|
+
if (useCardMode && currentAICard) {
|
|
588
|
+
try {
|
|
528
589
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
529
590
|
log?.debug?.(
|
|
530
|
-
`[DingTalk] Skipping
|
|
591
|
+
`[DingTalk] Skipping AI Card finalization because card is terminal: state=${currentAICard.state}`,
|
|
531
592
|
);
|
|
532
593
|
return;
|
|
533
594
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
595
|
+
|
|
596
|
+
const isNonEmptyString = (value: any): boolean =>
|
|
597
|
+
typeof value === "string" && value.trim().length > 0;
|
|
598
|
+
|
|
599
|
+
const hasLastCardContent = isNonEmptyString(lastCardContent);
|
|
600
|
+
const hasQueuedFinalString = isNonEmptyString(queuedFinal);
|
|
601
|
+
|
|
602
|
+
if (hasLastCardContent || hasQueuedFinalString) {
|
|
603
|
+
const finalContentCandidate =
|
|
604
|
+
hasLastCardContent && typeof lastCardContent === "string"
|
|
605
|
+
? lastCardContent
|
|
606
|
+
: typeof queuedFinal === "string"
|
|
607
|
+
? queuedFinal
|
|
608
|
+
: "";
|
|
609
|
+
if (isUnhandledStopReasonText(finalContentCandidate)) {
|
|
610
|
+
log?.warn?.(
|
|
611
|
+
`[DingTalk] Suppressed stop reason from AI Card final content: ${finalContentCandidate}`,
|
|
612
|
+
);
|
|
613
|
+
currentAICard.state = AICardStatus.FINISHED;
|
|
614
|
+
currentAICard.lastUpdated = Date.now();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const finalContent = finalContentCandidate;
|
|
618
|
+
await finishAICard(currentAICard, finalContent, log);
|
|
619
|
+
} else {
|
|
620
|
+
const lastStreamed = currentAICard.lastStreamedContent;
|
|
621
|
+
if (typeof lastStreamed === "string" && lastStreamed.trim().length > 0) {
|
|
622
|
+
await finishAICard(currentAICard, lastStreamed, log);
|
|
623
|
+
} else {
|
|
624
|
+
const defaultFinalContent = "β
Done";
|
|
625
|
+
log?.debug?.(
|
|
626
|
+
"[DingTalk] No textual content was produced; finalizing AI Card with default completion content.",
|
|
627
|
+
);
|
|
628
|
+
await finishAICard(currentAICard, defaultFinalContent, log);
|
|
544
629
|
}
|
|
545
630
|
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
// 5) Finalize card stream if card mode is active.
|
|
551
|
-
if (useCardMode && currentAICard) {
|
|
552
|
-
try {
|
|
553
|
-
if (isCardInTerminalState(currentAICard.state)) {
|
|
554
|
-
log?.debug?.(
|
|
555
|
-
`[DingTalk] Skipping AI Card finalization because card is terminal: state=${currentAICard.state}`,
|
|
556
|
-
);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const isNonEmptyString = (value: any): boolean =>
|
|
561
|
-
typeof value === "string" && value.trim().length > 0;
|
|
562
|
-
|
|
563
|
-
const hasLastCardContent = isNonEmptyString(lastCardContent);
|
|
564
|
-
const hasQueuedFinalString = isNonEmptyString(queuedFinal);
|
|
565
|
-
|
|
566
|
-
if (hasLastCardContent || hasQueuedFinalString) {
|
|
567
|
-
const finalContentCandidate =
|
|
568
|
-
hasLastCardContent && typeof lastCardContent === "string"
|
|
569
|
-
? lastCardContent
|
|
570
|
-
: typeof queuedFinal === "string"
|
|
571
|
-
? queuedFinal
|
|
572
|
-
: "";
|
|
573
|
-
if (isUnhandledStopReasonText(finalContentCandidate)) {
|
|
574
|
-
log?.warn?.(
|
|
575
|
-
`[DingTalk] Suppressed stop reason from AI Card final content: ${finalContentCandidate}`,
|
|
576
|
-
);
|
|
577
|
-
currentAICard.state = AICardStatus.FINISHED;
|
|
578
|
-
currentAICard.lastUpdated = Date.now();
|
|
579
|
-
return;
|
|
631
|
+
} catch (err: any) {
|
|
632
|
+
log?.debug?.(`[DingTalk] AI Card finalization failed: ${err.message}`);
|
|
633
|
+
if (err?.response?.data !== undefined) {
|
|
634
|
+
log?.debug?.(formatDingTalkErrorPayloadLog("inbound.cardFinalize", err.response.data));
|
|
580
635
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
await finishAICard(currentAICard, defaultFinalContent, log);
|
|
589
|
-
}
|
|
590
|
-
} catch (err: any) {
|
|
591
|
-
log?.debug?.(`[DingTalk] AI Card finalization failed: ${err.message}`);
|
|
592
|
-
if (err?.response?.data !== undefined) {
|
|
593
|
-
log?.debug?.(formatDingTalkErrorPayloadLog("inbound.cardFinalize", err.response.data));
|
|
594
|
-
}
|
|
595
|
-
try {
|
|
596
|
-
if (currentAICard.state !== AICardStatus.FINISHED) {
|
|
597
|
-
currentAICard.state = AICardStatus.FAILED;
|
|
598
|
-
currentAICard.lastUpdated = Date.now();
|
|
636
|
+
try {
|
|
637
|
+
if (currentAICard.state !== AICardStatus.FINISHED) {
|
|
638
|
+
currentAICard.state = AICardStatus.FAILED;
|
|
639
|
+
currentAICard.lastUpdated = Date.now();
|
|
640
|
+
}
|
|
641
|
+
} catch (stateErr: any) {
|
|
642
|
+
log?.debug?.(`[DingTalk] Failed to update card state to FAILED: ${stateErr.message}`);
|
|
599
643
|
}
|
|
600
|
-
} catch (stateErr: any) {
|
|
601
|
-
log?.debug?.(`[DingTalk] Failed to update card state to FAILED: ${stateErr.message}`);
|
|
602
644
|
}
|
|
603
645
|
}
|
|
646
|
+
} finally {
|
|
647
|
+
releaseSessionLock();
|
|
604
648
|
}
|
|
605
649
|
}
|