@jeik/dingtalk-connector 0.8.27 → 0.8.28

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 (29) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.en.md +28 -35
  3. package/README.md +28 -43
  4. package/dist/background-work-Cxb4b-91.mjs +42 -0
  5. package/dist/card-content-cache-c4Ls96eJ.mjs +2 -0
  6. package/dist/chunk-upload-PwhogwfE.mjs +2 -0
  7. package/dist/{common-DE0gd0Jg.mjs → common-BZiVgd3A.mjs} +2 -2
  8. package/dist/{common-B_A78n8u.mjs → common-DgL28gCZ.mjs} +1 -1
  9. package/dist/{connection-fc6B4z4G.mjs → connection-CZ7ZoN5_.mjs} +313 -86
  10. package/dist/entry-bundled.mjs +1 -1
  11. package/dist/gateway-methods-BkI1gKaC.mjs +2 -0
  12. package/dist/{gateway-methods--aKsyMs-.mjs → gateway-methods-kNX3qvqw.mjs} +1 -1
  13. package/dist/index.mjs +2 -2
  14. package/dist/{message-handler-DQZiKx3i.mjs → message-handler-B1usz1wb.mjs} +238 -122
  15. package/dist/{messaging-WleKf54h.mjs → messaging-DVS3UKOp.mjs} +6 -2
  16. package/dist/{runtime-DzZo6Gvk.mjs → runtime-A6G7WAWn.mjs} +3 -3
  17. package/docs/RELEASE_NOTES_V0.8.28.md +56 -0
  18. package/package.json +2 -2
  19. package/src/core/connection.ts +465 -158
  20. package/src/core/message-handler.ts +173 -86
  21. package/src/reply-dispatcher.ts +234 -77
  22. package/src/services/messaging/card.ts +2 -0
  23. package/src/utils/background-work.ts +63 -0
  24. package/dist/card-content-cache-Dwd8HICP.mjs +0 -2
  25. package/dist/chunk-upload-DaLXXZH3.mjs +0 -2
  26. package/dist/gateway-methods-BQd4E0Sk.mjs +0 -2
  27. /package/dist/{card-content-cache-CYUzCaQJ.mjs → card-content-cache-bDQFz2u8.mjs} +0 -0
  28. /package/dist/{chunk-upload-BBQgGtcZ.mjs → chunk-upload-BE71drPg.mjs} +0 -0
  29. /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";
2
+ import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-A6G7WAWn.mjs";
3
3
  import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.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-DVS3UKOp.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-Cxb4b-91.mjs";
10
+ import "./chunk-upload-BE71drPg.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-BZiVgd3A.mjs";
12
+ import { n as lookupCardContent, t as collectCardLookupIds } from "./card-content-cache-bDQFz2u8.mjs";
12
13
  import * as fs from "fs";
13
14
  import * as path from "path";
14
15
  import * as os from "node:os";
