@soimy/dingtalk 3.2.0 → 3.4.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/LICENSE +21 -0
- package/README.md +796 -42
- package/index.ts +62 -0
- package/package.json +4 -2
- package/src/access-control.ts +83 -0
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
- package/src/ack-reaction-classifier.ts +75 -0
- package/src/ack-reaction-service.ts +182 -0
- package/src/attachment-text-extractor.ts +148 -0
- package/src/card-callback-service.ts +119 -0
- package/src/card-draft-controller.ts +114 -0
- package/src/card-service.ts +666 -26
- package/src/channel.ts +455 -150
- package/src/config-schema.ts +64 -6
- package/src/config.ts +161 -5
- package/src/connection-manager.ts +354 -47
- package/src/dedup.ts +1 -0
- package/src/docs-service.ts +198 -0
- package/src/draft-stream-loop.ts +119 -0
- package/src/feedback-learning-service.ts +643 -0
- package/src/feedback-learning-store.ts +543 -0
- package/src/group-members-store.ts +48 -14
- package/src/inbound-handler.ts +1374 -259
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +94 -50
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +487 -46
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +96 -1
- package/src/peer-id-registry.ts +102 -0
- package/src/persistence-store.ts +131 -0
- package/src/quoted-file-service.ts +385 -0
- package/src/reply-strategy-card.ts +225 -0
- package/src/reply-strategy-markdown.ts +55 -0
- package/src/reply-strategy-with-reaction.ts +190 -0
- package/src/reply-strategy.ts +72 -0
- package/src/send-service.ts +267 -45
- package/src/session-command-service.ts +147 -0
- package/src/session-lock.ts +2 -0
- package/src/session-peer-store.ts +77 -0
- package/src/session-routing.ts +33 -0
- package/src/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +151 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +261 -28
- package/src/utils.ts +231 -12
package/src/card-service.ts
CHANGED
|
@@ -1,47 +1,392 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import axios from "axios";
|
|
3
5
|
import { getAccessToken } from "./auth";
|
|
4
6
|
import { stripTargetPrefix } from "./config";
|
|
5
7
|
import { resolveOriginalPeerId } from "./peer-id-registry";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createSyntheticOutboundMsgId,
|
|
10
|
+
clearMessageContextCacheForTest,
|
|
11
|
+
DEFAULT_CARD_CONTENT_TTL_MS,
|
|
12
|
+
DEFAULT_CREATED_AT_MATCH_WINDOW_MS,
|
|
13
|
+
resolveByCreatedAtWindow,
|
|
14
|
+
upsertOutboundMessageContext,
|
|
15
|
+
} from "./message-context-store";
|
|
16
|
+
import {
|
|
17
|
+
readNamespaceJson,
|
|
18
|
+
resolveNamespacePath,
|
|
19
|
+
writeNamespaceJsonAtomic,
|
|
20
|
+
} from "./persistence-store";
|
|
7
21
|
import type {
|
|
8
22
|
AICardInstance,
|
|
9
23
|
AICardStreamingRequest,
|
|
10
24
|
DingTalkConfig,
|
|
25
|
+
DingTalkTrackingMetadata,
|
|
11
26
|
Logger,
|
|
27
|
+
QuotedRef,
|
|
12
28
|
} from "./types";
|
|
13
29
|
import { AICardStatus } from "./types";
|
|
30
|
+
import { formatDingTalkErrorPayloadLog, getProxyBypassOption } from "./utils";
|
|
14
31
|
|
|
15
32
|
const DINGTALK_API = "https://api.dingtalk.com";
|
|
16
33
|
// Thinking/tool stream snippets are truncated to keep card updates compact.
|
|
17
|
-
const
|
|
34
|
+
const CARD_STATE_FILE_VERSION = 1;
|
|
35
|
+
const CARD_PENDING_NAMESPACE = "cards.active.pending";
|
|
36
|
+
const RECOVERY_FINALIZE_MESSAGE = "⚠️ 上一次回复处理中断,已自动结束。请重新发送你的问题。";
|
|
37
|
+
const AICARD_DEGRADE_DEFAULT_MS = 30 * 60 * 1000;
|
|
38
|
+
const CARD_CACHE_MAX_PER_CONVERSATION = 20;
|
|
39
|
+
const CARD_CACHE_MAX_CONVERSATIONS = 500;
|
|
40
|
+
const DYNAMIC_SUMMARY_EXTENSION = { dynamicSummary: "true" } as const;
|
|
41
|
+
|
|
42
|
+
const aicardDegradeByAccount = new Map<string, { untilMs: number; reason: string }>();
|
|
43
|
+
const inMemoryCardContentStore = new Map<
|
|
44
|
+
string,
|
|
45
|
+
{
|
|
46
|
+
entries: Array<{ content: string; createdAt: number; expiresAt: number }>;
|
|
47
|
+
lastActiveAt: number;
|
|
48
|
+
}
|
|
49
|
+
>();
|
|
50
|
+
|
|
51
|
+
function pruneInMemoryCardContentEntries(
|
|
52
|
+
entries: Array<{ content: string; createdAt: number; expiresAt: number }>,
|
|
53
|
+
nowMs: number,
|
|
54
|
+
): Array<{ content: string; createdAt: number; expiresAt: number }> {
|
|
55
|
+
return entries.filter((entry) => nowMs < entry.expiresAt).slice(-CARD_CACHE_MAX_PER_CONVERSATION);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function touchInMemoryCardContentBucket(scopeKey: string, nowMs: number): {
|
|
59
|
+
entries: Array<{ content: string; createdAt: number; expiresAt: number }>;
|
|
60
|
+
lastActiveAt: number;
|
|
61
|
+
} {
|
|
62
|
+
const existing = inMemoryCardContentStore.get(scopeKey);
|
|
63
|
+
const bucket = existing
|
|
64
|
+
? {
|
|
65
|
+
entries: pruneInMemoryCardContentEntries(existing.entries, nowMs),
|
|
66
|
+
lastActiveAt: nowMs,
|
|
67
|
+
}
|
|
68
|
+
: { entries: [], lastActiveAt: nowMs };
|
|
69
|
+
inMemoryCardContentStore.set(scopeKey, bucket);
|
|
70
|
+
if (inMemoryCardContentStore.size > CARD_CACHE_MAX_CONVERSATIONS) {
|
|
71
|
+
let oldestKey: string | undefined;
|
|
72
|
+
let oldestTime = Infinity;
|
|
73
|
+
for (const [key, candidate] of inMemoryCardContentStore) {
|
|
74
|
+
if (candidate.lastActiveAt < oldestTime) {
|
|
75
|
+
oldestTime = candidate.lastActiveAt;
|
|
76
|
+
oldestKey = key;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (oldestKey) {
|
|
80
|
+
inMemoryCardContentStore.delete(oldestKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return bucket;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getAICardDegradeMs(config?: DingTalkConfig): number {
|
|
87
|
+
const raw = config?.aicardDegradeMs;
|
|
88
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 60_000) {
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
return AICARD_DEGRADE_DEFAULT_MS;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeDegradeErrorMessage(value: string): string {
|
|
95
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shouldTriggerAICardDegrade(err: unknown): boolean {
|
|
99
|
+
const maybeErr = err as {
|
|
100
|
+
response?: { status?: number; data?: { message?: string } };
|
|
101
|
+
message?: string;
|
|
102
|
+
};
|
|
103
|
+
const status = maybeErr.response?.status;
|
|
104
|
+
const msg = normalizeDegradeErrorMessage(
|
|
105
|
+
String(maybeErr.response?.data?.message || maybeErr.message || ""),
|
|
106
|
+
);
|
|
107
|
+
if (status === 403 || status === 429) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (typeof status === "number" && status >= 500 && status < 600) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
"ipnotinwhitelist",
|
|
115
|
+
"forbiddenaccessdenied",
|
|
116
|
+
"timeout",
|
|
117
|
+
"etimedout",
|
|
118
|
+
"econnreset",
|
|
119
|
+
"eaiagain",
|
|
120
|
+
"sockethangup",
|
|
121
|
+
"badgateway",
|
|
122
|
+
].some((keyword) => msg.includes(keyword));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isAICardDegraded(accountId: string): boolean {
|
|
126
|
+
const state = aicardDegradeByAccount.get(accountId);
|
|
127
|
+
if (!state) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (Date.now() >= state.untilMs) {
|
|
131
|
+
aicardDegradeByAccount.delete(accountId);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getAICardDegradeState(
|
|
138
|
+
accountId: string,
|
|
139
|
+
): { remainingMs: number; reason: string } | null {
|
|
140
|
+
const state = aicardDegradeByAccount.get(accountId);
|
|
141
|
+
if (!state) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const remainingMs = state.untilMs - Date.now();
|
|
145
|
+
if (remainingMs <= 0) {
|
|
146
|
+
aicardDegradeByAccount.delete(accountId);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return { remainingMs, reason: state.reason };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function activateAICardDegrade(
|
|
153
|
+
accountId: string,
|
|
154
|
+
reason: string,
|
|
155
|
+
config?: DingTalkConfig,
|
|
156
|
+
log?: Logger,
|
|
157
|
+
): void {
|
|
158
|
+
const durationMs = getAICardDegradeMs(config);
|
|
159
|
+
const untilMs = Date.now() + durationMs;
|
|
160
|
+
const existed = isAICardDegraded(accountId);
|
|
161
|
+
aicardDegradeByAccount.set(accountId, { untilMs, reason });
|
|
162
|
+
const minutes = Math.round(durationMs / 60000);
|
|
163
|
+
if (existed) {
|
|
164
|
+
log?.warn?.(
|
|
165
|
+
`[DingTalk][AICard][Degrade] Extended for account=${accountId}, minutes=${minutes}, reason=${reason}`,
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
log?.warn?.(
|
|
169
|
+
`[DingTalk][AICard][Degrade] Activated for account=${accountId}, minutes=${minutes}, reason=${reason}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function clearAICardDegrade(accountId: string, log?: Logger): void {
|
|
175
|
+
if (!aicardDegradeByAccount.has(accountId)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const reason = aicardDegradeByAccount.get(accountId)?.reason || "";
|
|
179
|
+
aicardDegradeByAccount.delete(accountId);
|
|
180
|
+
log?.info?.(`[DingTalk][AICard][Degrade] Cleared for account=${accountId}, lastReason=${reason}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractCardProcessQueryKey(payload: unknown): string | undefined {
|
|
184
|
+
if (!payload || typeof payload !== "object") {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const data = payload as Record<string, any>;
|
|
188
|
+
const deliverResults = data.result?.deliverResults;
|
|
189
|
+
if (Array.isArray(deliverResults)) {
|
|
190
|
+
for (const item of deliverResults) {
|
|
191
|
+
if (typeof item?.carrierId === "string" && item.carrierId.trim()) {
|
|
192
|
+
return item.carrierId.trim();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface CreateAICardOptions {
|
|
200
|
+
accountId?: string;
|
|
201
|
+
storePath?: string;
|
|
202
|
+
persistPending?: boolean;
|
|
203
|
+
contextConversationId?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface PendingCardRecord {
|
|
207
|
+
accountId: string;
|
|
208
|
+
cardInstanceId: string;
|
|
209
|
+
outTrackId?: string;
|
|
210
|
+
conversationId: string;
|
|
211
|
+
contextConversationId?: string;
|
|
212
|
+
createdAt: number;
|
|
213
|
+
lastUpdated: number;
|
|
214
|
+
state: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface PendingCardStateFile {
|
|
218
|
+
version: number;
|
|
219
|
+
updatedAt: number;
|
|
220
|
+
pendingCards: PendingCardRecord[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getCardStateFilePath(storePath?: string): string | null {
|
|
224
|
+
if (!storePath) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return resolveNamespacePath(CARD_PENDING_NAMESPACE, {
|
|
228
|
+
storePath,
|
|
229
|
+
format: "json",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getLegacyCardStateFilePath(storePath?: string): string | null {
|
|
234
|
+
if (!storePath) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return path.join(path.dirname(storePath), "dingtalk-active-cards.json");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizePendingState(parsed: Partial<PendingCardStateFile>): PendingCardStateFile {
|
|
241
|
+
const records = Array.isArray(parsed.pendingCards) ? parsed.pendingCards : [];
|
|
242
|
+
return {
|
|
243
|
+
version: typeof parsed.version === "number" ? parsed.version : CARD_STATE_FILE_VERSION,
|
|
244
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
245
|
+
pendingCards: records.filter((entry): entry is PendingCardRecord =>
|
|
246
|
+
Boolean(
|
|
247
|
+
entry &&
|
|
248
|
+
typeof entry.accountId === "string" &&
|
|
249
|
+
typeof entry.cardInstanceId === "string" &&
|
|
250
|
+
(entry.outTrackId === undefined || typeof entry.outTrackId === "string") &&
|
|
251
|
+
typeof entry.conversationId === "string",
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readPendingCardState(storePath?: string, log?: Logger): PendingCardStateFile {
|
|
258
|
+
if (!storePath) {
|
|
259
|
+
return { version: CARD_STATE_FILE_VERSION, updatedAt: Date.now(), pendingCards: [] };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const filePath = getCardStateFilePath(storePath);
|
|
263
|
+
const legacyPath = getLegacyCardStateFilePath(storePath);
|
|
264
|
+
|
|
265
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
266
|
+
const parsed = readNamespaceJson<Partial<PendingCardStateFile>>(CARD_PENDING_NAMESPACE, {
|
|
267
|
+
storePath,
|
|
268
|
+
format: "json",
|
|
269
|
+
fallback: {},
|
|
270
|
+
log,
|
|
271
|
+
});
|
|
272
|
+
return normalizePendingState(parsed);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
if (!legacyPath || !fs.existsSync(legacyPath)) {
|
|
277
|
+
return { version: CARD_STATE_FILE_VERSION, updatedAt: Date.now(), pendingCards: [] };
|
|
278
|
+
}
|
|
279
|
+
const raw = fs.readFileSync(legacyPath, "utf-8");
|
|
280
|
+
if (!raw.trim()) {
|
|
281
|
+
return { version: CARD_STATE_FILE_VERSION, updatedAt: Date.now(), pendingCards: [] };
|
|
282
|
+
}
|
|
283
|
+
const normalized = normalizePendingState(JSON.parse(raw) as Partial<PendingCardStateFile>);
|
|
284
|
+
writePendingCardState(normalized, storePath, log);
|
|
285
|
+
return normalized;
|
|
286
|
+
} catch (err: unknown) {
|
|
287
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
+
log?.warn?.(`[DingTalk][AICard] Failed to read pending card state: ${message}`);
|
|
289
|
+
return { version: CARD_STATE_FILE_VERSION, updatedAt: Date.now(), pendingCards: [] };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function writePendingCardState(
|
|
294
|
+
state: PendingCardStateFile,
|
|
295
|
+
storePath?: string,
|
|
296
|
+
log?: Logger,
|
|
297
|
+
): void {
|
|
298
|
+
if (!storePath) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
writeNamespaceJsonAtomic(CARD_PENDING_NAMESPACE, {
|
|
302
|
+
storePath,
|
|
303
|
+
format: "json",
|
|
304
|
+
data: state,
|
|
305
|
+
log,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function upsertPendingCard(card: AICardInstance, storePath?: string, log?: Logger): void {
|
|
310
|
+
if (!card.accountId || !storePath) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const state = readPendingCardState(storePath, log);
|
|
314
|
+
const next: PendingCardRecord = {
|
|
315
|
+
accountId: card.accountId,
|
|
316
|
+
cardInstanceId: card.cardInstanceId,
|
|
317
|
+
outTrackId: card.outTrackId,
|
|
318
|
+
conversationId: card.conversationId,
|
|
319
|
+
contextConversationId: card.contextConversationId,
|
|
320
|
+
createdAt: card.createdAt,
|
|
321
|
+
lastUpdated: card.lastUpdated,
|
|
322
|
+
state: card.state,
|
|
323
|
+
};
|
|
324
|
+
const index = state.pendingCards.findIndex((item) => item.cardInstanceId === card.cardInstanceId);
|
|
325
|
+
if (index >= 0) {
|
|
326
|
+
state.pendingCards[index] = next;
|
|
327
|
+
} else {
|
|
328
|
+
state.pendingCards.push(next);
|
|
329
|
+
}
|
|
330
|
+
state.updatedAt = Date.now();
|
|
331
|
+
writePendingCardState(state, storePath, log);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function removePendingCard(card: AICardInstance, log?: Logger): void {
|
|
335
|
+
if (!card.accountId || !card.storePath) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
removePendingCardById(card.cardInstanceId, card.storePath, log);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function removePendingCardById(cardInstanceId: string, storePath?: string, log?: Logger): void {
|
|
342
|
+
if (!storePath) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const state = readPendingCardState(storePath, log);
|
|
346
|
+
const remaining = state.pendingCards.filter((item) => item.cardInstanceId !== cardInstanceId);
|
|
347
|
+
if (remaining.length === state.pendingCards.length) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
state.pendingCards = remaining;
|
|
351
|
+
state.updatedAt = Date.now();
|
|
352
|
+
writePendingCardState(state, storePath, log);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function listPendingCardsByAccount(
|
|
356
|
+
accountId: string,
|
|
357
|
+
storePath?: string,
|
|
358
|
+
log?: Logger,
|
|
359
|
+
): PendingCardRecord[] {
|
|
360
|
+
const state = readPendingCardState(storePath, log);
|
|
361
|
+
return state.pendingCards.filter((item) => item.accountId === accountId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeRecoveredState(state: string): AICardInstance["state"] {
|
|
365
|
+
if (state === AICardStatus.PROCESSING || state === AICardStatus.INPUTING) {
|
|
366
|
+
return state;
|
|
367
|
+
}
|
|
368
|
+
return AICardStatus.PROCESSING;
|
|
369
|
+
}
|
|
18
370
|
|
|
19
371
|
// Helper to identify card terminal states.
|
|
20
372
|
export function isCardInTerminalState(state: string): boolean {
|
|
21
373
|
return state === AICardStatus.FINISHED || state === AICardStatus.FAILED;
|
|
22
374
|
}
|
|
23
375
|
|
|
24
|
-
export function formatContentForCard(content: string, type: "thinking" | "tool"): string {
|
|
376
|
+
export function formatContentForCard(content: string | undefined, type: "thinking" | "tool"): string {
|
|
25
377
|
if (!content) {
|
|
26
378
|
return "";
|
|
27
379
|
}
|
|
28
380
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
content.slice(0, THINKING_TRUNCATE_LENGTH) +
|
|
32
|
-
(content.length > THINKING_TRUNCATE_LENGTH ? "…" : "");
|
|
381
|
+
const emoji = type === "thinking" ? "🤔" : "🛠️";
|
|
382
|
+
const label = type === "thinking" ? "思考中" : "工具执行";
|
|
33
383
|
|
|
34
|
-
|
|
35
|
-
const quotedLines = truncated
|
|
384
|
+
const escaped = content
|
|
36
385
|
.split("\n")
|
|
37
386
|
.map((line) => line.replace(/^_(?=[^ ])/, "*").replace(/(?<=[^ ])_(?=$)/, "*"))
|
|
38
|
-
.map((line) => `> ${line}`)
|
|
39
387
|
.join("\n");
|
|
40
388
|
|
|
41
|
-
|
|
42
|
-
const label = type === "thinking" ? "思考中" : "工具执行";
|
|
43
|
-
|
|
44
|
-
return `${emoji} **${label}**\n${quotedLines}`;
|
|
389
|
+
return `${emoji} **${label}**\n\n${escaped}`;
|
|
45
390
|
}
|
|
46
391
|
|
|
47
392
|
async function sendTemplateMismatchNotification(
|
|
@@ -95,26 +440,129 @@ export async function sendProactiveCardText(
|
|
|
95
440
|
conversationId: string,
|
|
96
441
|
content: string,
|
|
97
442
|
log?: Logger,
|
|
98
|
-
): Promise<{ ok: boolean; error?: string }> {
|
|
443
|
+
): Promise<{ ok: boolean; error?: string } & DingTalkTrackingMetadata> {
|
|
99
444
|
try {
|
|
100
|
-
const card = await createAICard(config, conversationId, log);
|
|
445
|
+
const card = await createAICard(config, conversationId, log, { persistPending: false });
|
|
101
446
|
if (!card) {
|
|
102
447
|
return { ok: false, error: "Failed to create AI card" };
|
|
103
448
|
}
|
|
104
449
|
await finishAICard(card, content, log);
|
|
105
|
-
return {
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
processQueryKey: card.processQueryKey,
|
|
453
|
+
outTrackId: card.outTrackId,
|
|
454
|
+
cardInstanceId: card.cardInstanceId,
|
|
455
|
+
};
|
|
106
456
|
} catch (err: any) {
|
|
107
457
|
log?.error?.(`[DingTalk][AICard] Proactive card send failed: ${err.message}`);
|
|
108
458
|
return { ok: false, error: err.message };
|
|
109
459
|
}
|
|
110
460
|
}
|
|
111
461
|
|
|
462
|
+
export async function recoverPendingCardsForAccount(
|
|
463
|
+
config: DingTalkConfig,
|
|
464
|
+
accountId: string,
|
|
465
|
+
storePath?: string,
|
|
466
|
+
log?: Logger,
|
|
467
|
+
): Promise<number> {
|
|
468
|
+
return finalizePendingCardsByAccount(
|
|
469
|
+
config,
|
|
470
|
+
accountId,
|
|
471
|
+
RECOVERY_FINALIZE_MESSAGE,
|
|
472
|
+
storePath,
|
|
473
|
+
"recover",
|
|
474
|
+
log,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function finalizeActiveCardsForAccount(
|
|
479
|
+
config: DingTalkConfig,
|
|
480
|
+
accountId: string,
|
|
481
|
+
reason: string,
|
|
482
|
+
storePath?: string,
|
|
483
|
+
log?: Logger,
|
|
484
|
+
): Promise<number> {
|
|
485
|
+
return finalizePendingCardsByAccount(config, accountId, reason, storePath, "finalize", log);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function finalizePendingCardsByAccount(
|
|
489
|
+
config: DingTalkConfig,
|
|
490
|
+
accountId: string,
|
|
491
|
+
reason: string,
|
|
492
|
+
storePath: string | undefined,
|
|
493
|
+
mode: "recover" | "finalize",
|
|
494
|
+
log?: Logger,
|
|
495
|
+
): Promise<number> {
|
|
496
|
+
if (!storePath) {
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const pendingCards = listPendingCardsByAccount(accountId, storePath, log).filter(
|
|
501
|
+
(item) => !isCardInTerminalState(item.state),
|
|
502
|
+
);
|
|
503
|
+
if (pendingCards.length === 0) {
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let token = "";
|
|
508
|
+
try {
|
|
509
|
+
token = await getAccessToken(config, log);
|
|
510
|
+
} catch (err: any) {
|
|
511
|
+
const tokenFailureScope =
|
|
512
|
+
mode === "recover" ? "pending card recovery" : "finalizing active cards";
|
|
513
|
+
log?.warn?.(
|
|
514
|
+
`[DingTalk][AICard] Failed to fetch token for ${tokenFailureScope}: ${err.message}`,
|
|
515
|
+
);
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let finalizedCount = 0;
|
|
520
|
+
for (const entry of pendingCards) {
|
|
521
|
+
const card: AICardInstance = {
|
|
522
|
+
cardInstanceId: entry.cardInstanceId,
|
|
523
|
+
accessToken: token,
|
|
524
|
+
conversationId: entry.conversationId,
|
|
525
|
+
contextConversationId: entry.contextConversationId,
|
|
526
|
+
accountId: entry.accountId,
|
|
527
|
+
storePath,
|
|
528
|
+
outTrackId: entry.outTrackId,
|
|
529
|
+
createdAt: entry.createdAt || Date.now(),
|
|
530
|
+
lastUpdated: entry.lastUpdated || Date.now(),
|
|
531
|
+
state: normalizeRecoveredState(entry.state),
|
|
532
|
+
config,
|
|
533
|
+
};
|
|
534
|
+
try {
|
|
535
|
+
await finishAICard(card, reason, log);
|
|
536
|
+
finalizedCount += 1;
|
|
537
|
+
} catch (err: any) {
|
|
538
|
+
const action = mode === "recover" ? "recover" : "finalize";
|
|
539
|
+
log?.warn?.(
|
|
540
|
+
`[DingTalk][AICard] Failed to ${action} active card ${entry.cardInstanceId}: ${err.message}`,
|
|
541
|
+
);
|
|
542
|
+
removePendingCardById(entry.cardInstanceId, storePath, log);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return finalizedCount;
|
|
546
|
+
}
|
|
547
|
+
|
|
112
548
|
export async function createAICard(
|
|
113
549
|
config: DingTalkConfig,
|
|
114
550
|
conversationId: string,
|
|
115
551
|
log?: Logger,
|
|
552
|
+
options: CreateAICardOptions = {},
|
|
116
553
|
): Promise<AICardInstance | null> {
|
|
554
|
+
const accountId = options.accountId ?? "default";
|
|
555
|
+
if (isAICardDegraded(accountId)) {
|
|
556
|
+
const state = getAICardDegradeState(accountId);
|
|
557
|
+
log?.warn?.(
|
|
558
|
+
`[DingTalk][AICard][Degrade] Skip create for account=${accountId}, remainingMs=${state?.remainingMs || 0}, reason=${state?.reason || "unknown"}`,
|
|
559
|
+
);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
117
563
|
try {
|
|
564
|
+
const shouldPersistPending =
|
|
565
|
+
options.persistPending ?? Boolean(options.accountId && options.storePath);
|
|
118
566
|
const token = await getAccessToken(config, log);
|
|
119
567
|
// Use randomUUID to avoid collisions across workers/restarts.
|
|
120
568
|
const cardInstanceId = `card_${randomUUID()}`;
|
|
@@ -129,11 +577,15 @@ export async function createAICard(
|
|
|
129
577
|
|
|
130
578
|
// DingTalk createAndDeliver API payload.
|
|
131
579
|
const cardTemplateKey = config.cardTemplateKey || "content";
|
|
580
|
+
const cardParamMap = {
|
|
581
|
+
config: JSON.stringify({ autoLayout: true, enableForward: true }),
|
|
582
|
+
[cardTemplateKey]: "",
|
|
583
|
+
};
|
|
132
584
|
const createAndDeliverBody = {
|
|
133
585
|
cardTemplateId: config.cardTemplateId,
|
|
134
586
|
outTrackId: cardInstanceId,
|
|
135
587
|
cardData: {
|
|
136
|
-
cardParamMap
|
|
588
|
+
cardParamMap,
|
|
137
589
|
},
|
|
138
590
|
callbackType: "STREAM",
|
|
139
591
|
imGroupOpenSpaceModel: { supportForward: true },
|
|
@@ -143,10 +595,17 @@ export async function createAICard(
|
|
|
143
595
|
: `dtv1.card//IM_ROBOT.${conversationId}`,
|
|
144
596
|
userIdType: 1,
|
|
145
597
|
imGroupOpenDeliverModel: isGroup
|
|
146
|
-
? {
|
|
598
|
+
? {
|
|
599
|
+
robotCode: config.robotCode || config.clientId,
|
|
600
|
+
extension: DYNAMIC_SUMMARY_EXTENSION,
|
|
601
|
+
}
|
|
147
602
|
: undefined,
|
|
148
603
|
imRobotOpenDeliverModel: !isGroup
|
|
149
|
-
? {
|
|
604
|
+
? {
|
|
605
|
+
spaceType: "IM_ROBOT",
|
|
606
|
+
robotCode: config.robotCode || config.clientId,
|
|
607
|
+
extension: DYNAMIC_SUMMARY_EXTENSION,
|
|
608
|
+
}
|
|
150
609
|
: undefined,
|
|
151
610
|
};
|
|
152
611
|
|
|
@@ -165,23 +624,61 @@ export async function createAICard(
|
|
|
165
624
|
createAndDeliverBody,
|
|
166
625
|
{
|
|
167
626
|
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
627
|
+
...getProxyBypassOption(config),
|
|
168
628
|
},
|
|
169
629
|
);
|
|
170
630
|
log?.debug?.(
|
|
171
631
|
`[DingTalk][AICard] CreateAndDeliver response: status=${resp.status} data=${JSON.stringify(resp.data)}`,
|
|
172
632
|
);
|
|
633
|
+
const responseData = resp.data as
|
|
634
|
+
| {
|
|
635
|
+
result?: DingTalkTrackingMetadata;
|
|
636
|
+
processQueryKey?: unknown;
|
|
637
|
+
outTrackId?: unknown;
|
|
638
|
+
cardInstanceId?: unknown;
|
|
639
|
+
}
|
|
640
|
+
| undefined;
|
|
641
|
+
const responseTracking = responseData?.result;
|
|
642
|
+
const processQueryKey =
|
|
643
|
+
typeof responseTracking?.processQueryKey === "string" &&
|
|
644
|
+
responseTracking.processQueryKey.trim()
|
|
645
|
+
? responseTracking.processQueryKey.trim()
|
|
646
|
+
: typeof responseData?.processQueryKey === "string" && responseData.processQueryKey.trim()
|
|
647
|
+
? responseData.processQueryKey.trim()
|
|
648
|
+
: undefined;
|
|
649
|
+
const outTrackId =
|
|
650
|
+
typeof responseTracking?.outTrackId === "string" && responseTracking.outTrackId.trim()
|
|
651
|
+
? responseTracking.outTrackId.trim()
|
|
652
|
+
: typeof responseData?.outTrackId === "string" && responseData.outTrackId.trim()
|
|
653
|
+
? responseData.outTrackId.trim()
|
|
654
|
+
: cardInstanceId;
|
|
655
|
+
const resolvedCardInstanceId =
|
|
656
|
+
typeof responseTracking?.cardInstanceId === "string" && responseTracking.cardInstanceId.trim()
|
|
657
|
+
? responseTracking.cardInstanceId.trim()
|
|
658
|
+
: typeof responseData?.cardInstanceId === "string" && responseData.cardInstanceId.trim()
|
|
659
|
+
? responseData.cardInstanceId.trim()
|
|
660
|
+
: cardInstanceId;
|
|
173
661
|
|
|
174
662
|
// Return the AI card instance with config reference for token refresh/recovery.
|
|
175
663
|
const aiCardInstance: AICardInstance = {
|
|
176
|
-
cardInstanceId,
|
|
664
|
+
cardInstanceId: resolvedCardInstanceId,
|
|
177
665
|
accessToken: token,
|
|
178
666
|
conversationId,
|
|
667
|
+
contextConversationId: options.contextConversationId || conversationId,
|
|
668
|
+
accountId,
|
|
669
|
+
storePath: options.storePath,
|
|
179
670
|
createdAt: Date.now(),
|
|
180
671
|
lastUpdated: Date.now(),
|
|
181
672
|
state: AICardStatus.PROCESSING,
|
|
182
673
|
config,
|
|
674
|
+
processQueryKey: processQueryKey || extractCardProcessQueryKey(resp.data),
|
|
675
|
+
outTrackId,
|
|
183
676
|
};
|
|
677
|
+
if (shouldPersistPending) {
|
|
678
|
+
upsertPendingCard(aiCardInstance, options.storePath, log);
|
|
679
|
+
}
|
|
184
680
|
|
|
681
|
+
clearAICardDegrade(accountId, log);
|
|
185
682
|
return aiCardInstance;
|
|
186
683
|
} catch (err: any) {
|
|
187
684
|
log?.error?.(`[DingTalk][AICard] Create failed: ${err.message}`);
|
|
@@ -194,6 +691,14 @@ export async function createAICard(
|
|
|
194
691
|
formatDingTalkErrorPayloadLog("card.create", err.response.data, "[DingTalk][AICard]"),
|
|
195
692
|
);
|
|
196
693
|
}
|
|
694
|
+
if (shouldTriggerAICardDegrade(err)) {
|
|
695
|
+
activateAICardDegrade(
|
|
696
|
+
accountId,
|
|
697
|
+
`card.create:${err?.response?.status || "unknown"}`,
|
|
698
|
+
config,
|
|
699
|
+
log,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
197
702
|
return null;
|
|
198
703
|
}
|
|
199
704
|
}
|
|
@@ -227,7 +732,7 @@ export async function streamAICard(
|
|
|
227
732
|
|
|
228
733
|
// Always use full replacement to make client rendering deterministic.
|
|
229
734
|
const streamBody: AICardStreamingRequest = {
|
|
230
|
-
outTrackId: card.cardInstanceId,
|
|
735
|
+
outTrackId: card.outTrackId || card.cardInstanceId,
|
|
231
736
|
guid: randomUUID(),
|
|
232
737
|
key: card.config?.cardTemplateKey || "content",
|
|
233
738
|
content: content,
|
|
@@ -246,6 +751,7 @@ export async function streamAICard(
|
|
|
246
751
|
"x-acs-dingtalk-access-token": card.accessToken,
|
|
247
752
|
"Content-Type": "application/json",
|
|
248
753
|
},
|
|
754
|
+
...(card.config ? getProxyBypassOption(card.config) : {}),
|
|
249
755
|
});
|
|
250
756
|
log?.debug?.(
|
|
251
757
|
`[DingTalk][AICard] Streaming response: status=${streamResp.status}, data=${JSON.stringify(streamResp.data)}`,
|
|
@@ -255,6 +761,7 @@ export async function streamAICard(
|
|
|
255
761
|
card.lastStreamedContent = content;
|
|
256
762
|
if (finished) {
|
|
257
763
|
card.state = AICardStatus.FINISHED;
|
|
764
|
+
removePendingCard(card, log);
|
|
258
765
|
} else if (card.state === AICardStatus.PROCESSING) {
|
|
259
766
|
card.state = AICardStatus.INPUTING;
|
|
260
767
|
}
|
|
@@ -279,6 +786,7 @@ export async function streamAICard(
|
|
|
279
786
|
|
|
280
787
|
card.state = AICardStatus.FAILED;
|
|
281
788
|
card.lastUpdated = Date.now();
|
|
789
|
+
removePendingCard(card, log);
|
|
282
790
|
await sendTemplateMismatchNotification(card, errorMsg, log);
|
|
283
791
|
throw err;
|
|
284
792
|
}
|
|
@@ -293,6 +801,7 @@ export async function streamAICard(
|
|
|
293
801
|
"x-acs-dingtalk-access-token": card.accessToken,
|
|
294
802
|
"Content-Type": "application/json",
|
|
295
803
|
},
|
|
804
|
+
...(card.config ? getProxyBypassOption(card.config) : {}),
|
|
296
805
|
});
|
|
297
806
|
log?.debug?.(
|
|
298
807
|
`[DingTalk][AICard] Retry after token refresh succeeded: status=${retryResp.status}`,
|
|
@@ -301,6 +810,7 @@ export async function streamAICard(
|
|
|
301
810
|
card.lastStreamedContent = content;
|
|
302
811
|
if (finished) {
|
|
303
812
|
card.state = AICardStatus.FINISHED;
|
|
813
|
+
removePendingCard(card, log);
|
|
304
814
|
} else if (card.state === AICardStatus.PROCESSING) {
|
|
305
815
|
card.state = AICardStatus.INPUTING;
|
|
306
816
|
}
|
|
@@ -321,9 +831,16 @@ export async function streamAICard(
|
|
|
321
831
|
|
|
322
832
|
card.state = AICardStatus.FAILED;
|
|
323
833
|
card.lastUpdated = Date.now();
|
|
324
|
-
log
|
|
325
|
-
|
|
326
|
-
|
|
834
|
+
removePendingCard(card, log);
|
|
835
|
+
if (card.accountId && shouldTriggerAICardDegrade(err)) {
|
|
836
|
+
activateAICardDegrade(
|
|
837
|
+
card.accountId,
|
|
838
|
+
`card.stream:${err?.response?.status || "unknown"}`,
|
|
839
|
+
card.config,
|
|
840
|
+
log,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
log?.error?.(`[DingTalk][AICard] Streaming update failed: ${err.message}`);
|
|
327
844
|
if (err.response?.data !== undefined) {
|
|
328
845
|
log?.error?.(
|
|
329
846
|
formatDingTalkErrorPayloadLog("card.stream", err.response.data, "[DingTalk][AICard]"),
|
|
@@ -337,8 +854,131 @@ export async function finishAICard(
|
|
|
337
854
|
card: AICardInstance,
|
|
338
855
|
content: string,
|
|
339
856
|
log?: Logger,
|
|
857
|
+
options: { quotedRef?: QuotedRef } = {},
|
|
340
858
|
): Promise<void> {
|
|
341
859
|
log?.debug?.(`[DingTalk][AICard] Starting finish, final content length=${content.length}`);
|
|
342
|
-
// Finalize by streaming one last full payload with isFinalize=true.
|
|
343
860
|
await streamAICard(card, content, true, log);
|
|
861
|
+
if (card.conversationId && content.trim() && card.accountId && card.processQueryKey) {
|
|
862
|
+
const primaryConversationId = card.contextConversationId || card.conversationId;
|
|
863
|
+
cacheCardContentByProcessQueryKey(
|
|
864
|
+
card.accountId,
|
|
865
|
+
primaryConversationId,
|
|
866
|
+
card.processQueryKey,
|
|
867
|
+
content,
|
|
868
|
+
card.storePath,
|
|
869
|
+
options.quotedRef,
|
|
870
|
+
log,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function cacheCardContentByProcessQueryKey(
|
|
876
|
+
accountId: string,
|
|
877
|
+
conversationId: string,
|
|
878
|
+
processQueryKey: string,
|
|
879
|
+
content: string,
|
|
880
|
+
storePath?: string,
|
|
881
|
+
quotedRef?: QuotedRef,
|
|
882
|
+
log?: Logger,
|
|
883
|
+
): void {
|
|
884
|
+
if (!processQueryKey.trim() || !content.trim() || !storePath) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
log?.debug?.(
|
|
888
|
+
`[DingTalk][QuotedRef][Persist] direction=outbound scope=${conversationId} messageType=card ` +
|
|
889
|
+
`processQueryKey=${processQueryKey} quotedRef=${quotedRef ? JSON.stringify(quotedRef) : "(none)"}`,
|
|
890
|
+
);
|
|
891
|
+
upsertOutboundMessageContext({
|
|
892
|
+
storePath,
|
|
893
|
+
accountId,
|
|
894
|
+
conversationId,
|
|
895
|
+
createdAt: Date.now(),
|
|
896
|
+
text: content,
|
|
897
|
+
messageType: "card",
|
|
898
|
+
ttlMs: DEFAULT_CARD_CONTENT_TTL_MS,
|
|
899
|
+
topic: null,
|
|
900
|
+
quotedRef,
|
|
901
|
+
delivery: {
|
|
902
|
+
processQueryKey,
|
|
903
|
+
kind: "proactive-card",
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function cacheCardContent(
|
|
909
|
+
accountId: string,
|
|
910
|
+
conversationId: string,
|
|
911
|
+
content: string,
|
|
912
|
+
createdAt: number,
|
|
913
|
+
storePath?: string,
|
|
914
|
+
): void {
|
|
915
|
+
if (!storePath) {
|
|
916
|
+
// This fallback only serves short-lived, no-storePath sessions. It is kept
|
|
917
|
+
// local to card-service instead of using the shared message context store
|
|
918
|
+
// because there is no durable scope to share across modules or restarts.
|
|
919
|
+
const scopeKey = `${accountId}:${conversationId}`;
|
|
920
|
+
const nowMs = Date.now();
|
|
921
|
+
const bucket = touchInMemoryCardContentBucket(scopeKey, nowMs);
|
|
922
|
+
bucket.entries.push({ content, createdAt, expiresAt: nowMs + DEFAULT_CARD_CONTENT_TTL_MS });
|
|
923
|
+
bucket.entries.sort((left, right) => left.createdAt - right.createdAt);
|
|
924
|
+
bucket.entries = bucket.entries.slice(-CARD_CACHE_MAX_PER_CONVERSATION);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
upsertOutboundMessageContext({
|
|
928
|
+
storePath,
|
|
929
|
+
accountId,
|
|
930
|
+
conversationId,
|
|
931
|
+
msgId: createSyntheticOutboundMsgId(createdAt),
|
|
932
|
+
createdAt,
|
|
933
|
+
text: content,
|
|
934
|
+
messageType: "card",
|
|
935
|
+
ttlMs: DEFAULT_CARD_CONTENT_TTL_MS,
|
|
936
|
+
topic: null,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export function findCardContent(
|
|
941
|
+
accountId: string,
|
|
942
|
+
conversationId: string,
|
|
943
|
+
repliedCreatedAt: number,
|
|
944
|
+
storePath?: string,
|
|
945
|
+
): string | null {
|
|
946
|
+
if (!storePath) {
|
|
947
|
+
const scopeKey = `${accountId}:${conversationId}`;
|
|
948
|
+
const nowMs = Date.now();
|
|
949
|
+
const bucket = inMemoryCardContentStore.get(scopeKey);
|
|
950
|
+
if (!bucket) {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
bucket.entries = pruneInMemoryCardContentEntries(bucket.entries, nowMs);
|
|
954
|
+
bucket.lastActiveAt = nowMs;
|
|
955
|
+
if (bucket.entries.length === 0) {
|
|
956
|
+
inMemoryCardContentStore.delete(scopeKey);
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
let bestContent: string | null = null;
|
|
960
|
+
let bestDelta = Infinity;
|
|
961
|
+
for (const entry of bucket.entries) {
|
|
962
|
+
const delta = Math.abs(entry.createdAt - repliedCreatedAt);
|
|
963
|
+
if (delta <= DEFAULT_CREATED_AT_MATCH_WINDOW_MS && delta < bestDelta) {
|
|
964
|
+
bestDelta = delta;
|
|
965
|
+
bestContent = entry.content;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return bestContent;
|
|
969
|
+
}
|
|
970
|
+
const record = resolveByCreatedAtWindow({
|
|
971
|
+
storePath,
|
|
972
|
+
accountId,
|
|
973
|
+
conversationId,
|
|
974
|
+
createdAt: repliedCreatedAt,
|
|
975
|
+
windowMs: DEFAULT_CREATED_AT_MATCH_WINDOW_MS,
|
|
976
|
+
direction: "outbound",
|
|
977
|
+
});
|
|
978
|
+
return record?.text || null;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export function clearCardContentCacheForTest(): void {
|
|
982
|
+
inMemoryCardContentStore.clear();
|
|
983
|
+
clearMessageContextCacheForTest();
|
|
344
984
|
}
|