@soimy/dingtalk 3.6.6 → 3.6.8

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 (40) hide show
  1. package/dist/index.js +1433 -344
  2. package/dist/index.js.map +4 -4
  3. package/dist/src/card/ask-user-question-context.d.ts +5 -2
  4. package/dist/src/card/ask-user-question-context.d.ts.map +1 -1
  5. package/dist/src/card/ask-user-question-store.d.ts +42 -0
  6. package/dist/src/card/ask-user-question-store.d.ts.map +1 -0
  7. package/dist/src/card/ask-user-question.d.ts +20 -0
  8. package/dist/src/card/ask-user-question.d.ts.map +1 -1
  9. package/dist/src/card/card-action-handler.d.ts +1 -0
  10. package/dist/src/card/card-action-handler.d.ts.map +1 -1
  11. package/dist/src/card-service.d.ts +4 -1
  12. package/dist/src/card-service.d.ts.map +1 -1
  13. package/dist/src/gateway/channel-gateway.d.ts.map +1 -1
  14. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +23 -0
  15. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts.map +1 -0
  16. package/dist/src/gateway/inbound-session-queue.d.ts +46 -0
  17. package/dist/src/gateway/inbound-session-queue.d.ts.map +1 -0
  18. package/dist/src/gateway/reply-session-conflict.d.ts +22 -0
  19. package/dist/src/gateway/reply-session-conflict.d.ts.map +1 -0
  20. package/dist/src/inbound-handler.d.ts.map +1 -1
  21. package/dist/src/onboarding.d.ts.map +1 -1
  22. package/dist/src/targeting/agent-routing.d.ts +17 -1
  23. package/dist/src/targeting/agent-routing.d.ts.map +1 -1
  24. package/dist/src/types.d.ts +35 -0
  25. package/dist/src/types.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/access-control.ts +1 -1
  28. package/src/card/ask-user-question-context.ts +9 -1
  29. package/src/card/ask-user-question-store.ts +294 -0
  30. package/src/card/ask-user-question.ts +398 -37
  31. package/src/card/card-action-handler.ts +2 -0
  32. package/src/card-service.ts +55 -3
  33. package/src/gateway/channel-gateway.ts +51 -30
  34. package/src/gateway/inbound-session-queue-dispatcher.ts +304 -0
  35. package/src/gateway/inbound-session-queue.ts +244 -0
  36. package/src/gateway/reply-session-conflict.ts +82 -0
  37. package/src/inbound-handler.ts +254 -57
  38. package/src/onboarding.ts +6 -2
  39. package/src/targeting/agent-routing.ts +84 -19
  40. package/src/types.ts +36 -0
