@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoyuanchao/dingtalk",
3
- "version": "1.4.19",
3
+ "version": "1.4.20",
4
4
  "type": "module",
5
5
  "description": "DingTalk channel plugin for Clawdbot with Stream Mode support",
6
6
  "license": "MIT",
@@ -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 && msg.sessionWebhook) {
857
+ if (account.config.showThinking && replyTarget.sessionWebhook) {
701
858
  try {
702
- await sendViaSessionWebhook(msg.sessionWebhook, '正在思考...');
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