@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
@@ -1,14 +1,15 @@
1
1
  import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
2
- import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-DzZo6Gvk.mjs";
3
- import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
2
+ import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-DbqGRyhI.mjs";
3
+ import { n as createLoggerFromConfig, r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
6
- import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-WleKf54h.mjs";
6
+ import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-CK0Wz3Ze.mjs";
7
7
  import { a as QUEUE_BUSY_ACK_PHRASES, n as normalizeSlashCommand, t as buildSessionContext } from "./session-DJ4jYqPv.mjs";
8
8
  import { c as formatSenderRoles, f as getUserProfile, h as recallEmotionReply, l as getAccessToken, o as formatHiredDateShanghai, r as addEmotionReply, s as formatSenderDisplayLabel, t as DINGTALK_API, u as getOapiAccessToken$1 } from "./utils-legacy-DQ3HeGHh.mjs";
9
- import "./chunk-upload-BBQgGtcZ.mjs";
10
- import { a as toLocalPath, i as VIDEO_MARKER_PATTERN, o as uploadMediaToDingTalk, r as FILE_MARKER_PATTERN, t as AUDIO_MARKER_PATTERN } from "./common-DE0gd0Jg.mjs";
11
- import { n as lookupCardContent, t as collectCardLookupIds } from "./card-content-cache-CYUzCaQJ.mjs";
9
+ import { r as trackBackgroundWork } from "./background-work-DkaSDqWk.mjs";
10
+ import "./chunk-upload-CIU5G_ay.mjs";
11
+ import { a as toLocalPath, i as VIDEO_MARKER_PATTERN, o as uploadMediaToDingTalk, r as FILE_MARKER_PATTERN, t as AUDIO_MARKER_PATTERN } from "./common-BfIYs1yC.mjs";
12
+ import { n as lookupCardContent, t as collectCardLookupIds } from "./card-content-cache-DT_OYThh.mjs";
12
13
  import * as fs from "fs";
13
14
  import * as path from "path";
14
15
  import * as os from "node:os";
@@ -62,7 +63,7 @@ function resolveAgentWorkspaceDir(cfg, agentId) {
62
63
  * 直接复用 media.ts 权威实现,避免 reply 路径与 message 路径两套逻辑分叉。
63
64
  */
64
65
  async function processLocalImages(content, oapiToken, log) {
65
- const { processLocalImages: impl } = await import("./media-BqpUGCni.mjs");
66
+ const { processLocalImages: impl } = await import("./media-DDtX37T2.mjs");
66
67
  return impl(content, oapiToken, log);
67
68
  }
68
69
  //#endregion
@@ -261,10 +262,36 @@ function createDingtalkReplyDispatcher(params) {
261
262
  accountId
262
263
  });
263
264
  const log = createLoggerFromConfig(account.config, `DingTalk:${accountId}`);
264
- let currentCardTarget = null;
265
+ let currentCardTarget = preCreatedCard ?? null;
265
266
  let accumulatedText = "";
266
267
  const deliveredFinalTexts = /* @__PURE__ */ new Set();
267
268
  let sessionClosed = false;
269
+ /**
270
+ * 群聊 fail-before-reply 时 OpenClaw 会把用户可见文案收成 NO_REPLY(静默),
271
+ * 但 payload 仍带 rawError/isError。normalize 会在 deliver 前丢掉整包,
272
+ * 导致 closeStreaming 空文本 → answerCard 误定格「思考完成」。
273
+ * 用 onSkip 捕获后写入此处,供 closeStreaming 定稿错误卡。
274
+ */
275
+ let pendingErrorText = null;
276
+ if (preCreatedCard && !isDirect) registerActiveCard(conversationId, preCreatedCard);
277
+ if (preCreatedCard) log.info(`[DingTalk] 启动即挂接即时建卡 currentCardTarget=${preCreatedCard.cardInstanceId} isDirect=${isDirect}`);
278
+ /** 从 isError/rawError payload 提取中文错误文案(含被静默丢弃的群聊失败) */
279
+ const captureErrorFromPayload = (payload, source) => {
280
+ const p = payload;
281
+ if (!p) return null;
282
+ const raw = readPayloadRawError(p);
283
+ const text = typeof p.text === "string" ? p.text.trim() : "";
284
+ const looksSilent = !text || /^NO_REPLY$/i.test(text) || text.split(/\s+/).every((t) => /^NO_REPLY$/i.test(t));
285
+ let errorText = null;
286
+ if (raw) errorText = matchModelErrorText(raw, { includeCatchAll: true });
287
+ if (!errorText && p.isError && text && !looksSilent) errorText = matchModelErrorText(text, { includeCatchAll: true }) ?? text;
288
+ if (!errorText && (raw || p.isError)) errorText = MODEL_ERROR_GENERIC;
289
+ if (!errorText) return null;
290
+ pendingErrorText = errorText;
291
+ if (!accumulatedText.trim() || looksSilent) accumulatedText = errorText;
292
+ log.info(`[DingTalk][errorCapture] source=${source} len=${errorText.length} preview=${errorText.slice(0, 60)}`);
293
+ return errorText;
294
+ };
268
295
  let asyncModeFullResponse = "";
269
296
  let currentToolLine = "";
270
297
  /**
@@ -415,10 +442,11 @@ function createDingtalkReplyDispatcher(params) {
415
442
  }
416
443
  }
417
444
  };
418
- streamWriteChain = streamWriteChain.then(() => runWrite(force)).catch((e) => {
445
+ const writePromise = streamWriteChain.then(() => runWrite(force));
446
+ streamWriteChain = writePromise.catch((e) => {
419
447
  log.warn(`[DingTalk][stream] 队列任务失败(不中断后续):${e?.message || e}`);
420
448
  });
421
- return streamWriteChain;
449
+ return writePromise;
422
450
  };
423
451
  /** 定稿前:取消尾随定时器,强制推送全文并等待队列排空 */
424
452
  const flushCardStream = async (content, card) => {
@@ -559,8 +587,13 @@ function createDingtalkReplyDispatcher(params) {
559
587
  clearTrailingFlush();
560
588
  await streamWriteChain;
561
589
  let finalText = finalClean(pickFinalText());
590
+ if (!finalText.trim() && pendingErrorText) {
591
+ finalText = pendingErrorText;
592
+ log.info(`[DingTalk][closeStreaming] 使用 pendingErrorText 定稿(群聊静默失败恢复),len=${finalText.length}`);
593
+ }
562
594
  const hadRealAnswer = finalText.trim().length > 0;
563
- log.info(`[DingTalk][closeStreaming] 最终答案来源=${finalMarkedText !== null ? "marker[-final-]" : lastAnswerText ? "非reasoning/累积取长" : "accumulatedText兜底"},长度=${finalText.length},lastAnswerLen=${lastAnswerText.length},accLen=${accumulatedText.length}`);
595
+ const isErrorFinal = Boolean(pendingErrorText);
596
+ log.info(`[DingTalk][closeStreaming] 最终答案来源=${finalMarkedText !== null ? "marker[-final-]" : isErrorFinal ? "pendingErrorText" : lastAnswerText ? "非reasoning/累积取长" : "accumulatedText兜底"},长度=${finalText.length},lastAnswerLen=${lastAnswerText.length},accLen=${accumulatedText.length},isErrorFinal=${isErrorFinal}`);
564
597
  if (!finalText.trim()) {
565
598
  finalText = "✅ 任务执行完成(无文本输出)";
566
599
  log.info(`[DingTalk][closeStreaming] 累积文本为空,使用默认提示文案`);
@@ -573,22 +606,20 @@ function createDingtalkReplyDispatcher(params) {
573
606
  type: "group",
574
607
  openConversationId: conversationId
575
608
  };
576
- console.log(`[DingTalk][LocalImage] closeStreaming 开始媒体处理 | target=${JSON.stringify(target)} hasToken=${!!oapiToken} textLen=${finalText.length}`);
577
609
  const mdImgCount = (finalText.match(/!\[[^\]]*\]\([^)]+\)/g) || []).length;
578
- console.log(`[DingTalk][LocalImage] closeStreaming 正文 markdown 图数量=${mdImgCount} preview=${JSON.stringify(finalText.slice(0, 200))}`);
610
+ if (mdImgCount > 0 || isDingtalkDebug()) log.info(`[DingTalk][LocalImage] closeStreaming 媒体处理 | target=${JSON.stringify(target)} hasToken=${!!oapiToken} textLen=${finalText.length} mdImgs=${mdImgCount}`);
579
611
  if (oapiToken) {
580
612
  const beforeImg = finalText;
581
613
  finalText = await processLocalImages(finalText, oapiToken, log);
582
- if (beforeImg !== finalText) console.log(`[DingTalk][LocalImage] closeStreaming 图片处理 | ${beforeImg.length}→${finalText.length}`);
614
+ if (beforeImg !== finalText) log.info(`[DingTalk][LocalImage] closeStreaming 图片已处理 | ${beforeImg.length}→${finalText.length}`);
583
615
  try {
584
- const { hasResidualLocalMdImagesOutsideCode } = await import("./media-BqpUGCni.mjs");
585
- if (hasResidualLocalMdImagesOutsideCode(finalText)) console.warn(`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`);
616
+ const { hasResidualLocalMdImagesOutsideCode } = await import("./media-DDtX37T2.mjs");
617
+ if (hasResidualLocalMdImagesOutsideCode(finalText)) log.warn(`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`);
586
618
  } catch {}
587
619
  finalText = await processVideoMarkers(finalText, "", account.config, oapiToken, log, true, target);
588
620
  finalText = await processAudioMarkers(finalText, "", account.config, oapiToken, log, true, target);
589
621
  finalText = await uploadAndReplaceFileMarkers(finalText, "", account.config, oapiToken, log, true, target);
590
- log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
591
- const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
622
+ const { processRawMediaPaths } = await import("./media-DDtX37T2.mjs");
592
623
  finalText = await processRawMediaPaths(finalText, account.config, oapiToken, log, target);
593
624
  log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
594
625
  } else log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
@@ -602,7 +633,7 @@ function createDingtalkReplyDispatcher(params) {
602
633
  }
603
634
  } else log.info(`[DingTalk][closeStreaming] 养成系统:onCommandOutput 监听到 ${productsToProcess.size} 个 dws 产品: ${[...productsToProcess].join(", ")}`);
604
635
  if (productsToProcess.size > 0) {
605
- const { GamificationEngine } = await import("./game-xiyou-CAT0ilSj.mjs");
636
+ const { GamificationEngine } = await import("./game-xiyou-uwsv23AC.mjs");
606
637
  const engine = GamificationEngine.getInstanceForUser(senderId);
607
638
  if (engine.isEnabled()) {
608
639
  const primaryProduct = [...productsToProcess][0];
@@ -632,7 +663,15 @@ function createDingtalkReplyDispatcher(params) {
632
663
  const answerTplId = (account.config?.answerCardTemplateId)?.trim() || "d246b7f5-1783-4e9b-bb46-bef52d63050e.schema";
633
664
  const answerActToken = Number(account.config?.answerActToken) || 500;
634
665
  const answerTokens = estimateTokens(finalText);
635
- if (!hadRealAnswer) {
666
+ if (isErrorFinal) {
667
+ log.info(`[DingTalk][closeStreaming] answerCard 模式:错误定稿原卡(len=${finalText.length}),不建答案卡、不定格思考完成`);
668
+ try {
669
+ await flushCardStream(finalText, cardSnapshot);
670
+ } catch (e) {
671
+ log.warn(`[DingTalk][closeStreaming] 错误终稿 flush 失败(继续 FINISH):${e?.message || e}`);
672
+ }
673
+ await finishAICard(cardSnapshot, finalText, account.config, log, void 0, void 0, conversationId);
674
+ } else if (!hadRealAnswer) {
636
675
  log.info(`[DingTalk][closeStreaming] answerCard 模式:无真实答案,仅定格原卡思考完成(不建答案卡)`);
637
676
  await finalizeOriginalToDone();
638
677
  } else if (answerTokens <= answerActToken) {
@@ -686,14 +725,63 @@ function createDingtalkReplyDispatcher(params) {
686
725
  accumulatedText = "";
687
726
  }
688
727
  };
728
+ /**
729
+ * 统一错误定稿:模型/dispatch 异常时把中文提示写到 AI Card 并 FINISHED。
730
+ * 供 dispatcher onError 与 message-handler catch(同步 throw)共用。
731
+ * 不依赖 OpenClaw 返回的 replyOptions.onError(SDK 根本不导出该字段)。
732
+ */
733
+ const finalizeDispatchError = async (error, kind = "invoke") => {
734
+ const errorMsg = String(error);
735
+ log.error(`[DingTalk][finalizeDispatchError] kind=${kind} failed: ${errorMsg}`);
736
+ await startStreaming();
737
+ const errorText = matchModelErrorText(errorMsg, { includeCatchAll: true }) ?? "⚠️ 模型请求异常,请稍后重试";
738
+ log.warn(`[DingTalk][finalizeDispatchError] 错误提示: ${errorText.slice(0, 80)}`);
739
+ pendingErrorText = errorText;
740
+ accumulatedText = errorText;
741
+ if (!currentCardTarget && !sessionClosed) if (preCreatedCard) try {
742
+ currentCardTarget = preCreatedCard;
743
+ if (!isDirect) registerActiveCard(conversationId, preCreatedCard);
744
+ log.info(`[DingTalk][finalizeDispatchError] 复用即时建卡显示错误(cardInstanceId=${preCreatedCard.cardInstanceId})`);
745
+ } catch (cardErr) {
746
+ log.warn(`[DingTalk][finalizeDispatchError] 复用即时建卡失败:${cardErr.message},降级发送普通消息`);
747
+ try {
748
+ await sendMessage(account.config, sessionWebhook, accumulatedText, {
749
+ useMarkdown: false,
750
+ log: params.runtime.log
751
+ });
752
+ } catch (sendErr) {
753
+ log.error(`[DingTalk][finalizeDispatchError] 降级发送普通消息失败:${sendErr.message}`);
754
+ }
755
+ }
756
+ else {
757
+ log.warn(`[DingTalk][finalizeDispatchError] 无可用 AI Card,降级发送普通消息`);
758
+ try {
759
+ await sendMessage(account.config, sessionWebhook, accumulatedText, {
760
+ useMarkdown: false,
761
+ log: params.runtime.log
762
+ });
763
+ } catch (sendErr) {
764
+ log.error(`[DingTalk][finalizeDispatchError] 降级发送普通消息失败:${sendErr.message}`);
765
+ }
766
+ }
767
+ await closeStreaming();
768
+ };
689
769
  const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
690
770
  ...prefixOptions,
691
771
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
772
+ onSkip: (payload, info) => {
773
+ const raw = readPayloadRawError(payload);
774
+ const isErr = Boolean(payload?.isError);
775
+ if (!raw && !isErr) return;
776
+ log.warn(`[DingTalk][onSkip] 群聊静默失败 kind=${info?.kind} reason=${info?.reason} rawLen=${raw.length}`);
777
+ captureErrorFromPayload(payload, `onSkip:${info?.kind ?? "?"}:${info?.reason ?? "?"}`);
778
+ },
692
779
  onReplyStart: () => {
693
780
  log.info(`[DingTalk][onReplyStart] 开始回复,流式 enabled=${streamingEnabled}`);
694
781
  deliveredFinalTexts.clear();
695
782
  finalMarkedText = null;
696
783
  lastAnswerText = "";
784
+ pendingErrorText = null;
697
785
  processMarkerLogged = false;
698
786
  finalMarkerLogged = false;
699
787
  markerSystemActive = false;
@@ -710,6 +798,7 @@ function createDingtalkReplyDispatcher(params) {
710
798
  let text = payload.text ?? "";
711
799
  const rawError = readPayloadRawError(payload);
712
800
  if (rawError) {
801
+ captureErrorFromPayload(payload, `deliver:${info?.kind ?? "?"}`);
713
802
  const matchedErrorText = matchModelErrorText(rawError, { includeCatchAll: true });
714
803
  if (matchedErrorText) {
715
804
  log.warn(`[DingTalk][deliver] 检测到上游 rawError,替换为中文提示(raw=${rawError.slice(0, 120)})`);
@@ -720,14 +809,22 @@ function createDingtalkReplyDispatcher(params) {
720
809
  observeReply(payload.text, payload);
721
810
  await startStreaming();
722
811
  if (isNonAnswerPayload(payload)) {
812
+ const isModelErrorFinal = Boolean(payload.isError) && info?.kind === "final" && Boolean(text.trim());
813
+ if (isModelErrorFinal) {
814
+ accumulatedText = text;
815
+ log.info(`[DingTalk][deliver] 将上游异常 final 文本存入 accumulatedText(len=${text.length}),防止 onIdle 覆盖`);
816
+ } else if (payload.isError && !accumulatedText.trim() && text.trim()) {
817
+ accumulatedText = text;
818
+ log.info(`[DingTalk][deliver] 将上游异常 payload 文本存入 accumulatedText(len=${text.length}),防止 onIdle 覆盖`);
819
+ }
723
820
  if (streamingEnabled && currentCardTarget && !asyncMode && finalMarkedText === null) try {
724
- await enqueueCardStream(displayClean(text));
821
+ await enqueueCardStream(displayClean(text), { force: isModelErrorFinal });
725
822
  } catch (e) {
726
823
  if (!isQpsLimitError(e)) log.warn(`[DingTalk][deliver] 状态/错误 payload 写卡失败:${e?.message || e}`);
727
824
  }
728
- if (payload.isError && !accumulatedText.trim() && text.trim()) {
729
- accumulatedText = text;
730
- log.info(`[DingTalk][deliver] 将上游异常 payload 文本存入 accumulatedText(len=${text.length}),防止 onIdle 覆盖`);
825
+ if (isModelErrorFinal) {
826
+ log.info(`[DingTalk][deliver] 上游 isError final,主动 closeStreaming 定稿错误卡`);
827
+ await closeStreaming();
731
828
  }
732
829
  log.info(`[DingTalk][deliver] 非答案 payload(isError=${payload.isError},isStatusNotice=${payload.isStatusNotice}),仅展示不计入最终答案`);
733
830
  return;
@@ -744,7 +841,7 @@ function createDingtalkReplyDispatcher(params) {
744
841
  const oapiToken = await getOapiAccessToken(account.config);
745
842
  if (oapiToken) {
746
843
  log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
747
- const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
844
+ const { processRawMediaPaths } = await import("./media-DDtX37T2.mjs");
748
845
  text = await processRawMediaPaths(text, account.config, oapiToken, log, target);
749
846
  log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
750
847
  }
@@ -838,40 +935,9 @@ function createDingtalkReplyDispatcher(params) {
838
935
  }
839
936
  },
840
937
  onError: async (error, info) => {
841
- const errorMsg = String(error);
842
- log.error(`[DingTalk][onError] ${info.kind} reply failed: ${errorMsg}`);
843
- params.runtime.error?.(`dingtalk[${account.accountId}] ${info.kind} reply failed: ${errorMsg}`);
844
- await startStreaming();
845
- const errorText = matchModelErrorText(errorMsg, { includeCatchAll: true }) ?? "⚠️ 模型请求异常,请稍后重试";
846
- log.warn(`[DingTalk][onError] 错误提示: ${errorText.slice(0, 80)}`);
847
- if (!accumulatedText.trim()) accumulatedText = errorText;
848
- if (!currentCardTarget && !sessionClosed) if (preCreatedCard) try {
849
- currentCardTarget = preCreatedCard;
850
- if (!isDirect) registerActiveCard(conversationId, preCreatedCard);
851
- log.info(`[DingTalk][onError] 复用即时建卡显示错误(cardInstanceId=${preCreatedCard.cardInstanceId})`);
852
- } catch (cardErr) {
853
- log.warn(`[DingTalk][onError] 复用即时建卡失败:${cardErr.message},降级发送普通消息`);
854
- try {
855
- await sendMessage(account.config, sessionWebhook, accumulatedText, {
856
- useMarkdown: false,
857
- log: params.runtime.log
858
- });
859
- } catch (sendErr) {
860
- log.error(`[DingTalk][onError] 降级发送普通消息失败:${sendErr.message}`);
861
- }
862
- }
863
- else {
864
- log.warn(`[DingTalk][onError] 卡片已关闭且无即时建卡,降级发送普通消息`);
865
- try {
866
- await sendMessage(account.config, sessionWebhook, accumulatedText, {
867
- useMarkdown: false,
868
- log: params.runtime.log
869
- });
870
- } catch (sendErr) {
871
- log.error(`[DingTalk][onError] 降级发送普通消息失败:${sendErr.message}`);
872
- }
873
- }
874
- await closeStreaming();
938
+ log.error(`[DingTalk][onError] ${info.kind} reply failed: ${String(error)}`);
939
+ params.runtime.error?.(`dingtalk[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
940
+ await finalizeDispatchError(error, info.kind);
875
941
  typingCallbacks.onIdle?.();
876
942
  },
877
943
  onIdle: async () => {
@@ -905,7 +971,7 @@ function createDingtalkReplyDispatcher(params) {
905
971
  currentToolLine = "";
906
972
  await startStreaming();
907
973
  if (currentCardTarget) try {
908
- const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-B_A78n8u.mjs");
974
+ const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-C0Oaf0z-.mjs");
909
975
  await enqueueCardStream(buildCardContent().replace(FILE_MARKER_PATTERN, "").replace(VIDEO_MARKER_PATTERN, "").replace(AUDIO_MARKER_PATTERN, "").trim(), { force: true });
910
976
  } catch (err) {
911
977
  if (!isQpsLimitError(err)) log.error(`[DingTalk][onPartialReply] final-marker 刷卡失败:${err.message}`);
@@ -918,7 +984,7 @@ function createDingtalkReplyDispatcher(params) {
918
984
  if (payload.text?.trim()) hasModelStreamText = true;
919
985
  currentToolLine = "";
920
986
  try {
921
- const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-B_A78n8u.mjs");
987
+ const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-C0Oaf0z-.mjs");
922
988
  await enqueueCardStream(buildCardContent().replace(FILE_MARKER_PATTERN, "").replace(VIDEO_MARKER_PATTERN, "").replace(AUDIO_MARKER_PATTERN, "").trim());
923
989
  } catch (err) {
924
990
  if (isQpsLimitError(err)) log.warn(`[DingTalk][onPartialReply] QPS 限流,已排队尾随重试 latestLen=${latestCardContent.length}`);
@@ -957,12 +1023,20 @@ function createDingtalkReplyDispatcher(params) {
957
1023
  }
958
1024
  },
959
1025
  markDispatchIdle,
960
- getAsyncModeResponse: () => asyncModeFullResponse
1026
+ getAsyncModeResponse: () => asyncModeFullResponse,
1027
+ /** 同步 dispatch 异常时由 message-handler catch 调用,统一 finish 错误卡 */
1028
+ finalizeDispatchError
961
1029
  };
962
1030
  }
963
1031
  //#endregion
964
1032
  //#region src/core/message-handler.ts
965
1033
  /**
1034
+ * 单条会话队列任务最长执行时间。
1035
+ * 503 重试 + 工具循环偶发挂死时,若不超时,后续同会话消息会永远堵在 previousTask 上,
1036
+ * 表现:连发多条「网关没反应 / 不贴表情」(表情在排队 ACK 或 dequeued 后才贴)。
1037
+ */
1038
+ const SESSION_QUEUE_TASK_TIMEOUT_MS = 180 * 1e3;
1039
+ /**
966
1040
  * 会话消息队列管理
967
1041
  * 用于确保同一会话+agent的消息按顺序处理,避免并发冲突导致AI返回空响应
968
1042
  * 队列键格式:{sessionId}:{agentId}
@@ -1105,8 +1179,9 @@ function extractQuotedMsgText(container, maxDepth, conversationId) {
1105
1179
  conversationId,
1106
1180
  allowConversationRecent: false
1107
1181
  }) || "";
1108
- if (cardText) console.log(`[DingTalk][Quote] interactiveCard 已从 CardCache 精确回填 | ids=${ids.join(",") || "-"} len=${cardText.length}`);
1109
- else {
1182
+ if (cardText) {
1183
+ if (isDingtalkDebug()) console.log(`[DingTalk][Quote] interactiveCard 已从 CardCache 精确回填 | ids=${ids.join(",") || "-"} len=${cardText.length}`);
1184
+ } else if (isDingtalkDebug()) {
1110
1185
  const dump = JSON.stringify(repliedMsg ?? {}).slice(0, 800);
1111
1186
  console.log(`[DingTalk][Quote] interactiveCard 无正文且缓存未命中(未使用会话最近兜底) | conv=${conversationId || "-"} keys=${Object.keys(contentObj || {}).join(",")} repliedMsg=${dump}`);
1112
1187
  }
@@ -1129,13 +1204,13 @@ function extractQuotedMsgText(container, maxDepth, conversationId) {
1129
1204
  });
1130
1205
  if (cached) {
1131
1206
  bodyText = cached;
1132
- console.log(`[DingTalk][Quote] msgType=${msgType} 已从 CardCache 精确回填 | ids=${ids.join(",") || "-"} len=${cached.length}`);
1207
+ if (isDingtalkDebug()) console.log(`[DingTalk][Quote] msgType=${msgType} 已从 CardCache 精确回填 | ids=${ids.join(",") || "-"} len=${cached.length}`);
1133
1208
  } else if (/card|interactive/i.test(String(msgType || "")) || ids.length > 0) {
1134
1209
  bodyText = "钉钉卡片消息。";
1135
- console.log(`[DingTalk][Quote] 未知卡片类引用 msgType=${msgType} 已简化占位 | keys=${Object.keys(contentObj || {}).join(",")}`);
1136
- } else console.log(`[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(",")} dump=${JSON.stringify(repliedMsg ?? {}).slice(0, 600)}`);
1210
+ if (isDingtalkDebug()) console.log(`[DingTalk][Quote] 未知卡片类引用 msgType=${msgType} 已简化占位 | keys=${Object.keys(contentObj || {}).join(",")}`);
1211
+ } else if (isDingtalkDebug()) console.log(`[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(",")} dump=${JSON.stringify(repliedMsg ?? {}).slice(0, 600)}`);
1137
1212
  } catch {
1138
- console.log(`[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(",")}`);
1213
+ if (isDingtalkDebug()) console.log(`[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(",")}`);
1139
1214
  }
1140
1215
  break;
1141
1216
  }
@@ -1926,7 +2001,7 @@ async function handleDingTalkMessageInternal(params) {
1926
2001
  const rawText = content.text || "";
1927
2002
  let userContent = normalizeSlashCommand(rawText) || (content.imageUrls.length > 0 ? "请描述这张图片" : "");
1928
2003
  try {
1929
- const { GamificationEngine, isGamificationCommand } = await import("./game-xiyou-CAT0ilSj.mjs");
2004
+ const { GamificationEngine, isGamificationCommand } = await import("./game-xiyou-uwsv23AC.mjs");
1930
2005
  if (isGamificationCommand(rawText)) {
1931
2006
  const engine = GamificationEngine.getInstanceForUser(senderId);
1932
2007
  if (rawText.trim().startsWith("/西游") || engine.isEnabled()) {
@@ -2100,6 +2175,8 @@ async function handleDingTalkMessageInternal(params) {
2100
2175
  log?.warn?.(`Failed to send acknowledgment: ${ackErr?.message || ackErr}`);
2101
2176
  }
2102
2177
  }
2178
+ let earlyCard = params.preCreatedCard;
2179
+ let finalizeDispatchError;
2103
2180
  try {
2104
2181
  const core = getDingtalkRuntime();
2105
2182
  let finalContent = userContent;
@@ -2165,7 +2242,6 @@ async function handleDingTalkMessageInternal(params) {
2165
2242
  });
2166
2243
  const groupReplyModeForCard = config.groupReplyMode || "aicard";
2167
2244
  const streamingEnabledForCard = !(!isDirect && (groupReplyModeForCard === "text" || groupReplyModeForCard === "markdown")) && config.streaming !== false;
2168
- let earlyCard = params.preCreatedCard;
2169
2245
  if (!earlyCard && streamingEnabledForCard && !asyncMode) try {
2170
2246
  earlyCard = await createAICardForTarget(config, isDirect ? {
2171
2247
  type: "user",
@@ -2183,7 +2259,7 @@ async function handleDingTalkMessageInternal(params) {
2183
2259
  log.warn(`[DingTalk][earlyCard] 即时建卡失败(降级延迟建卡):${e?.message}`);
2184
2260
  earlyCard = void 0;
2185
2261
  }
2186
- const { dispatcher, replyOptions, markDispatchIdle, getAsyncModeResponse } = createDingtalkReplyDispatcher({
2262
+ const { dispatcher, replyOptions, markDispatchIdle, getAsyncModeResponse, finalizeDispatchError: finalizeErr } = createDingtalkReplyDispatcher({
2187
2263
  cfg,
2188
2264
  agentId: matchedAgentId,
2189
2265
  runtime,
@@ -2196,6 +2272,7 @@ async function handleDingTalkMessageInternal(params) {
2196
2272
  asyncMode,
2197
2273
  preCreatedCard: earlyCard
2198
2274
  });
2275
+ finalizeDispatchError = finalizeErr;
2199
2276
  if (config.clientId) {
2200
2277
  const botIdentityHint = `[DingTalk Bot Context] Current bot clientId: ${String(config.clientId)}. When executing \`dws chat message send-by-bot\`, always pass \`--client-id ${String(config.clientId)}\` to ensure messages are sent from the correct bot.`;
2201
2278
  finalContent = finalContent ? `${finalContent}\n\n${botIdentityHint}` : botIdentityHint;
@@ -2239,7 +2316,7 @@ async function handleDingTalkMessageInternal(params) {
2239
2316
  finalText = await processVideoMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2240
2317
  finalText = await processAudioMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2241
2318
  finalText = await uploadAndReplaceFileMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2242
- const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
2319
+ const { processRawMediaPaths } = await import("./media-DDtX37T2.mjs");
2243
2320
  finalText = await processRawMediaPaths(finalText, config, oapiToken, log, mediaTarget);
2244
2321
  }
2245
2322
  const textToSend = finalText.trim() || "✅ 任务执行完成(无文本输出)";
@@ -2265,28 +2342,42 @@ async function handleDingTalkMessageInternal(params) {
2265
2342
  }
2266
2343
  } catch (err) {
2267
2344
  log?.error?.(`SDK dispatch 失败: ${err.message}`);
2268
- const errorText = matchModelErrorText(err.message, { includeCatchAll: true }) ?? `⚠️ 抱歉,处理请求时出错: ${err.message}`;
2269
- if (earlyCard) try {
2270
- await streamAICard(earlyCard, errorText, true, config, log);
2271
- } catch (cardErr) {
2272
- log?.error?.(`更新错误卡片失败: ${cardErr.message}`);
2345
+ if (finalizeDispatchError) try {
2346
+ log?.info?.(`[DingTalk] 统一错误定稿:调用 finalizeDispatchError 处理同步异常`);
2347
+ await finalizeDispatchError(err, "invoke");
2348
+ } catch (finalizeErr) {
2349
+ log?.error?.(`[DingTalk] finalizeDispatchError 失败: ${finalizeErr?.message || finalizeErr}`);
2273
2350
  }
2274
- else try {
2275
- const token = await getAccessToken(config);
2276
- const body = {
2277
- msgtype: "text",
2278
- text: { content: errorText }
2279
- };
2280
- if (!isDirect) body.at = {
2281
- atUserIds: [senderId],
2282
- isAtAll: false
2283
- };
2284
- await dingtalkHttp.post(sessionWebhook, body, { headers: {
2285
- "x-acs-dingtalk-access-token": token,
2286
- "Content-Type": "application/json"
2287
- } });
2288
- } catch (fallbackErr) {
2289
- log?.error?.(`错误消息发送也失败: ${fallbackErr.message}`);
2351
+ else {
2352
+ const errorText = matchModelErrorText(err.message, { includeCatchAll: true }) ?? `⚠️ 抱歉,处理请求时出错: ${err.message}`;
2353
+ let cardSuccess = false;
2354
+ if (earlyCard) try {
2355
+ await finishAICard(earlyCard, errorText, config, log, void 0, void 0, data.conversationId);
2356
+ cardSuccess = true;
2357
+ log?.info?.(`[DingTalk] 极端兜底:finishAICard 错误卡成功`);
2358
+ } catch (cardErr) {
2359
+ log?.error?.(`更新错误卡片失败: ${cardErr.message}`);
2360
+ }
2361
+ if (!cardSuccess) {
2362
+ log?.warn?.(`[DingTalk] 无可用错误定稿路径,走纯文本发送`);
2363
+ try {
2364
+ const token = await getAccessToken(config);
2365
+ const body = {
2366
+ msgtype: "text",
2367
+ text: { content: errorText }
2368
+ };
2369
+ if (!isDirect) body.at = {
2370
+ atUserIds: [senderId],
2371
+ isAtAll: false
2372
+ };
2373
+ await dingtalkHttp.post(sessionWebhook, body, { headers: {
2374
+ "x-acs-dingtalk-access-token": token,
2375
+ "Content-Type": "application/json"
2376
+ } });
2377
+ } catch (fallbackErr) {
2378
+ log?.error?.(`错误消息发送也失败: ${fallbackErr.message}`);
2379
+ }
2380
+ }
2290
2381
  }
2291
2382
  }
2292
2383
  try {
@@ -2334,54 +2425,33 @@ async function handleDingTalkMessage(params) {
2334
2425
  const queueKey = `${baseSessionId}:${matchedAgentId}`;
2335
2426
  try {
2336
2427
  sessionLastActivity.set(queueKey, Date.now());
2337
- const isQueueBusy = sessionQueues.has(queueKey);
2338
2428
  const previousTask = sessionQueues.get(queueKey) || Promise.resolve();
2339
- let preCreatedCard;
2340
- if (isQueueBusy) {
2341
- const ackPhrases = QUEUE_BUSY_ACK_PHRASES;
2342
- const ackText = ackPhrases[Math.floor(Math.random() * ackPhrases.length)];
2343
- const groupReplyMode = config.groupReplyMode || "aicard";
2344
- if (!isDirect && (groupReplyMode === "text" || groupReplyMode === "markdown")) try {
2345
- await sendProactive(config, { openConversationId: data.conversationId }, ackText, {
2346
- msgType: "text",
2347
- useAICard: false,
2348
- fallbackToNormal: true
2349
- });
2350
- log?.info?.(`[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`);
2351
- } catch (ackErr) {
2352
- log?.warn?.(`[队列] 发送普通 ACK 失败: ${ackErr?.message || ackErr}`);
2429
+ const isQueueBusy = sessionQueues.has(queueKey);
2430
+ const cardHolder = {
2431
+ card: void 0,
2432
+ ackDone: !isQueueBusy
2433
+ };
2434
+ const currentTask = previousTask.then(async () => {
2435
+ log?.info?.(`[队列] 开始处理消息,queueKey=${queueKey} wasBusy=${isQueueBusy} timeoutMs=${SESSION_QUEUE_TASK_TIMEOUT_MS}`);
2436
+ if (isQueueBusy && !cardHolder.ackDone) {
2437
+ const waitStart = Date.now();
2438
+ while (!cardHolder.ackDone && Date.now() - waitStart < 2e3) await new Promise((r) => setTimeout(r, 50));
2353
2439
  }
2354
- else {
2355
- const cardTarget = isDirect ? {
2356
- type: "user",
2357
- userId: senderId
2358
- } : {
2359
- type: "group",
2360
- openConversationId: data.conversationId
2361
- };
2362
- try {
2363
- const card = await createAICardForTarget(config, cardTarget, log);
2364
- if (card) {
2365
- await streamAICard(card, ackText, false, config, log);
2366
- preCreatedCard = card;
2367
- log?.info?.(`[队列] 队列繁忙,已创建排队 ACK Card,cardInstanceId=${card.cardInstanceId}`);
2368
- } else log?.warn?.(`[队列] 创建排队 ACK Card 失败(返回 null),跳过 ACK`);
2369
- addEmotionReply(config, data, log).catch((err) => {
2370
- log?.warn?.(`[队列] 贴排队表情失败: ${err.message}`);
2371
- });
2372
- } catch (ackErr) {
2373
- log?.warn?.(`[队列] 创建排队 ACK Card 异常: ${ackErr?.message || ackErr}`);
2374
- }
2440
+ let timeoutId;
2441
+ try {
2442
+ await Promise.race([handleDingTalkMessageInternal({
2443
+ ...params,
2444
+ preCreatedCard: cardHolder.card,
2445
+ emotionAlreadyAdded: true
2446
+ }), new Promise((_, reject) => {
2447
+ timeoutId = setTimeout(() => {
2448
+ reject(/* @__PURE__ */ new Error(`会话队列任务超时(${SESSION_QUEUE_TASK_TIMEOUT_MS}ms),释放队列以免堵死后续消息`));
2449
+ }, SESSION_QUEUE_TASK_TIMEOUT_MS);
2450
+ })]);
2451
+ log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
2452
+ } finally {
2453
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
2375
2454
  }
2376
- }
2377
- const currentTask = previousTask.then(async () => {
2378
- log?.info?.(`[队列] 开始处理消息,queueKey=${queueKey}`);
2379
- await handleDingTalkMessageInternal({
2380
- ...params,
2381
- preCreatedCard,
2382
- emotionAlreadyAdded: isQueueBusy
2383
- });
2384
- log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
2385
2455
  }).catch((err) => {
2386
2456
  log?.error?.(`[队列] 消息处理异常,queueKey=${queueKey}, error=${err.message}`);
2387
2457
  }).finally(() => {
@@ -2391,9 +2461,51 @@ async function handleDingTalkMessage(params) {
2391
2461
  }
2392
2462
  });
2393
2463
  sessionQueues.set(queueKey, currentTask);
2464
+ trackBackgroundWork(currentTask, queueKey);
2465
+ log?.info?.(`[队列] 已登记 queueKey=${queueKey} busy=${isQueueBusy} depth≈pending`);
2466
+ addEmotionReply(config, data, log).catch((err) => {
2467
+ log?.warn?.(`[队列] 即时贴表情失败: ${err?.message || err}`);
2468
+ });
2469
+ if (isQueueBusy) {
2470
+ const ackPhrases = QUEUE_BUSY_ACK_PHRASES;
2471
+ const ackText = ackPhrases[Math.floor(Math.random() * ackPhrases.length)];
2472
+ const groupReplyMode = config.groupReplyMode || "aicard";
2473
+ const skipAICard = !isDirect && (groupReplyMode === "text" || groupReplyMode === "markdown");
2474
+ (async () => {
2475
+ try {
2476
+ if (skipAICard) {
2477
+ await sendProactive(config, { openConversationId: data.conversationId }, ackText, {
2478
+ msgType: "text",
2479
+ useAICard: false,
2480
+ fallbackToNormal: true
2481
+ });
2482
+ log?.info?.(`[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`);
2483
+ } else {
2484
+ const card = await createAICardForTarget(config, isDirect ? {
2485
+ type: "user",
2486
+ userId: senderId
2487
+ } : {
2488
+ type: "group",
2489
+ openConversationId: data.conversationId
2490
+ }, log);
2491
+ if (card) {
2492
+ await streamAICard(card, ackText, false, config, log);
2493
+ cardHolder.card = card;
2494
+ log?.info?.(`[队列] 队列繁忙,已创建排队 ACK Card,cardInstanceId=${card.cardInstanceId}`);
2495
+ } else log?.warn?.(`[队列] 创建排队 ACK Card 失败(返回 null)`);
2496
+ }
2497
+ } catch (ackErr) {
2498
+ log?.warn?.(`[队列] 繁忙 ACK 异常: ${ackErr?.message || ackErr}`);
2499
+ } finally {
2500
+ cardHolder.ackDone = true;
2501
+ }
2502
+ })();
2503
+ }
2394
2504
  } catch (err) {
2395
2505
  log?.error?.(`[队列] 队列管理异常,直接处理: ${err.message}`);
2396
- handleDingTalkMessageInternal(params);
2506
+ trackBackgroundWork(handleDingTalkMessageInternal(params).catch((e) => {
2507
+ log?.error?.(`[队列] 直接处理也失败: ${e.message}`);
2508
+ }), "queue-fallback");
2397
2509
  }
2398
2510
  }
2399
2511
  //#endregion