@jeik/dingtalk-connector 0.8.27 → 0.8.29

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 (43) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.en.md +40 -82
  3. package/README.md +50 -93
  4. package/dist/background-work-DkaSDqWk.mjs +53 -0
  5. package/dist/card-content-cache-C7-XHhHw.mjs +2 -0
  6. package/dist/{card-content-cache-CYUzCaQJ.mjs → card-content-cache-DT_OYThh.mjs} +15 -1
  7. package/dist/chunk-upload-BKM1y4Ba.mjs +2 -0
  8. package/dist/{chunk-upload-BBQgGtcZ.mjs → chunk-upload-CIU5G_ay.mjs} +1 -1
  9. package/dist/{common-DE0gd0Jg.mjs → common-BfIYs1yC.mjs} +3 -3
  10. package/dist/{common-B_A78n8u.mjs → common-C0Oaf0z-.mjs} +1 -1
  11. package/dist/connection-obtrPkj4.mjs +641 -0
  12. package/dist/entry-bundled.mjs +1 -1
  13. package/dist/{gateway-methods--aKsyMs-.mjs → gateway-methods-BdcpfxEZ.mjs} +1 -1
  14. package/dist/gateway-methods-DQ6TwOAr.mjs +2 -0
  15. package/dist/index.mjs +2 -2
  16. package/dist/{logger-mZ9OSbmD.mjs → logger-CnBTcwyq.mjs} +16 -2
  17. package/dist/logger-D8H2IcYz.mjs +2 -0
  18. package/dist/{media-BqpUGCni.mjs → media-DDtX37T2.mjs} +1 -1
  19. package/dist/{media-DnQB2BrL.mjs → media-g8tJtpfU.mjs} +29 -20
  20. package/dist/{message-handler-DQZiKx3i.mjs → message-handler-BrO2i7tg.mjs} +251 -139
  21. package/dist/{messaging-WleKf54h.mjs → messaging-CK0Wz3Ze.mjs} +23 -21
  22. package/dist/{runtime-DzZo6Gvk.mjs → runtime-DbqGRyhI.mjs} +6 -5
  23. package/dist/{utils-Bjh4r_qS.mjs → utils-Bwz2Fls_.mjs} +1 -1
  24. package/docs/RELEASE_NOTES_V0.8.28.md +56 -0
  25. package/docs/RELEASE_NOTES_V0.8.29.md +57 -0
  26. package/openclaw.plugin.json +9 -5
  27. package/package.json +2 -2
  28. package/src/config/schema.ts +2 -1
  29. package/src/core/connection.ts +504 -220
  30. package/src/core/message-handler.ts +197 -102
  31. package/src/reply-dispatcher.ts +244 -93
  32. package/src/services/media.ts +60 -27
  33. package/src/services/messaging/card-content-cache.ts +11 -6
  34. package/src/services/messaging/card.ts +2 -0
  35. package/src/services/messaging.ts +52 -38
  36. package/src/utils/background-work.ts +65 -0
  37. package/src/utils/logger.ts +23 -2
  38. package/dist/card-content-cache-Dwd8HICP.mjs +0 -2
  39. package/dist/chunk-upload-DaLXXZH3.mjs +0 -2
  40. package/dist/connection-fc6B4z4G.mjs +0 -423
  41. package/dist/gateway-methods-BQd4E0Sk.mjs +0 -2
  42. package/dist/logger-BmJkQkm1.mjs +0 -2
  43. /package/dist/{game-xiyou-CAT0ilSj.mjs → game-xiyou-uwsv23AC.mjs} +0 -0
@@ -56,7 +56,7 @@ import {
56
56
  uploadAndReplaceFileMarkers
57
57
  } from "../services/media/index.ts";
58
58
  import { sendProactive, type AICardTarget } from "../services/messaging/index.ts";
