@soimy/dingtalk 3.5.3 → 3.6.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 +4 -1
- package/index.ts +7 -0
- package/openclaw.plugin.json +153 -5
- package/package.json +1 -1
- package/src/auth.ts +5 -2
- package/src/card/card-markdown-image-reroute.ts +106 -0
- package/src/card/card-run-registry.ts +54 -1
- package/src/card/card-stop-handler.ts +10 -20
- package/src/card/card-template.ts +14 -3
- package/src/card/statusline-renderer.ts +94 -0
- package/src/card-draft-controller.ts +245 -52
- package/src/card-service.ts +408 -23
- package/src/channel.ts +24 -1083
- package/src/config-schema.ts +21 -1
- package/src/config.ts +139 -66
- package/src/device-registration.ts +245 -0
- package/src/gateway/channel-gateway.ts +637 -0
- package/src/inbound-handler.ts +1276 -975
- package/src/media-utils.ts +6 -0
- package/src/message-context-store.ts +183 -85
- package/src/message-utils.ts +124 -16
- package/src/messaging/btw-deliver.ts +85 -0
- package/src/messaging/channel-actions.ts +174 -0
- package/src/messaging/channel-outbound.ts +158 -0
- package/src/onboarding.ts +333 -235
- package/src/path-utils.ts +49 -0
- package/src/platform/channel-status.ts +81 -0
- package/src/reply-strategy-card.ts +373 -64
- package/src/reply-strategy-markdown.ts +1 -1
- package/src/reply-strategy-types.ts +93 -0
- package/src/reply-strategy-with-reaction.ts +1 -1
- package/src/reply-strategy.ts +14 -72
- package/src/run-usage-store.ts +59 -0
- package/src/secret-input.ts +216 -0
- package/src/send-service.ts +115 -3
- package/src/session-state.ts +62 -0
- package/src/targeting/agent-name-matcher.ts +28 -0
- package/src/targeting/agent-routing.ts +30 -5
- package/src/types.ts +48 -157
package/src/media-utils.ts
CHANGED
|
@@ -534,6 +534,12 @@ export function resolveOutboundMediaType(params: {
|
|
|
534
534
|
return explicitType;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
// Audio files default to "file" (attachment) unless asVoice is explicitly set.
|
|
538
|
+
// This prevents mp3/wav/ogg/amr from being sent as voice messages unexpectedly.
|
|
539
|
+
if (detectedType === "voice") {
|
|
540
|
+
return "file";
|
|
541
|
+
}
|
|
542
|
+
|
|
537
543
|
return detectedType;
|
|
538
544
|
}
|
|
539
545
|
|
|
@@ -11,8 +11,17 @@ export const DEFAULT_CREATED_AT_MATCH_WINDOW_MS = 2000;
|
|
|
11
11
|
const MAX_RECORDS_PER_SCOPE = 1000;
|
|
12
12
|
|
|
13
13
|
export type MessageContextDirection = "inbound" | "outbound";
|
|
14
|
-
export type MessageAliasKind =
|
|
15
|
-
|
|
14
|
+
export type MessageAliasKind =
|
|
15
|
+
| "inboundMsgId"
|
|
16
|
+
| "messageId"
|
|
17
|
+
| "processQueryKey"
|
|
18
|
+
| "outTrackId"
|
|
19
|
+
| "cardInstanceId";
|
|
20
|
+
export type MessageDeliveryKind =
|
|
21
|
+
| "session"
|
|
22
|
+
| "proactive-text"
|
|
23
|
+
| "proactive-card"
|
|
24
|
+
| "proactive-media";
|
|
16
25
|
export const DEFAULT_OUTBOUND_SENDER = {
|
|
17
26
|
senderId: "bot",
|
|
18
27
|
senderName: "OpenClaw",
|
|
@@ -47,6 +56,7 @@ export interface MessageRecord {
|
|
|
47
56
|
quotedMessageId?: string;
|
|
48
57
|
media?: {
|
|
49
58
|
downloadCode?: string;
|
|
59
|
+
downloadCodes?: string[];
|
|
50
60
|
spaceId?: string;
|
|
51
61
|
fileId?: string;
|
|
52
62
|
};
|
|
@@ -96,6 +106,7 @@ interface BaseUpsertParams {
|
|
|
96
106
|
quotedMessageId?: string;
|
|
97
107
|
media?: {
|
|
98
108
|
downloadCode?: string;
|
|
109
|
+
downloadCodes?: string[];
|
|
99
110
|
spaceId?: string;
|
|
100
111
|
fileId?: string;
|
|
101
112
|
};
|
|
@@ -120,7 +131,11 @@ interface ScopeParams {
|
|
|
120
131
|
const stateCache = new Map<string, MessageContextState>();
|
|
121
132
|
|
|
122
133
|
function getScopeKey(params: ScopeParams): string {
|
|
123
|
-
return JSON.stringify([
|
|
134
|
+
return JSON.stringify([
|
|
135
|
+
params.storePath || "__memory__",
|
|
136
|
+
params.accountId,
|
|
137
|
+
params.conversationId || null,
|
|
138
|
+
]);
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
function fallbackState(): MessageContextState {
|
|
@@ -145,19 +160,26 @@ function normalizeMedia(value: unknown): MessageRecord["media"] | undefined {
|
|
|
145
160
|
if (!candidate) {
|
|
146
161
|
return undefined;
|
|
147
162
|
}
|
|
148
|
-
const downloadCode =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
const downloadCode =
|
|
164
|
+
typeof candidate.downloadCode === "string" && candidate.downloadCode.trim()
|
|
165
|
+
? candidate.downloadCode.trim()
|
|
166
|
+
: undefined;
|
|
167
|
+
const downloadCodes =
|
|
168
|
+
Array.isArray(candidate.downloadCodes) && candidate.downloadCodes.length > 0
|
|
169
|
+
? candidate.downloadCodes.filter((c: unknown) => typeof c === "string" && c.trim())
|
|
170
|
+
: undefined;
|
|
171
|
+
const spaceId =
|
|
172
|
+
typeof candidate.spaceId === "string" && candidate.spaceId.trim()
|
|
173
|
+
? candidate.spaceId.trim()
|
|
174
|
+
: undefined;
|
|
175
|
+
const fileId =
|
|
176
|
+
typeof candidate.fileId === "string" && candidate.fileId.trim()
|
|
177
|
+
? candidate.fileId.trim()
|
|
178
|
+
: undefined;
|
|
179
|
+
if (!downloadCode && (!downloadCodes || downloadCodes.length === 0) && !spaceId && !fileId) {
|
|
158
180
|
return undefined;
|
|
159
181
|
}
|
|
160
|
-
return { downloadCode, spaceId, fileId };
|
|
182
|
+
return { downloadCode, downloadCodes, spaceId, fileId };
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
function normalizeQuotedRef(value: unknown): QuotedRef | undefined {
|
|
@@ -180,7 +202,9 @@ function normalizeQuotedRef(value: unknown): QuotedRef | undefined {
|
|
|
180
202
|
? candidate.key
|
|
181
203
|
: undefined;
|
|
182
204
|
const valueString =
|
|
183
|
-
typeof candidate.value === "string" && candidate.value.trim()
|
|
205
|
+
typeof candidate.value === "string" && candidate.value.trim()
|
|
206
|
+
? candidate.value.trim()
|
|
207
|
+
: undefined;
|
|
184
208
|
const fallbackCreatedAt =
|
|
185
209
|
typeof candidate.fallbackCreatedAt === "number" && Number.isFinite(candidate.fallbackCreatedAt)
|
|
186
210
|
? candidate.fallbackCreatedAt
|
|
@@ -213,21 +237,26 @@ function normalizeDelivery(value: unknown): MessageRecord["delivery"] | undefine
|
|
|
213
237
|
if (!candidate) {
|
|
214
238
|
return undefined;
|
|
215
239
|
}
|
|
216
|
-
const messageId =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
const messageId =
|
|
241
|
+
typeof candidate.messageId === "string" && candidate.messageId.trim()
|
|
242
|
+
? candidate.messageId.trim()
|
|
243
|
+
: undefined;
|
|
244
|
+
const processQueryKey =
|
|
245
|
+
typeof candidate.processQueryKey === "string" && candidate.processQueryKey.trim()
|
|
246
|
+
? candidate.processQueryKey.trim()
|
|
247
|
+
: undefined;
|
|
248
|
+
const outTrackId =
|
|
249
|
+
typeof candidate.outTrackId === "string" && candidate.outTrackId.trim()
|
|
250
|
+
? candidate.outTrackId.trim()
|
|
251
|
+
: undefined;
|
|
252
|
+
const cardInstanceId =
|
|
253
|
+
typeof candidate.cardInstanceId === "string" && candidate.cardInstanceId.trim()
|
|
254
|
+
? candidate.cardInstanceId.trim()
|
|
255
|
+
: undefined;
|
|
256
|
+
const kind =
|
|
257
|
+
typeof candidate.kind === "string" && candidate.kind.trim()
|
|
258
|
+
? (candidate.kind.trim() as MessageDeliveryKind)
|
|
259
|
+
: undefined;
|
|
231
260
|
if (!messageId && !processQueryKey && !outTrackId && !cardInstanceId && !kind) {
|
|
232
261
|
return undefined;
|
|
233
262
|
}
|
|
@@ -248,30 +277,46 @@ function normalizeMessageRecord(value: unknown): MessageRecord | null {
|
|
|
248
277
|
if (!candidate) {
|
|
249
278
|
return null;
|
|
250
279
|
}
|
|
251
|
-
const msgId =
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
280
|
+
const msgId =
|
|
281
|
+
typeof candidate.msgId === "string" && candidate.msgId.trim() ? candidate.msgId.trim() : "";
|
|
282
|
+
const direction =
|
|
283
|
+
candidate.direction === "outbound"
|
|
284
|
+
? "outbound"
|
|
285
|
+
: candidate.direction === "inbound"
|
|
286
|
+
? "inbound"
|
|
287
|
+
: null;
|
|
288
|
+
const accountId =
|
|
289
|
+
typeof candidate.accountId === "string" && candidate.accountId.trim()
|
|
290
|
+
? candidate.accountId.trim()
|
|
291
|
+
: "";
|
|
292
|
+
const conversationId =
|
|
293
|
+
typeof candidate.conversationId === "string" && candidate.conversationId.trim()
|
|
294
|
+
? candidate.conversationId.trim()
|
|
295
|
+
: null;
|
|
296
|
+
const createdAt =
|
|
297
|
+
typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt)
|
|
298
|
+
? candidate.createdAt
|
|
299
|
+
: NaN;
|
|
300
|
+
const updatedAt =
|
|
301
|
+
typeof candidate.updatedAt === "number" && Number.isFinite(candidate.updatedAt)
|
|
302
|
+
? candidate.updatedAt
|
|
303
|
+
: Date.now();
|
|
265
304
|
if (!msgId || !direction || !accountId || !Number.isFinite(createdAt)) {
|
|
266
305
|
return null;
|
|
267
306
|
}
|
|
268
|
-
const expiresAt =
|
|
269
|
-
|
|
270
|
-
|
|
307
|
+
const expiresAt =
|
|
308
|
+
typeof candidate.expiresAt === "number" && Number.isFinite(candidate.expiresAt)
|
|
309
|
+
? candidate.expiresAt
|
|
310
|
+
: undefined;
|
|
271
311
|
return {
|
|
272
312
|
msgId,
|
|
273
313
|
direction,
|
|
274
|
-
topic:
|
|
314
|
+
topic:
|
|
315
|
+
candidate.topic === null
|
|
316
|
+
? null
|
|
317
|
+
: typeof candidate.topic === "string"
|
|
318
|
+
? candidate.topic
|
|
319
|
+
: null,
|
|
275
320
|
accountId,
|
|
276
321
|
conversationId,
|
|
277
322
|
createdAt,
|
|
@@ -279,16 +324,26 @@ function normalizeMessageRecord(value: unknown): MessageRecord | null {
|
|
|
279
324
|
expiresAt,
|
|
280
325
|
messageType: typeof candidate.messageType === "string" ? candidate.messageType : undefined,
|
|
281
326
|
text: typeof candidate.text === "string" ? candidate.text : undefined,
|
|
282
|
-
attachmentText:
|
|
327
|
+
attachmentText:
|
|
328
|
+
typeof candidate.attachmentText === "string" ? candidate.attachmentText : undefined,
|
|
283
329
|
attachmentTextSource: normalizeAttachmentTextSource(candidate.attachmentTextSource),
|
|
284
330
|
attachmentTextTruncated: candidate.attachmentTextTruncated === true ? true : undefined,
|
|
285
331
|
attachmentFileName:
|
|
286
332
|
typeof candidate.attachmentFileName === "string" ? candidate.attachmentFileName : undefined,
|
|
287
333
|
quotedRef: normalizeQuotedRef(candidate.quotedRef),
|
|
288
|
-
senderId:
|
|
289
|
-
|
|
334
|
+
senderId:
|
|
335
|
+
typeof candidate.senderId === "string" && candidate.senderId.trim()
|
|
336
|
+
? candidate.senderId.trim()
|
|
337
|
+
: undefined,
|
|
338
|
+
senderName:
|
|
339
|
+
typeof candidate.senderName === "string" && candidate.senderName.trim()
|
|
340
|
+
? candidate.senderName.trim()
|
|
341
|
+
: undefined,
|
|
290
342
|
mentions: normalizeMentions(candidate.mentions),
|
|
291
|
-
chatType:
|
|
343
|
+
chatType:
|
|
344
|
+
candidate.chatType === "direct" || candidate.chatType === "group"
|
|
345
|
+
? candidate.chatType
|
|
346
|
+
: undefined,
|
|
292
347
|
quotedMessageId:
|
|
293
348
|
typeof candidate.quotedMessageId === "string" && candidate.quotedMessageId.trim()
|
|
294
349
|
? candidate.quotedMessageId.trim()
|
|
@@ -327,10 +382,17 @@ function buildAliasEntries(record: MessageRecord): Array<[string, string]> {
|
|
|
327
382
|
}
|
|
328
383
|
|
|
329
384
|
function isRecordExpired(record: MessageRecord, nowMs: number): boolean {
|
|
330
|
-
return
|
|
385
|
+
return (
|
|
386
|
+
typeof record.expiresAt === "number" &&
|
|
387
|
+
Number.isFinite(record.expiresAt) &&
|
|
388
|
+
nowMs >= record.expiresAt
|
|
389
|
+
);
|
|
331
390
|
}
|
|
332
391
|
|
|
333
|
-
function normalizeState(
|
|
392
|
+
function normalizeState(
|
|
393
|
+
state: MessageContextState,
|
|
394
|
+
nowMs: number,
|
|
395
|
+
): { state: MessageContextState; removed: number } {
|
|
334
396
|
const normalizedRecords: Record<string, MessageRecord> = {};
|
|
335
397
|
const sortedRecords = Object.values(state.records)
|
|
336
398
|
.map((record) => normalizeMessageRecord(record))
|
|
@@ -363,12 +425,15 @@ function hydrateState(params: ScopeParams, nowMs: number): MessageContextState {
|
|
|
363
425
|
if (!params.storePath) {
|
|
364
426
|
return fallbackState();
|
|
365
427
|
}
|
|
366
|
-
const persisted = readNamespaceJson<Partial<PersistedMessageContextState>>(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
428
|
+
const persisted = readNamespaceJson<Partial<PersistedMessageContextState>>(
|
|
429
|
+
MESSAGE_CONTEXT_NAMESPACE,
|
|
430
|
+
{
|
|
431
|
+
storePath: params.storePath,
|
|
432
|
+
scope: { accountId: params.accountId, conversationId: params.conversationId || undefined },
|
|
433
|
+
format: "json",
|
|
434
|
+
fallback: { version: MESSAGE_CONTEXT_VERSION, updatedAt: Date.now(), records: {} },
|
|
435
|
+
},
|
|
436
|
+
);
|
|
372
437
|
const parsedRecords = asRecord(persisted.records) || {};
|
|
373
438
|
const hydrated = fallbackState();
|
|
374
439
|
hydrated.updatedAt = typeof persisted.updatedAt === "number" ? persisted.updatedAt : Date.now();
|
|
@@ -426,14 +491,20 @@ function mergeText(existing: string | undefined, next: string | undefined): stri
|
|
|
426
491
|
return next;
|
|
427
492
|
}
|
|
428
493
|
|
|
429
|
-
function mergeAttachmentText(
|
|
494
|
+
function mergeAttachmentText(
|
|
495
|
+
existing: string | undefined,
|
|
496
|
+
next: string | undefined,
|
|
497
|
+
): string | undefined {
|
|
430
498
|
if (typeof next !== "string") {
|
|
431
499
|
return existing;
|
|
432
500
|
}
|
|
433
501
|
return next;
|
|
434
502
|
}
|
|
435
503
|
|
|
436
|
-
function mergeQuotedRef(
|
|
504
|
+
function mergeQuotedRef(
|
|
505
|
+
existing: QuotedRef | undefined,
|
|
506
|
+
next: QuotedRef | undefined,
|
|
507
|
+
): QuotedRef | undefined {
|
|
437
508
|
if (!existing) {
|
|
438
509
|
return next;
|
|
439
510
|
}
|
|
@@ -448,14 +519,20 @@ function mergeQuotedRef(existing: QuotedRef | undefined, next: QuotedRef | undef
|
|
|
448
519
|
};
|
|
449
520
|
}
|
|
450
521
|
|
|
451
|
-
function mergeStringField(
|
|
522
|
+
function mergeStringField(
|
|
523
|
+
existing: string | undefined,
|
|
524
|
+
next: string | undefined,
|
|
525
|
+
): string | undefined {
|
|
452
526
|
if (typeof next !== "string" || !next.trim()) {
|
|
453
527
|
return existing;
|
|
454
528
|
}
|
|
455
529
|
return next.trim();
|
|
456
530
|
}
|
|
457
531
|
|
|
458
|
-
function mergeMentions(
|
|
532
|
+
function mergeMentions(
|
|
533
|
+
existing: string[] | undefined,
|
|
534
|
+
next: string[] | undefined,
|
|
535
|
+
): string[] | undefined {
|
|
459
536
|
if (!next) {
|
|
460
537
|
return existing;
|
|
461
538
|
}
|
|
@@ -474,6 +551,7 @@ function mergeMedia(
|
|
|
474
551
|
}
|
|
475
552
|
return {
|
|
476
553
|
downloadCode: next.downloadCode || existing.downloadCode,
|
|
554
|
+
downloadCodes: next.downloadCodes || existing.downloadCodes,
|
|
477
555
|
spaceId: next.spaceId || existing.spaceId,
|
|
478
556
|
fileId: next.fileId || existing.fileId,
|
|
479
557
|
};
|
|
@@ -512,7 +590,10 @@ function mergeAttachmentTextTruncated(
|
|
|
512
590
|
return next === undefined ? existing : next;
|
|
513
591
|
}
|
|
514
592
|
|
|
515
|
-
function mergeAttachmentFileName(
|
|
593
|
+
function mergeAttachmentFileName(
|
|
594
|
+
existing: string | undefined,
|
|
595
|
+
next: string | undefined,
|
|
596
|
+
): string | undefined {
|
|
516
597
|
if (typeof next !== "string") {
|
|
517
598
|
return existing;
|
|
518
599
|
}
|
|
@@ -521,7 +602,11 @@ function mergeAttachmentFileName(existing: string | undefined, next: string | un
|
|
|
521
602
|
|
|
522
603
|
function resolveExistingMsgId(
|
|
523
604
|
state: MessageContextState,
|
|
524
|
-
params: {
|
|
605
|
+
params: {
|
|
606
|
+
direction: MessageContextDirection;
|
|
607
|
+
msgId?: string;
|
|
608
|
+
delivery?: MessageRecord["delivery"];
|
|
609
|
+
},
|
|
525
610
|
nowMs: number,
|
|
526
611
|
): string | undefined {
|
|
527
612
|
if (params.direction === "inbound" && params.msgId) {
|
|
@@ -556,11 +641,19 @@ function resolveExistingMsgId(
|
|
|
556
641
|
return undefined;
|
|
557
642
|
}
|
|
558
643
|
|
|
559
|
-
function computeExpiresAt(
|
|
644
|
+
function computeExpiresAt(
|
|
645
|
+
nowMs: number,
|
|
646
|
+
ttlMs?: number,
|
|
647
|
+
ttlReferenceMs?: number,
|
|
648
|
+
): number | undefined {
|
|
560
649
|
if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
561
650
|
return undefined;
|
|
562
651
|
}
|
|
563
|
-
return (
|
|
652
|
+
return (
|
|
653
|
+
(typeof ttlReferenceMs === "number" && Number.isFinite(ttlReferenceMs)
|
|
654
|
+
? ttlReferenceMs
|
|
655
|
+
: nowMs) + ttlMs
|
|
656
|
+
);
|
|
564
657
|
}
|
|
565
658
|
|
|
566
659
|
function pruneStateByCreatedAt(
|
|
@@ -622,11 +715,15 @@ function upsertRecord(
|
|
|
622
715
|
if (params.cleanupCreatedAtTtlDays && params.cleanupCreatedAtTtlDays > 0) {
|
|
623
716
|
state = pruneStateByCreatedAt(state, params.cleanupCreatedAtTtlDays, nowMs).state;
|
|
624
717
|
}
|
|
625
|
-
const existingMsgId = resolveExistingMsgId(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
718
|
+
const existingMsgId = resolveExistingMsgId(
|
|
719
|
+
state,
|
|
720
|
+
{
|
|
721
|
+
direction: params.direction,
|
|
722
|
+
msgId: params.msgId,
|
|
723
|
+
delivery: params.delivery,
|
|
724
|
+
},
|
|
725
|
+
nowMs,
|
|
726
|
+
);
|
|
630
727
|
const canonicalMsgId =
|
|
631
728
|
existingMsgId ||
|
|
632
729
|
params.msgId ||
|
|
@@ -648,9 +745,7 @@ function upsertRecord(
|
|
|
648
745
|
createdAt: existing?.createdAt ?? params.createdAt,
|
|
649
746
|
updatedAt: nowMs,
|
|
650
747
|
expiresAt:
|
|
651
|
-
expiresAt === undefined
|
|
652
|
-
? existing?.expiresAt
|
|
653
|
-
: Math.max(expiresAt, existing?.expiresAt ?? 0),
|
|
748
|
+
expiresAt === undefined ? existing?.expiresAt : Math.max(expiresAt, existing?.expiresAt ?? 0),
|
|
654
749
|
messageType: params.messageType || existing?.messageType,
|
|
655
750
|
text: mergeText(existing?.text, params.text),
|
|
656
751
|
attachmentText: mergeAttachmentText(existing?.attachmentText, params.attachmentText),
|
|
@@ -692,7 +787,9 @@ export function upsertInboundMessageContext(params: UpsertInboundMessageContextP
|
|
|
692
787
|
);
|
|
693
788
|
}
|
|
694
789
|
|
|
695
|
-
export function upsertOutboundMessageContext(
|
|
790
|
+
export function upsertOutboundMessageContext(
|
|
791
|
+
params: UpsertOutboundMessageContextParams,
|
|
792
|
+
): string | undefined {
|
|
696
793
|
return upsertRecord({
|
|
697
794
|
...params,
|
|
698
795
|
direction: "outbound",
|
|
@@ -780,7 +877,10 @@ export function resolveByQuotedRef(
|
|
|
780
877
|
`[DingTalk][QuotedRef] Resolve outbound by ${quotedRef.key}=${quotedRef.value} hit=no accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`,
|
|
781
878
|
);
|
|
782
879
|
}
|
|
783
|
-
if (
|
|
880
|
+
if (
|
|
881
|
+
typeof quotedRef.fallbackCreatedAt === "number" &&
|
|
882
|
+
Number.isFinite(quotedRef.fallbackCreatedAt)
|
|
883
|
+
) {
|
|
784
884
|
const record = resolveByCreatedAtWindow({
|
|
785
885
|
...params,
|
|
786
886
|
createdAt: quotedRef.fallbackCreatedAt,
|
|
@@ -828,9 +928,7 @@ export function resolveByCreatedAtWindow(
|
|
|
828
928
|
return bestRecord;
|
|
829
929
|
}
|
|
830
930
|
|
|
831
|
-
export function cleanupExpiredMessageContexts(
|
|
832
|
-
params: ScopeParams & { nowMs?: number },
|
|
833
|
-
): number {
|
|
931
|
+
export function cleanupExpiredMessageContexts(params: ScopeParams & { nowMs?: number }): number {
|
|
834
932
|
const nowMs = params.nowMs ?? Date.now();
|
|
835
933
|
const state = loadState(params, nowMs);
|
|
836
934
|
const beforeCount = Object.keys(state.records).length;
|
|
@@ -850,12 +948,12 @@ export function clearMessageContextCacheForTest(): void {
|
|
|
850
948
|
/**
|
|
851
949
|
* Lists non-expired message-context records for one account/conversation scope in createdAt ascending order.
|
|
852
950
|
*/
|
|
853
|
-
export function listMessageContexts(
|
|
854
|
-
params: ScopeParams & { nowMs?: number },
|
|
855
|
-
): MessageRecord[] {
|
|
951
|
+
export function listMessageContexts(params: ScopeParams & { nowMs?: number }): MessageRecord[] {
|
|
856
952
|
const nowMs = params.nowMs ?? Date.now();
|
|
857
953
|
const state = loadState(params, nowMs);
|
|
858
954
|
return state.recentByCreatedAt
|
|
859
955
|
.map((msgId) => state.records[msgId])
|
|
860
|
-
.filter(
|
|
956
|
+
.filter(
|
|
957
|
+
(record): record is MessageRecord => Boolean(record) && !isRecordExpired(record, nowMs),
|
|
958
|
+
);
|
|
861
959
|
}
|
package/src/message-utils.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AtMention,
|
|
3
|
+
DingTalkInboundMessage,
|
|
4
|
+
MessageContent,
|
|
5
|
+
QuotedInfo,
|
|
6
|
+
SendMessageOptions,
|
|
7
|
+
} from "./types";
|
|
3
8
|
|
|
4
9
|
interface DingTalkDocMeta {
|
|
5
10
|
spaceId: string;
|
|
6
11
|
fileId: string;
|
|
7
12
|
}
|
|
8
13
|
|
|
14
|
+
const UNKNOWN_PERSON_LABEL = "某人";
|
|
15
|
+
|
|
9
16
|
function parseBizCustomActionUrl(url: string | undefined): DingTalkDocMeta | null {
|
|
10
17
|
if (!url || typeof url !== "string") {
|
|
11
18
|
return null;
|
|
@@ -76,7 +83,7 @@ function extractRichTextQuoteParts(
|
|
|
76
83
|
? part.atName
|
|
77
84
|
: typeof textValue === "string"
|
|
78
85
|
? textValue
|
|
79
|
-
:
|
|
86
|
+
: UNKNOWN_PERSON_LABEL;
|
|
80
87
|
textParts.push(`@${atName}`);
|
|
81
88
|
continue;
|
|
82
89
|
}
|
|
@@ -94,7 +101,8 @@ function extractRichTextQuoteParts(
|
|
|
94
101
|
return {
|
|
95
102
|
summary,
|
|
96
103
|
pictureDownloadCode,
|
|
97
|
-
pictureDownloadCodes:
|
|
104
|
+
pictureDownloadCodes:
|
|
105
|
+
uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes : undefined,
|
|
98
106
|
};
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -115,7 +123,101 @@ function trimString(value: string | undefined): string | undefined {
|
|
|
115
123
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
const MAX_CHAT_RECORD_ENTRIES = 30;
|
|
127
|
+
|
|
128
|
+
function stringifyChatRecordContentValue(value: unknown): string | undefined {
|
|
129
|
+
if (typeof value === "string") {
|
|
130
|
+
return trimString(value);
|
|
131
|
+
}
|
|
132
|
+
if (!value || typeof value !== "object") {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const record = value as Record<string, unknown>;
|
|
136
|
+
const content = record.content;
|
|
137
|
+
const contentText =
|
|
138
|
+
typeof content === "string"
|
|
139
|
+
? trimString(content)
|
|
140
|
+
: content && typeof content === "object"
|
|
141
|
+
? trimString((content as Record<string, unknown>).text as string | undefined)
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
// Preserve DingTalk's observed chatRecord text priority: explicit text first,
|
|
145
|
+
// then nested content text, then the legacy message fallback.
|
|
146
|
+
return (
|
|
147
|
+
trimString(record.text as string | undefined) ||
|
|
148
|
+
contentText ||
|
|
149
|
+
trimString(record.message as string | undefined)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getChatRecordEntriesSource(content: Record<string, unknown> | undefined): unknown {
|
|
154
|
+
return content?.chatRecord ?? content?.records ?? content?.messages;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatChatRecordEntries(rawRecord: unknown): string[] {
|
|
158
|
+
let entries = rawRecord;
|
|
159
|
+
if (typeof rawRecord === "string") {
|
|
160
|
+
const trimmed = rawRecord.trim();
|
|
161
|
+
if (!trimmed || trimmed === "[]") {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
entries = JSON.parse(trimmed);
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!Array.isArray(entries)) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
return entries
|
|
174
|
+
.map((entry) => {
|
|
175
|
+
if (!entry || typeof entry !== "object") {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const record = entry as Record<string, unknown>;
|
|
179
|
+
const sender =
|
|
180
|
+
trimString(record.senderName as string | undefined) ||
|
|
181
|
+
trimString(record.senderNick as string | undefined) ||
|
|
182
|
+
trimString(record.sender as string | undefined) ||
|
|
183
|
+
trimString(record.senderId as string | undefined) ||
|
|
184
|
+
UNKNOWN_PERSON_LABEL;
|
|
185
|
+
const body = stringifyChatRecordContentValue(
|
|
186
|
+
record.content ?? record.text ?? record.message ?? record.body,
|
|
187
|
+
);
|
|
188
|
+
return body ? `${sender}: ${body}` : undefined;
|
|
189
|
+
})
|
|
190
|
+
.filter((line): line is string => Boolean(line))
|
|
191
|
+
.slice(0, MAX_CHAT_RECORD_ENTRIES);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatChatRecordPreview(
|
|
195
|
+
content: Record<string, unknown> | undefined,
|
|
196
|
+
options: { useTitleAsLabel?: boolean } = {},
|
|
197
|
+
): string | undefined {
|
|
198
|
+
const summary = typeof content?.summary === "string" ? content.summary.trim() : "";
|
|
199
|
+
const title = typeof content?.title === "string" ? content.title.trim() : "";
|
|
200
|
+
const rawRecord = getChatRecordEntriesSource(content);
|
|
201
|
+
const recordLines = formatChatRecordEntries(rawRecord);
|
|
202
|
+
const parts: string[] = [];
|
|
203
|
+
if (summary && summary !== "[]") {
|
|
204
|
+
const label = options.useTitleAsLabel
|
|
205
|
+
? title
|
|
206
|
+
? `[${title}] `
|
|
207
|
+
: "[聊天记录] "
|
|
208
|
+
: "[聊天记录摘要] ";
|
|
209
|
+
parts.push(`${label}${summary}`);
|
|
210
|
+
}
|
|
211
|
+
if (recordLines.length > 0) {
|
|
212
|
+
parts.push(`[聊天记录内容]\n${recordLines.join("\n")}`);
|
|
213
|
+
}
|
|
214
|
+
return parts.join("\n\n") || undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildQuotedMessageTypePlaceholder(
|
|
218
|
+
messageType: string | undefined,
|
|
219
|
+
fileName?: string,
|
|
220
|
+
): string | undefined {
|
|
119
221
|
switch (messageType) {
|
|
120
222
|
case "text":
|
|
121
223
|
return undefined;
|
|
@@ -147,8 +249,7 @@ function buildLegacyQuoteMessagePreview(message: DingTalkInboundMessage["quoteMe
|
|
|
147
249
|
const previewMessageType = trimString(message?.msgtype);
|
|
148
250
|
return {
|
|
149
251
|
previewText:
|
|
150
|
-
trimString(message?.text?.content) ||
|
|
151
|
-
buildQuotedMessageTypePlaceholder(previewMessageType),
|
|
252
|
+
trimString(message?.text?.content) || buildQuotedMessageTypePlaceholder(previewMessageType),
|
|
152
253
|
previewMessageType,
|
|
153
254
|
previewSenderId: trimString(message?.senderId),
|
|
154
255
|
};
|
|
@@ -202,7 +303,10 @@ function buildRepliedMessagePreview(params: {
|
|
|
202
303
|
return {
|
|
203
304
|
isQuotedFile: true,
|
|
204
305
|
fileCreatedAt: repliedMsg.createdAt,
|
|
205
|
-
previewText: buildQuotedMessageTypePlaceholder(
|
|
306
|
+
previewText: buildQuotedMessageTypePlaceholder(
|
|
307
|
+
repliedMsgType,
|
|
308
|
+
hasFileName ? fileName : undefined,
|
|
309
|
+
),
|
|
206
310
|
previewMessageType: repliedMsgType,
|
|
207
311
|
...(hasFileName ? { previewFileName: fileName } : {}),
|
|
208
312
|
previewSenderId: trimString(repliedMsg.senderId),
|
|
@@ -235,11 +339,11 @@ function buildRepliedMessagePreview(params: {
|
|
|
235
339
|
}
|
|
236
340
|
|
|
237
341
|
if (repliedMsgType === "chatRecord") {
|
|
238
|
-
const summary = typeof content?.summary === "string" ? content.summary.trim() : "";
|
|
239
|
-
const title = typeof content?.title === "string" ? content.title.trim() : "";
|
|
240
|
-
const chatRecordLabel = title ? `[${title}] ` : "[聊天记录] ";
|
|
241
342
|
return {
|
|
242
|
-
previewText:
|
|
343
|
+
previewText:
|
|
344
|
+
formatChatRecordPreview(content as Record<string, unknown> | undefined, {
|
|
345
|
+
useTitleAsLabel: true,
|
|
346
|
+
}) || buildQuotedMessageTypePlaceholder("chatRecord"),
|
|
243
347
|
previewMessageType: "chatRecord",
|
|
244
348
|
previewSenderId: trimString(repliedMsg.senderId),
|
|
245
349
|
};
|
|
@@ -517,7 +621,10 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
517
621
|
mediaPath: pictureDownloadCode,
|
|
518
622
|
mediaPaths: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes : undefined,
|
|
519
623
|
mediaType: pictureDownloadCode ? "image" : undefined,
|
|
520
|
-
mediaTypes:
|
|
624
|
+
mediaTypes:
|
|
625
|
+
uniquePictureDownloadCodes.length > 0
|
|
626
|
+
? uniquePictureDownloadCodes.map(() => "image")
|
|
627
|
+
: undefined,
|
|
521
628
|
messageType: "richText",
|
|
522
629
|
quoted: quoted ?? undefined,
|
|
523
630
|
atMentions,
|
|
@@ -605,7 +712,8 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
605
712
|
if (msgtype === "chatRecord") {
|
|
606
713
|
const content = data.content as Record<string, unknown> | undefined;
|
|
607
714
|
const summary = typeof content?.summary === "string" ? content.summary.trim() : "";
|
|
608
|
-
const rawRecord = content
|
|
715
|
+
const rawRecord = getChatRecordEntriesSource(content);
|
|
716
|
+
const chatRecordText = formatChatRecordPreview(content);
|
|
609
717
|
if (
|
|
610
718
|
summary === "[]" ||
|
|
611
719
|
(typeof rawRecord === "string" && rawRecord.trim() === "[]") ||
|
|
@@ -619,9 +727,9 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
619
727
|
atUserDingtalkIds,
|
|
620
728
|
};
|
|
621
729
|
}
|
|
622
|
-
if (
|
|
730
|
+
if (chatRecordText) {
|
|
623
731
|
return {
|
|
624
|
-
text:
|
|
732
|
+
text: chatRecordText,
|
|
625
733
|
messageType: "chatRecord",
|
|
626
734
|
quoted: quoted ?? undefined,
|
|
627
735
|
atMentions,
|