@@ -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] 累积文本为空,使用默认提示文案`);
@@ -602,7 +635,7 @@ function createDingtalkReplyDispatcher(params) {
602
635
  }
603
636
  } else log.info(`[DingTalk][closeStreaming] 养成系统:onCommandOutput 监听到 ${productsToProcess.size} 个 dws 产品: ${[...productsToProcess].join(", ")}`);
604
637
  if (productsToProcess.size > 0) {
605
- const { GamificationEngine } = await import("./game-xiyou-CAT0ilSj.mjs");
638
+ const { GamificationEngine } = await import("./game-xiyou-uwsv23AC.mjs");
606
639
  const engine = GamificationEngine.getInstanceForUser(senderId);
607
640
  if (engine.isEnabled()) {
608
641
  const primaryProduct = [...productsToProcess][0];
@@ -632,7 +665,15 @@ function createDingtalkReplyDispatcher(params) {
632
665
  const answerTplId = (account.config?.answerCardTemplateId)?.trim() || "d246b7f5-1783-4e9b-bb46-bef52d63050e.schema";
633
666
  const answerActToken = Number(account.config?.answerActToken) || 500;
634
667
  const answerTokens = estimateTokens(finalText);
635
- if (!hadRealAnswer) {
668
+ if (isErrorFinal) {
669
+ log.info(`[DingTalk][closeStreaming] answerCard 模式:错误定稿原卡(len=${finalText.length}),不建答案卡、不定格思考完成`);
670
+ try {
671
+ await flushCardStream(finalText, cardSnapshot);
672
+ } catch (e) {
673
+ log.warn(`[DingTalk][closeStreaming] 错误终稿 flush 失败(继续 FINISH):${e?.message || e}`);
674
+ }
675
+ await finishAICard(cardSnapshot, finalText, account.config, log, void 0, void 0, conversationId);
676
+ } else if (!hadRealAnswer) {
636
677
  log.info(`[DingTalk][closeStreaming] answerCard 模式:无真实答案,仅定格原卡思考完成(不建答案卡)`);
637
678
  await finalizeOriginalToDone();
638
679
  } else if (answerTokens <= answerActToken) {
@@ -686,14 +727,66 @@ function createDingtalkReplyDispatcher(params) {
686
727
  accumulatedText = "";
687
728
  }
688
729
  };
730
+ /**
731
+ * 统一错误定稿:模型/dispatch 异常时把中文提示写到 AI Card 并 FINISHED。
732
+ * 供 dispatcher onError 与 message-handler catch(同步 throw)共用。
733
+ * 不依赖 OpenClaw 返回的 replyOptions.onError(SDK 根本不导出该字段)。
734
+ */
735
+ const finalizeDispatchError = async (error, kind = "invoke") => {
736
+ const errorMsg = String(error);
737
+ log.error(`[DingTalk][finalizeDispatchError] kind=${kind} failed: ${errorMsg}`);
738
+ await startStreaming();
739
+ const errorText = matchModelErrorText(errorMsg, { includeCatchAll: true }) ?? "⚠️ 模型请求异常,请稍后重试";
740
+ log.warn(`[DingTalk][finalizeDispatchError] 错误提示: ${errorText.slice(0, 80)}`);
741
+ pendingErrorText = errorText;
742
+ accumulatedText = errorText;
743
+ if (!currentCardTarget && !sessionClosed) if (preCreatedCard) try {
744
+ currentCardTarget = preCreatedCard;
745
+ if (!isDirect) registerActiveCard(conversationId, preCreatedCard);
746
+ log.info(`[DingTalk][finalizeDispatchError] 复用即时建卡显示错误(cardInstanceId=${preCreatedCard.cardInstanceId})`);
747
+ } catch (cardErr) {
748
+ log.warn(`[DingTalk][finalizeDispatchError] 复用即时建卡失败:${cardErr.message},降级发送普通消息`);
749
+ try {
750
+ await sendMessage(account.config, sessionWebhook, accumulatedText, {
751
+ useMarkdown: false,
752
+ log: params.runtime.log
753
+ });
754
+ } catch (sendErr) {
755
+ log.error(`[DingTalk][finalizeDispatchError] 降级发送普通消息失败:${sendErr.message}`);
756
+ }
757
+ }
758
+ else {
759
+ log.warn(`[DingTalk][finalizeDispatchError] 无可用 AI Card,降级发送普通消息`);
760
+ try {
761
+ await sendMessage(account.config, sessionWebhook, accumulatedText, {
762
+ useMarkdown: false,
763
+ log: params.runtime.log
764
+ });
765
+ } catch (sendErr) {
766
+ log.error(`[DingTalk][finalizeDispatchError] 降级发送普通消息失败:${sendErr.message}`);
767
+ }
768
+ }
769
+ await closeStreaming();
770
+ };
689
771
  const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
690
772
  ...prefixOptions,
691
773
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
774
+ onSkip: (payload, info) => {
775
+ const raw = readPayloadRawError(payload);
776
+ const isErr = Boolean(payload?.isError);
777
+ if (!raw && !isErr) {
778
+ log.info(`[DingTalk][onSkip] kind=${info?.kind} reason=${info?.reason} 非错误静默(NO_REPLY/empty),跳过错误捕获`);
779
+ return;
780
+ }
781
+ log.warn(`[DingTalk][onSkip] kind=${info?.kind} reason=${info?.reason} isError=${isErr} rawLen=${raw.length}(群聊静默失败常见)`);
782
+ captureErrorFromPayload(payload, `onSkip:${info?.kind ?? "?"}:${info?.reason ?? "?"}`);
783
+ },
692
784
  onReplyStart: () => {
693
785
  log.info(`[DingTalk][onReplyStart] 开始回复,流式 enabled=${streamingEnabled}`);
694
786
  deliveredFinalTexts.clear();
695
787
  finalMarkedText = null;
696
788
  lastAnswerText = "";
789
+ pendingErrorText = null;
697
790
  processMarkerLogged = false;
698
791
  finalMarkerLogged = false;
699
792
  markerSystemActive = false;
@@ -710,6 +803,7 @@ function createDingtalkReplyDispatcher(params) {
710
803
  let text = payload.text ?? "";
711
804
  const rawError = readPayloadRawError(payload);
712
805
  if (rawError) {
806
+ captureErrorFromPayload(payload, `deliver:${info?.kind ?? "?"}`);
713
807
  const matchedErrorText = matchModelErrorText(rawError, { includeCatchAll: true });
714
808
  if (matchedErrorText) {
715
809
  log.warn(`[DingTalk][deliver] 检测到上游 rawError,替换为中文提示(raw=${rawError.slice(0, 120)})`);
@@ -720,14 +814,22 @@ function createDingtalkReplyDispatcher(params) {
720
814
  observeReply(payload.text, payload);
721
815
  await startStreaming();
722
816
  if (isNonAnswerPayload(payload)) {
817
+ const isModelErrorFinal = Boolean(payload.isError) && info?.kind === "final" && Boolean(text.trim());
818
+ if (isModelErrorFinal) {
819
+ accumulatedText = text;
820
+ log.info(`[DingTalk][deliver] 将上游异常 final 文本存入 accumulatedText(len=${text.length}),防止 onIdle 覆盖`);
821
+ } else if (payload.isError && !accumulatedText.trim() && text.trim()) {
822
+ accumulatedText = text;
823
+ log.info(`[DingTalk][deliver] 将上游异常 payload 文本存入 accumulatedText(len=${text.length}),防止 onIdle 覆盖`);
824
+ }
723
825
  if (streamingEnabled && currentCardTarget && !asyncMode && finalMarkedText === null) try {
724
- await enqueueCardStream(displayClean(text));
826
+ await enqueueCardStream(displayClean(text), { force: isModelErrorFinal });
725
827
  } catch (e) {
726
828
  if (!isQpsLimitError(e)) log.warn(`[DingTalk][deliver] 状态/错误 payload 写卡失败:${e?.message || e}`);
727
829
  }
728
- if (payload.isError && !accumulatedText.trim() && text.trim()) {
729
- accumulatedText = text;
730
- log.info(`[DingTalk][deliver] 将上游异常 payload 文本存入 accumulatedText(len=${text.length}),防止 onIdle 覆盖`);
830
+ if (isModelErrorFinal) {
831
+ log.info(`[DingTalk][deliver] 上游 isError final,主动 closeStreaming 定稿错误卡`);
832
+ await closeStreaming();
731
833
  }
732
834
  log.info(`[DingTalk][deliver] 非答案 payload(isError=${payload.isError},isStatusNotice=${payload.isStatusNotice}),仅展示不计入最终答案`);
733
835
  return;
@@ -838,40 +940,9 @@ function createDingtalkReplyDispatcher(params) {
838
940
  }
839
941
  },
840
942
  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();
943
+ log.error(`[DingTalk][onError] ${info.kind} reply failed: ${String(error)}`);
944
+ params.runtime.error?.(`dingtalk[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
945
+ await finalizeDispatchError(error, info.kind);
875
946
  typingCallbacks.onIdle?.();
876
947
  },