59
- import { createAICardForTarget, streamAICard, registerActiveCard, type AICardInstance } from "../services/messaging/card.ts";
59
+ import { createAICardForTarget, streamAICard, finishAICard, registerActiveCard, type AICardInstance } from "../services/messaging/card.ts";
60
60
  import {
61
61
  collectCardLookupIds,
62
62
  lookupCardContent,
@@ -66,11 +66,19 @@ import { createDingtalkReplyDispatcher, matchModelErrorText } from "../reply-dis
66
66
  import { normalizeSlashCommand } from "../utils/session.ts";
67
67
  import { getDingtalkRuntime } from "../runtime.ts";
68
68
  import { dingtalkHttp } from '../utils/http-client.ts';
69
- import { createLoggerFromConfig } from '../utils/index.ts';
69
+ import { createLoggerFromConfig, isDingtalkDebug } from '../utils/index.ts';
70
+ import { trackBackgroundWork } from "../utils/background-work.ts";
70
71
  import * as fs from 'fs';
71
72
  import * as path from 'path';
72
73
  import * as os from 'os';
73
74
 
75
+ /**
76
+ * 单条会话队列任务最长执行时间。
77
+ * 503 重试 + 工具循环偶发挂死时,若不超时,后续同会话消息会永远堵在 previousTask 上,
78
+ * 表现:连发多条「网关没反应 / 不贴表情」(表情在排队 ACK 或 dequeued 后才贴)。
79
+ */
80
+ const SESSION_QUEUE_TASK_TIMEOUT_MS = 3 * 60 * 1000; // 3 分钟
81
+
74
82
  // ============ 常量 ============
75
83
 
76
84
  const AI_CARD_TEMPLATE_ID = '02fcf2f4-5e02-4a85-b672-46d1f715543e.schema';
@@ -350,11 +358,13 @@ function extractQuotedMsgText(
350
358
  allowConversationRecent: false,
351
359
  }) || '';
352
360
  if (cardText) {
353
- console.log(
354
- `[DingTalk][Quote] interactiveCard 已从 CardCache 精确回填 | ids=${ids.join(',') || '-'} len=${cardText.length}`,
355
- );
356
- } else {
357
- // 强制 dump 载荷,便于确认钉钉到底给了哪些字段
361
+ if (isDingtalkDebug()) {
362
+ console.log(
363
+ `[DingTalk][Quote] interactiveCard 已从 CardCache 精确回填 | ids=${ids.join(',') || '-'} len=${cardText.length}`,
364
+ );
365
+ }
366
+ } else if (isDingtalkDebug()) {
367
+ // 调试:dump 载荷确认钉钉给了哪些字段
358
368
  const dump = JSON.stringify(repliedMsg ?? {}).slice(0, 800);
359
369
  console.log(
360
370
  `[DingTalk][Quote] interactiveCard 无正文且缓存未命中(未使用会话最近兜底) | conv=${conversationId || '-'} keys=${Object.keys(contentObj || {}).join(',')} repliedMsg=${dump}`,
@@ -390,26 +400,32 @@ function extractQuotedMsgText(
390
400
  });
391
401
  if (cached) {
392
402
  bodyText = cached;
393
- console.log(
394
- `[DingTalk][Quote] msgType=${msgType} 已从 CardCache 精确回填 | ids=${ids.join(',') || '-'} len=${cached.length}`,
395
- );
403
+ if (isDingtalkDebug()) {
404
+ console.log(
405
+ `[DingTalk][Quote] msgType=${msgType} 已从 CardCache 精确回填 | ids=${ids.join(',') || '-'} len=${cached.length}`,
406
+ );
407
+ }
396
408
  } else if (
397
409
  /card|interactive/i.test(String(msgType || "")) ||
398
410
  ids.length > 0
399
411
  ) {
400
412
  bodyText = "钉钉卡片消息。";
401
- console.log(
402
- `[DingTalk][Quote] 未知卡片类引用 msgType=${msgType} 已简化占位 | keys=${Object.keys(contentObj || {}).join(',')}`,
403
- );
404
- } else {
413
+ if (isDingtalkDebug()) {
414
+ console.log(
415
+ `[DingTalk][Quote] 未知卡片类引用 msgType=${msgType} 已简化占位 | keys=${Object.keys(contentObj || {}).join(',')}`,
416
+ );
417
+ }
418
+ } else if (isDingtalkDebug()) {
405
419
  console.log(
406
420
  `[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(',')} dump=${JSON.stringify(repliedMsg ?? {}).slice(0, 600)}`,
407
421
  );
408
422
  }
409
423
  } catch {
410
- console.log(
411
- `[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(',')}`,
412
- );
424
+ if (isDingtalkDebug()) {
425
+ console.log(
426
+ `[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(',')}`,
427
+ );
428
+ }
413
429
  }
414
430
  }
415
431
  break;
