@yaoyuanchao/dingtalk 1.7.3 → 1.7.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/monitor.ts +18 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoyuanchao/dingtalk",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "type": "module",
5
5
  "description": "DingTalk channel plugin for ClawdBot/OpenClaw with Stream Mode support",
6
6
  "license": "MIT",
package/src/monitor.ts CHANGED
@@ -308,8 +308,11 @@ export async function startDingTalkMonitor(ctx: DingTalkMonitorContext): Promise
308
308
  touchActivity(); // Track message activity for heartbeat
309
309
 
310
310
  // Deduplication: skip messages already processed (e.g. re-delivered after reconnect)
311
+ // Check both protocol-level messageId AND business-level msgId, because
312
+ // DingTalk re-delivers with a NEW protocol messageId after reconnect but
313
+ // the same business msgId.
311
314
  if (isDuplicateMessage(protocolMsgId)) {
312
- log?.info?.("[dingtalk] Duplicate message skipped: " + protocolMsgId);
315
+ log?.info?.("[dingtalk] Duplicate message skipped (protocol): " + protocolMsgId);
313
316
  return { status: "SUCCESS", message: "OK" };
314
317
  }
315
318
  markMessageProcessed(protocolMsgId);
@@ -317,6 +320,15 @@ export async function startDingTalkMonitor(ctx: DingTalkMonitorContext): Promise
317
320
  try {
318
321
  const data: DingTalkRobotMessage = typeof downstream.data === "string"
319
322
  ? JSON.parse(downstream.data) : downstream.data;
323
+
324
+ // Business-level dedup: same msgId re-delivered with different protocol ID
325
+ const bizMsgId = data.msgId;
326
+ if (bizMsgId && isDuplicateMessage('biz:' + bizMsgId)) {
327
+ log?.info?.("[dingtalk] Duplicate message skipped (bizMsgId): " + bizMsgId);
328
+ return { status: "SUCCESS", message: "OK" };
329
+ }
330
+ if (bizMsgId) markMessageProcessed('biz:' + bizMsgId);
331
+
320
332
  setStatus?.({ lastInboundAt: Date.now() });
321
333
  await processInboundMessage(data, ctx);
322
334
  } catch (err) {
@@ -1656,7 +1668,6 @@ async function dispatchWithFullPipeline(params: {
1656
1668
  log, setStatus, onFirstReply } = params;
1657
1669
 
1658
1670
  let firstReplyFired = false;
1659
- let typingSafetyTimeout: ReturnType<typeof setTimeout> | null = null;
1660
1671
 
1661
1672
  // 1. Resolve agent route via own bindings matching (like official plugin).
1662
1673
  // OpenClaw's resolveAgentRoute doesn't handle accountId correctly for multi-account.
@@ -1752,7 +1763,6 @@ async function dispatchWithFullPipeline(params: {
1752
1763
  // Recall typing indicator on first delivery
1753
1764
  if (!firstReplyFired && onFirstReply) {
1754
1765
  firstReplyFired = true;
1755
- if (typingSafetyTimeout) { clearTimeout(typingSafetyTimeout); typingSafetyTimeout = null; }
1756
1766
  await onFirstReply().catch((err) => {
1757
1767
  log?.info?.("[dingtalk] onFirstReply error: " + err);
1758
1768
  });
@@ -1780,21 +1790,11 @@ async function dispatchWithFullPipeline(params: {
1780
1790
  await rt.channel.reply.dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyOptions });
1781
1791
  } finally {
1782
1792
  markDispatchIdle();
1783
- // Don't recall typing immediately — dispatchReplyFromConfig resolves when
1784
- // dispatch is *initiated*, not when the agent finishes. The agent may still
1785
- // be doing tool calls for minutes before producing the first text reply.
1786
- // The deliver callback above handles the normal recall on first delivery.
1787
- // Set a safety timeout to recall if no delivery ever arrives (edge case).
1788
- if (!firstReplyFired && onFirstReply) {
1789
- const TYPING_SAFETY_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
1790
- typingSafetyTimeout = setTimeout(async () => {
1791
- if (!firstReplyFired && onFirstReply) {
1792
- firstReplyFired = true;
1793
- log?.info?.('[dingtalk] Typing safety timeout — recalling after no delivery');
1794
- await onFirstReply().catch(() => {});
1795
- }
1796
- }, TYPING_SAFETY_TIMEOUT_MS);
1797
- }
1793
+ // Don't recall typing here — dispatchReplyFromConfig resolves when dispatch
1794
+ // is *initiated*, not when the agent finishes. The agent may still be doing
1795
+ // tool calls for many minutes before producing text. The deliver callback
1796
+ // above handles recall on first delivery. If the agent crashes without
1797
+ // replying, the emoji reaction simply stays acceptable tradeoff.
1798
1798
  }
1799
1799
 
1800
1800
  // 10. Record activity