@soimy/dingtalk 3.3.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/README.md +141 -12
- package/package.json +2 -2
- 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 +60 -28
- package/src/config-schema.ts +28 -6
- package/src/config.ts +29 -5
- package/src/inbound-handler.ts +694 -520
- 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 +62 -5
- 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 +140 -52
- 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 +114 -9
- package/src/quote-journal.ts +0 -242
- package/src/quoted-msg-cache.ts +0 -226
package/src/types.ts
CHANGED
|
@@ -19,6 +19,11 @@ import type {
|
|
|
19
19
|
} from "openclaw/plugin-sdk";
|
|
20
20
|
import { mergeAccountWithDefaults } from "./config";
|
|
21
21
|
|
|
22
|
+
export type AckReactionMode = "off" | "emoji" | "kaomoji";
|
|
23
|
+
// Accept arbitrary strings for backward compatibility; the recommended
|
|
24
|
+
// explicit modes remain: "off" | "emoji" | "kaomoji".
|
|
25
|
+
export type AckReactionConfigValue = string;
|
|
26
|
+
|
|
22
27
|
export interface DingtalkPluginModule {
|
|
23
28
|
id: string;
|
|
24
29
|
name: string;
|
|
@@ -39,16 +44,18 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
39
44
|
name?: string;
|
|
40
45
|
enabled?: boolean;
|
|
41
46
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
42
|
-
groupPolicy?: "open" | "allowlist";
|
|
47
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
43
48
|
allowFrom?: string[];
|
|
49
|
+
groupAllowFrom?: string[];
|
|
50
|
+
displayNameResolution?: "disabled" | "all";
|
|
44
51
|
mediaUrlAllowlist?: string[];
|
|
45
52
|
journalTTLDays?: number;
|
|
46
|
-
ackReaction?:
|
|
53
|
+
ackReaction?: AckReactionConfigValue;
|
|
47
54
|
debug?: boolean;
|
|
48
55
|
messageType?: "markdown" | "card";
|
|
49
56
|
cardTemplateId?: string;
|
|
50
57
|
cardTemplateKey?: string;
|
|
51
|
-
groups?: Record<string, { systemPrompt?: string }>;
|
|
58
|
+
groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
|
|
52
59
|
accounts?: Record<string, DingTalkConfig>;
|
|
53
60
|
// Connection robustness configuration
|
|
54
61
|
maxConnectionAttempts?: number;
|
|
@@ -87,6 +94,10 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
87
94
|
feedbackLearningAutoApply?: boolean;
|
|
88
95
|
/** @deprecated Use learningNoteTtlMs */
|
|
89
96
|
feedbackLearningNoteTtlMs?: number;
|
|
97
|
+
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
98
|
+
convertMarkdownTables?: boolean;
|
|
99
|
+
/** @mention the sender after card finalization in group chats; value is the message text */
|
|
100
|
+
cardAtSender?: string;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/**
|
|
@@ -101,16 +112,18 @@ export interface DingTalkChannelConfig {
|
|
|
101
112
|
agentId?: string;
|
|
102
113
|
name?: string;
|
|
103
114
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
104
|
-
groupPolicy?: "open" | "allowlist";
|
|
115
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
105
116
|
allowFrom?: string[];
|
|
117
|
+
groupAllowFrom?: string[];
|
|
118
|
+
displayNameResolution?: "disabled" | "all";
|
|
106
119
|
mediaUrlAllowlist?: string[];
|
|
107
120
|
journalTTLDays?: number;
|
|
108
|
-
ackReaction?:
|
|
121
|
+
ackReaction?: AckReactionConfigValue;
|
|
109
122
|
debug?: boolean;
|
|
110
123
|
messageType?: "markdown" | "card";
|
|
111
124
|
cardTemplateId?: string;
|
|
112
125
|
cardTemplateKey?: string;
|
|
113
|
-
groups?: Record<string, { systemPrompt?: string }>;
|
|
126
|
+
groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
|
|
114
127
|
accounts?: Record<string, DingTalkConfig>;
|
|
115
128
|
maxConnectionAttempts?: number;
|
|
116
129
|
initialReconnectDelay?: number;
|
|
@@ -148,6 +161,10 @@ export interface DingTalkChannelConfig {
|
|
|
148
161
|
feedbackLearningAutoApply?: boolean;
|
|
149
162
|
/** @deprecated Use learningNoteTtlMs */
|
|
150
163
|
feedbackLearningNoteTtlMs?: number;
|
|
164
|
+
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
165
|
+
convertMarkdownTables?: boolean;
|
|
166
|
+
/** @mention the sender after card finalization in group chats; value is the message text */
|
|
167
|
+
cardAtSender?: string;
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
/**
|
|
@@ -207,6 +224,14 @@ export interface DingTalkInboundMessage {
|
|
|
207
224
|
msgId: string;
|
|
208
225
|
msgtype: string;
|
|
209
226
|
createAt: number;
|
|
227
|
+
/**
|
|
228
|
+
* @ 提及的用户列表(消息顶层,与 text 同级)
|
|
229
|
+
* 包含通过 @picker 选中的所有真实钉钉用户和机器人
|
|
230
|
+
* 格式: [{ dingtalkId: "$:LWCP_v1:$xxx" }]
|
|
231
|
+
*/
|
|
232
|
+
atUsers?: Array<{
|
|
233
|
+
dingtalkId: string;
|
|
234
|
+
}>;
|
|
210
235
|
text?: {
|
|
211
236
|
content: string;
|
|
212
237
|
isReplyMsg?: boolean; // 是否是回复消息
|
|
@@ -218,6 +243,7 @@ export interface DingTalkInboundMessage {
|
|
|
218
243
|
content?: {
|
|
219
244
|
text?: string;
|
|
220
245
|
downloadCode?: string;
|
|
246
|
+
fileName?: string;
|
|
221
247
|
biz_custom_action_url?: string;
|
|
222
248
|
richText?: Array<{
|
|
223
249
|
msgType?: string;
|
|
@@ -242,6 +268,7 @@ export interface DingTalkInboundMessage {
|
|
|
242
268
|
type: string;
|
|
243
269
|
text?: string;
|
|
244
270
|
atName?: string;
|
|
271
|
+
atUserId?: string;
|
|
245
272
|
downloadCode?: string;
|
|
246
273
|
}>;
|
|
247
274
|
quoteContent?: string;
|
|
@@ -267,24 +294,58 @@ export interface DingTalkInboundMessage {
|
|
|
267
294
|
sessionWebhook: string;
|
|
268
295
|
}
|
|
269
296
|
|
|
297
|
+
export type QuotedRefKey = "msgId" | "processQueryKey" | "messageId" | "outTrackId" | "cardInstanceId";
|
|
298
|
+
|
|
299
|
+
export type AttachmentTextSource = "text" | "html" | "pdf" | "docx";
|
|
300
|
+
|
|
301
|
+
export interface QuotedRef {
|
|
302
|
+
targetDirection: "inbound" | "outbound";
|
|
303
|
+
key?: QuotedRefKey;
|
|
304
|
+
value?: string;
|
|
305
|
+
fallbackCreatedAt?: number;
|
|
306
|
+
}
|
|
307
|
+
|
|
270
308
|
/**
|
|
271
309
|
* Quoted/reply message metadata extracted from repliedMsg.
|
|
272
310
|
* Populated when isReplyMsg is true; downstream handlers use these fields
|
|
273
311
|
* to download quoted media or look up cached card content.
|
|
274
312
|
*/
|
|
275
313
|
export interface QuotedInfo {
|
|
276
|
-
prefix: string;
|
|
277
314
|
mediaDownloadCode?: string;
|
|
278
315
|
mediaType?: string;
|
|
279
316
|
isQuotedFile?: boolean;
|
|
280
317
|
isQuotedCard?: boolean;
|
|
281
318
|
isQuotedDocCard?: boolean;
|
|
282
|
-
docSpaceId?: string;
|
|
283
|
-
docFileId?: string;
|
|
284
319
|
cardCreatedAt?: number;
|
|
285
320
|
processQueryKey?: string;
|
|
286
321
|
fileCreatedAt?: number;
|
|
287
322
|
msgId?: string;
|
|
323
|
+
previewText?: string;
|
|
324
|
+
previewMessageType?: string;
|
|
325
|
+
previewFileName?: string;
|
|
326
|
+
previewSenderId?: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @ 提及信息
|
|
331
|
+
*/
|
|
332
|
+
export interface AtMention {
|
|
333
|
+
/** @ 显示的名字(去除 @ 前缀) */
|
|
334
|
+
name: string;
|
|
335
|
+
/** 钉钉用户 ID(如果是 @ 真人) */
|
|
336
|
+
userId?: string;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Agent 名字匹配结果
|
|
341
|
+
*/
|
|
342
|
+
export interface AgentNameMatch {
|
|
343
|
+
/** 匹配到的 agent ID */
|
|
344
|
+
agentId: string;
|
|
345
|
+
/** 匹配来源:'name' | 'id' */
|
|
346
|
+
matchSource: "name" | "id";
|
|
347
|
+
/** 匹配到的名字 */
|
|
348
|
+
matchedName: string;
|
|
288
349
|
}
|
|
289
350
|
|
|
290
351
|
/**
|
|
@@ -300,6 +361,16 @@ export interface MessageContent {
|
|
|
300
361
|
docSpaceId?: string;
|
|
301
362
|
docFileId?: string;
|
|
302
363
|
quoted?: QuotedInfo;
|
|
364
|
+
/** @ 提及列表(从文本或 richText 提取的名字) */
|
|
365
|
+
atMentions?: AtMention[];
|
|
366
|
+
/**
|
|
367
|
+
* 通过 @picker 选中的真实钉钉用户的 dingtalkId 列表
|
|
368
|
+
* - 仅包含真实钉钉用户和机器人,不包含 agent 名
|
|
369
|
+
* - 用于排除真人:如果 atMentions 中的名字匹配到 agent,说明是 agent;
|
|
370
|
+
* 如果没匹配到 agent 且有 atUserDingtalkIds,则可能是真人
|
|
371
|
+
* - 注意:无法将 dingtalkId 映射到具体名字,因为 webhook 不提供此映射
|
|
372
|
+
*/
|
|
373
|
+
atUserDingtalkIds?: string[];
|
|
303
374
|
}
|
|
304
375
|
|
|
305
376
|
/**
|
|
@@ -318,6 +389,10 @@ export interface SendMessageOptions {
|
|
|
318
389
|
accountId?: string;
|
|
319
390
|
storePath?: string;
|
|
320
391
|
cardUpdateMode?: "append";
|
|
392
|
+
quotedRef?: QuotedRef;
|
|
393
|
+
/** Force markdown/text delivery even when messageType is "card". Bypasses card
|
|
394
|
+
* creation while preserving journal writes and other side-effects. */
|
|
395
|
+
forceMarkdown?: boolean;
|
|
321
396
|
}
|
|
322
397
|
|
|
323
398
|
export interface DingTalkTrackingMetadata {
|
|
@@ -344,6 +419,18 @@ export interface SessionWebhookResponse {
|
|
|
344
419
|
};
|
|
345
420
|
}
|
|
346
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Sub-agent routing options for parameterized message handling
|
|
424
|
+
*/
|
|
425
|
+
export interface SubAgentOptions {
|
|
426
|
+
/** The agent ID to route to */
|
|
427
|
+
agentId: string;
|
|
428
|
+
/** Prefix to add to response messages (e.g., "[AgentName] ") */
|
|
429
|
+
responsePrefix: string;
|
|
430
|
+
/** The matched agent name */
|
|
431
|
+
matchedName: string;
|
|
432
|
+
}
|
|
433
|
+
|
|
347
434
|
/**
|
|
348
435
|
* Message handler parameters
|
|
349
436
|
*/
|
|
@@ -354,6 +441,19 @@ export interface HandleDingTalkMessageParams {
|
|
|
354
441
|
sessionWebhook: string;
|
|
355
442
|
log?: Logger;
|
|
356
443
|
dingtalkConfig: DingTalkConfig;
|
|
444
|
+
/**
|
|
445
|
+
* When set, routes message to the specified sub-agent instead of main agent.
|
|
446
|
+
* This enables reuse of the main message handling logic for sub-agents.
|
|
447
|
+
*/
|
|
448
|
+
subAgentOptions?: SubAgentOptions;
|
|
449
|
+
/**
|
|
450
|
+
* Pre-downloaded media for sub-agent calls.
|
|
451
|
+
* When set, skips media download to avoid duplication in recursive calls.
|
|
452
|
+
*/
|
|
453
|
+
preDownloadedMedia?: {
|
|
454
|
+
mediaPath?: string;
|
|
455
|
+
mediaType?: string;
|
|
456
|
+
};
|
|
357
457
|
}
|
|
358
458
|
|
|
359
459
|
/**
|
|
@@ -553,6 +653,7 @@ export interface AICardInstance {
|
|
|
553
653
|
processQueryKey?: string;
|
|
554
654
|
accessToken: string;
|
|
555
655
|
conversationId: string;
|
|
656
|
+
contextConversationId?: string;
|
|
556
657
|
accountId?: string;
|
|
557
658
|
storePath?: string;
|
|
558
659
|
createdAt: number;
|
|
@@ -680,6 +781,8 @@ export function resolveDingTalkAccount(
|
|
|
680
781
|
dmPolicy: dingtalk?.dmPolicy,
|
|
681
782
|
groupPolicy: dingtalk?.groupPolicy,
|
|
682
783
|
allowFrom: dingtalk?.allowFrom,
|
|
784
|
+
groupAllowFrom: dingtalk?.groupAllowFrom,
|
|
785
|
+
displayNameResolution: dingtalk?.displayNameResolution,
|
|
683
786
|
journalTTLDays: dingtalk?.journalTTLDays,
|
|
684
787
|
ackReaction: dingtalk?.ackReaction,
|
|
685
788
|
debug: dingtalk?.debug,
|
|
@@ -707,6 +810,8 @@ export function resolveDingTalkAccount(
|
|
|
707
810
|
feedbackLearningEnabled: dingtalk?.feedbackLearningEnabled,
|
|
708
811
|
feedbackLearningAutoApply: dingtalk?.feedbackLearningAutoApply,
|
|
709
812
|
feedbackLearningNoteTtlMs: dingtalk?.feedbackLearningNoteTtlMs,
|
|
813
|
+
convertMarkdownTables: dingtalk?.convertMarkdownTables,
|
|
814
|
+
cardAtSender: dingtalk?.cardAtSender,
|
|
710
815
|
};
|
|
711
816
|
return {
|
|
712
817
|
...config,
|
package/src/quote-journal.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { readNamespaceJson, writeNamespaceJsonAtomic } from "./persistence-store";
|
|
2
|
-
|
|
3
|
-
const QUOTE_JOURNAL_NAMESPACE = "quoted.msg-journal";
|
|
4
|
-
const QUOTE_JOURNAL_VERSION = 1;
|
|
5
|
-
export const DEFAULT_JOURNAL_TTL_DAYS = 7;
|
|
6
|
-
const MAX_RECORDS_PER_SCOPE = 1000;
|
|
7
|
-
|
|
8
|
-
type JournalEntry = {
|
|
9
|
-
msgId: string;
|
|
10
|
-
messageType: string;
|
|
11
|
-
text?: string;
|
|
12
|
-
createdAt: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type QuoteJournalState = {
|
|
16
|
-
version: number;
|
|
17
|
-
updatedAt: number;
|
|
18
|
-
records: JournalEntry[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const stateCache = new Map<string, QuoteJournalState>();
|
|
22
|
-
|
|
23
|
-
function getScopeKey(params: {
|
|
24
|
-
storePath: string;
|
|
25
|
-
accountId: string;
|
|
26
|
-
conversationId: string | null;
|
|
27
|
-
}): string {
|
|
28
|
-
return JSON.stringify([
|
|
29
|
-
params.storePath,
|
|
30
|
-
params.accountId,
|
|
31
|
-
params.conversationId || null,
|
|
32
|
-
]);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function fallbackState(): QuoteJournalState {
|
|
36
|
-
return {
|
|
37
|
-
version: QUOTE_JOURNAL_VERSION,
|
|
38
|
-
updatedAt: Date.now(),
|
|
39
|
-
records: [],
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function normalizeEntry(entry: unknown): JournalEntry | null {
|
|
44
|
-
if (!entry || typeof entry !== "object") {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const candidate = entry as Partial<JournalEntry>;
|
|
48
|
-
if (typeof candidate.msgId !== "string" || typeof candidate.createdAt !== "number") {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
msgId: candidate.msgId,
|
|
53
|
-
messageType: typeof candidate.messageType === "string" ? candidate.messageType : "text",
|
|
54
|
-
text: typeof candidate.text === "string" ? candidate.text : undefined,
|
|
55
|
-
createdAt: candidate.createdAt,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function normalizeState(parsed: Partial<QuoteJournalState>): QuoteJournalState {
|
|
60
|
-
const records = Array.isArray(parsed.records)
|
|
61
|
-
? parsed.records.map((entry) => normalizeEntry(entry)).filter((entry): entry is JournalEntry => entry !== null)
|
|
62
|
-
: [];
|
|
63
|
-
return {
|
|
64
|
-
version: typeof parsed.version === "number" ? parsed.version : QUOTE_JOURNAL_VERSION,
|
|
65
|
-
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
66
|
-
records,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function loadState(params: {
|
|
71
|
-
storePath: string;
|
|
72
|
-
accountId: string;
|
|
73
|
-
conversationId: string | null;
|
|
74
|
-
}): QuoteJournalState {
|
|
75
|
-
const scopeKey = getScopeKey(params);
|
|
76
|
-
const cached = stateCache.get(scopeKey);
|
|
77
|
-
if (cached) {
|
|
78
|
-
return cached;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const persisted = readNamespaceJson<Partial<QuoteJournalState>>(QUOTE_JOURNAL_NAMESPACE, {
|
|
82
|
-
storePath: params.storePath,
|
|
83
|
-
scope: { accountId: params.accountId, conversationId: params.conversationId || undefined },
|
|
84
|
-
format: "json",
|
|
85
|
-
fallback: fallbackState(),
|
|
86
|
-
});
|
|
87
|
-
const normalized = normalizeState(persisted);
|
|
88
|
-
stateCache.set(scopeKey, normalized);
|
|
89
|
-
return normalized;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function writeState(params: {
|
|
93
|
-
storePath: string;
|
|
94
|
-
accountId: string;
|
|
95
|
-
conversationId: string | null;
|
|
96
|
-
state: QuoteJournalState;
|
|
97
|
-
}): void {
|
|
98
|
-
stateCache.set(getScopeKey(params), params.state);
|
|
99
|
-
writeNamespaceJsonAtomic(QUOTE_JOURNAL_NAMESPACE, {
|
|
100
|
-
storePath: params.storePath,
|
|
101
|
-
scope: { accountId: params.accountId, conversationId: params.conversationId || undefined },
|
|
102
|
-
format: "json",
|
|
103
|
-
data: params.state,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function pruneByTtl(records: JournalEntry[], ttlDays: number, nowMs: number): JournalEntry[] {
|
|
108
|
-
if (!ttlDays || ttlDays <= 0) {
|
|
109
|
-
return records;
|
|
110
|
-
}
|
|
111
|
-
const cutoff = nowMs - ttlDays * 24 * 60 * 60 * 1000;
|
|
112
|
-
return records.filter((entry) => entry.createdAt >= cutoff);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function capRecords(records: JournalEntry[]): JournalEntry[] {
|
|
116
|
-
if (records.length <= MAX_RECORDS_PER_SCOPE) {
|
|
117
|
-
return records;
|
|
118
|
-
}
|
|
119
|
-
return records.slice(-MAX_RECORDS_PER_SCOPE);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function appendQuoteJournalEntry(params: {
|
|
123
|
-
storePath: string;
|
|
124
|
-
accountId: string;
|
|
125
|
-
conversationId: string | null;
|
|
126
|
-
msgId: string;
|
|
127
|
-
messageType: string;
|
|
128
|
-
text?: string;
|
|
129
|
-
createdAt: number;
|
|
130
|
-
ttlDays?: number;
|
|
131
|
-
nowMs?: number;
|
|
132
|
-
}): void {
|
|
133
|
-
const now = params.nowMs ?? Date.now();
|
|
134
|
-
const ttlDays = params.ttlDays ?? DEFAULT_JOURNAL_TTL_DAYS;
|
|
135
|
-
const state = loadState(params);
|
|
136
|
-
const records = pruneByTtl(state.records, ttlDays, now);
|
|
137
|
-
records.push({
|
|
138
|
-
msgId: params.msgId,
|
|
139
|
-
messageType: params.messageType,
|
|
140
|
-
text: params.text,
|
|
141
|
-
createdAt: params.createdAt,
|
|
142
|
-
});
|
|
143
|
-
const cappedRecords = capRecords(records);
|
|
144
|
-
writeState({
|
|
145
|
-
storePath: params.storePath,
|
|
146
|
-
accountId: params.accountId,
|
|
147
|
-
conversationId: params.conversationId,
|
|
148
|
-
state: {
|
|
149
|
-
version: QUOTE_JOURNAL_VERSION,
|
|
150
|
-
updatedAt: now,
|
|
151
|
-
records: cappedRecords,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function cleanupExpiredQuoteJournalEntries(params: {
|
|
157
|
-
storePath: string;
|
|
158
|
-
accountId: string;
|
|
159
|
-
conversationId: string | null;
|
|
160
|
-
ttlDays: number;
|
|
161
|
-
nowMs?: number;
|
|
162
|
-
}): number {
|
|
163
|
-
const now = params.nowMs ?? Date.now();
|
|
164
|
-
const state = loadState(params);
|
|
165
|
-
const kept = pruneByTtl(state.records, params.ttlDays, now);
|
|
166
|
-
const removed = state.records.length - kept.length;
|
|
167
|
-
if (removed > 0) {
|
|
168
|
-
writeState({
|
|
169
|
-
storePath: params.storePath,
|
|
170
|
-
accountId: params.accountId,
|
|
171
|
-
conversationId: params.conversationId,
|
|
172
|
-
state: {
|
|
173
|
-
version: QUOTE_JOURNAL_VERSION,
|
|
174
|
-
updatedAt: now,
|
|
175
|
-
records: kept,
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
return removed;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function resolveQuotedMessageById(params: {
|
|
183
|
-
storePath: string;
|
|
184
|
-
accountId: string;
|
|
185
|
-
conversationId: string | null;
|
|
186
|
-
originalMsgId: string;
|
|
187
|
-
ttlDays?: number;
|
|
188
|
-
nowMs?: number;
|
|
189
|
-
}): { msgId: string; text?: string; createdAt: number } | null {
|
|
190
|
-
const state = loadState(params);
|
|
191
|
-
const now = params.nowMs ?? Date.now();
|
|
192
|
-
const ttlDays = params.ttlDays ?? DEFAULT_JOURNAL_TTL_DAYS;
|
|
193
|
-
const records = capRecords(pruneByTtl(state.records, ttlDays, now));
|
|
194
|
-
for (let i = records.length - 1; i >= 0; i--) {
|
|
195
|
-
const entry = records[i];
|
|
196
|
-
if (entry.msgId === params.originalMsgId) {
|
|
197
|
-
return { msgId: entry.msgId, text: entry.text, createdAt: entry.createdAt };
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export async function appendOutboundToQuoteJournal(params: {
|
|
204
|
-
storePath: string;
|
|
205
|
-
accountId: string;
|
|
206
|
-
conversationId: string | null;
|
|
207
|
-
messageId?: string;
|
|
208
|
-
text?: string;
|
|
209
|
-
messageType?: string;
|
|
210
|
-
log?: unknown;
|
|
211
|
-
}): Promise<void> {
|
|
212
|
-
try {
|
|
213
|
-
if (!params.messageId) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
appendQuoteJournalEntry({
|
|
217
|
-
storePath: params.storePath,
|
|
218
|
-
accountId: params.accountId,
|
|
219
|
-
conversationId: params.conversationId || null,
|
|
220
|
-
msgId: params.messageId,
|
|
221
|
-
messageType: params.messageType || "outbound",
|
|
222
|
-
text: params.text,
|
|
223
|
-
createdAt: Date.now(),
|
|
224
|
-
});
|
|
225
|
-
} catch (err) {
|
|
226
|
-
(params.log as { debug?: (message: string) => void } | undefined)?.debug?.(
|
|
227
|
-
`[quote-journal] appendOutbound failed: ${String(err)}`,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export async function appendProactiveOutboundJournal(params: {
|
|
233
|
-
storePath: string;
|
|
234
|
-
accountId: string;
|
|
235
|
-
conversationId: string | null;
|
|
236
|
-
messageId?: string;
|
|
237
|
-
text?: string;
|
|
238
|
-
messageType?: string;
|
|
239
|
-
log?: unknown;
|
|
240
|
-
}): Promise<void> {
|
|
241
|
-
return appendOutboundToQuoteJournal(params);
|
|
242
|
-
}
|