package/dist/index.js CHANGED
@@ -790,7 +790,7 @@ function cleanupOrphanedTempFiles(log) {
790
790
  let cleaned = 0;
791
791
  try {
792
792
  const files = fs.readdirSync(tempDir);
793
- const now = Date.now();
793
+ const now2 = Date.now();
794
794
  const maxAge = 24 * 60 * 60 * 1e3;
795
795
  for (const file of files) {
796
796
  if (!dingtalkPattern.test(file)) {
@@ -799,7 +799,7 @@ function cleanupOrphanedTempFiles(log) {
799
799
  const filePath = path2.join(tempDir, file);
800
800
  try {
801
801
  const stats = fs.statSync(filePath);
802
- if (now - stats.mtime.getTime() > maxAge) {
802
+ if (now2 - stats.mtime.getTime() > maxAge) {
803
803
  fs.unlinkSync(filePath);
804
804
  cleaned++;
805
805
  log?.debug?.(`[DingTalk] Cleaned up orphaned temp file: ${file}`);
@@ -846,9 +846,9 @@ function getCurrentTimestamp() {
846
846
  var accessTokenCache = /* @__PURE__ */ new Map();
847
847
  async function getAccessToken(config, log) {
848
848
  const cacheKey = config.clientId;
849
- const now = Date.now();
849
+ const now2 = Date.now();
850
850
  const cached = accessTokenCache.get(cacheKey);
851
- if (cached && cached.expiry > now + 6e4) {
851
+ if (cached && cached.expiry > now2 + 6e4) {
852
852
  return cached.accessToken;
853
853
  }
854
854
  const runtimeConfig = await resolveRuntimeConfig(config, log);
@@ -863,7 +863,7 @@ async function getAccessToken(config, log) {
863
863
  );
864
864
  accessTokenCache.set(cacheKey, {
865
865
  accessToken: response.data.accessToken,
866
- expiry: now + response.data.expireIn * 1e3
866
+ expiry: now2 + response.data.expireIn * 1e3
867
867
  });
868
868
  return response.data.accessToken;
869
869
  },
@@ -1050,7 +1050,7 @@ function normalizeAllowFrom(list) {
1050
1050
  function isSenderAllowed(params) {
1051
1051
  const { allow, senderId } = params;
1052
1052
  if (!allow.hasEntries) {
1053
- return true;
1053
+ return false;
1054
1054
  }
1055
1055
  if (allow.hasWildcard) {
1056
1056
  return true;
@@ -2681,7 +2681,7 @@ function normalizePendingState(parsed) {
2681
2681
  updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
2682
2682
  pendingCards: records2.filter(
2683
2683
  (entry) => Boolean(
2684
- entry && typeof entry.accountId === "string" && typeof entry.cardInstanceId === "string" && (entry.outTrackId === void 0 || typeof entry.outTrackId === "string") && typeof entry.conversationId === "string" && (entry.lastContent === void 0 || typeof entry.lastContent === "string") && (entry.lastBlockListJson === void 0 || typeof entry.lastBlockListJson === "string") && (entry.streamLifecycleOpened === void 0 || typeof entry.streamLifecycleOpened === "boolean")
2684
+ entry && typeof entry.accountId === "string" && typeof entry.cardInstanceId === "string" && (entry.outTrackId === void 0 || typeof entry.outTrackId === "string") && (entry.processQueryKey === void 0 || typeof entry.processQueryKey === "string") && typeof entry.conversationId === "string" && (entry.lastContent === void 0 || typeof entry.lastContent === "string") && (entry.lastBlockListJson === void 0 || typeof entry.lastBlockListJson === "string") && (entry.streamLifecycleOpened === void 0 || typeof entry.streamLifecycleOpened === "boolean") && (entry.recoveryAction === void 0 || entry.recoveryAction === "finalize" || entry.recoveryAction === "recall")
2685
2685
  )
2686
2686
  )
2687
2687
  };
@@ -2738,6 +2738,7 @@ function upsertPendingCard(card, storePath, log) {
2738
2738
  accountId: card.accountId,
2739
2739
  cardInstanceId: card.cardInstanceId,
2740
2740
  outTrackId: card.outTrackId,
2741
+ processQueryKey: card.processQueryKey,
2741
2742
  conversationId: card.conversationId,
2742
2743
  contextConversationId: card.contextConversationId,
2743
2744
  createdAt: card.createdAt,
@@ -2800,6 +2801,28 @@ function removePendingCardById(cardInstanceId, storePath, log) {
2800
2801
  state.updatedAt = Date.now();
2801
2802
  writePendingCardState(state, storePath, log);
2802
2803
  }
2804
+ function retainPendingCardForRecovery(card, recoveryAction, log) {
2805
+ if (!card.accountId || !card.storePath) {
2806
+ return;
2807
+ }
2808
+ const state = readPendingCardState(card.storePath, log);
2809
+ const index = state.pendingCards.findIndex((item) => item.cardInstanceId === card.cardInstanceId);
2810
+ if (index < 0) {
2811
+ return;
2812
+ }
2813
+ const existing = state.pendingCards[index];
2814
+ state.pendingCards[index] = {
2815
+ ...existing,
2816
+ state: AICardStatus.FAILED,
2817
+ lastUpdated: Date.now(),
2818
+ lastContent: card.lastStreamedContent ?? existing.lastContent,
2819
+ lastBlockListJson: card.lastBlockListJson ?? existing.lastBlockListJson,
2820
+ streamLifecycleOpened: card.streamLifecycleOpened,
2821
+ recoveryAction
2822
+ };
2823
+ state.updatedAt = Date.now();
2824
+ writePendingCardState(state, card.storePath, log);
2825
+ }
2803
2826
  function listPendingCardsByAccount(accountId, storePath, log) {
2804
2827
  const state = readPendingCardState(storePath, log);
2805
2828
  return state.pendingCards.filter((item) => item.accountId === accountId);
@@ -2827,6 +2850,37 @@ async function ensureFreshToken(card, log) {
2827
2850
  }
2828
2851
  }
2829
2852
  }
2853
+ async function sendTemplateMismatchNotification(card, text, log) {
2854
+ const config = card.config;
2855
+ if (!config) {
2856
+ return;
2857
+ }
2858
+ try {
2859
+ const token = await getAccessToken(config, log);
2860
+ const { targetId, isExplicitUser } = stripTargetPrefix(card.conversationId);
2861
+ const resolvedTarget = resolveOriginalPeerId(targetId);
2862
+ const isGroup = !isExplicitUser && resolvedTarget.startsWith("cid");
2863
+ const url = isGroup ? "https://api.dingtalk.com/v1.0/robot/groupMessages/send" : "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
2864
+ const payload = {
2865
+ robotCode: resolveRobotCode(config),
2866
+ msgKey: "sampleMarkdown",
2867
+ msgParam: JSON.stringify({ title: "OpenClaw \u63D0\u9192", text })
2868
+ };
2869
+ if (isGroup) {
2870
+ payload.openConversationId = resolvedTarget;
2871
+ } else {
2872
+ payload.userIds = [resolvedTarget];
2873
+ }
2874
+ await http_client_default({
2875
+ url,
2876
+ method: "POST",
2877
+ data: payload,
2878
+ headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" }
2879
+ });
2880
+ } catch (sendErr) {
2881
+ log?.warn?.(`[DingTalk][AICard] Failed to send error notification to user: ${sendErr.message}`);
2882
+ }
2883
+ }
2830
2884
  async function sendProactiveCardText(config, conversationId, content, log) {
2831
2885
  try {
2832
2886
  const card = await createAICard(config, conversationId, log, { persistPending: false });
@@ -2867,7 +2921,7 @@ async function finalizePendingCardsByAccount(config, accountId, reason, storePat
2867
2921
  return 0;
2868
2922
  }
2869
2923
  const pendingCards = listPendingCardsByAccount(accountId, storePath, log).filter(
2870
- (item) => !isCardInTerminalState(item.state)
2924
+ (item) => item.recoveryAction === "recall" && mode === "recover" || item.recoveryAction !== "recall" && (!isCardInTerminalState(item.state) || item.recoveryAction === "finalize")
2871
2925
  );
2872
2926
  if (pendingCards.length === 0) {
2873
2927
  return 0;
@@ -2892,6 +2946,7 @@ async function finalizePendingCardsByAccount(config, accountId, reason, storePat
2892
2946
  accountId: entry.accountId,
2893
2947
  storePath,
2894
2948
  outTrackId: entry.outTrackId,
2949
+ processQueryKey: entry.processQueryKey,
2895
2950
  createdAt: entry.createdAt || Date.now(),
2896
2951
  lastUpdated: entry.lastUpdated || Date.now(),
2897
2952
  state: normalizeRecoveredState(entry.state),
@@ -2901,6 +2956,12 @@ async function finalizePendingCardsByAccount(config, accountId, reason, storePat
2901
2956
  streamLifecycleOpened: entry.streamLifecycleOpened
2902
2957
  };
2903
2958
  try {
2959
+ if (entry.recoveryAction === "recall") {
2960
+ if (await recallAICardMessage(card, log)) {
2961
+ finalizedCount += 1;
2962
+ }
2963
+ continue;
2964
+ }
2904
2965
  await finalizeStoppedAICard(card, {
2905
2966
  reason,
2906
2967
  previousContent: entry.lastContent,
@@ -3177,6 +3238,9 @@ async function commitAICardBlocks(card, options, log) {
3177
3238
  } catch (err) {
3178
3239
  const message = err instanceof Error ? err.message : String(err);
3179
3240
  log?.error?.(`[DingTalk][AICard] Finalize via instances API failed: ${message}`);
3241
+ card.state = AICardStatus.FAILED;
3242
+ card.lastUpdated = Date.now();
3243
+ retainPendingCardForRecovery(card, "finalize", log);
3180
3244
  throw err;
3181
3245
  }
3182
3246
  if (card.conversationId && options.content.trim() && card.accountId && card.processQueryKey) {
@@ -3196,6 +3260,35 @@ async function commitAICardBlocks(card, options, log) {
3196
3260
  removePendingCard(card, log);
3197
3261
  log?.info?.(`[DingTalk][AICard] Card finalized: outTrackId=${card.outTrackId || card.cardInstanceId} state=FINISHED`);
3198
3262
  }
3263
+ async function streamAICard(card, content, finished = false, log, options = {}) {
3264
+ if (isCardInTerminalState(card.state)) {
3265
+ log?.debug?.(
3266
+ `[DingTalk][AICard] Skip stream update because card already terminal: outTrackId=${card.cardInstanceId} state=${card.state}`
3267
+ );
3268
+ return;
3269
+ }
3270
+ const template = DINGTALK_CARD_TEMPLATE;
3271
+ try {
3272
+ await putAICardStreamingField(card, template.contentKey, content, finished, log);
3273
+ card.lastStreamedContent = content;
3274
+ if (finished) {
3275
+ card.state = AICardStatus.FINISHED;
3276
+ removePendingCard(card, log);
3277
+ } else if (card.state === AICardStatus.PROCESSING) {
3278
+ card.state = AICardStatus.INPUTING;
3279
+ upsertPendingCard(card, card.storePath, log);
3280
+ }
3281
+ } catch (err) {
3282
+ card.state = AICardStatus.FAILED;
3283
+ card.lastUpdated = Date.now();
3284
+ retainPendingCardForRecovery(card, options.recoveryAction ?? "finalize", log);
3285
+ if (err.response?.status === 500 && err.response?.data?.code === "unknownError") {
3286
+ const errorMsg = "\u26A0\uFE0F **[DingTalk] AI Card \u4E32\u6D41\u66F4\u65B0\u5931\u8D25 (500 unknownError)**\n\n\u8FD9\u901A\u5E38\u8868\u793A\u5F53\u524D\u5185\u7F6E\u6A21\u677F\u5951\u7EA6\u4E0E\u9489\u9489\u4FA7\u6A21\u677F\u5B57\u6BB5\u4E0D\u4E00\u81F4\uFF0C\u5F53\u524D\u53CA\u540E\u7EED\u6D88\u606F\u5C06\u81EA\u52A8\u56DE\u9000\u4E3A Markdown \u53D1\u9001\u3002";
3287
+ await sendTemplateMismatchNotification(card, errorMsg, log);
3288
+ }
3289
+ throw err;
3290
+ }
3291
+ }
3199
3292
  function getCardRecallTarget(card) {
3200
3293
  const { targetId, isExplicitUser } = stripTargetPrefix(card.conversationId);
3201
3294
  const resolvedTarget = resolveOriginalPeerId(targetId);
@@ -3365,9 +3458,9 @@ function ensureSweepTimer() {
3365
3458
  return;
3366
3459
  }
3367
3460
  sweepTimer = setInterval(() => {
3368
- const now = Date.now();
3461
+ const now2 = Date.now();
3369
3462
  for (const [key, record] of records) {
3370
- if (now - record.registeredAt > CARD_RUN_TTL_MS) {
3463
+ if (now2 - record.registeredAt > CARD_RUN_TTL_MS) {
3371
3464
  records.delete(key);
3372
3465
  }
3373
3466
  }
@@ -4944,6 +5037,130 @@ async function handleInboundCommandDispatch(params) {
4944
5037
  return false;
4945
5038
  }
4946
5039
 
5040
+ // src/gateway/inbound-session-queue.ts
5041
+ var SESSION_QUEUE_TTL_MS = 5 * 60 * 1e3;
5042
+ var SESSION_QUEUE_CLEANUP_INTERVAL_MS = 60 * 1e3;
5043
+ var MAX_INBOUND_SESSION_QUEUE_DEPTH = 8;
5044
+ var MAX_INBOUND_SESSION_QUEUE_WAIT_MS = 15 * 60 * 1e3;
5045
+ var sessionQueues = /* @__PURE__ */ new Map();
5046
+ var sessionLastActivity = /* @__PURE__ */ new Map();
5047
+ var sessionQueueDepths = /* @__PURE__ */ new Map();
5048
+ var cleanupTimer = null;
5049
+ var InboundSessionQueueWaitTimeoutError = class extends Error {
5050
+ constructor(queueKey) {
5051
+ super(`Inbound session queue wait timed out for ${queueKey}`);
5052
+ this.name = "InboundSessionQueueWaitTimeoutError";
5053
+ }
5054
+ };
5055
+ function ensureCleanupTimer() {
5056
+ if (cleanupTimer) {
5057
+ return;
5058
+ }
5059
+ cleanupTimer = setInterval(() => {
5060
+ const now2 = Date.now();
5061
+ for (const [key, lastSeen] of sessionLastActivity) {
5062
+ if (now2 - lastSeen > SESSION_QUEUE_TTL_MS && !sessionQueues.has(key)) {
5063
+ sessionLastActivity.delete(key);
5064
+ }
5065
+ }
5066
+ }, SESSION_QUEUE_CLEANUP_INTERVAL_MS);
5067
+ if (typeof cleanupTimer?.unref === "function") {
5068
+ cleanupTimer.unref();
5069
+ }
5070
+ }
5071
+ function isInboundSessionQueueBusy(queueKey) {
5072
+ return sessionQueues.has(queueKey);
5073
+ }
5074
+ function getInboundSessionQueueDepth(queueKey) {
5075
+ return sessionQueueDepths.get(queueKey) ?? 0;
5076
+ }
5077
+ function chainInboundSessionTask(queueKey, task, options = {}) {
5078
+ const hadPriorTask = sessionQueues.has(queueKey);
5079
+ const previousTail = sessionQueues.get(queueKey) ?? Promise.resolve();
5080
+ sessionLastActivity.set(queueKey, Date.now());
5081
+ sessionQueueDepths.set(queueKey, getInboundSessionQueueDepth(queueKey) + 1);
5082
+ ensureCleanupTimer();
5083
+ let timedOut = false;
5084
+ let timeout;
5085
+ let resolveWaitingCaller;
5086
+ let rejectWaitingCaller;
5087
+ const maxQueueWaitMs = Math.max(0, options.maxQueueWaitMs ?? 0);
5088
+ const caller = maxQueueWaitMs > 0 && hadPriorTask ? new Promise((resolve3, reject) => {
5089
+ resolveWaitingCaller = resolve3;
5090
+ rejectWaitingCaller = reject;
5091
+ timeout = setTimeout(() => {
5092
+ timedOut = true;
5093
+ reject(new InboundSessionQueueWaitTimeoutError(queueKey));
5094
+ }, maxQueueWaitMs);
5095
+ if (typeof timeout.unref === "function") {
5096
+ timeout.unref();
5097
+ }
5098
+ }) : void 0;
5099
+ if (caller) {
5100
+ void caller.catch(() => void 0);
5101
+ }
5102
+ const current = previousTail.then(() => {
5103
+ if (timeout) {
5104
+ clearTimeout(timeout);
5105
+ timeout = void 0;
5106
+ }
5107
+ if (timedOut) {
5108
+ throw new InboundSessionQueueWaitTimeoutError(queueKey);
5109
+ }
5110
+ return task();
5111
+ });
5112
+ const tail = current.then(
5113
+ () => void 0,
5114
+ () => {
5115
+ }
5116
+ );
5117
+ sessionQueues.set(queueKey, tail);
5118
+ const cleanup = () => {
5119
+ const nextDepth = Math.max(0, getInboundSessionQueueDepth(queueKey) - 1);
5120
+ if (nextDepth) {
5121
+ sessionQueueDepths.set(queueKey, nextDepth);
5122
+ } else {
5123
+ sessionQueueDepths.delete(queueKey);
5124
+ }
5125
+ if (sessionQueues.get(queueKey) === tail) {
5126
+ sessionQueues.delete(queueKey);
5127
+ sessionLastActivity.delete(queueKey);
5128
+ }
5129
+ };
5130
+ void current.then(cleanup, cleanup);
5131
+ if (!caller) {
5132
+ return current;
5133
+ }
5134
+ void current.then(
5135
+ (value) => {
5136
+ if (timeout) {
5137
+ clearTimeout(timeout);
5138
+ }
5139
+ resolveWaitingCaller?.(value);
5140
+ },
5141
+ (error) => {
5142
+ if (timeout) {
5143
+ clearTimeout(timeout);
5144
+ }
5145
+ rejectWaitingCaller?.(error);
5146
+ }
5147
+ );
5148
+ return caller;
5149
+ }
5150
+ var QUEUE_BUSY_ACK_PHRASES = [
5151
+ "\u4E0A\u4E00\u6761\u8FD8\u6CA1\u7ED3\u675F\uFF0C\u8FD9\u6761\u6211\u5DF2\u7ECF\u8BB0\u4E0B\uFF0C\u7A0D\u540E\u6309\u987A\u5E8F\u7EE7\u7EED\u5904\u7406\u3002",
5152
+ "\u5F53\u524D\u8FD8\u5728\u5FD9\uFF0C\u4F60\u7684\u65B0\u6D88\u606F\u5DF2\u7ECF\u6392\u961F\uFF0C\u4E0A\u4E00\u6761\u5B8C\u6210\u540E\u6211\u9A6C\u4E0A\u7EE7\u7EED\u3002",
5153
+ "\u6211\u8FD9\u8FB9\u8FD8\u5728\u5904\u7406\u4E0A\u4E00\u6761\uFF0C\u8FD9\u6761\u5DF2\u52A0\u5165\u961F\u5217\uFF0C\u5B8C\u6210\u540E\u7EE7\u7EED\u5904\u7406\u3002"
5154
+ ];
5155
+ function pickQueueBusyAckPhrase(seed) {
5156
+ const list = QUEUE_BUSY_ACK_PHRASES;
5157
+ const index = seed === void 0 ? Math.floor(Math.random() * list.length) : seed % list.length;
5158
+ return list[index];
5159
+ }
5160
+
5161
+ // src/send-service.ts
5162
+ import * as path6 from "node:path";
5163
+
4947
5164
  // src/media-utils.ts
4948
5165
  import { randomUUID as randomUUID3 } from "node:crypto";
4949
5166
  import * as os4 from "node:os";
@@ -6217,96 +6434,6 @@ function extractMessageContent(data) {
6217
6434
  };
6218
6435
  }
6219
6436
 
6220
- // src/messaging/attachment-text-extractor.ts
6221
- import fs4 from "node:fs/promises";
6222
- import path6 from "node:path";
6223
- var MAX_EXTRACTED_TEXT_CHARS = 6e3;
6224
- var MAX_ATTACHMENT_EXTRACT_BYTES = 2 * 1024 * 1024;
6225
- async function isFileTooLarge(filePath) {
6226
- const stat = await fs4.stat(filePath);
6227
- return stat.size > MAX_ATTACHMENT_EXTRACT_BYTES;
6228
- }
6229
- function isTextLikeMimeType(mimeType) {
6230
- if (!mimeType) {
6231
- return false;
6232
- }
6233
- return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "application/javascript";
6234
- }
6235
- function normalizeWhitespace(text) {
6236
- return text.replace(/\r\n/g, "\n").split("\0").join("").replace(/[ \t]{2,}/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
6237
- }
6238
- function limitExtractedText(text) {
6239
- const normalized = normalizeWhitespace(text);
6240
- if (!normalized) {
6241
- return null;
6242
- }
6243
- const truncated = normalized.length > MAX_EXTRACTED_TEXT_CHARS;
6244
- const limited = truncated ? `${normalized.slice(0, MAX_EXTRACTED_TEXT_CHARS)}
6245
-
6246
- [\u5185\u5BB9\u5DF2\u622A\u65AD]` : normalized;
6247
- return {
6248
- text: limited,
6249
- truncated,
6250
- sourceType: "text"
6251
- };
6252
- }
6253
- function stripHtml(html) {
6254
- return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&").replace(/&#39;/gi, "'").replace(/&quot;/gi, '"');
6255
- }
6256
- async function extractTextLikeFile(filePath) {
6257
- const raw = await fs4.readFile(filePath, "utf8");
6258
- return limitExtractedText(raw);
6259
- }
6260
- async function extractPdf(filePath) {
6261
- const pdfParseModule = await import("pdf-parse");
6262
- const fileBuffer = await fs4.readFile(filePath);
6263
- const parser = new pdfParseModule.PDFParse({ data: new Uint8Array(fileBuffer) });
6264
- try {
6265
- const result = await parser.getText();
6266
- const limited = limitExtractedText(result.text || "");
6267
- return limited ? { ...limited, sourceType: "pdf" } : null;
6268
- } finally {
6269
- await parser.destroy();
6270
- }
6271
- }
6272
- async function extractDocx(filePath) {
6273
- const mammothModule = await import("mammoth");
6274
- const result = await mammothModule.extractRawText({ path: filePath });
6275
- const limited = limitExtractedText(result.value || "");
6276
- return limited ? { ...limited, sourceType: "docx" } : null;
6277
- }
6278
- async function extractHtml(filePath) {
6279
- const raw = await fs4.readFile(filePath, "utf8");
6280
- const limited = limitExtractedText(stripHtml(raw));
6281
- return limited ? { ...limited, sourceType: "html" } : null;
6282
- }
6283
- async function extractAttachmentText(input) {
6284
- const mimeType = input.mimeType?.toLowerCase();
6285
- const fileName = (input.fileName || path6.basename(input.path)).toLowerCase();
6286
- if (mimeType?.startsWith("image/") || mimeType?.startsWith("audio/") || mimeType?.startsWith("video/")) {
6287
- return null;
6288
- }
6289
- if (await isFileTooLarge(input.path)) {
6290
- return null;
6291
- }
6292
- if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || fileName.endsWith(".docx")) {
6293
- return extractDocx(input.path);
6294
- }
6295
- if (mimeType === "application/pdf" || fileName.endsWith(".pdf")) {
6296
- return extractPdf(input.path);
6297
- }
6298
- if (mimeType === "text/html" || mimeType === "application/xhtml+xml" || fileName.endsWith(".html") || fileName.endsWith(".htm")) {
6299
- return extractHtml(input.path);
6300
- }
6301
- if (isTextLikeMimeType(mimeType) || fileName.endsWith(".txt") || fileName.endsWith(".md") || fileName.endsWith(".markdown") || fileName.endsWith(".csv") || fileName.endsWith(".json") || fileName.endsWith(".xml") || fileName.endsWith(".log")) {
6302
- return extractTextLikeFile(input.path);
6303
- }
6304
- return null;
6305
- }
6306
-
6307
- // src/send-service.ts
6308
- import * as path7 from "node:path";
6309
-
6310
6437
  // src/proactive-risk-registry.ts
6311
6438
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6312
6439
  var store = /* @__PURE__ */ new Map();
@@ -6694,9 +6821,9 @@ async function sendProactiveMedia(config, target, mediaPath, mediaType, options
6694
6821
  const durationMs = uploadedDurationMs ?? await getVoiceDurationMs(mediaPath, mediaType, log, { preReadBuffer: buffer });
6695
6822
  msgParam = JSON.stringify({ mediaId, duration: String(durationMs) });
6696
6823
  } else {
6697
- const filename = path7.basename(mediaPath);
6824
+ const filename = path6.basename(mediaPath);
6698
6825
  const defaultExt = mediaType === "video" ? "mp4" : "file";
6699
- const ext = path7.extname(mediaPath).slice(1) || defaultExt;
6826
+ const ext = path6.extname(mediaPath).slice(1) || defaultExt;
6700
6827
  msgKey = "sampleFile";
6701
6828
  msgParam = JSON.stringify({ mediaId, fileName: filename, fileType: ext });
6702
6829
  }
@@ -6799,7 +6926,7 @@ async function sendMedia(config, target, mediaInput, options = {}) {
6799
6926
  let preparedMedia;
6800
6927
  try {
6801
6928
  preparedMedia = await prepareMediaInput(mediaInput, log, config.mediaUrlAllowlist);
6802
- const mediaPath = preparedMedia.cleanup ? preparedMedia.path : path7.resolve(process.cwd(), preparedMedia.path);
6929
+ const mediaPath = preparedMedia.cleanup ? preparedMedia.path : path6.resolve(process.cwd(), preparedMedia.path);
6803
6930
  const mediaType = resolveOutboundMediaType({
6804
6931
  mediaType: options.mediaType,
6805
6932
  mediaPath,
@@ -6855,7 +6982,7 @@ async function sendBySession(config, sessionWebhook, text, options = {}) {
6855
6982
  mediaLocalRoots: options.mediaLocalRoots
6856
6983
  });
6857
6984
  if (uploadResult) {
6858
- const imageMarkdown = `![${path7.basename(options.mediaPath)}](${uploadResult.mediaId})`;
6985
+ const imageMarkdown = `![${path6.basename(options.mediaPath)}](${uploadResult.mediaId})`;
6859
6986
  text = text ? `${text}
6860
6987
 
6861
6988
  ${imageMarkdown}` : imageMarkdown;
@@ -7038,70 +7165,348 @@ async function sendMessage(config, conversationId, text, options = {}) {
7038
7165
  }
7039
7166
  }
7040
7167
 
7041
- // src/messaging/btw-deliver.ts
7042
- var MAX_QUESTION_LENGTH = 80;
7043
- var LEADING_MENTIONS_RE = /^(?:@\S+\s+)*/u;
7044
- function stripLeadingMentions(text) {
7045
- return text.replace(LEADING_MENTIONS_RE, "");
7046
- }
7047
- function buildBtwBlockquote(senderName, rawQuestion) {
7048
- const stripped = stripLeadingMentions(rawQuestion);
7049
- const codePoints = Array.from(stripped);
7050
- const truncated = codePoints.length > MAX_QUESTION_LENGTH ? `${codePoints.slice(0, MAX_QUESTION_LENGTH).join("")}\u2026` : stripped;
7051
- const senderPrefix = senderName ? `${senderName}: ` : "";
7052
- return `> ${senderPrefix}${truncated}
7053
-
7054
- `;
7168
+ // src/gateway/inbound-session-queue-dispatcher.ts
7169
+ var QUEUE_FULL_ACK = "\u5F53\u524D\u6D88\u606F\u8F83\u591A\uFF0C\u5DF2\u8FBE\u5230\u672C\u4F1A\u8BDD\u6392\u961F\u4E0A\u9650\uFF1B\u8BF7\u7B49\u5F85\u4E0A\u4E00\u8F6E\u5B8C\u6210\u540E\u518D\u53D1\u9001\u3002";
7170
+ var QUEUE_WAIT_TIMEOUT_ACK = "\u4E0A\u4E00\u8F6E\u5904\u7406\u65F6\u95F4\u8F83\u957F\uFF0C\u8FD9\u6761\u6D88\u606F\u672A\u6267\u884C\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u53D1\u9001\u3002";
7171
+ var QUEUE_HANDLER_FAILURE_ACK = "\u672C\u6B21\u5904\u7406\u5F02\u5E38\uFF0C\u672A\u80FD\u5B8C\u6210\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u53D1\u9001\u3002";
7172
+ var MIN_QUEUE_ACK_CARD_VISIBLE_MS = 750;
7173
+ var queuedAckVisibleAt = /* @__PURE__ */ new WeakMap();
7174
+ function shouldPrepareQueueAckCard(input) {
7175
+ return input.dingtalkConfig.messageType === "card";
7176
+ }
7177
+ async function keepQueueAckCardVisible(card) {
7178
+ const visibleAt = queuedAckVisibleAt.get(card);
7179
+ if (!visibleAt) {
7180
+ return;
7181
+ }
7182
+ const remainingMs = MIN_QUEUE_ACK_CARD_VISIBLE_MS - (Date.now() - visibleAt);
7183
+ if (remainingMs > 0) {
7184
+ await new Promise((resolve3) => setTimeout(resolve3, remainingMs));
7185
+ }
7055
7186
  }
7056
- async function deliverBtwReply(args) {
7057
- const blockquote = buildBtwBlockquote(args.senderName, args.rawQuestion);
7058
- const fullText = `${blockquote}${args.replyText}`;
7187
+ async function settleUnusedQueueAckCard(input, card) {
7188
+ if (isCardInTerminalState(card.state)) {
7189
+ return;
7190
+ }
7059
7191
  try {
7060
- const result = await sendMessage(args.config, args.to, fullText, {
7061
- log: args.log,
7062
- accountId: args.accountId,
7063
- storePath: args.storePath,
7064
- conversationId: args.conversationId,
7065
- sessionWebhook: args.sessionWebhook,
7066
- forceMarkdown: true
7067
- });
7068
- if (!result.ok) {
7069
- args.log?.warn?.(
7070
- `[DingTalk] BTW reply delivery returned not-ok: ${result.error ?? "unknown"}`
7071
- );
7192
+ if (await recallAICardMessage(card, input.log)) {
7193
+ return;
7072
7194
  }
7073
- return { ok: result.ok, error: result.error };
7074
7195
  } catch (err) {
7075
- const error = err instanceof Error ? err.message : String(err);
7076
- args.log?.warn?.(`[DingTalk] BTW reply delivery threw: ${error}`);
7077
- return { ok: false, error };
7196
+ input.log?.warn?.(
7197
+ `[DingTalk] Failed to recall unused queue acknowledgement card: ${err instanceof Error ? err.message : String(err)}`
7198
+ );
7078
7199
  }
7200
+ await sendQueueTerminalAck(input, "\u5DF2\u7ED3\u675F\u6392\u961F\u786E\u8BA4\uFF0C\u8BF7\u4EE5\u672C\u6B21\u5B9E\u9645\u56DE\u590D\u4E3A\u51C6\u3002", card);
7079
7201
  }
7080
-
7081
- // src/messaging/quoted-ref.ts
7082
- function firstTrimmedString3(...candidates) {
7083
- for (const candidate of candidates) {
7084
- if (typeof candidate === "string" && candidate.trim()) {
7085
- return candidate.trim();
7086
- }
7202
+ async function settleFailedQueueAckCard(input, card) {
7203
+ if (isCardInTerminalState(card.state)) {
7204
+ return;
7087
7205
  }
7088
- return void 0;
7206
+ await sendQueueTerminalAck(input, QUEUE_HANDLER_FAILURE_ACK, card);
7089
7207
  }
7090
- function firstFiniteNumber(...candidates) {
7091
- for (const candidate of candidates) {
7092
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
7093
- return candidate;
7208
+ async function dispatchInboundViaSessionQueue(input, handler) {
7209
+ const queueKey = input.sessionKey;
7210
+ if (!queueKey) {
7211
+ return handler(void 0);
7212
+ }
7213
+ const wasBusy = isInboundSessionQueueBusy(queueKey);
7214
+ if (getInboundSessionQueueDepth(queueKey) >= MAX_INBOUND_SESSION_QUEUE_DEPTH) {
7215
+ await sendQueueTerminalAck(input, QUEUE_FULL_ACK);
7216
+ return void 0;
7217
+ }
7218
+ let queuedAckState = "queued";
7219
+ const preCreatedCardPromise = wasBusy && shouldPrepareQueueAckCard(input) ? tryPrepareQueueAckCard(
7220
+ input,
7221
+ () => queuedAckState === "timed-out" ? { content: QUEUE_WAIT_TIMEOUT_ACK, finished: true } : { content: pickQueueBusyAckPhrase(), finished: false }
7222
+ ) : void 0;
7223
+ try {
7224
+ return await chainInboundSessionTask(
7225
+ queueKey,
7226
+ async () => {
7227
+ const preCreatedCard = preCreatedCardPromise ? await preCreatedCardPromise : void 0;
7228
+ if (!preCreatedCard) {
7229
+ return handler(void 0);
7230
+ }
7231
+ await keepQueueAckCardVisible(preCreatedCard);
7232
+ let handlerFailed = false;
7233
+ try {
7234
+ return await handler(preCreatedCard);
7235
+ } catch (err) {
7236
+ handlerFailed = true;
7237
+ await settleFailedQueueAckCard(input, preCreatedCard);
7238
+ throw err;
7239
+ } finally {
7240
+ if (!handlerFailed && !isCardInTerminalState(preCreatedCard.state)) {
7241
+ await settleUnusedQueueAckCard(input, preCreatedCard);
7242
+ }
7243
+ }
7244
+ },
7245
+ {
7246
+ maxQueueWaitMs: wasBusy ? MAX_INBOUND_SESSION_QUEUE_WAIT_MS : void 0
7247
+ }
7248
+ );
7249
+ } catch (err) {
7250
+ if (err instanceof InboundSessionQueueWaitTimeoutError) {
7251
+ queuedAckState = "timed-out";
7252
+ await sendQueueTerminalAck(
7253
+ input,
7254
+ QUEUE_WAIT_TIMEOUT_ACK,
7255
+ preCreatedCardPromise ? await preCreatedCardPromise : void 0
7256
+ );
7257
+ return void 0;
7094
7258
  }
7259
+ throw err;
7095
7260
  }
7096
- return void 0;
7097
7261
  }
7098
- function buildInboundQuotedRef(data, content) {
7099
- const repliedMsg = data.text?.repliedMsg;
7100
- const repliedMsgId = firstTrimmedString3(repliedMsg?.msgId, data.originalMsgId, content.quoted?.msgId);
7101
- const fallbackCreatedAt = firstFiniteNumber(
7102
- repliedMsg?.createdAt,
7103
- content.quoted?.cardCreatedAt,
7104
- content.quoted?.fileCreatedAt
7262
+ async function tryPrepareQueueAckCard(input, ack) {
7263
+ const { dingtalkConfig, data, log, to, storePath, quoteContent } = input;
7264
+ if (!data) {
7265
+ return void 0;
7266
+ }
7267
+ if (!to) {
7268
+ return void 0;
7269
+ }
7270
+ let card = null;
7271
+ let ackFinished = false;
7272
+ try {
7273
+ card = await createAICard(dingtalkConfig, to, log, {
7274
+ accountId: input.accountId,
7275
+ storePath,
7276
+ quoteContent
7277
+ });
7278
+ if (!card) {
7279
+ return void 0;
7280
+ }
7281
+ const { content, finished } = ack();
7282
+ ackFinished = finished;
7283
+ await streamAICard(card, content, finished, log, {
7284
+ recoveryAction: finished ? "finalize" : "recall"
7285
+ });
7286
+ if (!finished) {
7287
+ queuedAckVisibleAt.set(card, Date.now());
7288
+ }
7289
+ if (finished) {
7290
+ return card;
7291
+ }
7292
+ void attachNativeAckReaction(
7293
+ dingtalkConfig,
7294
+ { msgId: data.msgId, conversationId: data.conversationId },
7295
+ log
7296
+ ).catch((err) => {
7297
+ log?.debug?.(
7298
+ `[DingTalk] Queue-busy ack reaction attach failed: ${err instanceof Error ? err.message : String(err)}`
7299
+ );
7300
+ });
7301
+ log?.info?.(
7302
+ `[DingTalk] Inbound message queued behind active run for session=${input.sessionKey}; pre-created ACK card outTrackId=${card.cardInstanceId}.`
7303
+ );
7304
+ return card;
7305
+ } catch (err) {
7306
+ if (card && !ackFinished) {
7307
+ try {
7308
+ await recallAICardMessage(card, log);
7309
+ } catch (recallErr) {
7310
+ log?.warn?.(
7311
+ `[DingTalk] Failed to recall queue ACK card after prepare failure: ${recallErr instanceof Error ? recallErr.message : String(recallErr)}`
7312
+ );
7313
+ }
7314
+ }
7315
+ log?.warn?.(
7316
+ `[DingTalk] Queue-busy ACK card prepare failed: ${err instanceof Error ? err.message : String(err)}`
7317
+ );
7318
+ return void 0;
7319
+ }
7320
+ }
7321
+ async function sendQueueTerminalAck(input, content, preCreatedCard) {
7322
+ const { dingtalkConfig, data, log, to, storePath } = input;
7323
+ try {
7324
+ if (preCreatedCard) {
7325
+ try {
7326
+ await streamAICard(preCreatedCard, content, true, log);
7327
+ return;
7328
+ } catch (err) {
7329
+ log?.warn?.(
7330
+ `[DingTalk] Queue acknowledgement card finalization failed; falling back to text: ${err instanceof Error ? err.message : String(err)}`
7331
+ );
7332
+ }
7333
+ } else {
7334
+ const card = await tryPrepareQueueAckCard(input, () => ({ content, finished: true }));
7335
+ if (card) {
7336
+ return;
7337
+ }
7338
+ }
7339
+ if (!to) {
7340
+ return;
7341
+ }
7342
+ const result = await sendMessage(dingtalkConfig, to, content, {
7343
+ sessionWebhook: data.sessionWebhook,
7344
+ log,
7345
+ accountId: input.accountId,
7346
+ storePath,
7347
+ conversationId: data.conversationId
7348
+ });
7349
+ if (!result.ok) {
7350
+ log?.warn?.(`[DingTalk] Queue terminal acknowledgement failed: ${result.error || "unknown"}`);
7351
+ }
7352
+ } catch (err) {
7353
+ log?.warn?.(
7354
+ `[DingTalk] Queue terminal acknowledgement delivery failed: ${err instanceof Error ? err.message : String(err)}`
7355
+ );
7356
+ }
7357
+ }
7358
+
7359
+ // src/messaging/attachment-text-extractor.ts
7360
+ import fs4 from "node:fs/promises";
7361
+ import path7 from "node:path";
7362
+ var MAX_EXTRACTED_TEXT_CHARS = 6e3;
7363
+ var MAX_ATTACHMENT_EXTRACT_BYTES = 2 * 1024 * 1024;
7364
+ async function isFileTooLarge(filePath) {
7365
+ const stat = await fs4.stat(filePath);
7366
+ return stat.size > MAX_ATTACHMENT_EXTRACT_BYTES;
7367
+ }
7368
+ function isTextLikeMimeType(mimeType) {
7369
+ if (!mimeType) {
7370
+ return false;
7371
+ }
7372
+ return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "application/javascript";
7373
+ }
7374
+ function normalizeWhitespace(text) {
7375
+ return text.replace(/\r\n/g, "\n").split("\0").join("").replace(/[ \t]{2,}/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
7376
+ }
7377
+ function limitExtractedText(text) {
7378
+ const normalized = normalizeWhitespace(text);
7379
+ if (!normalized) {
7380
+ return null;
7381
+ }
7382
+ const truncated = normalized.length > MAX_EXTRACTED_TEXT_CHARS;
7383
+ const limited = truncated ? `${normalized.slice(0, MAX_EXTRACTED_TEXT_CHARS)}
7384
+
7385
+ [\u5185\u5BB9\u5DF2\u622A\u65AD]` : normalized;
7386
+ return {
7387
+ text: limited,
7388
+ truncated,
7389
+ sourceType: "text"
7390
+ };
7391
+ }
7392
+ function stripHtml(html) {
7393
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&").replace(/&#39;/gi, "'").replace(/&quot;/gi, '"');
7394
+ }
7395
+ async function extractTextLikeFile(filePath) {
7396
+ const raw = await fs4.readFile(filePath, "utf8");
7397
+ return limitExtractedText(raw);
7398
+ }
7399
+ async function extractPdf(filePath) {
7400
+ const pdfParseModule = await import("pdf-parse");
7401
+ const fileBuffer = await fs4.readFile(filePath);
7402
+ const parser = new pdfParseModule.PDFParse({ data: new Uint8Array(fileBuffer) });
7403
+ try {
7404
+ const result = await parser.getText();
7405
+ const limited = limitExtractedText(result.text || "");
7406
+ return limited ? { ...limited, sourceType: "pdf" } : null;
7407
+ } finally {
7408
+ await parser.destroy();
7409
+ }
7410
+ }
7411
+ async function extractDocx(filePath) {
7412
+ const mammothModule = await import("mammoth");
7413
+ const result = await mammothModule.extractRawText({ path: filePath });
7414
+ const limited = limitExtractedText(result.value || "");
7415
+ return limited ? { ...limited, sourceType: "docx" } : null;
7416
+ }
7417
+ async function extractHtml(filePath) {
7418
+ const raw = await fs4.readFile(filePath, "utf8");
7419
+ const limited = limitExtractedText(stripHtml(raw));
7420
+ return limited ? { ...limited, sourceType: "html" } : null;
7421
+ }
7422
+ async function extractAttachmentText(input) {
7423
+ const mimeType = input.mimeType?.toLowerCase();
7424
+ const fileName = (input.fileName || path7.basename(input.path)).toLowerCase();
7425
+ if (mimeType?.startsWith("image/") || mimeType?.startsWith("audio/") || mimeType?.startsWith("video/")) {
7426
+ return null;
7427
+ }
7428
+ if (await isFileTooLarge(input.path)) {
7429
+ return null;
7430
+ }
7431
+ if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || fileName.endsWith(".docx")) {
7432
+ return extractDocx(input.path);
7433
+ }
7434
+ if (mimeType === "application/pdf" || fileName.endsWith(".pdf")) {
7435
+ return extractPdf(input.path);
7436
+ }
7437
+ if (mimeType === "text/html" || mimeType === "application/xhtml+xml" || fileName.endsWith(".html") || fileName.endsWith(".htm")) {
7438
+ return extractHtml(input.path);
7439
+ }
7440
+ if (isTextLikeMimeType(mimeType) || fileName.endsWith(".txt") || fileName.endsWith(".md") || fileName.endsWith(".markdown") || fileName.endsWith(".csv") || fileName.endsWith(".json") || fileName.endsWith(".xml") || fileName.endsWith(".log")) {
7441
+ return extractTextLikeFile(input.path);
7442
+ }
7443
+ return null;
7444
+ }
7445
+
7446
+ // src/messaging/btw-deliver.ts
7447
+ var MAX_QUESTION_LENGTH = 80;
7448
+ var LEADING_MENTIONS_RE = /^(?:@\S+\s+)*/u;
7449
+ function stripLeadingMentions(text) {
7450
+ return text.replace(LEADING_MENTIONS_RE, "");
7451
+ }
7452
+ function buildBtwBlockquote(senderName, rawQuestion) {
7453
+ const stripped = stripLeadingMentions(rawQuestion);
7454
+ const codePoints = Array.from(stripped);
7455
+ const truncated = codePoints.length > MAX_QUESTION_LENGTH ? `${codePoints.slice(0, MAX_QUESTION_LENGTH).join("")}\u2026` : stripped;
7456
+ const senderPrefix = senderName ? `${senderName}: ` : "";
7457
+ return `> ${senderPrefix}${truncated}
7458
+
7459
+ `;
7460
+ }
7461
+ async function deliverBtwReply(args) {
7462
+ const blockquote = buildBtwBlockquote(args.senderName, args.rawQuestion);
7463
+ const fullText = `${blockquote}${args.replyText}`;
7464
+ try {
7465
+ const result = await sendMessage(args.config, args.to, fullText, {
7466
+ log: args.log,
7467
+ accountId: args.accountId,
7468
+ storePath: args.storePath,
7469
+ conversationId: args.conversationId,
7470
+ sessionWebhook: args.sessionWebhook,
7471
+ forceMarkdown: true
7472
+ });
7473
+ if (!result.ok) {
7474
+ args.log?.warn?.(
7475
+ `[DingTalk] BTW reply delivery returned not-ok: ${result.error ?? "unknown"}`
7476
+ );
7477
+ }
7478
+ return { ok: result.ok, error: result.error };
7479
+ } catch (err) {
7480
+ const error = err instanceof Error ? err.message : String(err);
7481
+ args.log?.warn?.(`[DingTalk] BTW reply delivery threw: ${error}`);
7482
+ return { ok: false, error };
7483
+ }
7484
+ }
7485
+
7486
+ // src/messaging/quoted-ref.ts
7487
+ function firstTrimmedString3(...candidates) {
7488
+ for (const candidate of candidates) {
7489
+ if (typeof candidate === "string" && candidate.trim()) {
7490
+ return candidate.trim();
7491
+ }
7492
+ }
7493
+ return void 0;
7494
+ }
7495
+ function firstFiniteNumber(...candidates) {
7496
+ for (const candidate of candidates) {
7497
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
7498
+ return candidate;
7499
+ }
7500
+ }
7501
+ return void 0;
7502
+ }
7503
+ function buildInboundQuotedRef(data, content) {
7504
+ const repliedMsg = data.text?.repliedMsg;
7505
+ const repliedMsgId = firstTrimmedString3(repliedMsg?.msgId, data.originalMsgId, content.quoted?.msgId);
7506
+ const fallbackCreatedAt = firstFiniteNumber(
7507
+ repliedMsg?.createdAt,
7508
+ content.quoted?.cardCreatedAt,
7509
+ content.quoted?.fileCreatedAt
7105
7510
  );
7106
7511
  const isOutboundQuoted = firstTrimmedString3(data.originalProcessQueryKey) !== void 0 || repliedMsg?.senderId === data.chatbotUserId || content.quoted?.isQuotedCard === true;
7107
7512
  if (isOutboundQuoted) {
@@ -7622,6 +8027,47 @@ async function resolveQuotedFile(config, params, log) {
7622
8027
  }
7623
8028
  }
7624
8029
 
8030
+ // src/gateway/reply-session-conflict.ts
8031
+ var REPLY_SESSION_CONFLICT_PATTERN = /reply session initialization conflicted/i;
8032
+ function readErrorMessage(error) {
8033
+ if (error instanceof Error) {
8034
+ return error.message;
8035
+ }
8036
+ if (typeof error === "string") {
8037
+ return error;
8038
+ }
8039
+ if (error && typeof error === "object" && "message" in error) {
8040
+ const msg = error.message;
8041
+ return typeof msg === "string" ? msg : "";
8042
+ }
8043
+ return "";
8044
+ }
8045
+ function isReplySessionConflictError(error) {
8046
+ return REPLY_SESSION_CONFLICT_PATTERN.test(readErrorMessage(error));
8047
+ }
8048
+ var sleep = (ms) => new Promise((resolve3) => {
8049
+ setTimeout(resolve3, ms);
8050
+ });
8051
+ async function withReplySessionConflictRetry(fn, options = {}) {
8052
+ const maxRetries = options.maxRetries ?? 3;
8053
+ const baseDelayMs = options.baseDelayMs ?? 1500;
8054
+ const sessionLabel = options.sessionKey ?? "?";
8055
+ for (let attempt = 0; ; attempt += 1) {
8056
+ try {
8057
+ return await fn();
8058
+ } catch (error) {
8059
+ if (!isReplySessionConflictError(error) || attempt >= maxRetries) {
8060
+ throw error;
8061
+ }
8062
+ const delay = baseDelayMs * (attempt + 1);
8063
+ options.log?.warn?.(
8064
+ `[DingTalk] Reply session initialization conflicted for session=${sessionLabel}; active run still draining. Retry ${attempt + 1}/${maxRetries} after ${delay}ms.`
8065
+ );
8066
+ await sleep(delay);
8067
+ }
8068
+ }
8069
+ }
8070
+
7625
8071
  // src/card/reasoning-answer-split.ts
7626
8072
  var THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
7627
8073
  function isWrappedReasoningLine(line) {
@@ -9789,10 +10235,62 @@ async function dispatchSubAgents(params) {
9789
10235
  dingtalkConfig,
9790
10236
  sessionWebhook,
9791
10237
  extractedContent,
10238
+ sessionPeer,
10239
+ onRoutesResolved,
9792
10240
  handleMessage,
9793
10241
  downloadMedia: download,
9794
- log
10242
+ log,
10243
+ inboundQueueEligible
9795
10244
  } = params;
10245
+ let helperMissingWarningSent = false;
10246
+ const sendHelperMissingWarning = async () => {
10247
+ if (helperMissingWarningSent) {
10248
+ return;
10249
+ }
10250
+ helperMissingWarningSent = true;
10251
+ try {
10252
+ const isGroup = data.conversationType !== "1";
10253
+ const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
10254
+ await sendBySession(
10255
+ dingtalkConfig,
10256
+ sessionWebhook,
10257
+ "\u26A0\uFE0F \u5F53\u524D\u5BBF\u4E3B\u7248\u672C\u4E0D\u652F\u6301 DingTalk \u5B50\u52A9\u624B\u8DEF\u7531\u6240\u9700\u7684 session helper\uFF0C\u8BF7\u5347\u7EA7 OpenClaw \u540E\u91CD\u8BD5\u3002",
10258
+ sendOptions
10259
+ );
10260
+ } catch (notifyError) {
10261
+ log?.debug?.(
10262
+ `[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
10263
+ );
10264
+ }
10265
+ };
10266
+ let resolvedTargets;
10267
+ try {
10268
+ const rt = getDingTalkRuntime();
10269
+ resolvedTargets = matchedAgents.map((agent) => ({
10270
+ agent,
10271
+ route: {
10272
+ agentId: agent.agentId,
10273
+ sessionKey: buildAgentSessionKey({
10274
+ rt,
10275
+ cfg,
10276
+ accountId,
10277
+ agentId: agent.agentId,
10278
+ peerKind: sessionPeer.kind,
10279
+ peerId: sessionPeer.peerId
10280
+ }),
10281
+ mainSessionKey: ""
10282
+ }
10283
+ }));
10284
+ } catch (error) {
10285
+ const message = getErrorMessage(error);
10286
+ log?.error?.(`[DingTalk] Failed to resolve sub-agent routes: ${message}`);
10287
+ if (error instanceof HostRoutingHelperUnavailableError) {
10288
+ await sendHelperMissingWarning();
10289
+ return;
10290
+ }
10291
+ throw error;
10292
+ }
10293
+ onRoutesResolved?.(resolvedTargets);
9796
10294
  let preDownloadedMedia;
9797
10295
  const robotCode = resolveRobotCode(dingtalkConfig);
9798
10296
  if (robotCode) {
@@ -9815,8 +10313,8 @@ async function dispatchSubAgents(params) {
9815
10313
  };
9816
10314
  }
9817
10315
  }
9818
- let helperMissingWarningSent = false;
9819
- for (const agentMatch of matchedAgents) {
10316
+ for (const target of resolvedTargets) {
10317
+ const agentMatch = target.agent;
9820
10318
  try {
9821
10319
  await handleMessage({
9822
10320
  cfg,
@@ -9825,6 +10323,7 @@ async function dispatchSubAgents(params) {
9825
10323
  sessionWebhook,
9826
10324
  log,
9827
10325
  dingtalkConfig,
10326
+ routeOverride: target.route,
9828
10327
  subAgentOptions: {
9829
10328
  agentId: agentMatch.agentId,
9830
10329
  responsePrefix: commandText ? "" : `> \u{1F916} **${sanitizeAgentName(agentMatch.matchedName)}**:
@@ -9833,27 +10332,20 @@ async function dispatchSubAgents(params) {
9833
10332
  matchedName: agentMatch.matchedName,
9834
10333
  commandText
9835
10334
  },
9836
- preDownloadedMedia
10335
+ preDownloadedMedia,
10336
+ // Propagate queue eligibility so each recursive sub-agent handler
10337
+ // enters the handler-owned queue on its own `target.route.sessionKey`
10338
+ // instead of bypassing it (which previously left @子Agent messages
10339
+ // exposed to reply-session conflicts when the sub-agent session was
10340
+ // already busy). Synthetic callers without this flag keep the legacy
10341
+ // direct-dispatch behavior.
10342
+ inboundQueueEligible
9837
10343
  });
9838
10344
  } catch (error) {
9839
10345
  const message = getErrorMessage(error);
9840
10346
  log?.error?.(`[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`);
9841
- if (error instanceof HostRoutingHelperUnavailableError && !helperMissingWarningSent) {
9842
- helperMissingWarningSent = true;
9843
- try {
9844
- const isGroup = data.conversationType !== "1";
9845
- const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
9846
- await sendBySession(
9847
- dingtalkConfig,
9848
- sessionWebhook,
9849
- "\u26A0\uFE0F \u5F53\u524D\u5BBF\u4E3B\u7248\u672C\u4E0D\u652F\u6301 DingTalk \u5B50\u52A9\u624B\u8DEF\u7531\u6240\u9700\u7684 session helper\uFF0C\u8BF7\u5347\u7EA7 OpenClaw \u540E\u91CD\u8BD5\u3002",
9850
- sendOptions
9851
- );
9852
- } catch (notifyError) {
9853
- log?.debug?.(
9854
- `[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
9855
- );
9856
- }
10347
+ if (error instanceof HostRoutingHelperUnavailableError) {
10348
+ await sendHelperMissingWarning();
9857
10349
  }
9858
10350
  }
9859
10351
  }
@@ -10555,6 +11047,8 @@ async function handleDingTalkMessageInner(params) {
10555
11047
  sessionWebhook,
10556
11048
  log,
10557
11049
  dingtalkConfig,
11050
+ inboundOrigin = "stream",
11051
+ routeOverride,
10558
11052
  subAgentOptions,
10559
11053
  preDownloadedMedia
10560
11054
  } = params;
@@ -10749,32 +11243,42 @@ async function handleDingTalkMessageInner(params) {
10749
11243
  config: dingtalkConfig
10750
11244
  });
10751
11245
  const messageTarget = subAgentOptions ? null : resolveMessageTarget({ extractedContent, cfg, isGroup });
10752
- let route;
10753
- if (subAgentOptions) {
10754
- route = {
10755
- agentId: subAgentOptions.agentId,
10756
- sessionKey: buildAgentSessionKey({
10757
- rt,
10758
- cfg,
10759
- accountId,
10760
- agentId: subAgentOptions.agentId,
10761
- peerKind: sessionPeer.kind,
10762
- peerId: sessionPeer.peerId
10763
- }),
10764
- mainSessionKey: ""
10765
- };
10766
- } else {
10767
- route = rt.channel.routing.resolveAgentRoute({
10768
- cfg,
10769
- channel: "dingtalk",
10770
- accountId,
10771
- peer: { kind: sessionPeer.kind, id: sessionPeer.peerId }
11246
+ const invalidateQuestionRoutes = (routes) => {
11247
+ if (inboundOrigin === "ask-user") {
11248
+ return;
11249
+ }
11250
+ const scopeKeys = new Set(
11251
+ routes.map((resolvedRoute) => `${accountId}:${resolvedRoute.sessionKey}:${senderId}`)
11252
+ );
11253
+ const invalidatedRecords = [];
11254
+ for (const questionScopeKey of scopeKeys) {
11255
+ try {
11256
+ invalidatedRecords.push(
11257
+ ...invalidateAskUserQuestionsForScope({
11258
+ storePath: accountStorePath,
11259
+ accountId,
11260
+ questionScopeKey,
11261
+ reason: "superseded_by_message",
11262
+ log
11263
+ })
11264
+ );
11265
+ } catch (err) {
11266
+ log?.warn?.(
11267
+ `[DingTalk][AskUser] Failed to invalidate pending cards before inbound dispatch scope=${questionScopeKey}: ${String(err)}`
11268
+ );
11269
+ }
11270
+ }
11271
+ if (invalidatedRecords.length === 0) {
11272
+ return;
11273
+ }
11274
+ void syncInvalidatedAskUserQuestionCards({
11275
+ records: invalidatedRecords,
11276
+ config: dingtalkConfig,
11277
+ log
11278
+ }).catch((err) => {
11279
+ log?.warn?.(`[DingTalk][AskUser] Card invalidation sync failed: ${String(err)}`);
10772
11280
  });
10773
- }
10774
- const questionContext = getDingTalkQuestionContext();
10775
- if (questionContext) {
10776
- questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
10777
- }
11281
+ };
10778
11282
  if (messageTarget && messageTarget.kind !== "default") {
10779
11283
  if (messageTarget.kind === "subagent-command") {
10780
11284
  await dispatchSubAgents({
@@ -10786,9 +11290,12 @@ async function handleDingTalkMessageInner(params) {
10786
11290
  dingtalkConfig,
10787
11291
  sessionWebhook,
10788
11292
  extractedContent,
11293
+ sessionPeer,
11294
+ onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
10789
11295
  handleMessage: handleDingTalkMessage,
10790
11296
  downloadMedia,
10791
- log
11297
+ log,
11298
+ inboundQueueEligible: params.inboundQueueEligible
10792
11299
  });
10793
11300
  return;
10794
11301
  }
@@ -10814,13 +11321,54 @@ async function handleDingTalkMessageInner(params) {
10814
11321
  dingtalkConfig,
10815
11322
  sessionWebhook,
10816
11323
  extractedContent,
11324
+ sessionPeer,
11325
+ onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
10817
11326
  handleMessage: handleDingTalkMessage,
10818
11327
  downloadMedia,
10819
- log
11328
+ log,
11329
+ inboundQueueEligible: params.inboundQueueEligible
10820
11330
  });
10821
11331
  return;
10822
11332
  }
10823
11333
  }
11334
+ let route;
11335
+ if (routeOverride) {
11336
+ route = routeOverride;
11337
+ } else if (subAgentOptions) {
11338
+ route = {
11339
+ agentId: subAgentOptions.agentId,
11340
+ sessionKey: buildAgentSessionKey({
11341
+ rt,
11342
+ cfg,
11343
+ accountId,
11344
+ agentId: subAgentOptions.agentId,
11345
+ peerKind: sessionPeer.kind,
11346
+ peerId: sessionPeer.peerId
11347
+ }),
11348
+ mainSessionKey: ""
11349
+ };
11350
+ } else {
11351
+ route = rt.channel.routing.resolveAgentRoute({
11352
+ cfg,
11353
+ channel: "dingtalk",
11354
+ accountId,
11355
+ peer: { kind: sessionPeer.kind, id: sessionPeer.peerId }
11356
+ });
11357
+ }
11358
+ const questionContext = getDingTalkQuestionContext();
11359
+ if (questionContext) {
11360
+ questionContext.resolvedRoute = route;
11361
+ questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
11362
+ questionContext.storePath = accountStorePath;
11363
+ questionContext.continuationSubAgentOptions = subAgentOptions ? {
11364
+ agentId: subAgentOptions.agentId,
11365
+ responsePrefix: subAgentOptions.responsePrefix,
11366
+ matchedName: subAgentOptions.matchedName
11367
+ } : void 0;
11368
+ }
11369
+ if (!subAgentOptions) {
11370
+ invalidateQuestionRoutes([route]);
11371
+ }
10824
11372
  const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
10825
11373
  agentId: route.agentId
10826
11374
  });
@@ -10856,7 +11404,29 @@ async function handleDingTalkMessageInner(params) {
10856
11404
  const quotedRef = buildInboundQuotedRef(data, extractedContent);
10857
11405
  const replyQuotedRef = createReplyQuotedRef(data.msgId);
10858
11406
  const content = extractedContent;
10859
- const isBtwBypass = isBtwRequestText(stripLeadingMentions(content.text).trim());
11407
+ const controlText = stripLeadingMentions(content.text).trim();
11408
+ const isBtwBypass = isBtwRequestText(controlText);
11409
+ const isAbortBypass = isAbortRequestText(controlText);
11410
+ if (params.inboundQueueEligible && !params.inboundQueueHandled && inboundOrigin !== "ask-user" && !isBtwBypass && !isAbortBypass) {
11411
+ await dispatchInboundViaSessionQueue(
11412
+ {
11413
+ accountId,
11414
+ data,
11415
+ dingtalkConfig,
11416
+ sessionKey: route.sessionKey,
11417
+ to,
11418
+ storePath: accountStorePath,
11419
+ quoteContent: rawInboundText.slice(0, 200),
11420
+ log
11421
+ },
11422
+ (preCreatedCard) => handleDingTalkMessage({
11423
+ ...params,
11424
+ preCreatedCard,
11425
+ inboundQueueHandled: true
11426
+ })
11427
+ );
11428
+ return;
11429
+ }
10860
11430
  const taskInfoConversationId = groupId || to;
10861
11431
  const agentDisplayName = getAgentDisplayName({
10862
11432
  subAgentOptions,
@@ -10887,7 +11457,6 @@ async function handleDingTalkMessageInner(params) {
10887
11457
  let cardFlightKey;
10888
11458
  if (questionContext) {
10889
11459
  questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
10890
- questionCardTookOver = true;
10891
11460
  if (cardFlightKey) {
10892
11461
  cardCreationInFlight.delete(cardFlightKey);
10893
11462
  cardFlightKey = void 0;
@@ -10908,26 +11477,37 @@ async function handleDingTalkMessageInner(params) {
10908
11477
  log?.warn?.(
10909
11478
  `[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`
10910
11479
  );
11480
+ return false;
10911
11481
  }
11482
+ questionCardTookOver = true;
10912
11483
  if (!currentAICard) {
10913
- return;
11484
+ return true;
10914
11485
  }
10915
11486
  if (isCardInTerminalState(currentAICard.state)) {
10916
- return;
11487
+ return true;
11488
+ }
11489
+ let recalled = false;
11490
+ try {
11491
+ recalled = await recallAICardMessage(currentAICard, log);
11492
+ } catch (err) {
11493
+ log?.warn?.(
11494
+ `[DingTalk][AskUser] Question card took over, but AI card recall errored question=${questionId} outTrackId=${outTrackId}: ${err instanceof Error ? err.message : String(err)}`
11495
+ );
11496
+ return true;
10917
11497
  }
10918
- const recalled = await recallAICardMessage(currentAICard, log);
10919
11498
  if (!recalled) {
10920
11499
  log?.warn?.(
10921
11500
  `[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`
10922
11501
  );
10923
- return;
11502
+ return true;
10924
11503
  }
10925
11504
  log?.info?.(
10926
11505
  `[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`
10927
11506
  );
11507
+ return true;
10928
11508
  };
10929
11509
  }
10930
- if (useCardMode && !isBtwBypass) {
11510
+ if (useCardMode && !isBtwBypass && !params.preCreatedCard) {
10931
11511
  const key = `${accountId}:${to}`;
10932
11512
  if (cardCreationInFlight.has(key)) {
10933
11513
  useCardMode = false;
@@ -10945,7 +11525,7 @@ async function handleDingTalkMessageInner(params) {
10945
11525
  `[DingTalk][AICard] conversationType=${data.conversationType}, conversationId=${to}`
10946
11526
  );
10947
11527
  const inboundQuoteText = rawInboundText.slice(0, 200);
10948
- const aiCard = await createAICard(dingtalkConfig, to, log, {
11528
+ const aiCard = params.preCreatedCard ?? await createAICard(dingtalkConfig, to, log, {
10949
11529
  accountId,
10950
11530
  storePath: accountStorePath,
10951
11531
  contextConversationId: groupId,
@@ -11609,8 +12189,7 @@ ${attachmentExtractedText}` : inboundBody;
11609
12189
  }
11610
12190
  });
11611
12191
  log?.info?.(`[DingTalk] Inbound: from=${senderName} text="${content.text.slice(0, 50)}..."`);
11612
- const textForAbortCheck = stripLeadingMentions(inboundText).trim();
11613
- if (isAbortRequestText(textForAbortCheck)) {
12192
+ if (isAbortBypass) {
11614
12193
  log?.info?.(
11615
12194
  `[DingTalk] Abort request detected, bypassing session lock for session=${route.sessionKey}`
11616
12195
  );
@@ -11871,53 +12450,57 @@ ${attachmentExtractedText}` : inboundBody;
11871
12450
  inboundText: rawInboundText,
11872
12451
  taskMeta
11873
12452
  });
11874
- try {
11875
- let deliveredFinalCount = 0;
11876
- const dispatchResult = await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
11877
- ctx,
11878
- cfg,
11879
- dispatcherOptions: {
11880
- responsePrefix: subAgentOptions?.responsePrefix || "",
11881
- deliver: async (payload, info) => {
11882
- if (isCurrentCardStopRequested()) {
11883
- log?.debug?.(
11884
- "[DingTalk][CardStop] Ignoring reply delivery because stop was already requested"
12453
+ let deliveredFinalCount = 0;
12454
+ const runDispatch = () => rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
12455
+ ctx,
12456
+ cfg,
12457
+ dispatcherOptions: {
12458
+ responsePrefix: subAgentOptions?.responsePrefix || "",
12459
+ deliver: async (payload, info) => {
12460
+ if (isCurrentCardStopRequested()) {
12461
+ log?.debug?.(
12462
+ "[DingTalk][CardStop] Ignoring reply delivery because stop was already requested"
12463
+ );
12464
+ return;
12465
+ }
12466
+ try {
12467
+ if (info?.kind === "final") {
12468
+ deliveredFinalCount += 1;
12469
+ }
12470
+ const inlineReplyPayload = parseInlineReplyPayloadText(payload.text);
12471
+ const mediaUrls = extractMediaUrls(payload, inlineReplyPayload);
12472
+ const richPayload = payload;
12473
+ const replyKind = info?.kind || "block";
12474
+ if (questionCardTookOver) {
12475
+ log?.info?.(
12476
+ `[DingTalk][AskUser] Suppressed ${replyKind} reply after question card took over`
11885
12477
  );
11886
12478
  return;
11887
12479
  }
11888
- try {
11889
- if (info?.kind === "final") {
11890
- deliveredFinalCount += 1;
11891
- }
11892
- const inlineReplyPayload = parseInlineReplyPayloadText(payload.text);
11893
- const mediaUrls = extractMediaUrls(payload, inlineReplyPayload);
11894
- const richPayload = payload;
11895
- const replyKind = info?.kind || "block";
11896
- if (questionCardTookOver) {
11897
- log?.info?.(
11898
- `[DingTalk][AskUser] Suppressed ${replyKind} reply after question card took over`
11899
- );
11900
- return;
11901
- }
11902
- await strategy.deliver({
11903
- text: inlineReplyPayload.text,
11904
- mediaUrls,
11905
- audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
11906
- kind: replyKind,
11907
- isError: payload.isError === true,
11908
- isReasoning: richPayload.isReasoning === true
11909
- });
11910
- } catch (err) {
11911
- log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
11912
- const responseData = getErrorResponseData(err);
11913
- if (responseData !== void 0) {
11914
- log?.error?.(formatDingTalkErrorPayloadLog("inbound.replyDeliver", responseData));
11915
- }
11916
- throw err;
12480
+ await strategy.deliver({
12481
+ text: inlineReplyPayload.text,
12482
+ mediaUrls,
12483
+ audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
12484
+ kind: replyKind,
12485
+ isError: payload.isError === true,
12486
+ isReasoning: richPayload.isReasoning === true
12487
+ });
12488
+ } catch (err) {
12489
+ log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
12490
+ const responseData = getErrorResponseData(err);
12491
+ if (responseData !== void 0) {
12492
+ log?.error?.(formatDingTalkErrorPayloadLog("inbound.replyDeliver", responseData));
11917
12493
  }
12494
+ throw err;
11918
12495
  }
11919
- },
11920
- replyOptions: strategy.getReplyOptions()
12496
+ }
12497
+ },
12498
+ replyOptions: strategy.getReplyOptions()
12499
+ });
12500
+ try {
12501
+ const dispatchResult = await withReplySessionConflictRetry(runDispatch, {
12502
+ log,
12503
+ sessionKey: route.sessionKey
11921
12504
  });
11922
12505
  const bufferedFinal = dispatchResult && typeof dispatchResult === "object" && "queuedFinal" in dispatchResult ? dispatchResult.queuedFinal : void 0;
11923
12506
  const finalCount = dispatchResult && typeof dispatchResult === "object" && "counts" in dispatchResult ? dispatchResult.counts?.final : void 0;
@@ -11947,6 +12530,51 @@ ${attachmentExtractedText}` : inboundBody;
11947
12530
  }
11948
12531
  } catch (dispatchErr) {
11949
12532
  const error = dispatchErr instanceof Error ? dispatchErr : new Error(getErrorMessage(dispatchErr));
12533
+ if (isReplySessionConflictError(error)) {
12534
+ log?.warn?.(
12535
+ `[DingTalk] Reply session still conflicted after retries for session=${route.sessionKey}; sending "processing" acknowledgement instead of dropping the message.`
12536
+ );
12537
+ const ackText = "\u6536\u5230\uFF0C\u4E0A\u4E00\u8F6E\u8FD8\u5728\u5904\u7406\u4E2D\uFF0C\u8BF7\u7A0D\u5019\u518D\u8BD5\u3002";
12538
+ if (replyMode === "card") {
12539
+ try {
12540
+ await strategy.deliver({
12541
+ text: ackText,
12542
+ mediaUrls: [],
12543
+ kind: "final",
12544
+ isReasoning: false
12545
+ });
12546
+ await strategy.finalize();
12547
+ return;
12548
+ } catch (cardAckErr) {
12549
+ log?.warn?.(
12550
+ `[DingTalk] Processing acknowledgement card finalize failed: ${getErrorMessage(cardAckErr)}`
12551
+ );
12552
+ return;
12553
+ }
12554
+ }
12555
+ try {
12556
+ if (sessionWebhook) {
12557
+ await sendBySession(dingtalkConfig, sessionWebhook, ackText, {
12558
+ log,
12559
+ accountId,
12560
+ storePath: accountStorePath
12561
+ });
12562
+ } else {
12563
+ await sendMessage(dingtalkConfig, to, ackText, {
12564
+ log,
12565
+ accountId,
12566
+ storePath: accountStorePath,
12567
+ conversationId: groupId
12568
+ });
12569
+ }
12570
+ } catch (ackErr) {
12571
+ log?.warn?.(
12572
+ `[DingTalk] Processing acknowledgement delivery failed: ${getErrorMessage(ackErr)}`
12573
+ );
12574
+ }
12575
+ await strategy.abort(error);
12576
+ return;
12577
+ }
11950
12578
  await strategy.abort(error);
11951
12579
  throw dispatchErr;
11952
12580
  }
@@ -11975,6 +12603,194 @@ ${attachmentExtractedText}` : inboundBody;
11975
12603
  }
11976
12604
  }
11977
12605
 
12606
+ // src/card/ask-user-question-store.ts
12607
+ var ASK_USER_LIFECYCLE_NAMESPACE = "cards.ask-user.lifecycle";
12608
+ var ACTIVE_TTL_MS = 5 * 60 * 1e3;
12609
+ var TOMBSTONE_TTL_MS = 30 * 60 * 1e3;
12610
+ function now(options) {
12611
+ return options.now?.() ?? Date.now();
12612
+ }
12613
+ function emptyState(timestamp) {
12614
+ return { version: 1, updatedAt: timestamp, records: [] };
12615
+ }
12616
+ function isActiveState(state) {
12617
+ return state === "reserved" || state === "pending" || state === "dispatching";
12618
+ }
12619
+ function isAnswerableState(state) {
12620
+ return state === "reserved" || state === "pending";
12621
+ }
12622
+ function loadState2(options) {
12623
+ const timestamp = now(options);
12624
+ const state = readNamespaceJson(ASK_USER_LIFECYCLE_NAMESPACE, {
12625
+ storePath: options.storePath,
12626
+ scope: { accountId: options.accountId },
12627
+ fallback: emptyState(timestamp),
12628
+ log: options.log
12629
+ });
12630
+ if (state.version !== 1 || !Array.isArray(state.records)) {
12631
+ return emptyState(timestamp);
12632
+ }
12633
+ return state;
12634
+ }
12635
+ function persistState(options, state) {
12636
+ state.updatedAt = now(options);
12637
+ writeNamespaceJsonAtomic(ASK_USER_LIFECYCLE_NAMESPACE, {
12638
+ storePath: options.storePath,
12639
+ scope: { accountId: options.accountId },
12640
+ data: state,
12641
+ log: options.log
12642
+ });
12643
+ }
12644
+ function toTerminal(record, reason, timestamp) {
12645
+ record.state = "terminal";
12646
+ record.terminalReason = reason;
12647
+ record.updatedAt = timestamp;
12648
+ record.expiresAt = timestamp + TOMBSTONE_TTL_MS;
12649
+ return record;
12650
+ }
12651
+ function cleanupState(state, timestamp) {
12652
+ let changed = false;
12653
+ for (const record of state.records) {
12654
+ if (isAnswerableState(record.state) && record.expiresAt <= timestamp) {
12655
+ toTerminal(record, "expired", timestamp);
12656
+ changed = true;
12657
+ }
12658
+ }
12659
+ const retained = state.records.filter(
12660
+ (record) => record.state !== "terminal" || record.expiresAt > timestamp
12661
+ );
12662
+ if (retained.length !== state.records.length) {
12663
+ state.records = retained;
12664
+ changed = true;
12665
+ }
12666
+ return changed;
12667
+ }
12668
+ function readCleanState(options) {
12669
+ const state = loadState2(options);
12670
+ if (cleanupState(state, now(options))) {
12671
+ persistState(options, state);
12672
+ }
12673
+ return state;
12674
+ }
12675
+ function findRecord(state, identifier) {
12676
+ if (identifier.outTrackId) {
12677
+ const byTrackId = state.records.find((record) => record.outTrackId === identifier.outTrackId);
12678
+ if (byTrackId) {
12679
+ return byTrackId;
12680
+ }
12681
+ }
12682
+ if (identifier.questionId) {
12683
+ return state.records.find((record) => record.questionId === identifier.questionId);
12684
+ }
12685
+ return void 0;
12686
+ }
12687
+ function reserveAskUserQuestion(options, input) {
12688
+ const timestamp = now(options);
12689
+ const state = readCleanState(options);
12690
+ const record = {
12691
+ questionId: input.questionId,
12692
+ accountId: options.accountId,
12693
+ questionScopeKey: input.questionScopeKey,
12694
+ outTrackId: input.outTrackId,
12695
+ title: input.title,
12696
+ state: "reserved",
12697
+ createdAt: timestamp,
12698
+ updatedAt: timestamp,
12699
+ expiresAt: timestamp + ACTIVE_TTL_MS
12700
+ };
12701
+ state.records = state.records.filter(
12702
+ (item) => item.questionId !== input.questionId && item.outTrackId !== input.outTrackId
12703
+ );
12704
+ state.records.push(record);
12705
+ persistState(options, state);
12706
+ return { ...record };
12707
+ }
12708
+ function activateAskUserQuestion(options, questionId) {
12709
+ const timestamp = now(options);
12710
+ const state = readCleanState(options);
12711
+ const record = findRecord(state, { questionId });
12712
+ if (!record || record.state !== "reserved") {
12713
+ return { record: record ? { ...record } : void 0, superseded: [] };
12714
+ }
12715
+ const superseded = [];
12716
+ for (const candidate of state.records) {
12717
+ if (candidate !== record && isAnswerableState(candidate.state) && candidate.questionScopeKey === record.questionScopeKey) {
12718
+ toTerminal(candidate, "superseded_by_question", timestamp);
12719
+ superseded.push({ ...candidate });
12720
+ }
12721
+ }
12722
+ record.state = "pending";
12723
+ record.updatedAt = timestamp;
12724
+ record.expiresAt = timestamp + ACTIVE_TTL_MS;
12725
+ persistState(options, state);
12726
+ return { record: { ...record }, superseded };
12727
+ }
12728
+ function claimAskUserQuestion(options, identifier) {
12729
+ const timestamp = now(options);
12730
+ const state = readCleanState(options);
12731
+ const record = findRecord(state, identifier);
12732
+ if (!record || record.state !== "pending") {
12733
+ return void 0;
12734
+ }
12735
+ record.state = "dispatching";
12736
+ record.updatedAt = timestamp;
12737
+ record.expiresAt = timestamp + ACTIVE_TTL_MS;
12738
+ persistState(options, state);
12739
+ return { ...record };
12740
+ }
12741
+ function terminateAskUserQuestion(options, questionId, reason) {
12742
+ const timestamp = now(options);
12743
+ const state = readCleanState(options);
12744
+ const record = findRecord(state, { questionId });
12745
+ if (!record || !isActiveState(record.state)) {
12746
+ return void 0;
12747
+ }
12748
+ toTerminal(record, reason, timestamp);
12749
+ persistState(options, state);
12750
+ return { ...record };
12751
+ }
12752
+ function invalidateAskUserQuestionsInScope(options, questionScopeKey, reason) {
12753
+ const timestamp = now(options);
12754
+ const state = readCleanState(options);
12755
+ const invalidated = [];
12756
+ for (const record of state.records) {
12757
+ if (isAnswerableState(record.state) && record.questionScopeKey === questionScopeKey) {
12758
+ toTerminal(record, reason, timestamp);
12759
+ invalidated.push({ ...record });
12760
+ }
12761
+ }
12762
+ if (invalidated.length > 0) {
12763
+ persistState(options, state);
12764
+ }
12765
+ return invalidated;
12766
+ }
12767
+ function resolveAskUserQuestion(options, identifier) {
12768
+ const record = findRecord(readCleanState(options), identifier);
12769
+ return record ? { ...record } : void 0;
12770
+ }
12771
+ function recoverAskUserQuestionsAfterRestart(options) {
12772
+ const timestamp = now(options);
12773
+ const state = loadState2(options);
12774
+ const retained = state.records.filter(
12775
+ (record) => record.state !== "terminal" || record.expiresAt > timestamp
12776
+ );
12777
+ const removedExpiredTombstones = retained.length !== state.records.length;
12778
+ state.records = retained;
12779
+ const recovered = [];
12780
+ for (const record of state.records) {
12781
+ if (!isActiveState(record.state)) {
12782
+ continue;
12783
+ }
12784
+ const reason = record.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated";
12785
+ toTerminal(record, reason, timestamp);
12786
+ recovered.push({ ...record });
12787
+ }
12788
+ if (recovered.length > 0 || removedExpiredTombstones) {
12789
+ persistState(options, state);
12790
+ }
12791
+ return recovered;
12792
+ }
12793
+
11978
12794
  // src/card/ask-user-question.ts
11979
12795
  var DINGTALK_API4 = "https://api.dingtalk.com";
11980
12796
  var PENDING_QUESTION_TTL_MS = 5 * 60 * 1e3;
@@ -12244,9 +13060,11 @@ function supersedePendingQuestionsInScope(ctx) {
12244
13060
  });
12245
13061
  }
12246
13062
  }
12247
- function storePendingQuestion(ctx) {
13063
+ function storePendingQuestion(ctx, options = {}) {
12248
13064
  ctx.ownerUserId = resolvePendingQuestionOwner(ctx);
12249
- supersedePendingQuestionsInScope(ctx);
13065
+ if (options.supersedeExisting !== false) {
13066
+ supersedePendingQuestionsInScope(ctx);
13067
+ }
12250
13068
  pendingQuestionsByTrackId.set(ctx.outTrackId, ctx);
12251
13069
  pendingQuestionsByQuestionId.set(ctx.questionId, ctx);
12252
13070
  addScopeIndex(ctx);
@@ -12254,7 +13072,9 @@ function storePendingQuestion(ctx) {
12254
13072
  if (!pendingQuestionsByTrackId.has(ctx.outTrackId) || ctx.submitted) {
12255
13073
  return;
12256
13074
  }
12257
- ctx.submitted = true;
13075
+ if (!claimPendingQuestionForDispatch(ctx)) {
13076
+ return;
13077
+ }
12258
13078
  consumePendingQuestion(ctx);
12259
13079
  addHandledQuestionTombstone(ctx, "expired");
12260
13080
  void updateQuestionCardBestEffort(ctx, {
@@ -12262,14 +13082,12 @@ function storePendingQuestion(ctx) {
12262
13082
  question_desc: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
12263
13083
  form_btn_text: "\u5DF2\u5931\u6548"
12264
13084
  });
12265
- setImmediate(() => {
12266
- void injectAnswerSyntheticMessage(ctx, buildExpiredAnswerMessage(ctx), "expired").catch(
12267
- (err) => {
12268
- ctx.log?.error?.(
12269
- `[DingTalk][AskUser] Failed to inject expired answer message: ${String(err)}`
12270
- );
12271
- }
12272
- );
13085
+ dispatchSyntheticAnswer({
13086
+ ctx,
13087
+ text: buildExpiredAnswerMessage(ctx),
13088
+ suffix: "expired",
13089
+ successReason: "expired",
13090
+ log: ctx.log
12273
13091
  });
12274
13092
  }, PENDING_QUESTION_TTL_MS);
12275
13093
  }
@@ -12294,6 +13112,123 @@ async function updateQuestionCardBestEffort(ctx, variables) {
12294
13112
  );
12295
13113
  }
12296
13114
  }
13115
+ function getAskUserStoreOptions(params) {
13116
+ if (!params.storePath) {
13117
+ return void 0;
13118
+ }
13119
+ return {
13120
+ storePath: params.storePath,
13121
+ accountId: params.accountId,
13122
+ log: params.log
13123
+ };
13124
+ }
13125
+ function terminalCardVariables(reason) {
13126
+ const descriptions = {
13127
+ delivery_failed: "\u95EE\u9898\u5361\u7247\u53D1\u9001\u5931\u8D25\u3002",
13128
+ superseded_by_question: "\u5DF2\u6709\u65B0\u7684\u95EE\u9898\u5361\u7247\uFF0C\u8BF7\u56DE\u7B54\u6700\u65B0\u5361\u7247\u3002",
13129
+ superseded_by_message: "\u4F60\u5728\u95EE\u9898\u5361\u7247\u53D1\u51FA\u540E\u53D1\u9001\u4E86\u65B0\u6D88\u606F\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\u3002\u8BF7\u91CD\u65B0\u53D1\u8D77\u9700\u8981\u586B\u5199\u7684\u95EE\u9898\u3002",
13130
+ expired: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
13131
+ cancelled: "\u5DF2\u53D6\u6D88\u3002",
13132
+ empty: "\u5DF2\u63D0\u4EA4\uFF0C\u672A\u586B\u5199\u4EFB\u4F55\u5185\u5BB9\u3002",
13133
+ submitted: "\u5DF2\u63D0\u4EA4\u3002",
13134
+ pause_failed: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
13135
+ restart_invalidated: "\u670D\u52A1\u5DF2\u91CD\u542F\uFF0C\u539F\u95EE\u9898\u4E0A\u4E0B\u6587\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
13136
+ restart_during_dispatch: "\u670D\u52A1\u5728\u5904\u7406\u56DE\u7B54\u671F\u95F4\u91CD\u542F\uFF0C\u672C\u6B21\u5904\u7406\u7ED3\u679C\u53EF\u80FD\u672A\u5B8C\u6210\uFF0C\u8BF7\u53D1\u9001\u65B0\u6D88\u606F\u7EE7\u7EED\u3002",
13137
+ dispatch_failed: "\u56DE\u7B54\u5DF2\u6536\u5230\uFF0C\u4F46\u672A\u80FD\u7EE7\u7EED\u4F1A\u8BDD\uFF0C\u8BF7\u53D1\u9001\u4E00\u6761\u666E\u901A\u6D88\u606F\u7EE7\u7EED\u3002"
13138
+ };
13139
+ return {
13140
+ card_status: reason === "cancelled" ? "cancelled" : reason === "submitted" || reason === "empty" ? "submitted" : "expired",
13141
+ question_desc: descriptions[reason],
13142
+ form_btn_text: reason === "cancelled" ? "\u5DF2\u53D6\u6D88" : reason === "submitted" || reason === "empty" ? "\u5DF2\u63D0\u4EA4" : "\u5DF2\u5931\u6548"
13143
+ };
13144
+ }
13145
+ async function updateLifecycleRecordCardBestEffort(params) {
13146
+ const reason = params.record.terminalReason;
13147
+ if (!reason || reason === "delivery_failed") {
13148
+ return;
13149
+ }
13150
+ try {
13151
+ const token = await getAccessToken(params.config, params.log);
13152
+ await updateCardVariables(
13153
+ params.record.outTrackId,
13154
+ terminalCardVariables(reason),
13155
+ token,
13156
+ params.config
13157
+ );
13158
+ } catch (err) {
13159
+ params.log?.warn?.(
13160
+ `[DingTalk][AskUser] Failed to update lifecycle card ${params.record.questionId}: ${String(err)}`
13161
+ );
13162
+ }
13163
+ }
13164
+ function consumeLifecyclePendingContext(record) {
13165
+ const ctx = pendingQuestionsByQuestionId.get(record.questionId) ?? pendingQuestionsByTrackId.get(record.outTrackId);
13166
+ if (!ctx) {
13167
+ return void 0;
13168
+ }
13169
+ ctx.submitted = true;
13170
+ consumePendingQuestion(ctx);
13171
+ addHandledQuestionTombstone(ctx, record.terminalReason === "expired" ? "expired" : "superseded");
13172
+ return ctx;
13173
+ }
13174
+ function invalidateAskUserQuestionsForScope(params) {
13175
+ const invalidated = invalidateAskUserQuestionsInScope(
13176
+ {
13177
+ storePath: params.storePath,
13178
+ accountId: params.accountId,
13179
+ log: params.log
13180
+ },
13181
+ params.questionScopeKey,
13182
+ params.reason
13183
+ );
13184
+ for (const record of invalidated) {
13185
+ consumeLifecyclePendingContext(record);
13186
+ }
13187
+ return invalidated;
13188
+ }
13189
+ async function syncInvalidatedAskUserQuestionCards(params) {
13190
+ await Promise.allSettled(
13191
+ params.records.map(
13192
+ (record) => updateLifecycleRecordCardBestEffort({
13193
+ record,
13194
+ config: params.config,
13195
+ log: params.log
13196
+ })
13197
+ )
13198
+ );
13199
+ }
13200
+ async function recoverAskUserQuestionsForAccount(params) {
13201
+ if (!params.storePath) {
13202
+ return 0;
13203
+ }
13204
+ const recovered = recoverAskUserQuestionsAfterRestart({
13205
+ storePath: params.storePath,
13206
+ accountId: params.accountId,
13207
+ log: params.log
13208
+ });
13209
+ for (const record of recovered) {
13210
+ consumeLifecyclePendingContext(record);
13211
+ await updateLifecycleRecordCardBestEffort({
13212
+ record,
13213
+ config: params.config,
13214
+ log: params.log
13215
+ });
13216
+ }
13217
+ return recovered.length;
13218
+ }
13219
+ async function terminatePendingQuestion(params) {
13220
+ const storeOptions = getAskUserStoreOptions(params.ctx);
13221
+ if (storeOptions) {
13222
+ terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.reason);
13223
+ }
13224
+ params.ctx.submitted = true;
13225
+ consumePendingQuestion(params.ctx);
13226
+ addHandledQuestionTombstone(
13227
+ params.ctx,
13228
+ params.reason === "cancelled" ? "cancelled" : params.reason === "empty" ? "empty" : params.reason === "submitted" ? "submitted" : params.reason === "expired" ? "expired" : "superseded"
13229
+ );
13230
+ await updateQuestionCardBestEffort(params.ctx, terminalCardVariables(params.reason));
13231
+ }
12297
13232
  function parseEmbeddedJson2(value) {
12298
13233
  if (typeof value !== "string") {
12299
13234
  return value;
@@ -12413,7 +13348,42 @@ async function injectAnswerSyntheticMessage(ctx, text, suffix) {
12413
13348
  data: syntheticData,
12414
13349
  sessionWebhook: ctx.sessionWebhook,
12415
13350
  log: ctx.log,
12416
- dingtalkConfig: ctx.dingtalkConfig
13351
+ dingtalkConfig: ctx.dingtalkConfig,
13352
+ inboundOrigin: "ask-user",
13353
+ routeOverride: ctx.resolvedRoute,
13354
+ subAgentOptions: ctx.continuationSubAgentOptions
13355
+ });
13356
+ }
13357
+ function claimPendingQuestionForDispatch(ctx) {
13358
+ const storeOptions = getAskUserStoreOptions(ctx);
13359
+ if (storeOptions) {
13360
+ return Boolean(
13361
+ claimAskUserQuestion(storeOptions, {
13362
+ questionId: ctx.questionId,
13363
+ outTrackId: ctx.outTrackId
13364
+ })
13365
+ );
13366
+ }
13367
+ if (ctx.submitted) {
13368
+ return false;
13369
+ }
13370
+ ctx.submitted = true;
13371
+ return true;
13372
+ }
13373
+ function dispatchSyntheticAnswer(params) {
13374
+ const storeOptions = getAskUserStoreOptions(params.ctx);
13375
+ void injectAnswerSyntheticMessage(params.ctx, params.text, params.suffix).then(() => {
13376
+ if (storeOptions) {
13377
+ terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.successReason);
13378
+ }
13379
+ }).catch((err) => {
13380
+ if (storeOptions) {
13381
+ terminateAskUserQuestion(storeOptions, params.ctx.questionId, "dispatch_failed");
13382
+ }
13383
+ void updateQuestionCardBestEffort(params.ctx, terminalCardVariables("dispatch_failed"));
13384
+ params.log?.error?.(
13385
+ `[DingTalk][AskUser] Failed to inject ${params.suffix} answer message: ${String(err)}`
13386
+ );
12417
13387
  });
12418
13388
  }
12419
13389
  async function handleDingTalkAskUserCardCallback(params) {
@@ -12425,8 +13395,43 @@ async function handleDingTalkAskUserCardCallback(params) {
12425
13395
  );
12426
13396
  return { handled: true };
12427
13397
  }
13398
+ const storeOptions = params.storePath ? {
13399
+ storePath: params.storePath,
13400
+ accountId: params.accountId,
13401
+ log: params.log
13402
+ } : void 0;
13403
+ const lifecycleRecord = storeOptions ? resolveAskUserQuestion(storeOptions, {
13404
+ questionId: parsed.actionId,
13405
+ outTrackId: parsed.outTrackId
13406
+ }) : void 0;
13407
+ if (lifecycleRecord?.state === "terminal") {
13408
+ params.log?.debug?.(
13409
+ `[DingTalk][AskUser] Ignoring terminal callback question=${lifecycleRecord.questionId} reason=${lifecycleRecord.terminalReason ?? "unknown"}`
13410
+ );
13411
+ await updateLifecycleRecordCardBestEffort({
13412
+ record: lifecycleRecord,
13413
+ config: params.config,
13414
+ log: params.log
13415
+ });
13416
+ return { handled: true };
13417
+ }
12428
13418
  const ctx = (parsed.outTrackId ? pendingQuestionsByTrackId.get(parsed.outTrackId) : void 0) ?? (parsed.actionId ? pendingQuestionsByQuestionId.get(parsed.actionId) : void 0);
12429
13419
  if (!ctx) {
13420
+ if (lifecycleRecord && storeOptions) {
13421
+ const recovered = terminateAskUserQuestion(
13422
+ storeOptions,
13423
+ lifecycleRecord.questionId,
13424
+ lifecycleRecord.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated"
13425
+ );
13426
+ if (recovered) {
13427
+ await updateLifecycleRecordCardBestEffort({
13428
+ record: recovered,
13429
+ config: params.config,
13430
+ log: params.log
13431
+ });
13432
+ }
13433
+ return { handled: true };
13434
+ }
12430
13435
  return { handled: false };
12431
13436
  }
12432
13437
  if (!isOwnerClick(ctx, params.clickerUserId)) {
@@ -12441,13 +13446,15 @@ async function handleDingTalkAskUserCardCallback(params) {
12441
13446
  );
12442
13447
  return { handled: true };
12443
13448
  }
12444
- if (ctx.submitted) {
13449
+ if (ctx.submitted || lifecycleRecord?.state === "dispatching") {
12445
13450
  params.log?.debug?.(`[DingTalk][AskUser] Duplicate submit ignored question=${ctx.questionId}`);
12446
13451
  return { handled: true };
12447
13452
  }
12448
13453
  const isCancel = parseBooleanLike(parsed.params.user_cancel) === true;
12449
- ctx.submitted = true;
12450
13454
  if (isCancel) {
13455
+ if (!claimPendingQuestionForDispatch(ctx)) {
13456
+ return { handled: true };
13457
+ }
12451
13458
  await updateQuestionCardBestEffort(ctx, {
12452
13459
  card_status: "cancelled",
12453
13460
  question_desc: "\u5DF2\u53D6\u6D88\u3002",
@@ -12455,25 +13462,25 @@ async function handleDingTalkAskUserCardCallback(params) {
12455
13462
  });
12456
13463
  consumePendingQuestion(ctx);
12457
13464
  addHandledQuestionTombstone(ctx, "cancelled");
12458
- setImmediate(() => {
12459
- void injectAnswerSyntheticMessage(ctx, buildCancelledAnswerMessage(ctx), "cancelled").catch(
12460
- (err) => {
12461
- params.log?.error?.(
12462
- `[DingTalk][AskUser] Failed to inject cancelled answer message: ${String(err)}`
12463
- );
12464
- }
12465
- );
13465
+ dispatchSyntheticAnswer({
13466
+ ctx,
13467
+ text: buildCancelledAnswerMessage(ctx),
13468
+ suffix: "cancelled",
13469
+ successReason: "cancelled",
13470
+ log: params.log
12466
13471
  });
12467
13472
  return { handled: true };
12468
13473
  }
12469
13474
  const form = asRecord5(parsed.params.form);
12470
13475
  if (!form) {
12471
- ctx.submitted = false;
12472
13476
  params.log?.warn?.(
12473
13477
  `[DingTalk][AskUser] Missing form payload question=${ctx.questionId} params=${JSON.stringify(parsed.params)}`
12474
13478
  );
12475
13479
  return { handled: true };
12476
13480
  }
13481
+ if (!claimPendingQuestionForDispatch(ctx)) {
13482
+ return { handled: true };
13483
+ }
12477
13484
  const answers = [];
12478
13485
  const selectedValues = [];
12479
13486
  for (const question of ctx.questions) {
@@ -12497,12 +13504,12 @@ async function handleDingTalkAskUserCardCallback(params) {
12497
13504
  });
12498
13505
  consumePendingQuestion(ctx);
12499
13506
  addHandledQuestionTombstone(ctx, "empty");
12500
- setImmediate(() => {
12501
- void injectAnswerSyntheticMessage(ctx, buildEmptyAnswerMessage(ctx), "empty").catch((err) => {
12502
- params.log?.error?.(
12503
- `[DingTalk][AskUser] Failed to inject empty answer message: ${String(err)}`
12504
- );
12505
- });
13507
+ dispatchSyntheticAnswer({
13508
+ ctx,
13509
+ text: buildEmptyAnswerMessage(ctx),
13510
+ suffix: "empty",
13511
+ successReason: "empty",
13512
+ log: params.log
12506
13513
  });
12507
13514
  return { handled: true };
12508
13515
  }
@@ -12517,10 +13524,12 @@ async function handleDingTalkAskUserCardCallback(params) {
12517
13524
  consumePendingQuestion(ctx);
12518
13525
  addHandledQuestionTombstone(ctx, "submitted");
12519
13526
  const message = buildAnswerMessage(ctx, answers);
12520
- setImmediate(() => {
12521
- void injectAnswerSyntheticMessage(ctx, message, "submitted").catch((err) => {
12522
- params.log?.error?.(`[DingTalk][AskUser] Failed to inject answer message: ${String(err)}`);
12523
- });
13527
+ dispatchSyntheticAnswer({
13528
+ ctx,
13529
+ text: message,
13530
+ suffix: "submitted",
13531
+ successReason: "submitted",
13532
+ log: params.log
12524
13533
  });
12525
13534
  return { handled: true };
12526
13535
  }
@@ -12647,13 +13656,12 @@ function registerDingTalkAskUserQuestionTool(api) {
12647
13656
  api.logger?.warn?.(`${TOOL_NAME}: registerTool unavailable, skipping tool registration`);
12648
13657
  return;
12649
13658
  }
12650
- registerTool.call(api, {
13659
+ const createTool = (context) => ({
12651
13660
  name: TOOL_NAME,
12652
13661
  label: "Ask User Question",
12653
13662
  description: "Ask the user a blocking question or collect structured input via an interactive DingTalk form card when the current task cannot continue without the user's answer. Returns immediately after sending the card. The user's answer will arrive as a new message in the conversation. Do NOT poll or re-call this tool \u2014 just wait for the response message. Use questions only for simple confirmation, single-select, multi-select, or simple free-text prompts. For simple selection questions, provide options; for simple free-text input, set options to an empty array. When collecting multiple missing values, when the user asks for a form, or when you would otherwise list required parameters for the user to fill, call this tool with top-level fields instead of replying with a markdown checklist. Do not call this tool for normal explanations, why/how questions, capability introductions, or cases where you can answer directly.",
12654
13663
  parameters: AskUserQuestionSchema,
12655
13664
  async execute(_toolCallId, params) {
12656
- const context = getDingTalkQuestionContext();
12657
13665
  if (!context) {
12658
13666
  return jsonToolResult({
12659
13667
  status: "failed",
@@ -12687,6 +13695,16 @@ function registerDingTalkAskUserQuestionTool(api) {
12687
13695
  selected_values: "[]",
12688
13696
  form: { fields }
12689
13697
  };
13698
+ const storeOptions = getAskUserStoreOptions(context);
13699
+ const canPersistLifecycle = Boolean(storeOptions && context.questionScopeKey);
13700
+ if (storeOptions && context.questionScopeKey) {
13701
+ reserveAskUserQuestion(storeOptions, {
13702
+ questionId,
13703
+ questionScopeKey: context.questionScopeKey,
13704
+ outTrackId,
13705
+ title
13706
+ });
13707
+ }
12690
13708
  try {
12691
13709
  await createAndDeliverQuestionCard({
12692
13710
  config: context.dingtalkConfig,
@@ -12698,6 +13716,9 @@ function registerDingTalkAskUserQuestionTool(api) {
12698
13716
  log: context.log
12699
13717
  });
12700
13718
  } catch (err) {
13719
+ if (storeOptions && canPersistLifecycle) {
13720
+ terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
13721
+ }
12701
13722
  const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
12702
13723
  return jsonToolResult({
12703
13724
  status: "failed",
@@ -12708,20 +13729,75 @@ function registerDingTalkAskUserQuestionTool(api) {
12708
13729
  ...context,
12709
13730
  onQuestionCardSent: void 0
12710
13731
  };
12711
- storePendingQuestion({
13732
+ const pendingQuestion = {
12712
13733
  ...pendingContext,
12713
13734
  questionId,
12714
13735
  outTrackId,
12715
13736
  title,
12716
13737
  questions: parsed,
12717
13738
  submitted: false
13739
+ };
13740
+ storePendingQuestion(pendingQuestion, {
13741
+ supersedeExisting: !canPersistLifecycle
12718
13742
  });
13743
+ if (storeOptions && canPersistLifecycle) {
13744
+ const activation = activateAskUserQuestion(storeOptions, questionId);
13745
+ if (activation.record?.state !== "pending") {
13746
+ const terminalReason = activation.record?.terminalReason ?? "superseded_by_message";
13747
+ if (activation.record) {
13748
+ consumeLifecyclePendingContext(activation.record);
13749
+ } else {
13750
+ pendingQuestion.submitted = true;
13751
+ consumePendingQuestion(pendingQuestion);
13752
+ addHandledQuestionTombstone(pendingQuestion, "superseded");
13753
+ }
13754
+ await updateQuestionCardBestEffort(
13755
+ pendingQuestion,
13756
+ terminalCardVariables(terminalReason)
13757
+ );
13758
+ return jsonToolResult({
13759
+ status: "failed",
13760
+ questionId,
13761
+ outTrackId,
13762
+ error: "\u95EE\u9898\u5361\u7247\u5728\u53D1\u9001\u671F\u95F4\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
13763
+ });
13764
+ }
13765
+ for (const superseded of activation.superseded) {
13766
+ const supersededContext = consumeLifecyclePendingContext(superseded);
13767
+ if (supersededContext) {
13768
+ void updateQuestionCardBestEffort(
13769
+ supersededContext,
13770
+ terminalCardVariables("superseded_by_question")
13771
+ );
13772
+ } else {
13773
+ void updateLifecycleRecordCardBestEffort({
13774
+ record: superseded,
13775
+ config: context.dingtalkConfig,
13776
+ log: context.log
13777
+ });
13778
+ }
13779
+ }
13780
+ }
13781
+ let takeoverSucceeded = void 0;
12719
13782
  try {
12720
- await context.onQuestionCardSent?.({ questionId, outTrackId });
13783
+ takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
12721
13784
  } catch (err) {
12722
13785
  context.log?.warn?.(
12723
13786
  `[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`
12724
13787
  );
13788
+ takeoverSucceeded = false;
13789
+ }
13790
+ if (takeoverSucceeded === false) {
13791
+ await terminatePendingQuestion({
13792
+ ctx: pendingQuestion,
13793
+ reason: "pause_failed"
13794
+ });
13795
+ return jsonToolResult({
13796
+ status: "failed",
13797
+ questionId,
13798
+ outTrackId,
13799
+ error: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
13800
+ });
12725
13801
  }
12726
13802
  context.log?.info?.(
12727
13803
  `[DingTalk][AskUser] question card sent question=${questionId} outTrackId=${outTrackId}`
@@ -12734,6 +13810,18 @@ function registerDingTalkAskUserQuestionTool(api) {
12734
13810
  });
12735
13811
  }
12736
13812
  });
13813
+ registerTool.call(
13814
+ api,
13815
+ (toolContext) => {
13816
+ const context = getDingTalkQuestionContext();
13817
+ const runtimeSessionKey = toolContext.sessionKey?.trim();
13818
+ const contextSessionKey = context?.resolvedRoute?.sessionKey.trim();
13819
+ return createTool(
13820
+ context && (!runtimeSessionKey || contextSessionKey === runtimeSessionKey) ? context : void 0
13821
+ );
13822
+ },
13823
+ { name: TOOL_NAME }
13824
+ );
12737
13825
  api.logger?.debug?.(`${TOOL_NAME}: registered tool`);
12738
13826
  }
12739
13827
 
@@ -12926,6 +14014,7 @@ async function handleCardAction(params) {
12926
14014
  payload: params.payload,
12927
14015
  cfg: params.cfg,
12928
14016
  accountId: params.accountId,
14017
+ storePath: params.storePath,
12929
14018
  config: params.config,
12930
14019
  clickerUserId: params.analysis.userId,
12931
14020
  log: params.log
@@ -13066,8 +14155,8 @@ var ConnectionManager = class _ConnectionManager {
13066
14155
  `[${this.accountId}] Runtime counters (${reason}): healthUnhealthyChecks=${c.healthUnhealthyChecks}, healthTriggeredReconnects=${c.healthTriggeredReconnects}, heartbeatMisses=${c.heartbeatMisses}, heartbeatTriggeredReconnects=${c.heartbeatTriggeredReconnects}, serverDisconnectMessages=${c.serverDisconnectMessages}, socketCloseEvents=${c.socketCloseEvents}, runtimeDisconnects=${c.runtimeDisconnects}, reconnectAttempts=${c.reconnectAttempts}, reconnectSuccess=${c.reconnectSuccess}, reconnectFailures=${c.reconnectFailures}`
13067
14156
  );
13068
14157
  }
13069
- recordSocketActivity(now = Date.now()) {
13070
- this.lastSocketActivityAt = now;
14158
+ recordSocketActivity(now2 = Date.now()) {
14159
+ this.lastSocketActivityAt = now2;
13071
14160
  this.consecutiveHeartbeatMisses = 0;
13072
14161
  }
13073
14162
  setupHeartbeat(socket) {
@@ -13079,13 +14168,13 @@ var ConnectionManager = class _ConnectionManager {
13079
14168
  if (socket.readyState !== 1) {
13080
14169
  return;
13081
14170
  }
13082
- const now = Date.now();
13083
- const idleMs = this.lastSocketActivityAt !== void 0 ? now - this.lastSocketActivityAt : _ConnectionManager.HEARTBEAT_INTERVAL_MS;
14171
+ const now2 = Date.now();
14172
+ const idleMs = this.lastSocketActivityAt !== void 0 ? now2 - this.lastSocketActivityAt : _ConnectionManager.HEARTBEAT_INTERVAL_MS;
13084
14173
  if (idleMs >= _ConnectionManager.HEARTBEAT_INTERVAL_MS) {
13085
14174
  this.consecutiveHeartbeatMisses += 1;
13086
14175
  this.runtimeCounters.heartbeatMisses += 1;
13087
14176
  if (this.consecutiveHeartbeatMisses >= _ConnectionManager.HEARTBEAT_MISS_THRESHOLD) {
13088
- const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now - this.lastHeartbeatPingAt : void 0;
14177
+ const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now2 - this.lastHeartbeatPingAt : void 0;
13089
14178
  this.log?.warn?.(
13090
14179
  `[${this.accountId}] Connection heartbeat missed ${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD} checks, triggering reconnection (lastPingAgoMs=${lastPingAgoMs ?? "n/a"})`
13091
14180
  );
@@ -13099,7 +14188,7 @@ var ConnectionManager = class _ConnectionManager {
13099
14188
  `[${this.accountId}] Connection heartbeat missed (${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD})`
13100
14189
  );
13101
14190
  }
13102
- this.lastHeartbeatPingAt = now;
14191
+ this.lastHeartbeatPingAt = now2;
13103
14192
  try {
13104
14193
  socket.ping("", true);
13105
14194
  } catch (err) {
@@ -13340,8 +14429,8 @@ var ConnectionManager = class _ConnectionManager {
13340
14429
  this.consecutiveUnhealthyChecks = 0;
13341
14430
  return;
13342
14431
  }
13343
- const now = Date.now();
13344
- const withinGraceWindow = this.connectedAt !== void 0 && now - this.connectedAt < _ConnectionManager.HEALTH_CHECK_GRACE_MS;
14432
+ const now2 = Date.now();
14433
+ const withinGraceWindow = this.connectedAt !== void 0 && now2 - this.connectedAt < _ConnectionManager.HEALTH_CHECK_GRACE_MS;
13345
14434
  if (withinGraceWindow) {
13346
14435
  this.consecutiveUnhealthyChecks = 0;
13347
14436
  return;
@@ -13699,12 +14788,12 @@ var MESSAGE_DEDUP_TTL = 6e4;
13699
14788
  var MESSAGE_DEDUP_MAX_SIZE = 1e3;
13700
14789
  var messageCounter = 0;
13701
14790
  function isMessageProcessed(dedupKey) {
13702
- const now = Date.now();
14791
+ const now2 = Date.now();
13703
14792
  const expiresAt = processedMessages.get(dedupKey);
13704
14793
  if (expiresAt === void 0) {
13705
14794
  return false;
13706
14795
  }
13707
- if (now >= expiresAt) {
14796
+ if (now2 >= expiresAt) {
13708
14797
  processedMessages.delete(dedupKey);
13709
14798
  return false;
13710
14799
  }
@@ -13714,9 +14803,9 @@ function markMessageProcessed(dedupKey) {
13714
14803
  const expiresAt = Date.now() + MESSAGE_DEDUP_TTL;
13715
14804
  processedMessages.set(dedupKey, expiresAt);
13716
14805
  if (processedMessages.size > MESSAGE_DEDUP_MAX_SIZE) {
13717
- const now = Date.now();
14806
+ const now2 = Date.now();
13718
14807
  for (const [key, expiry] of processedMessages.entries()) {
13719
- if (now >= expiry) {
14808
+ if (now2 >= expiry) {
13720
14809
  processedMessages.delete(key);
13721
14810
  }
13722
14811
  }
@@ -13735,9 +14824,9 @@ function markMessageProcessed(dedupKey) {
13735
14824
  messageCounter++;
13736
14825
  if (messageCounter >= 10) {
13737
14826
  messageCounter = 0;
13738
- const now = Date.now();
14827
+ const now2 = Date.now();
13739
14828
  for (const [key, expiry] of processedMessages.entries()) {
13740
- if (now >= expiry) {
14829
+ if (now2 >= expiry) {
13741
14830
  processedMessages.delete(key);
13742
14831
  }
13743
14832
  }
@@ -13794,7 +14883,6 @@ function instrumentConnectionStages(client) {
13794
14883
  }
13795
14884
  };
13796
14885
  }
13797
- var INFLIGHT_TTL_MS = 5 * 60 * 1e3;
13798
14886
  var processingDedupKeys = /* @__PURE__ */ new Map();
13799
14887
  var inboundCountersByAccount = /* @__PURE__ */ new Map();
13800
14888
  var INBOUND_COUNTER_LOG_EVERY = 10;
@@ -13864,6 +14952,23 @@ function createDingTalkGateway() {
13864
14952
  `[${account.accountId}] Failed to recover unfinished cards: ${err.message}`
13865
14953
  );
13866
14954
  }
14955
+ try {
14956
+ const recoveredQuestions = await recoverAskUserQuestionsForAccount({
14957
+ storePath: accountStorePath,
14958
+ accountId: account.accountId,
14959
+ config,
14960
+ log: pluginLog
14961
+ });
14962
+ if (recoveredQuestions > 0) {
14963
+ pluginLog?.info?.(
14964
+ `[${account.accountId}] Invalidated ${recoveredQuestions} unfinished Ask User card(s) from previous runtime`
14965
+ );
14966
+ }
14967
+ } catch (err) {
14968
+ pluginLog?.warn?.(
14969
+ `[${account.accountId}] Failed to recover Ask User cards: ${err.message}`
14970
+ );
14971
+ }
13867
14972
  const useConnectionManager = config.useConnectionManager ?? true;
13868
14973
  const applyStatusPatch = (patch) => {
13869
14974
  ctx.setStatus({
@@ -13921,7 +15026,9 @@ function createDingTalkGateway() {
13921
15026
  data,
13922
15027
  sessionWebhook: data.sessionWebhook,
13923
15028
  log: pluginLog,
13924
- dingtalkConfig: config
15029
+ dingtalkConfig: config,
15030
+ inboundOrigin: "stream",
15031
+ inboundQueueEligible: true
13925
15032
  });
13926
15033
  stats.processed += 1;
13927
15034
  if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
@@ -13936,22 +15043,14 @@ function createDingTalkGateway() {
13936
15043
  logInboundCounters(pluginLog, account.accountId, "dedup-skipped");
13937
15044
  return;
13938
15045
  }
13939
- const inflightSince = processingDedupKeys.get(dedupKey);
13940
- if (inflightSince !== void 0) {
13941
- if (Date.now() - inflightSince > INFLIGHT_TTL_MS) {
13942
- pluginLog?.warn?.(
13943
- `[${account.accountId}] Releasing stale in-flight lock for ${dedupKey} (held ${Date.now() - inflightSince}ms > TTL ${INFLIGHT_TTL_MS}ms)`
13944
- );
13945
- processingDedupKeys.delete(dedupKey);
13946
- } else {
13947
- pluginLog?.debug?.(
13948
- `[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`
13949
- );
13950
- stats.inflightSkipped += 1;
13951
- acknowledge();
13952
- logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
13953
- return;
13954
- }
15046
+ if (processingDedupKeys.has(dedupKey)) {
15047
+ pluginLog?.debug?.(
15048
+ `[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`
15049
+ );
15050
+ stats.inflightSkipped += 1;
15051
+ acknowledge();
15052
+ logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
15053
+ return;
13955
15054
  }
13956
15055
  acknowledge();
13957
15056
  processingDedupKeys.set(dedupKey, Date.now());
@@ -13962,7 +15061,9 @@ function createDingTalkGateway() {
13962
15061
  data,
13963
15062
  sessionWebhook: data.sessionWebhook,
13964
15063
  log: pluginLog,
13965
- dingtalkConfig: config
15064
+ dingtalkConfig: config,
15065
+ inboundOrigin: "stream",
15066
+ inboundQueueEligible: true
13966
15067
  });
13967
15068
  stats.processed += 1;
13968
15069
  markMessageProcessed(dedupKey);
@@ -14034,6 +15135,7 @@ function createDingTalkGateway() {
14034
15135
  analysis,
14035
15136
  cfg,
14036
15137
  accountId: account.accountId,
15138
+ storePath: accountStorePath,
14037
15139
  config,
14038
15140
  log: pluginLog
14039
15141
  });
@@ -14185,19 +15287,6 @@ function createDingTalkGateway() {
14185
15287
  lastError: null
14186
15288
  });
14187
15289
  } else if (state === "FAILED" /* FAILED */ || state === "DISCONNECTED" /* DISCONNECTED */) {
14188
- const robotKey = resolveRobotCode(config) || account.accountId;
14189
- let cleared = 0;
14190
- for (const key of processingDedupKeys.keys()) {
14191
- if (key.startsWith(`${robotKey}:`)) {
14192
- processingDedupKeys.delete(key);
14193
- cleared++;
14194
- }
14195
- }
14196
- if (cleared > 0) {
14197
- pluginLog?.info?.(
14198
- `[${account.accountId}] Cleared ${cleared} stale in-flight lock(s) on disconnect`
14199
- );
14200
- }
14201
15290
  applyStatusPatch({
14202
15291
  running: false,
14203
15292
  connected: false,
@@ -14338,13 +15427,13 @@ async function beginDeviceRegistration() {
14338
15427
  }) : null;
14339
15428
  abortPromise?.catch(() => {
14340
15429
  });
14341
- const sleep = () => new Promise((resolve3) => setTimeout(resolve3, interval * 1e3));
15430
+ const sleep2 = () => new Promise((resolve3) => setTimeout(resolve3, interval * 1e3));
14342
15431
  try {
14343
15432
  while (Date.now() < deadline) {
14344
15433
  if (signal?.aborted) {
14345
15434
  throw new RegistrationError("registration cancelled");
14346
15435
  }
14347
- await (abortPromise ? Promise.race([sleep(), abortPromise]) : sleep());
15436
+ await (abortPromise ? Promise.race([sleep2(), abortPromise]) : sleep2());
14348
15437
  if (signal?.aborted) {
14349
15438
  throw new RegistrationError("registration cancelled");
14350
15439
  }
@@ -14704,9 +15793,9 @@ async function configureDingTalkAccount(params) {
14704
15793
  let lastWaitingNote = 0;
14705
15794
  const result = await session.waitForResult({
14706
15795
  onWaiting: () => {
14707
- const now = Date.now();
14708
- if (now - lastWaitingNote >= 15e3) {
14709
- lastWaitingNote = now;
15796
+ const now2 = Date.now();
15797
+ if (now2 - lastWaitingNote >= 15e3) {
15798
+ lastWaitingNote = now2;
14710
15799
  prompter.note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling").catch(() => {
14711
15800
  });
14712
15801
  }