877
948
  onIdle: async () => {
@@ -905,7 +976,7 @@ function createDingtalkReplyDispatcher(params) {
905
976
  currentToolLine = "";
906
977
  await startStreaming();
907
978
  if (currentCardTarget) try {
908
- const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-B_A78n8u.mjs");
979
+ const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-DgL28gCZ.mjs");
909
980
  await enqueueCardStream(buildCardContent().replace(FILE_MARKER_PATTERN, "").replace(VIDEO_MARKER_PATTERN, "").replace(AUDIO_MARKER_PATTERN, "").trim(), { force: true });
910
981
  } catch (err) {
911
982
  if (!isQpsLimitError(err)) log.error(`[DingTalk][onPartialReply] final-marker 刷卡失败:${err.message}`);
@@ -918,7 +989,7 @@ function createDingtalkReplyDispatcher(params) {
918
989
  if (payload.text?.trim()) hasModelStreamText = true;
919
990
  currentToolLine = "";
920
991
  try {
921
- const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-B_A78n8u.mjs");
992
+ const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-DgL28gCZ.mjs");
922
993
  await enqueueCardStream(buildCardContent().replace(FILE_MARKER_PATTERN, "").replace(VIDEO_MARKER_PATTERN, "").replace(AUDIO_MARKER_PATTERN, "").trim());
923
994
  } catch (err) {
924
995
  if (isQpsLimitError(err)) log.warn(`[DingTalk][onPartialReply] QPS 限流,已排队尾随重试 latestLen=${latestCardContent.length}`);
@@ -957,12 +1028,20 @@ function createDingtalkReplyDispatcher(params) {
957
1028
  }
958
1029
  },
959
1030
  markDispatchIdle,
960
- getAsyncModeResponse: () => asyncModeFullResponse
1031
+ getAsyncModeResponse: () => asyncModeFullResponse,
1032
+ /** 同步 dispatch 异常时由 message-handler catch 调用,统一 finish 错误卡 */
1033
+ finalizeDispatchError
961
1034
  };
962
1035
  }
963
1036
  //#endregion
964
1037
  //#region src/core/message-handler.ts
965
1038
  /**
1039
+ * 单条会话队列任务最长执行时间。
1040
+ * 503 重试 + 工具循环偶发挂死时,若不超时,后续同会话消息会永远堵在 previousTask 上,
1041
+ * 表现:连发多条「网关没反应 / 不贴表情」(表情在排队 ACK 或 dequeued 后才贴)。
1042
+ */
1043
+ const SESSION_QUEUE_TASK_TIMEOUT_MS = 180 * 1e3;
1044
+ /**
966
1045
  * 会话消息队列管理
967
1046
  * 用于确保同一会话+agent的消息按顺序处理,避免并发冲突导致AI返回空响应
968
1047
  * 队列键格式:{sessionId}:{agentId}
@@ -1926,7 +2005,7 @@ async function handleDingTalkMessageInternal(params) {
1926
2005
  const rawText = content.text || "";
1927
2006
  let userContent = normalizeSlashCommand(rawText) || (content.imageUrls.length > 0 ? "请描述这张图片" : "");
1928
2007
  try {
1929
- const { GamificationEngine, isGamificationCommand } = await import("./game-xiyou-CAT0ilSj.mjs");
2008
+ const { GamificationEngine, isGamificationCommand } = await import("./game-xiyou-uwsv23AC.mjs");
1930
2009
  if (isGamificationCommand(rawText)) {
1931
2010
  const engine = GamificationEngine.getInstanceForUser(senderId);
1932
2011
  if (rawText.trim().startsWith("/西游") || engine.isEnabled()) {
@@ -2100,6 +2179,8 @@ async function handleDingTalkMessageInternal(params) {
2100
2179
  log?.warn?.(`Failed to send acknowledgment: ${ackErr?.message || ackErr}`);
2101
2180
  }
2102
2181
  }
2182
+ let earlyCard = params.preCreatedCard;
2183
+ let finalizeDispatchError;
2103
2184
  try {
2104
2185
  const core = getDingtalkRuntime();
2105
2186
  let finalContent = userContent;
@@ -2165,7 +2246,6 @@ async function handleDingTalkMessageInternal(params) {
2165
2246
  });
2166
2247
  const groupReplyModeForCard = config.groupReplyMode || "aicard";
2167
2248
  const streamingEnabledForCard = !(!isDirect && (groupReplyModeForCard === "text" || groupReplyModeForCard === "markdown")) && config.streaming !== false;
2168
- let earlyCard = params.preCreatedCard;
2169
2249
  if (!earlyCard && streamingEnabledForCard && !asyncMode) try {
2170
2250
  earlyCard = await createAICardForTarget(config, isDirect ? {
2171
2251
  type: "user",
@@ -2183,7 +2263,7 @@ async function handleDingTalkMessageInternal(params) {
2183
2263
  log.warn(`[DingTalk][earlyCard] 即时建卡失败(降级延迟建卡):${e?.message}`);
2184
2264
  earlyCard = void 0;
2185
2265
  }
2186
- const { dispatcher, replyOptions, markDispatchIdle, getAsyncModeResponse } = createDingtalkReplyDispatcher({
2266
+ const { dispatcher, replyOptions, markDispatchIdle, getAsyncModeResponse, finalizeDispatchError: finalizeErr } = createDingtalkReplyDispatcher({
2187
2267
  cfg,
2188
2268
  agentId: matchedAgentId,
2189
2269
  runtime,
@@ -2196,6 +2276,7 @@ async function handleDingTalkMessageInternal(params) {
2196
2276
  asyncMode,
2197
2277
  preCreatedCard: earlyCard
2198
2278
  });
2279
+ finalizeDispatchError = finalizeErr;
2199
2280
  if (config.clientId) {
2200
2281
  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
2282
  finalContent = finalContent ? `${finalContent}\n\n${botIdentityHint}` : botIdentityHint;
@@ -2265,28 +2346,42 @@ async function handleDingTalkMessageInternal(params) {
2265
2346
  }
2266
2347
  } catch (err) {
2267
2348
  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}`);
2349
+ if (finalizeDispatchError) try {
2350
+ log?.info?.(`[DingTalk] 统一错误定稿:调用 finalizeDispatchError 处理同步异常`);
2351
+ await finalizeDispatchError(err, "invoke");
2352
+ } catch (finalizeErr) {
2353
+ log?.error?.(`[DingTalk] finalizeDispatchError 失败: ${finalizeErr?.message || finalizeErr}`);
2273
2354
  }
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}`);
2355
+ else {
2356
+ const errorText = matchModelErrorText(err.message, { includeCatchAll: true }) ?? `⚠️ 抱歉,处理请求时出错: ${err.message}`;
2357
+ let cardSuccess = false;
2358
+ if (earlyCard) try {
2359
+ await finishAICard(earlyCard, errorText, config, log, void 0, void 0, data.conversationId);
2360
+ cardSuccess = true;
2361
+ log?.info?.(`[DingTalk] 极端兜底:finishAICard 错误卡成功`);
2362
+ } catch (cardErr) {
2363
+ log?.error?.(`更新错误卡片失败: ${cardErr.message}`);
2364
+ }
2365
+ if (!cardSuccess) {
2366
+ log?.warn?.(`[DingTalk] 无可用错误定稿路径,走纯文本发送`);
2367
+ try {
2368
+ const token = await getAccessToken(config);
2369
+ const body = {
2370
+ msgtype: "text",
2371
+ text: { content: errorText }
2372
+ };
2373
+ if (!isDirect) body.at = {
2374
+ atUserIds: [senderId],
2375
+ isAtAll: false
2376
+ };
2377
+ await dingtalkHttp.post(sessionWebhook, body, { headers: {
2378
+ "x-acs-dingtalk-access-token": token,
2379
+ "Content-Type": "application/json"
2380
+ } });
2381
+ } catch (fallbackErr) {
2382
+ log?.error?.(`错误消息发送也失败: ${fallbackErr.message}`);
2383
+ }
2384
+ }
2290
2385
  }
2291
2386
  }
2292
2387
  try {
@@ -2334,54 +2429,33 @@ async function handleDingTalkMessage(params) {
2334
2429
  const queueKey = `${baseSessionId}:${matchedAgentId}`;
2335
2430
  try {
2336
2431
  sessionLastActivity.set(queueKey, Date.now());
2337
- const isQueueBusy = sessionQueues.has(queueKey);
2338
2432
  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}`);
2433
+ const isQueueBusy = sessionQueues.has(queueKey);
2434
+ const cardHolder = {
2435
+ card: void 0,
2436
+ ackDone: !isQueueBusy
2437
+ };
2438
+ const currentTask = previousTask.then(async () => {
2439
+ log?.info?.(`[队列] 开始处理消息,queueKey=${queueKey} wasBusy=${isQueueBusy} timeoutMs=${SESSION_QUEUE_TASK_TIMEOUT_MS}`);
2440
+ if (isQueueBusy && !cardHolder.ackDone) {
2441
+ const waitStart = Date.now();
2442
+ while (!cardHolder.ackDone && Date.now() - waitStart < 2e3) await new Promise((r) => setTimeout(r, 50));
2353
2443
  }
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
- }
2444
+ let timeoutId;
2445
+ try {
2446
+ await Promise.race([handleDingTalkMessageInternal({
2447
+ ...params,
2448
+ preCreatedCard: cardHolder.card,
2449
+ emotionAlreadyAdded: true
2450
+ }), new Promise((_, reject) => {
2451
+ timeoutId = setTimeout(() => {
2452
+ reject(/* @__PURE__ */ new Error(`会话队列任务超时(${SESSION_QUEUE_TASK_TIMEOUT_MS}ms),释放队列以免堵死后续消息`));
2453
+ }, SESSION_QUEUE_TASK_TIMEOUT_MS);
2454
+ })]);
2455
+ log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
2456
+ } finally {
2457
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
2375
2458
  }
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
2459
  }).catch((err) => {
2386
2460
  log?.error?.(`[队列] 消息处理异常,queueKey=${queueKey}, error=${err.message}`);
2387
2461
  }).finally(() => {
@@ -2391,9 +2465,51 @@ async function handleDingTalkMessage(params) {
2391
2465
  }
2392
2466
  });
