@yaoyuanchao/dingtalk 1.7.4 → 1.7.6

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 +52 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoyuanchao/dingtalk",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
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
@@ -198,6 +198,30 @@ const AGGREGATION_DELAY_MS = 2000; // 2 seconds - balance between UX and catchin
198
198
 
199
199
  const sessionQueues = new Map<string, Promise<void>>();
200
200
  const sessionQueueLastActivity = new Map<string, number>();
201
+
202
+ // Track delivery activity per queue key to detect when the SDK's dispatch resolves
203
+ // before the agent's full turn completes (e.g. followup turns running in background).
204
+ // This supplements sessionQueues for the "busy" check.
205
+ const DELIVER_ACTIVITY_GRACE_MS = 8000; // 8s after last delivery, consider idle
206
+ const deliverActivityTimestamps = new Map<string, number>();
207
+
208
+ function markDeliverActivity(queueKey: string): void {
209
+ deliverActivityTimestamps.set(queueKey, Date.now());
210
+ }
211
+
212
+ function hasActiveDelivery(queueKey: string): boolean {
213
+ const ts = deliverActivityTimestamps.get(queueKey);
214
+ if (!ts) return false;
215
+ if (Date.now() - ts > DELIVER_ACTIVITY_GRACE_MS) {
216
+ deliverActivityTimestamps.delete(queueKey);
217
+ return false;
218
+ }
219
+ return true;
220
+ }
221
+
222
+ function clearDeliverActivity(queueKey: string): void {
223
+ deliverActivityTimestamps.delete(queueKey);
224
+ }
201
225
  const SESSION_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 min
202
226
 
