@soimy/dingtalk 3.3.0 → 3.4.1
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 +141 -12
- package/index.ts +71 -66
- package/package.json +6 -5
- package/src/access-control.ts +65 -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 +17 -4
- package/src/ack-reaction-service.ts +66 -19
- package/src/attachment-text-extractor.ts +2 -1
- package/src/card-service.ts +145 -257
- package/src/channel.ts +106 -47
- package/src/config-schema.ts +28 -6
- package/src/config.ts +30 -6
- package/src/connection-manager.ts +16 -5
- package/src/inbound-handler.ts +694 -520
- package/src/media-utils.ts +99 -36
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +221 -42
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +381 -269
- 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/runtime.ts +5 -7
- package/src/send-service.ts +164 -62
- package/src/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +152 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +124 -21
- package/src/quote-journal.ts +0 -242
- package/src/quoted-msg-cache.ts +0 -226
package/src/card-service.ts
CHANGED
|
@@ -5,6 +5,14 @@ import axios from "axios";
|
|
|
5
5
|
import { getAccessToken } from "./auth";
|
|
6
6
|
import { stripTargetPrefix } from "./config";
|
|
7
7
|
import { resolveOriginalPeerId } from "./peer-id-registry";
|
|
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";
|
|
8
16
|
import {
|
|
9
17
|
readNamespaceJson,
|
|
10
18
|
resolveNamespacePath,
|
|
@@ -16,6 +24,7 @@ import type {
|
|
|
16
24
|
DingTalkConfig,
|
|
17
25
|
DingTalkTrackingMetadata,
|
|
18
26
|
Logger,
|
|
27
|
+
QuotedRef,
|
|
19
28
|
} from "./types";
|
|
20
29
|
import { AICardStatus } from "./types";
|
|
21
30
|
import { formatDingTalkErrorPayloadLog, getProxyBypassOption } from "./utils";
|
|
@@ -24,11 +33,55 @@ const DINGTALK_API = "https://api.dingtalk.com";
|
|
|
24
33
|
// Thinking/tool stream snippets are truncated to keep card updates compact.
|
|
25
34
|
const CARD_STATE_FILE_VERSION = 1;
|
|
26
35
|
const CARD_PENDING_NAMESPACE = "cards.active.pending";
|
|
27
|
-
const CARD_PROCESS_QUERY_NAMESPACE = "cards.content.quote-process-query";
|
|
28
36
|
const RECOVERY_FINALIZE_MESSAGE = "⚠️ 上一次回复处理中断,已自动结束。请重新发送你的问题。";
|
|
29
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;
|
|
30
41
|
|
|
31
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
|
+
}
|
|
32
85
|
|
|
33
86
|
function getAICardDegradeMs(config?: DingTalkConfig): number {
|
|
34
87
|
const raw = config?.aicardDegradeMs;
|
|
@@ -147,6 +200,7 @@ interface CreateAICardOptions {
|
|
|
147
200
|
accountId?: string;
|
|
148
201
|
storePath?: string;
|
|
149
202
|
persistPending?: boolean;
|
|
203
|
+
contextConversationId?: string;
|
|
150
204
|
}
|
|
151
205
|
|
|
152
206
|
interface PendingCardRecord {
|
|
@@ -154,6 +208,7 @@ interface PendingCardRecord {
|
|
|
154
208
|
cardInstanceId: string;
|
|
155
209
|
outTrackId?: string;
|
|
156
210
|
conversationId: string;
|
|
211
|
+
contextConversationId?: string;
|
|
157
212
|
createdAt: number;
|
|
158
213
|
lastUpdated: number;
|
|
159
214
|
state: string;
|
|
@@ -261,6 +316,7 @@ function upsertPendingCard(card: AICardInstance, storePath?: string, log?: Logge
|
|
|
261
316
|
cardInstanceId: card.cardInstanceId,
|
|
262
317
|
outTrackId: card.outTrackId,
|
|
263
318
|
conversationId: card.conversationId,
|
|
319
|
+
contextConversationId: card.contextConversationId,
|
|
264
320
|
createdAt: card.createdAt,
|
|
265
321
|
lastUpdated: card.lastUpdated,
|
|
266
322
|
state: card.state,
|
|
@@ -466,6 +522,7 @@ async function finalizePendingCardsByAccount(
|
|
|
466
522
|
cardInstanceId: entry.cardInstanceId,
|
|
467
523
|
accessToken: token,
|
|
468
524
|
conversationId: entry.conversationId,
|
|
525
|
+
contextConversationId: entry.contextConversationId,
|
|
469
526
|
accountId: entry.accountId,
|
|
470
527
|
storePath,
|
|
471
528
|
outTrackId: entry.outTrackId,
|
|
@@ -538,10 +595,17 @@ export async function createAICard(
|
|
|
538
595
|
: `dtv1.card//IM_ROBOT.${conversationId}`,
|
|
539
596
|
userIdType: 1,
|
|
540
597
|
imGroupOpenDeliverModel: isGroup
|
|
541
|
-
? {
|
|
598
|
+
? {
|
|
599
|
+
robotCode: config.robotCode || config.clientId,
|
|
600
|
+
extension: DYNAMIC_SUMMARY_EXTENSION,
|
|
601
|
+
}
|
|
542
602
|
: undefined,
|
|
543
603
|
imRobotOpenDeliverModel: !isGroup
|
|
544
|
-
? {
|
|
604
|
+
? {
|
|
605
|
+
spaceType: "IM_ROBOT",
|
|
606
|
+
robotCode: config.robotCode || config.clientId,
|
|
607
|
+
extension: DYNAMIC_SUMMARY_EXTENSION,
|
|
608
|
+
}
|
|
545
609
|
: undefined,
|
|
546
610
|
};
|
|
547
611
|
|
|
@@ -600,6 +664,7 @@ export async function createAICard(
|
|
|
600
664
|
cardInstanceId: resolvedCardInstanceId,
|
|
601
665
|
accessToken: token,
|
|
602
666
|
conversationId,
|
|
667
|
+
contextConversationId: options.contextConversationId || conversationId,
|
|
603
668
|
accountId,
|
|
604
669
|
storePath: options.storePath,
|
|
605
670
|
createdAt: Date.now(),
|
|
@@ -789,214 +854,54 @@ export async function finishAICard(
|
|
|
789
854
|
card: AICardInstance,
|
|
790
855
|
content: string,
|
|
791
856
|
log?: Logger,
|
|
857
|
+
options: { quotedRef?: QuotedRef } = {},
|
|
792
858
|
): Promise<void> {
|
|
793
859
|
log?.debug?.(`[DingTalk][AICard] Starting finish, final content length=${content.length}`);
|
|
794
860
|
await streamAICard(card, content, true, log);
|
|
795
861
|
if (card.conversationId && content.trim() && card.accountId && card.processQueryKey) {
|
|
862
|
+
const primaryConversationId = card.contextConversationId || card.conversationId;
|
|
796
863
|
cacheCardContentByProcessQueryKey(
|
|
797
864
|
card.accountId,
|
|
798
|
-
|
|
865
|
+
primaryConversationId,
|
|
799
866
|
card.processQueryKey,
|
|
800
867
|
content,
|
|
801
868
|
card.storePath,
|
|
869
|
+
options.quotedRef,
|
|
870
|
+
log,
|
|
802
871
|
);
|
|
803
872
|
}
|
|
804
873
|
}
|
|
805
874
|
|
|
806
|
-
|
|
807
|
-
content: string;
|
|
808
|
-
createdAt: number;
|
|
809
|
-
expiresAt: number;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
interface PersistedProcessQueryCardContent {
|
|
813
|
-
updatedAt: number;
|
|
814
|
-
entries: Record<string, ProcessQueryCardContentEntry>;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function readProcessQueryCardContent(
|
|
818
|
-
accountId: string,
|
|
819
|
-
conversationId: string,
|
|
820
|
-
storePath?: string,
|
|
821
|
-
): PersistedProcessQueryCardContent {
|
|
822
|
-
if (!storePath) {
|
|
823
|
-
return { updatedAt: 0, entries: {} };
|
|
824
|
-
}
|
|
825
|
-
return readNamespaceJson<PersistedProcessQueryCardContent>(CARD_PROCESS_QUERY_NAMESPACE, {
|
|
826
|
-
storePath,
|
|
827
|
-
scope: { accountId, conversationId },
|
|
828
|
-
format: "json",
|
|
829
|
-
fallback: { updatedAt: 0, entries: {} },
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function writeProcessQueryCardContent(
|
|
834
|
-
accountId: string,
|
|
835
|
-
conversationId: string,
|
|
836
|
-
data: PersistedProcessQueryCardContent,
|
|
837
|
-
storePath?: string,
|
|
838
|
-
): void {
|
|
839
|
-
if (!storePath) {
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
writeNamespaceJsonAtomic(CARD_PROCESS_QUERY_NAMESPACE, {
|
|
843
|
-
storePath,
|
|
844
|
-
scope: { accountId, conversationId },
|
|
845
|
-
format: "json",
|
|
846
|
-
data,
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function purgeExpiredProcessQueryEntries(
|
|
851
|
-
persisted: PersistedProcessQueryCardContent,
|
|
852
|
-
nowMs: number,
|
|
853
|
-
): boolean {
|
|
854
|
-
let changed = false;
|
|
855
|
-
for (const [key, entry] of Object.entries(persisted.entries)) {
|
|
856
|
-
if (!entry || typeof entry.expiresAt !== "number" || nowMs >= entry.expiresAt) {
|
|
857
|
-
delete persisted.entries[key];
|
|
858
|
-
changed = true;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return changed;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
export function cacheCardContentByProcessQueryKey(
|
|
875
|
+
function cacheCardContentByProcessQueryKey(
|
|
865
876
|
accountId: string,
|
|
866
877
|
conversationId: string,
|
|
867
878
|
processQueryKey: string,
|
|
868
879
|
content: string,
|
|
869
880
|
storePath?: string,
|
|
881
|
+
quotedRef?: QuotedRef,
|
|
882
|
+
log?: Logger,
|
|
870
883
|
): void {
|
|
871
884
|
if (!processQueryKey.trim() || !content.trim() || !storePath) {
|
|
872
885
|
return;
|
|
873
886
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
createdAt: nowMs,
|
|
880
|
-
expiresAt: nowMs + CARD_CACHE_TTL_MS,
|
|
881
|
-
};
|
|
882
|
-
persisted.updatedAt = nowMs;
|
|
883
|
-
writeProcessQueryCardContent(accountId, conversationId, persisted, storePath);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
export function getCardContentByProcessQueryKey(
|
|
887
|
-
accountId: string,
|
|
888
|
-
conversationId: string,
|
|
889
|
-
processQueryKey: string,
|
|
890
|
-
storePath?: string,
|
|
891
|
-
): string | null {
|
|
892
|
-
if (!processQueryKey.trim() || !storePath) {
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
895
|
-
const nowMs = Date.now();
|
|
896
|
-
const persisted = readProcessQueryCardContent(accountId, conversationId, storePath);
|
|
897
|
-
const changed = purgeExpiredProcessQueryEntries(persisted, nowMs);
|
|
898
|
-
const entry = persisted.entries[processQueryKey];
|
|
899
|
-
if (!entry) {
|
|
900
|
-
if (changed) {
|
|
901
|
-
persisted.updatedAt = nowMs;
|
|
902
|
-
writeProcessQueryCardContent(accountId, conversationId, persisted, storePath);
|
|
903
|
-
}
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
return entry.content;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// ============ Card content cache (for quoted card lookup) ============
|
|
910
|
-
|
|
911
|
-
const CARD_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
912
|
-
const CARD_CACHE_MAX_PER_CONVERSATION = 20;
|
|
913
|
-
const CARD_CACHE_MAX_CONVERSATIONS = 500;
|
|
914
|
-
const CARD_CACHE_MATCH_WINDOW_MS = 2000;
|
|
915
|
-
const CARD_CONTENT_NAMESPACE = "cards.content.quote-lookup";
|
|
916
|
-
|
|
917
|
-
interface CardContentEntry {
|
|
918
|
-
content: string;
|
|
919
|
-
createdAt: number;
|
|
920
|
-
expiresAt: number;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
interface CardConversationBucket {
|
|
924
|
-
entries: CardContentEntry[];
|
|
925
|
-
lastActiveAt: number;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
interface PersistedCardContentBucket {
|
|
929
|
-
updatedAt: number;
|
|
930
|
-
entries: CardContentEntry[];
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const cardContentStore = new Map<string, CardConversationBucket>();
|
|
934
|
-
|
|
935
|
-
function loadCardContentBucketFromPersistence(
|
|
936
|
-
accountId: string,
|
|
937
|
-
conversationId: string,
|
|
938
|
-
storePath?: string,
|
|
939
|
-
): CardConversationBucket | null {
|
|
940
|
-
if (!storePath) {
|
|
941
|
-
return null;
|
|
942
|
-
}
|
|
943
|
-
const persisted = readNamespaceJson<PersistedCardContentBucket>(CARD_CONTENT_NAMESPACE, {
|
|
944
|
-
storePath,
|
|
945
|
-
scope: { accountId, conversationId },
|
|
946
|
-
format: "json",
|
|
947
|
-
fallback: { updatedAt: 0, entries: [] },
|
|
948
|
-
});
|
|
949
|
-
if (!Array.isArray(persisted.entries) || persisted.entries.length === 0) {
|
|
950
|
-
return null;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const now = Date.now();
|
|
954
|
-
const entries: CardContentEntry[] = [];
|
|
955
|
-
for (const entry of persisted.entries) {
|
|
956
|
-
if (
|
|
957
|
-
!entry ||
|
|
958
|
-
typeof entry.content !== "string" ||
|
|
959
|
-
typeof entry.createdAt !== "number" ||
|
|
960
|
-
typeof entry.expiresAt !== "number" ||
|
|
961
|
-
now >= entry.expiresAt
|
|
962
|
-
) {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const insertAt = entries.findIndex((item) => item.createdAt > entry.createdAt);
|
|
966
|
-
if (insertAt < 0) {
|
|
967
|
-
entries.push(entry);
|
|
968
|
-
} else {
|
|
969
|
-
entries.splice(insertAt, 0, entry);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
const normalizedEntries = entries.slice(-CARD_CACHE_MAX_PER_CONVERSATION);
|
|
973
|
-
|
|
974
|
-
if (normalizedEntries.length === 0) {
|
|
975
|
-
return null;
|
|
976
|
-
}
|
|
977
|
-
return {
|
|
978
|
-
entries: normalizedEntries,
|
|
979
|
-
lastActiveAt: now,
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function persistCardContentBucket(
|
|
984
|
-
accountId: string,
|
|
985
|
-
conversationId: string,
|
|
986
|
-
bucket: CardConversationBucket,
|
|
987
|
-
storePath?: string,
|
|
988
|
-
): void {
|
|
989
|
-
if (!storePath) {
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
writeNamespaceJsonAtomic(CARD_CONTENT_NAMESPACE, {
|
|
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({
|
|
993
892
|
storePath,
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
+
},
|
|
1000
905
|
});
|
|
1001
906
|
}
|
|
1002
907
|
|
|
@@ -1007,38 +912,29 @@ export function cacheCardContent(
|
|
|
1007
912
|
createdAt: number,
|
|
1008
913
|
storePath?: string,
|
|
1009
914
|
): void {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if (b.lastActiveAt < oldestTime) {
|
|
1020
|
-
oldestTime = b.lastActiveAt;
|
|
1021
|
-
oldestKey = key;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
if (oldestKey) {
|
|
1025
|
-
cardContentStore.delete(oldestKey);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
bucket.lastActiveAt = Date.now();
|
|
1030
|
-
|
|
1031
|
-
const now = Date.now();
|
|
1032
|
-
bucket.entries = bucket.entries.filter((e) => now < e.expiresAt);
|
|
1033
|
-
|
|
1034
|
-
bucket.entries.push({ content, createdAt, expiresAt: now + CARD_CACHE_TTL_MS });
|
|
1035
|
-
|
|
1036
|
-
if (bucket.entries.length > CARD_CACHE_MAX_PER_CONVERSATION) {
|
|
1037
|
-
bucket.entries.sort((a, b) => a.createdAt - b.createdAt);
|
|
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);
|
|
1038
924
|
bucket.entries = bucket.entries.slice(-CARD_CACHE_MAX_PER_CONVERSATION);
|
|
925
|
+
return;
|
|
1039
926
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
+
});
|
|
1042
938
|
}
|
|
1043
939
|
|
|
1044
940
|
export function findCardContent(
|
|
@@ -1047,50 +943,42 @@ export function findCardContent(
|
|
|
1047
943
|
repliedCreatedAt: number,
|
|
1048
944
|
storePath?: string,
|
|
1049
945
|
): string | null {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1056
|
-
bucket = loaded;
|
|
1057
|
-
if (cardContentStore.size > CARD_CACHE_MAX_CONVERSATIONS) {
|
|
1058
|
-
let oldestKey: string | undefined;
|
|
1059
|
-
let oldestTime = Infinity;
|
|
1060
|
-
for (const [key, b] of cardContentStore) {
|
|
1061
|
-
if (b.lastActiveAt < oldestTime) {
|
|
1062
|
-
oldestTime = b.lastActiveAt;
|
|
1063
|
-
oldestKey = key;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
if (oldestKey) {
|
|
1067
|
-
cardContentStore.delete(oldestKey);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
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;
|
|
1070
952
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
let bestContent: string | null = null;
|
|
1078
|
-
let bestDelta = Infinity;
|
|
1079
|
-
|
|
1080
|
-
for (const entry of bucket.entries) {
|
|
1081
|
-
if (Date.now() >= entry.expiresAt) {
|
|
1082
|
-
continue;
|
|
953
|
+
bucket.entries = pruneInMemoryCardContentEntries(bucket.entries, nowMs);
|
|
954
|
+
bucket.lastActiveAt = nowMs;
|
|
955
|
+
if (bucket.entries.length === 0) {
|
|
956
|
+
inMemoryCardContentStore.delete(scopeKey);
|
|
957
|
+
return null;
|
|
1083
958
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
+
}
|
|
1088
967
|
}
|
|
968
|
+
return bestContent;
|
|
1089
969
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
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;
|
|
1092
979
|
}
|
|
1093
980
|
|
|
1094
981
|
export function clearCardContentCacheForTest(): void {
|
|
1095
|
-
|
|
982
|
+
inMemoryCardContentStore.clear();
|
|
983
|
+
clearMessageContextCacheForTest();
|
|
1096
984
|
}
|