2393
2467
  sessionQueues.set(queueKey, currentTask);
2468
+ trackBackgroundWork(currentTask, queueKey);
2469
+ log?.info?.(`[队列] 已登记 queueKey=${queueKey} busy=${isQueueBusy} depth≈pending`);
2470
+ addEmotionReply(config, data, log).catch((err) => {
2471
+ log?.warn?.(`[队列] 即时贴表情失败: ${err?.message || err}`);
2472
+ });
2473
+ if (isQueueBusy) {
2474
+ const ackPhrases = QUEUE_BUSY_ACK_PHRASES;
2475
+ const ackText = ackPhrases[Math.floor(Math.random() * ackPhrases.length)];
2476
+ const groupReplyMode = config.groupReplyMode || "aicard";
2477
+ const skipAICard = !isDirect && (groupReplyMode === "text" || groupReplyMode === "markdown");
2478
+ (async () => {
2479
+ try {
2480
+ if (skipAICard) {
2481
+ await sendProactive(config, { openConversationId: data.conversationId }, ackText, {
2482
+ msgType: "text",
2483
+ useAICard: false,
2484
+ fallbackToNormal: true
2485
+ });
2486
+ log?.info?.(`[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`);
2487
+ } else {
2488
+ const card = await createAICardForTarget(config, isDirect ? {
2489
+ type: "user",
2490
+ userId: senderId
2491
+ } : {
2492
+ type: "group",
2493
+ openConversationId: data.conversationId
2494
+ }, log);
2495
+ if (card) {
2496
+ await streamAICard(card, ackText, false, config, log);
2497
+ cardHolder.card = card;
2498
+ log?.info?.(`[队列] 队列繁忙,已创建排队 ACK Card,cardInstanceId=${card.cardInstanceId}`);
2499
+ } else log?.warn?.(`[队列] 创建排队 ACK Card 失败(返回 null)`);
2500
+ }
2501
+ } catch (ackErr) {
2502
+ log?.warn?.(`[队列] 繁忙 ACK 异常: ${ackErr?.message || ackErr}`);
2503
+ } finally {
2504
+ cardHolder.ackDone = true;
2505
+ }
2506
+ })();
2507
+ }
2394
2508
  } catch (err) {
2395
2509
  log?.error?.(`[队列] 队列管理异常,直接处理: ${err.message}`);
2396
- handleDingTalkMessageInternal(params);
2510
+ trackBackgroundWork(handleDingTalkMessageInternal(params).catch((e) => {
2511
+ log?.error?.(`[队列] 直接处理也失败: ${e.message}`);
2512
+ }), "queue-fallback");
2397
2513
  }
