@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/types.ts
CHANGED
|
@@ -10,22 +10,21 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
|
+
ChannelPlugin as SDKChannelPlugin,
|
|
13
14
|
OpenClawConfig,
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
} from "openclaw/plugin-sdk/core";
|
|
16
|
+
import type {
|
|
16
17
|
ChannelAccountSnapshot as SDKChannelAccountSnapshot,
|
|
17
18
|
ChannelGatewayContext as SDKChannelGatewayContext,
|
|
18
|
-
|
|
19
|
-
} from "openclaw/plugin-sdk";
|
|
19
|
+
ChannelLogSink as SDKChannelLogSink,
|
|
20
|
+
} from "openclaw/plugin-sdk/channel-runtime";
|
|
21
|
+
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
|
20
22
|
import { mergeAccountWithDefaults } from "./config";
|
|
21
23
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
configSchema?: unknown;
|
|
27
|
-
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
28
|
-
}
|
|
24
|
+
export type AckReactionMode = "off" | "emoji" | "kaomoji";
|
|
25
|
+
// Accept arbitrary strings for backward compatibility; the recommended
|
|
26
|
+
// explicit modes remain: "off" | "emoji" | "kaomoji".
|
|
27
|
+
export type AckReactionConfigValue = string;
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* DingTalk channel configuration (extends base OpenClaw config)
|
|
@@ -39,16 +38,18 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
39
38
|
name?: string;
|
|
40
39
|
enabled?: boolean;
|
|
41
40
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
42
|
-
groupPolicy?: "open" | "allowlist";
|
|
41
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
43
42
|
allowFrom?: string[];
|
|
43
|
+
groupAllowFrom?: string[];
|
|
44
|
+
displayNameResolution?: "disabled" | "all";
|
|
44
45
|
mediaUrlAllowlist?: string[];
|
|
45
46
|
journalTTLDays?: number;
|
|
46
|
-
ackReaction?:
|
|
47
|
+
ackReaction?: AckReactionConfigValue;
|
|
47
48
|
debug?: boolean;
|
|
48
49
|
messageType?: "markdown" | "card";
|
|
49
50
|
cardTemplateId?: string;
|
|
50
51
|
cardTemplateKey?: string;
|
|
51
|
-
groups?: Record<string, { systemPrompt?: string }>;
|
|
52
|
+
groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
|
|
52
53
|
accounts?: Record<string, DingTalkConfig>;
|
|
53
54
|
// Connection robustness configuration
|
|
54
55
|
maxConnectionAttempts?: number;
|
|
@@ -87,6 +88,10 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
87
88
|
feedbackLearningAutoApply?: boolean;
|
|
88
89
|
/** @deprecated Use learningNoteTtlMs */
|
|
89
90
|
feedbackLearningNoteTtlMs?: number;
|
|
91
|
+
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
92
|
+
convertMarkdownTables?: boolean;
|
|
93
|
+
/** @mention the sender after card finalization in group chats; value is the message text */
|
|
94
|
+
cardAtSender?: string;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
/**
|
|
@@ -101,16 +106,18 @@ export interface DingTalkChannelConfig {
|
|
|
101
106
|
agentId?: string;
|
|
102
107
|
name?: string;
|
|
103
108
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
104
|
-
groupPolicy?: "open" | "allowlist";
|
|
109
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
105
110
|
allowFrom?: string[];
|
|
111
|
+
groupAllowFrom?: string[];
|
|
112
|
+
displayNameResolution?: "disabled" | "all";
|
|
106
113
|
mediaUrlAllowlist?: string[];
|
|
107
114
|
journalTTLDays?: number;
|
|
108
|
-
ackReaction?:
|
|
115
|
+
ackReaction?: AckReactionConfigValue;
|
|
109
116
|
debug?: boolean;
|
|
110
117
|
messageType?: "markdown" | "card";
|
|
111
118
|
cardTemplateId?: string;
|
|
112
119
|
cardTemplateKey?: string;
|
|
113
|
-
groups?: Record<string, { systemPrompt?: string }>;
|
|
120
|
+
groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
|
|
114
121
|
accounts?: Record<string, DingTalkConfig>;
|
|
115
122
|
maxConnectionAttempts?: number;
|
|
116
123
|
initialReconnectDelay?: number;
|
|
@@ -148,6 +155,10 @@ export interface DingTalkChannelConfig {
|
|
|
148
155
|
feedbackLearningAutoApply?: boolean;
|
|
149
156
|
/** @deprecated Use learningNoteTtlMs */
|
|
150
157
|
feedbackLearningNoteTtlMs?: number;
|
|
158
|
+
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
159
|
+
convertMarkdownTables?: boolean;
|
|
160
|
+
/** @mention the sender after card finalization in group chats; value is the message text */
|
|
161
|
+
cardAtSender?: string;
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
/**
|
|
@@ -207,6 +218,14 @@ export interface DingTalkInboundMessage {
|
|
|
207
218
|
msgId: string;
|
|
208
219
|
msgtype: string;
|
|
209
220
|
createAt: number;
|
|
221
|
+
/**
|
|
222
|
+
* @ 提及的用户列表(消息顶层,与 text 同级)
|
|
223
|
+
* 包含通过 @picker 选中的所有真实钉钉用户和机器人
|
|
224
|
+
* 格式: [{ dingtalkId: "$:LWCP_v1:$xxx" }]
|
|
225
|
+
*/
|
|
226
|
+
atUsers?: Array<{
|
|
227
|
+
dingtalkId: string;
|
|
228
|
+
}>;
|
|
210
229
|
text?: {
|
|
211
230
|
content: string;
|
|
212
231
|
isReplyMsg?: boolean; // 是否是回复消息
|
|
@@ -218,6 +237,7 @@ export interface DingTalkInboundMessage {
|
|
|
218
237
|
content?: {
|
|
219
238
|
text?: string;
|
|
220
239
|
downloadCode?: string;
|
|
240
|
+
fileName?: string;
|
|
221
241
|
biz_custom_action_url?: string;
|
|
222
242
|
richText?: Array<{
|
|
223
243
|
msgType?: string;
|
|
@@ -242,6 +262,7 @@ export interface DingTalkInboundMessage {
|
|
|
242
262
|
type: string;
|
|
243
263
|
text?: string;
|
|
244
264
|
atName?: string;
|
|
265
|
+
atUserId?: string;
|
|
245
266
|
downloadCode?: string;
|
|
246
267
|
}>;
|
|
247
268
|
quoteContent?: string;
|
|
@@ -267,24 +288,58 @@ export interface DingTalkInboundMessage {
|
|
|
267
288
|
sessionWebhook: string;
|
|
268
289
|
}
|
|
269
290
|
|
|
291
|
+
export type QuotedRefKey = "msgId" | "processQueryKey" | "messageId" | "outTrackId" | "cardInstanceId";
|
|
292
|
+
|
|
293
|
+
export type AttachmentTextSource = "text" | "html" | "pdf" | "docx";
|
|
294
|
+
|
|
295
|
+
export interface QuotedRef {
|
|
296
|
+
targetDirection: "inbound" | "outbound";
|
|
297
|
+
key?: QuotedRefKey;
|
|
298
|
+
value?: string;
|
|
299
|
+
fallbackCreatedAt?: number;
|
|
300
|
+
}
|
|
301
|
+
|
|
270
302
|
/**
|
|
271
303
|
* Quoted/reply message metadata extracted from repliedMsg.
|
|
272
304
|
* Populated when isReplyMsg is true; downstream handlers use these fields
|
|
273
305
|
* to download quoted media or look up cached card content.
|
|
274
306
|
*/
|
|
275
307
|
export interface QuotedInfo {
|
|
276
|
-
prefix: string;
|
|
277
308
|
mediaDownloadCode?: string;
|
|
278
309
|
mediaType?: string;
|
|
279
310
|
isQuotedFile?: boolean;
|
|
280
311
|
isQuotedCard?: boolean;
|
|
281
312
|
isQuotedDocCard?: boolean;
|
|
282
|
-
docSpaceId?: string;
|
|
283
|
-
docFileId?: string;
|
|
284
313
|
cardCreatedAt?: number;
|
|
285
314
|
processQueryKey?: string;
|
|
286
315
|
fileCreatedAt?: number;
|
|
287
316
|
msgId?: string;
|
|
317
|
+
previewText?: string;
|
|
318
|
+
previewMessageType?: string;
|
|
319
|
+
previewFileName?: string;
|
|
320
|
+
previewSenderId?: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @ 提及信息
|
|
325
|
+
*/
|
|
326
|
+
export interface AtMention {
|
|
327
|
+
/** @ 显示的名字(去除 @ 前缀) */
|
|
328
|
+
name: string;
|
|
329
|
+
/** 钉钉用户 ID(如果是 @ 真人) */
|
|
330
|
+
userId?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Agent 名字匹配结果
|
|
335
|
+
*/
|
|
336
|
+
export interface AgentNameMatch {
|
|
337
|
+
/** 匹配到的 agent ID */
|
|
338
|
+
agentId: string;
|
|
339
|
+
/** 匹配来源:'name' | 'id' */
|
|
340
|
+
matchSource: "name" | "id";
|
|
341
|
+
/** 匹配到的名字 */
|
|
342
|
+
matchedName: string;
|
|
288
343
|
}
|
|
289
344
|
|
|
290
345
|
/**
|
|
@@ -300,6 +355,16 @@ export interface MessageContent {
|
|
|
300
355
|
docSpaceId?: string;
|
|
301
356
|
docFileId?: string;
|
|
302
357
|
quoted?: QuotedInfo;
|
|
358
|
+
/** @ 提及列表(从文本或 richText 提取的名字) */
|
|
359
|
+
atMentions?: AtMention[];
|
|
360
|
+
/**
|
|
361
|
+
* 通过 @picker 选中的真实钉钉用户的 dingtalkId 列表
|
|
362
|
+
* - 仅包含真实钉钉用户和机器人,不包含 agent 名
|
|
363
|
+
* - 用于排除真人:如果 atMentions 中的名字匹配到 agent,说明是 agent;
|
|
364
|
+
* 如果没匹配到 agent 且有 atUserDingtalkIds,则可能是真人
|
|
365
|
+
* - 注意:无法将 dingtalkId 映射到具体名字,因为 webhook 不提供此映射
|
|
366
|
+
*/
|
|
367
|
+
atUserDingtalkIds?: string[];
|
|
303
368
|
}
|
|
304
369
|
|
|
305
370
|
/**
|
|
@@ -318,6 +383,12 @@ export interface SendMessageOptions {
|
|
|
318
383
|
accountId?: string;
|
|
319
384
|
storePath?: string;
|
|
320
385
|
cardUpdateMode?: "append";
|
|
386
|
+
quotedRef?: QuotedRef;
|
|
387
|
+
/** Force markdown/text delivery even when messageType is "card". Bypasses card
|
|
388
|
+
* creation while preserving journal writes and other side-effects. */
|
|
389
|
+
forceMarkdown?: boolean;
|
|
390
|
+
/** Allowed local roots for sandbox/container media path resolution. */
|
|
391
|
+
mediaLocalRoots?: string[];
|
|
321
392
|
}
|
|
322
393
|
|
|
323
394
|
export interface DingTalkTrackingMetadata {
|
|
@@ -344,6 +415,18 @@ export interface SessionWebhookResponse {
|
|
|
344
415
|
};
|
|
345
416
|
}
|
|
346
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Sub-agent routing options for parameterized message handling
|
|
420
|
+
*/
|
|
421
|
+
export interface SubAgentOptions {
|
|
422
|
+
/** The agent ID to route to */
|
|
423
|
+
agentId: string;
|
|
424
|
+
/** Prefix to add to response messages (e.g., "[AgentName] ") */
|
|
425
|
+
responsePrefix: string;
|
|
426
|
+
/** The matched agent name */
|
|
427
|
+
matchedName: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
347
430
|
/**
|
|
348
431
|
* Message handler parameters
|
|
349
432
|
*/
|
|
@@ -354,6 +437,19 @@ export interface HandleDingTalkMessageParams {
|
|
|
354
437
|
sessionWebhook: string;
|
|
355
438
|
log?: Logger;
|
|
356
439
|
dingtalkConfig: DingTalkConfig;
|
|
440
|
+
/**
|
|
441
|
+
* When set, routes message to the specified sub-agent instead of main agent.
|
|
442
|
+
* This enables reuse of the main message handling logic for sub-agents.
|
|
443
|
+
*/
|
|
444
|
+
subAgentOptions?: SubAgentOptions;
|
|
445
|
+
/**
|
|
446
|
+
* Pre-downloaded media for sub-agent calls.
|
|
447
|
+
* When set, skips media download to avoid duplication in recursive calls.
|
|
448
|
+
*/
|
|
449
|
+
preDownloadedMedia?: {
|
|
450
|
+
mediaPath?: string;
|
|
451
|
+
mediaType?: string;
|
|
452
|
+
};
|
|
357
453
|
}
|
|
358
454
|
|
|
359
455
|
/**
|
|
@@ -479,7 +575,9 @@ export interface GatewayStopResult {
|
|
|
479
575
|
/**
|
|
480
576
|
* DingTalk channel plugin definition
|
|
481
577
|
*/
|
|
482
|
-
export type DingTalkChannelPlugin = SDKChannelPlugin<ResolvedAccount & { configured: boolean }
|
|
578
|
+
export type DingTalkChannelPlugin = SDKChannelPlugin<ResolvedAccount & { configured: boolean }> & {
|
|
579
|
+
setupWizard?: ChannelSetupWizard;
|
|
580
|
+
};
|
|
483
581
|
|
|
484
582
|
/**
|
|
485
583
|
* Result of target resolution validation
|
|
@@ -553,6 +651,7 @@ export interface AICardInstance {
|
|
|
553
651
|
processQueryKey?: string;
|
|
554
652
|
accessToken: string;
|
|
555
653
|
conversationId: string;
|
|
654
|
+
contextConversationId?: string;
|
|
556
655
|
accountId?: string;
|
|
557
656
|
storePath?: string;
|
|
558
657
|
createdAt: number;
|
|
@@ -680,6 +779,8 @@ export function resolveDingTalkAccount(
|
|
|
680
779
|
dmPolicy: dingtalk?.dmPolicy,
|
|
681
780
|
groupPolicy: dingtalk?.groupPolicy,
|
|
682
781
|
allowFrom: dingtalk?.allowFrom,
|
|
782
|
+
groupAllowFrom: dingtalk?.groupAllowFrom,
|
|
783
|
+
displayNameResolution: dingtalk?.displayNameResolution,
|
|
683
784
|
journalTTLDays: dingtalk?.journalTTLDays,
|
|
684
785
|
ackReaction: dingtalk?.ackReaction,
|
|
685
786
|
debug: dingtalk?.debug,
|
|
@@ -707,6 +808,8 @@ export function resolveDingTalkAccount(
|
|
|
707
808
|
feedbackLearningEnabled: dingtalk?.feedbackLearningEnabled,
|
|
708
809
|
feedbackLearningAutoApply: dingtalk?.feedbackLearningAutoApply,
|
|
709
810
|
feedbackLearningNoteTtlMs: dingtalk?.feedbackLearningNoteTtlMs,
|
|
811
|
+
convertMarkdownTables: dingtalk?.convertMarkdownTables,
|
|
812
|
+
cardAtSender: dingtalk?.cardAtSender,
|
|
710
813
|
};
|
|
711
814
|
return {
|
|
712
815
|
...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
|
-
}
|