@soimy/dingtalk 3.5.3 → 3.6.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/index.ts +7 -0
- package/openclaw.plugin.json +104 -0
- package/package.json +1 -1
- package/src/card/card-markdown-image-reroute.ts +106 -0
- package/src/card/card-run-registry.ts +54 -1
- package/src/card/card-stop-handler.ts +10 -20
- package/src/card/card-template.ts +14 -3
- package/src/card/statusline-renderer.ts +94 -0
- package/src/card-draft-controller.ts +245 -52
- package/src/card-service.ts +368 -8
- package/src/channel.ts +19 -1081
- package/src/config-schema.ts +19 -0
- package/src/config.ts +117 -1
- package/src/device-registration.ts +245 -0
- package/src/gateway/channel-gateway.ts +636 -0
- package/src/inbound-handler.ts +147 -24
- package/src/media-utils.ts +6 -0
- package/src/message-utils.ts +124 -16
- package/src/messaging/btw-deliver.ts +85 -0
- package/src/messaging/channel-actions.ts +173 -0
- package/src/messaging/channel-outbound.ts +158 -0
- package/src/onboarding.ts +321 -232
- package/src/platform/channel-status.ts +81 -0
- package/src/reply-strategy-card.ts +373 -64
- package/src/reply-strategy-markdown.ts +1 -1
- package/src/reply-strategy-types.ts +93 -0
- package/src/reply-strategy-with-reaction.ts +1 -1
- package/src/reply-strategy.ts +14 -72
- package/src/run-usage-store.ts +59 -0
- package/src/send-service.ts +115 -3
- package/src/session-state.ts +62 -0
- package/src/targeting/agent-name-matcher.ts +28 -0
- package/src/types.ts +23 -147
package/src/card-service.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import type {
|
|
26
26
|
AICardInstance,
|
|
27
27
|
AICardStreamingRequest,
|
|
28
|
+
CardBlock,
|
|
28
29
|
DingTalkConfig,
|
|
29
30
|
DingTalkTrackingMetadata,
|
|
30
31
|
Logger,
|
|
@@ -53,7 +54,12 @@ export async function hideCardStopButton(
|
|
|
53
54
|
): Promise<void> {
|
|
54
55
|
for (let attempt = 0; ; attempt++) {
|
|
55
56
|
try {
|
|
56
|
-
await updateCardVariables(
|
|
57
|
+
await updateCardVariables(
|
|
58
|
+
outTrackId,
|
|
59
|
+
{ hasAction: String(STOP_ACTION_HIDDEN), stop_action: String(STOP_ACTION_HIDDEN) },
|
|
60
|
+
token,
|
|
61
|
+
config,
|
|
62
|
+
);
|
|
57
63
|
return;
|
|
58
64
|
} catch (err) {
|
|
59
65
|
if (attempt >= retries) {
|
|
@@ -204,6 +210,12 @@ export function clearAICardDegrade(accountId: string, log?: Logger): void {
|
|
|
204
210
|
log?.info?.(`[DingTalk][AICard][Degrade] Cleared for account=${accountId}, lastReason=${reason}`);
|
|
205
211
|
}
|
|
206
212
|
|
|
213
|
+
export function incrementCardDapiCount(card: AICardInstance): number {
|
|
214
|
+
const next = (card.dapiUsage || 0) + 1;
|
|
215
|
+
card.dapiUsage = next;
|
|
216
|
+
return next;
|
|
217
|
+
}
|
|
218
|
+
|
|
207
219
|
function extractCardProcessQueryKey(payload: unknown): string | undefined {
|
|
208
220
|
if (!payload || typeof payload !== "object") {
|
|
209
221
|
return undefined;
|
|
@@ -270,6 +282,7 @@ async function putAICardStreamingField(
|
|
|
270
282
|
`[DingTalk][AICard] Streaming response: status=${streamResp.status}, data=${JSON.stringify(streamResp.data)}`,
|
|
271
283
|
);
|
|
272
284
|
card.lastUpdated = Date.now();
|
|
285
|
+
incrementCardDapiCount(card);
|
|
273
286
|
} catch (err: any) {
|
|
274
287
|
if (err.response?.status === 401 && card.config && !tokenAlreadyRefreshed) {
|
|
275
288
|
log?.warn?.("[DingTalk][AICard] Received 401 error, attempting token refresh and retry...");
|
|
@@ -286,6 +299,7 @@ async function putAICardStreamingField(
|
|
|
286
299
|
`[DingTalk][AICard] Retry after token refresh succeeded: status=${retryResp.status}`,
|
|
287
300
|
);
|
|
288
301
|
card.lastUpdated = Date.now();
|
|
302
|
+
incrementCardDapiCount(card);
|
|
289
303
|
return;
|
|
290
304
|
} catch (retryErr: any) {
|
|
291
305
|
log?.error?.(`[DingTalk][AICard] Retry after token refresh failed: ${retryErr.message}`);
|
|
@@ -324,6 +338,10 @@ interface CreateAICardOptions {
|
|
|
324
338
|
storePath?: string;
|
|
325
339
|
persistPending?: boolean;
|
|
326
340
|
contextConversationId?: string;
|
|
341
|
+
/** Quote content to display in card header (shown when non-empty) */
|
|
342
|
+
quoteContent?: string;
|
|
343
|
+
/** Initial statusLine string to show on the first createAndDeliver render. */
|
|
344
|
+
statusLine?: string;
|
|
327
345
|
}
|
|
328
346
|
|
|
329
347
|
interface PendingCardRecord {
|
|
@@ -335,6 +353,8 @@ interface PendingCardRecord {
|
|
|
335
353
|
createdAt: number;
|
|
336
354
|
lastUpdated: number;
|
|
337
355
|
state: string;
|
|
356
|
+
lastContent?: string;
|
|
357
|
+
lastBlockListJson?: string;
|
|
338
358
|
}
|
|
339
359
|
|
|
340
360
|
interface PendingCardStateFile {
|
|
@@ -371,7 +391,9 @@ function normalizePendingState(parsed: Partial<PendingCardStateFile>): PendingCa
|
|
|
371
391
|
typeof entry.accountId === "string" &&
|
|
372
392
|
typeof entry.cardInstanceId === "string" &&
|
|
373
393
|
(entry.outTrackId === undefined || typeof entry.outTrackId === "string") &&
|
|
374
|
-
typeof entry.conversationId === "string"
|
|
394
|
+
typeof entry.conversationId === "string" &&
|
|
395
|
+
(entry.lastContent === undefined || typeof entry.lastContent === "string") &&
|
|
396
|
+
(entry.lastBlockListJson === undefined || typeof entry.lastBlockListJson === "string"),
|
|
375
397
|
),
|
|
376
398
|
),
|
|
377
399
|
};
|
|
@@ -443,6 +465,8 @@ function upsertPendingCard(card: AICardInstance, storePath?: string, log?: Logge
|
|
|
443
465
|
createdAt: card.createdAt,
|
|
444
466
|
lastUpdated: card.lastUpdated,
|
|
445
467
|
state: card.state,
|
|
468
|
+
lastContent: card.lastStreamedContent,
|
|
469
|
+
lastBlockListJson: card.lastBlockListJson,
|
|
446
470
|
};
|
|
447
471
|
const index = state.pendingCards.findIndex((item) => item.cardInstanceId === card.cardInstanceId);
|
|
448
472
|
if (index >= 0) {
|
|
@@ -454,6 +478,36 @@ function upsertPendingCard(card: AICardInstance, storePath?: string, log?: Logge
|
|
|
454
478
|
writePendingCardState(state, storePath, log);
|
|
455
479
|
}
|
|
456
480
|
|
|
481
|
+
function parseStoredBlockList(blockListJson?: string): CardBlock[] {
|
|
482
|
+
if (!blockListJson?.trim()) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const parsed = JSON.parse(blockListJson) as unknown;
|
|
487
|
+
return Array.isArray(parsed) ? (parsed as CardBlock[]) : [];
|
|
488
|
+
} catch {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function buildStoppedCardFinalizePayload(params: {
|
|
494
|
+
reason: string;
|
|
495
|
+
previousContent?: string;
|
|
496
|
+
previousBlockListJson?: string;
|
|
497
|
+
}): { blockListJson: string; content: string } {
|
|
498
|
+
const markerText = params.reason.trim();
|
|
499
|
+
const baseContent = params.previousContent?.trim() || "";
|
|
500
|
+
const blocks = parseStoredBlockList(params.previousBlockListJson);
|
|
501
|
+
blocks.push({ type: 0, markdown: markerText });
|
|
502
|
+
const content = baseContent
|
|
503
|
+
? `${baseContent}\n\n---\n*${markerText}*`
|
|
504
|
+
: markerText;
|
|
505
|
+
return {
|
|
506
|
+
blockListJson: JSON.stringify(blocks),
|
|
507
|
+
content,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
457
511
|
function removePendingCard(card: AICardInstance, log?: Logger): void {
|
|
458
512
|
if (!card.accountId || !card.storePath) {
|
|
459
513
|
return;
|
|
@@ -500,6 +554,26 @@ export function isCardInTerminalState(state: string): boolean {
|
|
|
500
554
|
);
|
|
501
555
|
}
|
|
502
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Ensure card access token is fresh (refresh if >90min old).
|
|
559
|
+
* Mutates card.accessToken in place if refreshed.
|
|
560
|
+
*/
|
|
561
|
+
async function ensureFreshToken(card: AICardInstance, log?: Logger): Promise<void> {
|
|
562
|
+
const tokenAge = Date.now() - card.createdAt;
|
|
563
|
+
const tokenRefreshThreshold = 90 * 60 * 1000;
|
|
564
|
+
|
|
565
|
+
if (tokenAge > tokenRefreshThreshold && card.config) {
|
|
566
|
+
log?.debug?.("[DingTalk][AICard] Token age exceeds threshold, refreshing...");
|
|
567
|
+
try {
|
|
568
|
+
card.accessToken = await getAccessToken(card.config, log);
|
|
569
|
+
log?.debug?.("[DingTalk][AICard] Token refreshed successfully");
|
|
570
|
+
} catch (err: unknown) {
|
|
571
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
572
|
+
log?.warn?.(`[DingTalk][AICard] Failed to refresh token: ${msg}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
503
577
|
export function formatContentForCard(content: string | undefined, type: "thinking" | "tool"): string {
|
|
504
578
|
if (!content) {
|
|
505
579
|
return "";
|
|
@@ -657,16 +731,23 @@ async function finalizePendingCardsByAccount(
|
|
|
657
731
|
lastUpdated: entry.lastUpdated || Date.now(),
|
|
658
732
|
state: normalizeRecoveredState(entry.state),
|
|
659
733
|
config,
|
|
734
|
+
lastStreamedContent: entry.lastContent,
|
|
735
|
+
lastBlockListJson: entry.lastBlockListJson,
|
|
660
736
|
};
|
|
661
737
|
try {
|
|
662
|
-
await
|
|
738
|
+
await finalizeStoppedAICard(card, {
|
|
739
|
+
reason,
|
|
740
|
+
previousContent: entry.lastContent,
|
|
741
|
+
previousBlockListJson: entry.lastBlockListJson,
|
|
742
|
+
}, log);
|
|
663
743
|
finalizedCount += 1;
|
|
664
|
-
} catch (err:
|
|
744
|
+
} catch (err: unknown) {
|
|
665
745
|
const action = mode === "recover" ? "recover" : "finalize";
|
|
746
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
666
747
|
log?.warn?.(
|
|
667
|
-
`[DingTalk][AICard] Failed to ${action} active card ${entry.cardInstanceId}: ${
|
|
748
|
+
`[DingTalk][AICard] Failed to ${action} active card ${entry.cardInstanceId}: ${message}`,
|
|
668
749
|
);
|
|
669
|
-
|
|
750
|
+
// Pending record intentionally kept for manual investigation
|
|
670
751
|
}
|
|
671
752
|
}
|
|
672
753
|
return finalizedCount;
|
|
@@ -705,8 +786,13 @@ export async function createAICard(
|
|
|
705
786
|
// Status is set to "streaming" via the streaming API immediately after creation.
|
|
706
787
|
const cardParamMap = {
|
|
707
788
|
config: JSON.stringify({ autoLayout: true, enableForward: true }),
|
|
708
|
-
[template.
|
|
709
|
-
|
|
789
|
+
[template.streamingKey]: "",
|
|
790
|
+
quoteContent: options.quoteContent || "",
|
|
791
|
+
...(options.statusLine?.trim() ? { statusLine: options.statusLine } : {}),
|
|
792
|
+
// V2 template uses hasAction (string), V1 uses stop_action (string)
|
|
793
|
+
// DingTalk cardParamMap requires all values to be strings
|
|
794
|
+
hasAction: String(STOP_ACTION_VISIBLE),
|
|
795
|
+
stop_action: String(STOP_ACTION_VISIBLE),
|
|
710
796
|
};
|
|
711
797
|
const createAndDeliverBody = {
|
|
712
798
|
cardTemplateId: template.templateId,
|
|
@@ -758,6 +844,13 @@ export async function createAICard(
|
|
|
758
844
|
cardInstanceId?: unknown;
|
|
759
845
|
}
|
|
760
846
|
| undefined;
|
|
847
|
+
const deliverResults = (responseData?.result as { deliverResults?: Array<{ success?: boolean; errorMsg?: string }> } | undefined)?.deliverResults;
|
|
848
|
+
if (Array.isArray(deliverResults)) {
|
|
849
|
+
const failedDelivery = deliverResults.find((item) => item?.success === false);
|
|
850
|
+
if (failedDelivery) {
|
|
851
|
+
throw new Error(failedDelivery.errorMsg?.trim() || "DingTalk card delivery failed");
|
|
852
|
+
}
|
|
853
|
+
}
|
|
761
854
|
const responseTracking = responseData?.result;
|
|
762
855
|
const processQueryKey =
|
|
763
856
|
typeof responseTracking?.processQueryKey === "string" &&
|
|
@@ -793,6 +886,7 @@ export async function createAICard(
|
|
|
793
886
|
config,
|
|
794
887
|
processQueryKey: processQueryKey || extractCardProcessQueryKey(resp.data),
|
|
795
888
|
outTrackId,
|
|
889
|
+
dapiUsage: 1,
|
|
796
890
|
};
|
|
797
891
|
if (shouldPersistPending) {
|
|
798
892
|
upsertPendingCard(aiCardInstance, options.storePath, log);
|
|
@@ -836,6 +930,216 @@ export async function createAICard(
|
|
|
836
930
|
}
|
|
837
931
|
}
|
|
838
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Update statusLine via PUT /v1.0/card/instances API.
|
|
935
|
+
*/
|
|
936
|
+
export async function updateAICardStatusLine(
|
|
937
|
+
card: AICardInstance,
|
|
938
|
+
statusLine: string,
|
|
939
|
+
log?: Logger,
|
|
940
|
+
): Promise<void> {
|
|
941
|
+
if (isCardInTerminalState(card.state) || !statusLine.trim()) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
await ensureFreshToken(card, log);
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
await updateCardVariables(
|
|
949
|
+
card.outTrackId || card.cardInstanceId,
|
|
950
|
+
{ statusLine },
|
|
951
|
+
card.accessToken,
|
|
952
|
+
card.config,
|
|
953
|
+
);
|
|
954
|
+
incrementCardDapiCount(card);
|
|
955
|
+
card.lastUpdated = Date.now();
|
|
956
|
+
} catch (err: unknown) {
|
|
957
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
958
|
+
log?.warn?.(`[DingTalk][AICard] StatusLine update failed: ${message}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export async function updateAICardBlockList(
|
|
963
|
+
card: AICardInstance,
|
|
964
|
+
blockListJson: string,
|
|
965
|
+
log?: Logger,
|
|
966
|
+
options?: { statusLine?: string },
|
|
967
|
+
): Promise<void> {
|
|
968
|
+
if (isCardInTerminalState(card.state)) {
|
|
969
|
+
log?.debug?.(
|
|
970
|
+
`[DingTalk][AICard] Skip blockList update because card already terminal: outTrackId=${card.cardInstanceId} state=${card.state}`,
|
|
971
|
+
);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Ensure token is fresh before API call
|
|
976
|
+
await ensureFreshToken(card, log);
|
|
977
|
+
|
|
978
|
+
const template = DINGTALK_CARD_TEMPLATE;
|
|
979
|
+
const params: Record<string, unknown> = {
|
|
980
|
+
[template.blockListKey]: blockListJson,
|
|
981
|
+
};
|
|
982
|
+
if (options?.statusLine?.trim()) {
|
|
983
|
+
params.statusLine = options.statusLine;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
await updateCardVariables(
|
|
988
|
+
card.outTrackId || card.cardInstanceId,
|
|
989
|
+
params,
|
|
990
|
+
card.accessToken,
|
|
991
|
+
card.config,
|
|
992
|
+
);
|
|
993
|
+
incrementCardDapiCount(card);
|
|
994
|
+
card.lastBlockListJson = blockListJson;
|
|
995
|
+
card.lastUpdated = Date.now();
|
|
996
|
+
if (card.state === AICardStatus.PROCESSING) {
|
|
997
|
+
card.state = AICardStatus.INPUTING;
|
|
998
|
+
}
|
|
999
|
+
upsertPendingCard(card, card.storePath, log);
|
|
1000
|
+
} catch (err: unknown) {
|
|
1001
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1002
|
+
log?.error?.(`[DingTalk][AICard] BlockList update failed: ${message}`);
|
|
1003
|
+
throw err;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Stream answer text to content key for real-time display.
|
|
1009
|
+
* Only used when cardRealTimeStream=true.
|
|
1010
|
+
* Uses streaming API because content is a simple string type.
|
|
1011
|
+
*/
|
|
1012
|
+
export async function streamAICardContent(
|
|
1013
|
+
card: AICardInstance,
|
|
1014
|
+
text: string,
|
|
1015
|
+
log?: Logger,
|
|
1016
|
+
): Promise<void> {
|
|
1017
|
+
if (isCardInTerminalState(card.state)) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const template = DINGTALK_CARD_TEMPLATE;
|
|
1021
|
+
await putAICardStreamingField(card, template.streamingKey, text, false, log);
|
|
1022
|
+
card.lastStreamedContent = text;
|
|
1023
|
+
upsertPendingCard(card, card.storePath, log);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Clear the streaming content key.
|
|
1028
|
+
* Called when transitioning from streaming to blockList commit.
|
|
1029
|
+
*/
|
|
1030
|
+
export async function clearAICardStreamingContent(
|
|
1031
|
+
card: AICardInstance,
|
|
1032
|
+
log?: Logger,
|
|
1033
|
+
): Promise<void> {
|
|
1034
|
+
if (isCardInTerminalState(card.state)) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const template = DINGTALK_CARD_TEMPLATE;
|
|
1038
|
+
try {
|
|
1039
|
+
await putAICardStreamingField(card, template.streamingKey, "", false, log);
|
|
1040
|
+
} catch (err: unknown) {
|
|
1041
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1042
|
+
log?.debug?.(`[DingTalk][AICard] Non-critical: failed to clear streaming content: ${message}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Options for finalizing an AI Card via instances API.
|
|
1048
|
+
* All variables are written in a single API call for V2 template compatibility.
|
|
1049
|
+
*/
|
|
1050
|
+
export interface FinalizeCardOptions {
|
|
1051
|
+
/** CardBlock[] JSON string for blockList variable */
|
|
1052
|
+
blockListJson: string;
|
|
1053
|
+
/** Pure markdown answer text for copy action (content variable) */
|
|
1054
|
+
content: string;
|
|
1055
|
+
/** Optional quoted message preview text */
|
|
1056
|
+
quoteContent?: string;
|
|
1057
|
+
/** Optional statusLine string for card template */
|
|
1058
|
+
statusLine?: string;
|
|
1059
|
+
/** Optional quoted message reference for caching */
|
|
1060
|
+
quotedRef?: QuotedRef;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Commit blocks and finalize card via single instances API call.
|
|
1065
|
+
* V2 template requires finalize through instances API (not streaming API).
|
|
1066
|
+
* Writes blockList, content, quoteContent, statusLine, and flowStatus in one call.
|
|
1067
|
+
*/
|
|
1068
|
+
export async function commitAICardBlocks(
|
|
1069
|
+
card: AICardInstance,
|
|
1070
|
+
options: FinalizeCardOptions,
|
|
1071
|
+
log?: Logger,
|
|
1072
|
+
): Promise<void> {
|
|
1073
|
+
if (isCardInTerminalState(card.state)) {
|
|
1074
|
+
log?.debug?.(
|
|
1075
|
+
`[DingTalk][AICard] Skip finalize because card already terminal: outTrackId=${card.cardInstanceId} state=${card.state}`,
|
|
1076
|
+
);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
await ensureFreshToken(card, log);
|
|
1081
|
+
|
|
1082
|
+
const template = DINGTALK_CARD_TEMPLATE;
|
|
1083
|
+
const updates: Record<string, unknown> = {
|
|
1084
|
+
[template.blockListKey]: options.blockListJson,
|
|
1085
|
+
[template.streamingKey]: options.content, // markdown content for display
|
|
1086
|
+
[template.copyContentKey]: options.content, // same markdown as String type for card copy action
|
|
1087
|
+
flowStatus: 3, // completed state - V2 template hides stop button automatically
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// Optional fields
|
|
1091
|
+
if (options.quoteContent?.trim()) {
|
|
1092
|
+
updates.quoteContent = options.quoteContent;
|
|
1093
|
+
}
|
|
1094
|
+
if (options.statusLine?.trim()) {
|
|
1095
|
+
updates.statusLine = options.statusLine;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
log?.debug?.(
|
|
1099
|
+
`[DingTalk][AICard] Finalizing via instances API: outTrackId=${card.outTrackId || card.cardInstanceId} ` +
|
|
1100
|
+
`blockListLen=${options.blockListJson.length} contentLen=${options.content.length} flowStatus=3` +
|
|
1101
|
+
(options.statusLine ? ` statusLine="${options.statusLine}"` : ""),
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
await updateCardVariables(
|
|
1106
|
+
card.outTrackId || card.cardInstanceId,
|
|
1107
|
+
updates,
|
|
1108
|
+
card.accessToken,
|
|
1109
|
+
card.config,
|
|
1110
|
+
);
|
|
1111
|
+
incrementCardDapiCount(card);
|
|
1112
|
+
card.lastBlockListJson = options.blockListJson;
|
|
1113
|
+
card.lastStreamedContent = options.content;
|
|
1114
|
+
card.lastUpdated = Date.now();
|
|
1115
|
+
} catch (err: unknown) {
|
|
1116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1117
|
+
log?.error?.(`[DingTalk][AICard] Finalize via instances API failed: ${message}`);
|
|
1118
|
+
throw err;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Cache card content for quote recovery
|
|
1122
|
+
if (card.conversationId && options.content.trim() && card.accountId && card.processQueryKey) {
|
|
1123
|
+
const primaryConversationId = card.contextConversationId || card.conversationId;
|
|
1124
|
+
cacheCardContentByProcessQueryKey(
|
|
1125
|
+
card.accountId,
|
|
1126
|
+
primaryConversationId,
|
|
1127
|
+
card.processQueryKey,
|
|
1128
|
+
options.content,
|
|
1129
|
+
card.storePath,
|
|
1130
|
+
options.quotedRef,
|
|
1131
|
+
log,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Update local state
|
|
1136
|
+
card.state = AICardStatus.FINISHED;
|
|
1137
|
+
card.lastUpdated = Date.now();
|
|
1138
|
+
removePendingCard(card, log);
|
|
1139
|
+
log?.info?.(`[DingTalk][AICard] Card finalized: outTrackId=${card.outTrackId || card.cardInstanceId} state=FINISHED`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
|
|
839
1143
|
export async function streamAICard(
|
|
840
1144
|
card: AICardInstance,
|
|
841
1145
|
content: string,
|
|
@@ -858,6 +1162,7 @@ export async function streamAICard(
|
|
|
858
1162
|
removePendingCard(card, log);
|
|
859
1163
|
} else if (card.state === AICardStatus.PROCESSING) {
|
|
860
1164
|
card.state = AICardStatus.INPUTING;
|
|
1165
|
+
upsertPendingCard(card, card.storePath, log);
|
|
861
1166
|
}
|
|
862
1167
|
} catch (err: any) {
|
|
863
1168
|
card.state = AICardStatus.FAILED;
|
|
@@ -873,6 +1178,14 @@ export async function streamAICard(
|
|
|
873
1178
|
}
|
|
874
1179
|
}
|
|
875
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* Finalize AI Card via streaming API.
|
|
1183
|
+
*
|
|
1184
|
+
* @deprecated For V2 template, use `commitAICardBlocks()` instead which finalizes
|
|
1185
|
+
* via instances API (single call writes blockList, content, flowStatus=3).
|
|
1186
|
+
* This function is kept for backward compatibility with V1 template and for
|
|
1187
|
+
* card-stop-handler which uses streaming API for immediate stop acknowledgment.
|
|
1188
|
+
*/
|
|
876
1189
|
export async function finishAICard(
|
|
877
1190
|
card: AICardInstance,
|
|
878
1191
|
content: string,
|
|
@@ -1039,6 +1352,53 @@ export async function finishStoppedAICard(
|
|
|
1039
1352
|
}
|
|
1040
1353
|
}
|
|
1041
1354
|
|
|
1355
|
+
export async function finalizeStoppedAICard(
|
|
1356
|
+
card: AICardInstance,
|
|
1357
|
+
options: {
|
|
1358
|
+
reason: string;
|
|
1359
|
+
previousContent?: string;
|
|
1360
|
+
previousBlockListJson?: string;
|
|
1361
|
+
},
|
|
1362
|
+
log?: Logger,
|
|
1363
|
+
): Promise<void> {
|
|
1364
|
+
if (isCardInTerminalState(card.state)) {
|
|
1365
|
+
log?.debug?.(
|
|
1366
|
+
`[DingTalk][AICard] finalizeStoppedAICard skipped — already terminal: ${card.state}`,
|
|
1367
|
+
);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
await ensureFreshToken(card, log);
|
|
1372
|
+
const template = DINGTALK_CARD_TEMPLATE;
|
|
1373
|
+
const payload = buildStoppedCardFinalizePayload(options);
|
|
1374
|
+
try {
|
|
1375
|
+
await updateCardVariables(
|
|
1376
|
+
card.outTrackId || card.cardInstanceId,
|
|
1377
|
+
{
|
|
1378
|
+
[template.blockListKey]: payload.blockListJson,
|
|
1379
|
+
[template.streamingKey]: payload.content,
|
|
1380
|
+
[template.copyContentKey]: payload.content,
|
|
1381
|
+
flowStatus: 3,
|
|
1382
|
+
},
|
|
1383
|
+
card.accessToken,
|
|
1384
|
+
card.config,
|
|
1385
|
+
);
|
|
1386
|
+
incrementCardDapiCount(card);
|
|
1387
|
+
card.lastBlockListJson = payload.blockListJson;
|
|
1388
|
+
card.lastStreamedContent = payload.content;
|
|
1389
|
+
card.state = AICardStatus.STOPPED;
|
|
1390
|
+
card.lastUpdated = Date.now();
|
|
1391
|
+
removePendingCard(card, log);
|
|
1392
|
+
} catch (err: unknown) {
|
|
1393
|
+
card.lastBlockListJson = payload.blockListJson;
|
|
1394
|
+
card.lastStreamedContent = payload.content;
|
|
1395
|
+
card.state = AICardStatus.STOPPED;
|
|
1396
|
+
card.lastUpdated = Date.now();
|
|
1397
|
+
// Keep pending record for manual investigation on API failure
|
|
1398
|
+
throw err;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1042
1402
|
function cacheCardContentByProcessQueryKey(
|
|
1043
1403
|
accountId: string,
|
|
1044
1404
|
conversationId: string,
|