203
227
  const QUEUE_BUSY_PHRASES = [
@@ -1395,7 +1419,11 @@ async function dispatchMessage(params: {
1395
1419
  const { account, log } = ctx;
1396
1420
 
1397
1421
  const queueKey = `${account.accountId}:${conversationId}`;
1398
- const isQueueBusy = sessionQueues.has(queueKey);
1422
+ // Check both the explicit queue AND recent delivery activity.
1423
+ // The SDK's dispatchReplyFromConfig may resolve before the agent's full turn
1424
+ // completes (followup turns run in background), clearing the queue entry
1425
+ // while deliveries are still happening.
1426
+ const isQueueBusy = sessionQueues.has(queueKey) || hasActiveDelivery(queueKey);
1399
1427
 
1400
1428
  // If queue is busy, add emotion reaction on user's message to indicate queued
1401
1429
  let queueAckCleanup: (() => Promise<void>) | null = null;
@@ -1437,11 +1465,13 @@ async function dispatchMessage(params: {
1437
1465
  // Clean up only if this is still the latest task
1438
1466
  if (sessionQueues.get(queueKey) === currentTask) {
1439
1467
  sessionQueues.delete(queueKey);
1468
+ log?.info?.("[dingtalk] Queue entry removed for " + queueKey + " (deliverActive=" + hasActiveDelivery(queueKey) + ")");
1440
1469
  }
1441
1470
  });
1442
1471
 
1443
1472
  sessionQueues.set(queueKey, currentTask);
1444
1473
  sessionQueueLastActivity.set(queueKey, Date.now());
1474
+ log?.info?.("[dingtalk] Queue entry set for " + queueKey + " (wasQueueBusy=" + isQueueBusy + ")");
1445
1475
 
1446
1476
  // Don't await — fire-and-forget so message buffering and SDK callback stay responsive
1447
1477
  }
@@ -1604,11 +1634,14 @@ async function dispatchMessageInternal(params: {
1604
1634
 
1605
1635
  // Await dispatch so per-session queue waits for reply delivery to complete
1606
1636
  // before starting the next queued message.
1637
+ const fallbackQueueKey = `${account.accountId}:${conversationId}`;
1607
1638
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1608
1639
  ctx: ctxPayload,
1609
1640
  cfg: actualCfg,
1610
1641
  dispatcherOptions: {
1611
1642
  deliver: async (payload: any) => {
1643
+ // Track delivery activity for queue busy detection
1644
+ markDeliverActivity(fallbackQueueKey);
1612
1645
  // Recall typing indicator on first delivery
1613
1646
  await cleanupTyping();
1614
1647
 
@@ -1668,7 +1701,6 @@ async function dispatchWithFullPipeline(params: {
1668
1701
  log, setStatus, onFirstReply } = params;
1669
1702
 
1670
1703
  let firstReplyFired = false;
1671
- let typingSafetyTimeout: ReturnType<typeof setTimeout> | null = null;
1672
1704
 
1673
1705
  // 1. Resolve agent route via own bindings matching (like official plugin).
1674
1706
  // OpenClaw's resolveAgentRoute doesn't handle accountId correctly for multi-account.
@@ -1758,13 +1790,15 @@ async function dispatchWithFullPipeline(params: {
1758
1790
  }
1759
1791
 
1760
1792
  // 8. Create typing-aware dispatcher
1793
+ const deliverQueueKey = `${account.accountId}:${conversationId}`;
1761
1794
  const { dispatcher, replyOptions, markDispatchIdle } = rt.channel.reply.createReplyDispatcherWithTyping({
1762
1795
  responsePrefix: '',
1763
1796
  deliver: async (payload: any) => {
1797
+ // Track delivery activity for queue busy detection
1798
+ markDeliverActivity(deliverQueueKey);
1764
1799
  // Recall typing indicator on first delivery
1765
1800
  if (!firstReplyFired && onFirstReply) {
1766
1801
  firstReplyFired = true;
1767
- if (typingSafetyTimeout) { clearTimeout(typingSafetyTimeout); typingSafetyTimeout = null; }
1768
1802
  await onFirstReply().catch((err) => {
1769
1803
  log?.info?.("[dingtalk] onFirstReply error: " + err);
1770
1804
  });
@@ -1789,24 +1823,24 @@ async function dispatchWithFullPipeline(params: {
1789
1823
 
1790
1824
  // 9. Dispatch reply from config
1791
1825
  try {
1826
+ log?.info?.("[dingtalk] dispatchReplyFromConfig started for " + deliverQueueKey);
1792
1827
  await rt.channel.reply.dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyOptions });
1828
+ log?.info?.("[dingtalk] dispatchReplyFromConfig completed for " + deliverQueueKey);
1793
1829
  } finally {
1794
- markDispatchIdle();
1795
- // Don't recall typing immediately dispatchReplyFromConfig resolves when
1796
- // dispatch is *initiated*, not when the agent finishes. The agent may still
1797
- // be doing tool calls for minutes before producing the first text reply.
1798
- // The deliver callback above handles the normal recall on first delivery.
1799
- // Set a safety timeout to recall if no delivery ever arrives (edge case).
1800
- if (!firstReplyFired && onFirstReply) {
1801
- const TYPING_SAFETY_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
1802
- typingSafetyTimeout = setTimeout(async () => {
1803
- if (!firstReplyFired && onFirstReply) {
1804
- firstReplyFired = true;
1805
- log?.info?.('[dingtalk] Typing safety timeout recalling after no delivery');
1806
- await onFirstReply().catch(() => {});
1807
- }
1808
- }, TYPING_SAFETY_TIMEOUT_MS);
1830
+ // OpenClaw 2026.4+ moved dispatcher.markComplete() + waitForIdle() out of
1831
+ // dispatchReplyFromConfig into the withReplyDispatcher wrapper. Since we call
1832
+ // dispatchReplyFromConfig directly (not through withReplyDispatcher), we must
1833
+ // do this ourselves to ensure all pending deliveries drain before returning.
1834
+ try {
1835
+ if (typeof dispatcher.markComplete === 'function') {
1836
+ dispatcher.markComplete();
1837
+ }
1838
+ await dispatcher.waitForIdle();
1839
+ log?.info?.("[dingtalk] dispatcher.waitForIdle completed for " + deliverQueueKey);
1840
+ } catch (err) {
1841
+ log?.info?.("[dingtalk] dispatcher settle error: " + err);
1809
1842
  }
1843
+ markDispatchIdle();
1810
1844
  }
1811
1845
 
1812
1846
  // 10. Record activity