@@ -1645,6 +1661,10 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
1645
1661
  }
1646
1662
 
1647
1663
  // ===== 使用 SDK 的 dispatchReplyFromConfig =====
1664
+ // 提升到 try 外:catch 需要 finish 即时建卡;replyOptions.onError 不存在(OpenClaw 不导出)
1665
+ let earlyCard: AICardInstance | undefined = params.preCreatedCard;
1666
+ let finalizeDispatchError: ((error: unknown, kind?: string) => Promise<void>) | undefined;
1667
+
1648
1668
  try {
1649
1669
  const core = getDingtalkRuntime();
1650
1670
 
@@ -1736,7 +1756,6 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
1736
1756
  const skipAICardForCard = !isDirect && (groupReplyModeForCard === 'text' || groupReplyModeForCard === 'markdown');
1737
1757
  const streamingEnabledForCard = !skipAICardForCard && (config as any).streaming !== false;
1738
1758
 
1739
- let earlyCard: AICardInstance | undefined = params.preCreatedCard;
1740
1759
  if (!earlyCard && streamingEnabledForCard && !asyncMode) {
1741
1760
  try {
1742
1761
  const earlyTarget: AICardTarget = isDirect
@@ -1757,7 +1776,13 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
1757
1776
  }
1758
1777
 
1759
1778
  // 创建 reply dispatcher,使用解析后的 agentId
1760
- const { dispatcher, replyOptions, markDispatchIdle, getAsyncModeResponse } = createDingtalkReplyDispatcher({
1779
+ const {
1780
+ dispatcher,
1781
+ replyOptions,
1782
+ markDispatchIdle,
1783
+ getAsyncModeResponse,
1784
+ finalizeDispatchError: finalizeErr,
1785
+ } = createDingtalkReplyDispatcher({
1761
1786
  cfg,
1762
1787
  agentId: matchedAgentId, // ✅ 使用手动匹配的 agentId
1763
1788
  runtime: runtime as RuntimeEnv,
@@ -1770,6 +1795,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
1770
1795
  asyncMode,
1771
1796
  preCreatedCard: earlyCard,
1772
1797
  });
1798
+ finalizeDispatchError = finalizeErr;
1773
1799
 
1774
1800
  // ===== 注入当前 bot 的 clientId(用于 dws CLI --client-id 参数) =====
1775
1801
  // 多 bot 场景下,AI 需要知道当前对话属于哪个 bot,以便在调用
@@ -1905,32 +1931,59 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
1905
1931
 
1906
1932
  } catch (err: any) {
1907
1933
  log?.error?.(`SDK dispatch 失败: ${err.message}`);
1908
-
1909
- // 解析上游错误文本(例如 503 No available channel),匹配中文提示文案
1910
- const errorText = matchModelErrorText(err.message, { includeCatchAll: true }) ?? `⚠️ 抱歉,处理请求时出错: ${err.message}`;
1911
1934
 
1912
- // 如果已经建了 AI Card,则更新报错状态到 Card,避免停留在"正在召唤大模型"
1913
- if (earlyCard) {
1935
+ // 统一错误定稿:优先走 dispatcher 的 finalizeDispatchError(finish AI Card + 中文提示)。
1936
+ // 注意:OpenClaw createReplyDispatcherWithTyping 返回的 replyOptions 不含 onError,
1937
+ // 绝不能依赖 replyOptions.onError(会永远 undefined / 甚至 catch 作用域 ReferenceError)。
1938
+ if (finalizeDispatchError) {
1914
1939
  try {
1915
- await streamAICard(earlyCard, errorText, true, config, log);
1916
- } catch (cardErr: any) {
1917
- log?.error?.(`更新错误卡片失败: ${cardErr.message}`);
1940
+ log?.info?.(`[DingTalk] 统一错误定稿:调用 finalizeDispatchError 处理同步异常`);
1941
+ await finalizeDispatchError(err, "invoke");
1942
+ } catch (finalizeErr: any) {
1943
+ log?.error?.(`[DingTalk] finalizeDispatchError 失败: ${finalizeErr?.message || finalizeErr}`);
1918
1944
  }
1919
1945
  } else {
1920
- // 降级:发送错误文本消息
1921
- try {
1922
- const token = await getAccessToken(config);
1923
- const body: any = {
1924
- msgtype: 'text',
1925
- text: { content: errorText }
1926
- };
1927
- if (!isDirect) body.at = { atUserIds: [senderId], isAtAll: false };
1928
-
1929
- await dingtalkHttp.post(sessionWebhook, body, {
1930
- headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
1931
- });
1932
- } catch (fallbackErr: any) {
1933
- log?.error?.(`错误消息发送也失败: ${fallbackErr.message}`);
1946
+ // dispatcher 尚未创建就失败:直接 finish 即时建卡,再不行发纯文本
1947
+ const errorText =
1948
+ matchModelErrorText(err.message, { includeCatchAll: true }) ??
1949
+ `⚠️ 抱歉,处理请求时出错: ${err.message}`;
1950
+ let cardSuccess = false;
1951
+ if (earlyCard) {
1952
+ try {
1953
+ await finishAICard(
1954
+ earlyCard,
1955
+ errorText,
1956
+ config,
1957
+ log,
1958
+ undefined,
1959
+ undefined,
1960
+ data.conversationId,
1961
+ );
1962
+ cardSuccess = true;
1963
+ log?.info?.(`[DingTalk] 极端兜底:finishAICard 错误卡成功`);
1964
+ } catch (cardErr: any) {
1965
+ log?.error?.(`更新错误卡片失败: ${cardErr.message}`);
1966
+ }
1967
+ }
1968
+ if (!cardSuccess) {
1969
+ log?.warn?.(`[DingTalk] 无可用错误定稿路径,走纯文本发送`);
1970
+ try {
1971
+ const token = await getAccessToken(config);
1972
+ const body: any = {
1973
+ msgtype: "text",
1974
+ text: { content: errorText },
1975
+ };
1976
+ if (!isDirect) body.at = { atUserIds: [senderId], isAtAll: false };
1977
+
1978
+ await dingtalkHttp.post(sessionWebhook, body, {
1979
+ headers: {
1980
+ "x-acs-dingtalk-access-token": token,
1981
+ "Content-Type": "application/json",
1982
+ },
1983
+ });
1984
+ } catch (fallbackErr: any) {
1985
+ log?.error?.(`错误消息发送也失败: ${fallbackErr.message}`);
1986
+ }
1934
1987
  }
1935
1988
  }
1936
1989
  }
@@ -2007,89 +2060,131 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
2007
2060
  // 更新会话活跃时间
2008
2061
  sessionLastActivity.set(queueKey, Date.now());
2009
2062
 
2010
- // 检测队列是否繁忙(入队前检查,此时 previousTask 尚未被当前消息覆盖)
2011
- const isQueueBusy = sessionQueues.has(queueKey);
2012
-
2013
- // 获取该会话+agent的上一个处理任务
2063
+ // ═══════════════════════════════════════════════════════════
2064
+ // ⚠️ 关键:下面「读 previous → 建 currentTask → set」之间禁止任何 await。
2065
+ // JS 虽是单线程,但 await 会让出执行权;若在 set 之前 await 繁忙 ACK(最多 3s),
2066
+ // 并发第 2/3 条会看到「队列仍只有第 1 条」甚至「空闲」,表现为:
2067
+ // - 任务进行中连发却不弹排队卡(要发两三次才命中 isQueueBusy)
2068
+ // - 多条消息错误地 chain 到同一 previous,乱序/并行
2069
+ // ═══════════════════════════════════════════════════════════
2014
2070
  const previousTask = sessionQueues.get(queueKey) || Promise.resolve();
2071
+ const isQueueBusy = sessionQueues.has(queueKey);
2015
2072
 
2016
- // 队列繁忙时:根据 groupReplyMode 决定是创建 AI Card 还是发送普通文本 ACK
2017
- let preCreatedCard: AICardInstance | undefined;
2018
- if (isQueueBusy) {
2019
- const ackPhrases = QUEUE_BUSY_ACK_PHRASES;
2020
- const ackText = ackPhrases[Math.floor(Math.random() * ackPhrases.length)];
2073
+ // 异步 ACK 写回的卡片:主任务 dequeue 时再读,避免 await ACK 再入队
2074
+ const cardHolder: { card?: AICardInstance; ackDone: boolean } = {
2075
+ card: undefined,
2076
+ ackDone: !isQueueBusy,
2077
+ };
2021
2078
 
2022
- // ✅ 检查 groupReplyMode:text/markdown 模式下不创建 AI Card,改用普通消息发送 ACK
2023
- const groupReplyMode = config.groupReplyMode || 'aicard';
2024
- const skipAICard = !isDirect && (groupReplyMode === 'text' || groupReplyMode === 'markdown');
2079
+ const currentTask = previousTask
2080
+ .then(async () => {
2081
+ log?.info?.(
2082
+ `[队列] 开始处理消息,queueKey=${queueKey} wasBusy=${isQueueBusy} timeoutMs=${SESSION_QUEUE_TASK_TIMEOUT_MS}`,
2083
+ );
2025
2084
 
2026
- if (skipAICard) {
2027
- // text/markdown 模式:使用普通消息发送 ACK,不创建 AI Card
2028
- try {
2029
- await sendProactive(config, { openConversationId: data.conversationId }, ackText, {
2030
- msgType: 'text',
2031
- useAICard: false,
2032
- fallbackToNormal: true,
2033
- });
2034
- log?.info?.(`[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`);
2035
- } catch (ackErr: any) {
2036
- log?.warn?.(`[队列] 发送普通 ACK 失败: ${ackErr?.message || ackErr}`);
2085
+ // 若排队 ACK 仍在建卡,最多再等 2s 拿 preCreatedCard,避免丢复用
2086
+ if (isQueueBusy && !cardHolder.ackDone) {
2087
+ const waitStart = Date.now();
2088
+ while (!cardHolder.ackDone && Date.now() - waitStart < 2_000) {
2089
+ await new Promise((r) => setTimeout(r, 50));
2090
+ }
2037
2091
  }
2038
- // text/markdown 模式不贴🤔表情(表情是 AI Card 场景的配套功能)
2039
- } else {
2040
- // aicard 模式:创建 AI Card 显示排队 ACK
2041
- const cardTarget: AICardTarget = isDirect
2042
- ? { type: 'user', userId: senderId }
2043
- : { type: 'group', openConversationId: data.conversationId };
2044
2092
 
2093
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
2045
2094
  try {
2046
- const card = await createAICardForTarget(config, cardTarget, log);
2047
- if (card) {
2048
- // 用 streamAICard 把 ACK 文案写入 Card(INPUTING 状态,表示正在处理中)
2049
- await streamAICard(card, ackText, false, config, log);
2050
- preCreatedCard = card;
2051
- log?.info?.(`[队列] 队列繁忙,已创建排队 ACK Card,cardInstanceId=${card.cardInstanceId}`);
2052
- } else {
2053
- log?.warn?.(`[队列] 创建排队 ACK Card 失败(返回 null),跳过 ACK`);
2054
- }
2055
- // 在发送 ACK 的同时立即贴上思考中表情,让用户知道消息已被接收
2056
- addEmotionReply(config, data, log).catch(err => {
2057
- log?.warn?.(`[队列] 贴排队表情失败: ${err.message}`);
2058
- });
2059
- } catch (ackErr: any) {
2060
- log?.warn?.(`[队列] 创建排队 ACK Card 异常: ${ackErr?.message || ackErr}`);
2095
+ await Promise.race([
2096
+ handleDingTalkMessageInternal({
2097
+ ...params,
2098
+ preCreatedCard: cardHolder.card,
2099
+ emotionAlreadyAdded: true,
2100
+ }),
2101
+ new Promise<never>((_, reject) => {
2102
+ timeoutId = setTimeout(() => {
2103
+ reject(
2104
+ new Error(
2105
+ `会话队列任务超时(${SESSION_QUEUE_TASK_TIMEOUT_MS}ms),释放队列以免堵死后续消息`,
2106
+ ),
2107
+ );
2108
+ }, SESSION_QUEUE_TASK_TIMEOUT_MS);
2109
+ }),
2110
+ ]);
2111
+ log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
2112
+ } finally {
2113
+ if (timeoutId !== undefined) clearTimeout(timeoutId);
2061
2114
  }
2062
- }
2063
- }
2064
-
2065
- // 创建当前消息的处理任务
2066
- const currentTask = previousTask
2067
- .then(async () => {
2068
- log?.info?.(`[队列] 开始处理消息,queueKey=${queueKey}`);
2069
- await handleDingTalkMessageInternal({ ...params, preCreatedCard, emotionAlreadyAdded: isQueueBusy });
2070
- log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
2071
2115
  })
2072
2116
  .catch((err: any) => {
2073
2117
  log?.error?.(`[队列] 消息处理异常,queueKey=${queueKey}, error=${err.message}`);
2074
- // 不抛出错误,避免阻塞后续消息
2075
2118
  })
2076
2119
  .finally(() => {
2077
- // 如果当前任务是队列中的最后一个任务,清理队列
2078
2120
  if (sessionQueues.get(queueKey) === currentTask) {
2079
2121
  sessionQueues.delete(queueKey);
2080
2122
  log?.info?.(`[队列] 队列已清空,queueKey=${queueKey}`);
2081
2123
  }
2082
2124
  });
2083
-
2084
- // 更新队列
2125
+
2126
+ // ★ 立刻占槽:必须在任何 await 之前,保证并发入站能看到 isQueueBusy
2085
2127
  sessionQueues.set(queueKey, currentTask);
2128
+ trackBackgroundWork(currentTask, queueKey);
2129
+ log?.info?.(
2130
+ `[队列] 已登记 queueKey=${queueKey} busy=${isQueueBusy} depth≈pending`,
2131
+ );
2132
+
2133
+ // ===== 登记之后再做用户反馈(可异步,不再阻塞占槽)=====
2134
+ addEmotionReply(config, data, log).catch((err: any) => {
2135
+ log?.warn?.(`[队列] 即时贴表情失败: ${err?.message || err}`);
2136
+ });
2086
2137
 
2087
- // 不等待任务完成,立即返回,不阻塞 WebSocket 消息接收
2088
- // 消息处理在后台异步执行,队列保证同一会话+agent的消息串行处理
2138
+ if (isQueueBusy) {
2139
+ const ackPhrases = QUEUE_BUSY_ACK_PHRASES;
2140
+ const ackText = ackPhrases[Math.floor(Math.random() * ackPhrases.length)];
2141
+ const groupReplyMode = config.groupReplyMode || "aicard";
2142
+ const skipAICard =
2143
+ !isDirect && (groupReplyMode === "text" || groupReplyMode === "markdown");
2144
+
2145
+ // 火并忘记:不 await,避免拖慢本回调、也不挡住下一条消息的占槽
2146
+ void (async () => {
2147
+ try {
2148
+ if (skipAICard) {
2149
+ await sendProactive(
2150
+ config,
2151
+ { openConversationId: data.conversationId },
2152
+ ackText,
2153
+ { msgType: "text", useAICard: false, fallbackToNormal: true },
2154
+ );
2155
+ log?.info?.(
2156
+ `[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`,
2157
+ );
2158
+ } else {
2159
+ const cardTarget: AICardTarget = isDirect
2160
+ ? { type: "user", userId: senderId }
2161
+ : { type: "group", openConversationId: data.conversationId };
2162
+ const card = await createAICardForTarget(config, cardTarget, log);
2163
+ if (card) {
2164
+ await streamAICard(card, ackText, false, config, log);
2165
+ cardHolder.card = card;
2166
+ log?.info?.(
2167
+ `[队列] 队列繁忙,已创建排队 ACK Card,cardInstanceId=${card.cardInstanceId}`,
2168
+ );
2169
+ } else {
2170
+ log?.warn?.(`[队列] 创建排队 ACK Card 失败(返回 null)`);
2171
+ }
2172
+ }
2173
+ } catch (ackErr: any) {
2174
+ log?.warn?.(`[队列] 繁忙 ACK 异常: ${ackErr?.message || ackErr}`);
2175
+ } finally {
2176
+ cardHolder.ackDone = true;
2177
+ }
2178
+ })();
2179
+ }
2180
+
2181
+ // 立即返回,不阻塞 WebSocket 收下一条
2089
2182
  } catch (err: any) {
2090
2183
  log?.error?.(`[队列] 队列管理异常,直接处理: ${err.message}`);
2091
- // 如果队列管理失败,直接调用内部处理函数(不阻塞)
2092
- void handleDingTalkMessageInternal(params);
2184
+ const fallback = handleDingTalkMessageInternal(params).catch((e: any) => {
2185
+ log?.error?.(`[队列] 直接处理也失败: ${e.message}`);
2186
+ });
2187
+ trackBackgroundWork(fallback, "queue-fallback");
2093
2188
  }
2094
2189
  }
2095
2190