2398
2514
  }
2399
2515
  //#endregion
@@ -371,7 +371,7 @@ async function finishAICard(card, content, config, log, contentVar, skipInputing
371
371
  const fixedContent = ensureTableBlankLines(cleanContent);
372
372
  log?.info?.(`[DingTalk][AICard] 开始 finish(一次性定稿,无流式回放),最终内容长度=${fixedContent.length}`);
373
373
  try {
374
- const { rememberCardContent } = await import("./card-content-cache-Dwd8HICP.mjs");
374
+ const { rememberCardContent } = await import("./card-content-cache-c4Ls96eJ.mjs");
375
375
  rememberCardContent({
376
376
  text: fixedContent,
377
377
  outTrackId: card.cardInstanceId,
@@ -429,8 +429,12 @@ async function finishAICard(card, content, config, log, contentVar, skipInputing
429
429
  return;
430
430
  } catch (retryErr) {
431
431
  log?.error?.(`[DingTalk][AICard] FINISHED 重试失败:${retryErr.message}`);
432
+ throw retryErr;
432
433
  }
433
- } else log?.error?.(`[DingTalk][AICard] FINISHED 更新失败:${err.message}`);
434
+ } else {
435
+ log?.error?.(`[DingTalk][AICard] FINISHED 更新失败:${err.message}`);
436
+ throw err;
437
+ }
434
438
  }
435
439
  }
