@yaoyuanchao/dingtalk 1.4.19 → 1.4.20
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/package.json +1 -1
- package/src/config-schema.ts +9 -0
- package/src/monitor.ts +159 -2
package/package.json
CHANGED
package/src/config-schema.ts
CHANGED
|
@@ -83,6 +83,15 @@ export const dingTalkConfigSchema = z.object({
|
|
|
83
83
|
),
|
|
84
84
|
longTextThreshold: z.number().int().positive().default(8000).optional()
|
|
85
85
|
.describe('Character threshold for longTextMode=file (default 8000)'),
|
|
86
|
+
|
|
87
|
+
// 消息聚合
|
|
88
|
+
messageAggregation: z.boolean().default(true)
|
|
89
|
+
.describe(
|
|
90
|
+
'Aggregate messages from the same sender within a short time window.\n' +
|
|
91
|
+
'Useful when DingTalk splits link cards into multiple messages.'
|
|
92
|
+
),
|
|
93
|
+
messageAggregationDelayMs: z.number().int().positive().default(2000).optional()
|
|
94
|
+
.describe('Time window in milliseconds to wait for additional messages (default 2000)'),
|
|
86
95
|
}).strict();
|
|
87
96
|
|
|
88
97
|
// 导出配置类型
|
package/src/monitor.ts
CHANGED
|
@@ -2,6 +2,35 @@ import type { DingTalkRobotMessage, ResolvedDingTalkAccount, ExtractedMessage }
|
|
|
2
2
|
import { sendViaSessionWebhook, sendMarkdownViaSessionWebhook, sendDingTalkRestMessage, batchGetUserInfo, downloadPicture, downloadMediaFile, cleanupOldMedia, uploadMediaFile, sendFileMessage, textToMarkdownFile } from "./api.js";
|
|
3
3
|
import { getDingTalkRuntime } from "./runtime.js";
|
|
4
4
|
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Message Aggregation Buffer
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// When users share links via DingTalk's "share link" feature, the message may
|
|
9
|
+
// arrive as multiple separate messages (text + URL). This buffer aggregates
|
|
10
|
+
// messages from the same sender within a short time window.
|
|
11
|
+
|
|
12
|
+
interface BufferedMessage {
|
|
13
|
+
messages: Array<{ text: string; timestamp: number; mediaPath?: string; mediaType?: string }>;
|
|
14
|
+
timer: ReturnType<typeof setTimeout>;
|
|
15
|
+
ctx: DingTalkMonitorContext;
|
|
16
|
+
msg: DingTalkRobotMessage; // Keep latest msg for reply target
|
|
17
|
+
replyTarget: any;
|
|
18
|
+
sessionKey: string;
|
|
19
|
+
isDm: boolean;
|
|
20
|
+
senderId: string;
|
|
21
|
+
senderName: string;
|
|
22
|
+
conversationId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const messageBuffer = new Map<string, BufferedMessage>();
|
|
26
|
+
const AGGREGATION_DELAY_MS = 2000; // 2 seconds - balance between UX and catching split messages
|
|
27
|
+
|
|
28
|
+
function getBufferKey(msg: DingTalkRobotMessage, accountId: string): string {
|
|
29
|
+
return `${accountId}:${msg.conversationId}:${msg.senderId || msg.senderStaffId}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
5
34
|
export interface DingTalkMonitorContext {
|
|
6
35
|
account: ResolvedDingTalkAccount;
|
|
7
36
|
cfg: any;
|
|
@@ -696,10 +725,138 @@ async function processInboundMessage(
|
|
|
696
725
|
account,
|
|
697
726
|
};
|
|
698
727
|
|
|
728
|
+
// Check if message aggregation is enabled
|
|
729
|
+
const aggregationEnabled = account.config.messageAggregation !== false;
|
|
730
|
+
const aggregationDelayMs = account.config.messageAggregationDelayMs ?? AGGREGATION_DELAY_MS;
|
|
731
|
+
|
|
732
|
+
if (aggregationEnabled) {
|
|
733
|
+
// Buffer this message for aggregation
|
|
734
|
+
await bufferMessageForAggregation({
|
|
735
|
+
msg, ctx, rawBody, replyTarget, sessionKey, isDm, senderId, senderName, conversationId,
|
|
736
|
+
mediaPath, mediaType,
|
|
737
|
+
});
|
|
738
|
+
return; // Actual dispatch happens when timer fires
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// No aggregation - dispatch immediately
|
|
742
|
+
await dispatchMessage({
|
|
743
|
+
ctx, msg, rawBody, replyTarget, sessionKey, isDm, senderId, senderName, conversationId,
|
|
744
|
+
mediaPath, mediaType,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Buffer a message for aggregation with other messages from the same sender.
|
|
750
|
+
*/
|
|
751
|
+
async function bufferMessageForAggregation(params: {
|
|
752
|
+
msg: DingTalkRobotMessage;
|
|
753
|
+
ctx: DingTalkMonitorContext;
|
|
754
|
+
rawBody: string;
|
|
755
|
+
replyTarget: any;
|
|
756
|
+
sessionKey: string;
|
|
757
|
+
isDm: boolean;
|
|
758
|
+
senderId: string;
|
|
759
|
+
senderName: string;
|
|
760
|
+
conversationId: string;
|
|
761
|
+
mediaPath?: string;
|
|
762
|
+
mediaType?: string;
|
|
763
|
+
}): Promise<void> {
|
|
764
|
+
const { msg, ctx, rawBody, replyTarget, sessionKey, isDm, senderId, senderName, conversationId, mediaPath, mediaType } = params;
|
|
765
|
+
const { account, log } = ctx;
|
|
766
|
+
const bufferKey = getBufferKey(msg, account.accountId);
|
|
767
|
+
const aggregationDelayMs = account.config.messageAggregationDelayMs ?? AGGREGATION_DELAY_MS;
|
|
768
|
+
|
|
769
|
+
const existing = messageBuffer.get(bufferKey);
|
|
770
|
+
|
|
771
|
+
if (existing) {
|
|
772
|
+
// Add to existing buffer
|
|
773
|
+
existing.messages.push({ text: rawBody, timestamp: Date.now(), mediaPath, mediaType });
|
|
774
|
+
// Update to latest msg for reply target (use latest sessionWebhook)
|
|
775
|
+
existing.msg = msg;
|
|
776
|
+
existing.replyTarget = replyTarget;
|
|
777
|
+
|
|
778
|
+
// Reset timer
|
|
779
|
+
clearTimeout(existing.timer);
|
|
780
|
+
existing.timer = setTimeout(() => {
|
|
781
|
+
flushMessageBuffer(bufferKey);
|
|
782
|
+
}, aggregationDelayMs);
|
|
783
|
+
|
|
784
|
+
log?.info?.(`[dingtalk] Message buffered, total: ${existing.messages.length} messages`);
|
|
785
|
+
} else {
|
|
786
|
+
// Create new buffer entry
|
|
787
|
+
const newEntry: BufferedMessage = {
|
|
788
|
+
messages: [{ text: rawBody, timestamp: Date.now(), mediaPath, mediaType }],
|
|
789
|
+
timer: setTimeout(() => {
|
|
790
|
+
flushMessageBuffer(bufferKey);
|
|
791
|
+
}, aggregationDelayMs),
|
|
792
|
+
ctx,
|
|
793
|
+
msg,
|
|
794
|
+
replyTarget,
|
|
795
|
+
sessionKey,
|
|
796
|
+
isDm,
|
|
797
|
+
senderId,
|
|
798
|
+
senderName,
|
|
799
|
+
conversationId,
|
|
800
|
+
};
|
|
801
|
+
messageBuffer.set(bufferKey, newEntry);
|
|
802
|
+
|
|
803
|
+
log?.info?.(`[dingtalk] Message buffered (new), waiting ${aggregationDelayMs}ms for more...`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Flush the message buffer and dispatch the combined message.
|
|
809
|
+
*/
|
|
810
|
+
async function flushMessageBuffer(bufferKey: string): Promise<void> {
|
|
811
|
+
const entry = messageBuffer.get(bufferKey);
|
|
812
|
+
if (!entry) return;
|
|
813
|
+
|
|
814
|
+
messageBuffer.delete(bufferKey);
|
|
815
|
+
|
|
816
|
+
const { messages, ctx, msg, replyTarget, sessionKey, isDm, senderId, senderName, conversationId } = entry;
|
|
817
|
+
const { log } = ctx;
|
|
818
|
+
|
|
819
|
+
// Combine all messages
|
|
820
|
+
const combinedText = messages.map(m => m.text).join('\n');
|
|
821
|
+
// Use the last media if any
|
|
822
|
+
const lastWithMedia = [...messages].reverse().find(m => m.mediaPath);
|
|
823
|
+
const mediaPath = lastWithMedia?.mediaPath;
|
|
824
|
+
const mediaType = lastWithMedia?.mediaType;
|
|
825
|
+
|
|
826
|
+
log?.info?.(`[dingtalk] Flushing buffer: ${messages.length} message(s) combined into ${combinedText.length} chars`);
|
|
827
|
+
|
|
828
|
+
// Dispatch the combined message
|
|
829
|
+
await dispatchMessage({
|
|
830
|
+
ctx, msg, rawBody: combinedText, replyTarget, sessionKey, isDm, senderId, senderName, conversationId,
|
|
831
|
+
mediaPath, mediaType,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Dispatch a message to the agent (after aggregation or immediately).
|
|
837
|
+
*/
|
|
838
|
+
async function dispatchMessage(params: {
|
|
839
|
+
ctx: DingTalkMonitorContext;
|
|
840
|
+
msg: DingTalkRobotMessage;
|
|
841
|
+
rawBody: string;
|
|
842
|
+
replyTarget: any;
|
|
843
|
+
sessionKey: string;
|
|
844
|
+
isDm: boolean;
|
|
845
|
+
senderId: string;
|
|
846
|
+
senderName: string;
|
|
847
|
+
conversationId: string;
|
|
848
|
+
mediaPath?: string;
|
|
849
|
+
mediaType?: string;
|
|
850
|
+
}): Promise<void> {
|
|
851
|
+
const { ctx, msg, rawBody, replyTarget, sessionKey, isDm, senderId, senderName, conversationId, mediaPath, mediaType } = params;
|
|
852
|
+
const { account, cfg, log, setStatus } = ctx;
|
|
853
|
+
const runtime = getDingTalkRuntime();
|
|
854
|
+
const isGroup = !isDm;
|
|
855
|
+
|
|
699
856
|
// Send thinking feedback (opt-in)
|
|
700
|
-
if (account.config.showThinking &&
|
|
857
|
+
if (account.config.showThinking && replyTarget.sessionWebhook) {
|
|
701
858
|
try {
|
|
702
|
-
await sendViaSessionWebhook(
|
|
859
|
+
await sendViaSessionWebhook(replyTarget.sessionWebhook, '正在思考...');
|
|
703
860
|
log?.info?.('[dingtalk] Sent thinking indicator');
|
|
704
861
|
} catch (_) {
|
|
705
862
|
// fire-and-forget, don't block processing
|