436
440
  //#endregion
@@ -3,7 +3,7 @@ import { a as resolveDingtalkAccount, c as addWildcardAllowFrom, d as hasConfigu
3
3
  import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import "./utils-CIfI_3Jh.mjs";
6
- import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-WleKf54h.mjs";
6
+ import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-DVS3UKOp.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { z, z as z$1 } from "zod";
9
9
  //#region src/secret-input.ts
@@ -955,8 +955,8 @@ async function monitorDingtalkProvider(opts = {}) {
955
955
  const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
956
956
  const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
957
957
  import("./accounts-BQptOmgB.mjs"),
958
- import("./message-handler-DQZiKx3i.mjs"),
959
- import("./connection-fc6B4z4G.mjs")
958
+ import("./message-handler-B1usz1wb.mjs"),
959
+ import("./connection-CZ7ZoN5_.mjs")
960
960
  ]);
961
961
  const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
962
962
  const { handleDingTalkMessage } = monitorAccountModule;
@@ -0,0 +1,56 @@
1
+ # Release Notes · v0.8.28
2
+
3
+ **发布日期:** 2026-07-25
4
+ **npm:** `@jeik/dingtalk-connector@0.8.28`
5
+ **定位:** 基于官方 **0.8.24** 的社区增强正式版
6
+
7
+ ---
8
+
9
+ ## 一句话
10
+
11
+ 连接更稳、消息更少丢、模型出错时提示更清楚;官方 0.8.24 的长连接改进已吸收,社区图片/答案卡等增强继续保留。
12
+
13
+ ---
14
+
15
+ ## 基于官方 0.8.24 做了什么
16
+
17
+ - 吸收官方对长连接的稳定性改进(少假死、少误重连)
18
+ - 长任务处理期间连接更不容易被中途打断
19
+ - 日志更干净,方便运维判断「是不是真坏了」
20
+
21
+ 未合入:官方给其他插件调用的「卡片开放接口」(对话主流程不需要)。
22
+
23
+ ---
24
+
25
+ ## 社区额外优化(效果)
26
+
27
+ 1. **真正能收消息才显示已连接** — 减少「显示在线、发了没反应」
28
+ 2. **连发 / 错误后 / 刚回完再发** — 消息进网关的成功率明显提高
29
+ 3. **上一条还在跑时再发** — 更容易马上看到排队提示和思考中表情
30
+ 4. **模型 503、欠费、无可用线路等** — 更容易定格成中文错误说明,少卡转圈
31
+ 5. **原有社区能力** — 大段答案独立卡片、图片全路径、中文错误映射等均保留
32
+
33
+ ---
34
+
35
+ ## 安装
36
+
37
+ ```bash
38
+ npx -y @jeik/dingtalk-connector@0.8.28 install --force
39
+ openclaw gateway restart
40
+ ```
41
+
42
+ 或:
43
+
44
+ ```bash
45
+ openclaw plugins install @jeik/dingtalk-connector@0.8.28 --force
46
+ openclaw gateway restart
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 建议验收
52
+
53
+ 1. 启动后日志出现「订阅已生效 / registered」类成功信息后再发消息
54
+ 2. 正常对话:先「召唤大模型」再流式/答案卡
55
+ 3. 故意用故障模型:应定格中文错误,而不是一直转圈
56
+ 4. 任务进行中再发一条:应较快出现排队/处理中反馈
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.27",
4
- "description": "OpenClaw DingTalk channel plugin (fork) | 钉钉 OpenClaw 渠道插件(fork 自官方,含标记/卡片渲染修复)",
3
+ "version": "0.8.28",
4
+ "description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版(基于官方 0.8.24,长连接与消息体验增强)",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {