@soimy/dingtalk 3.6.6 → 3.6.7

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.
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;
@@ -3365,9 +3365,9 @@ function ensureSweepTimer() {
3365
3365
  return;
3366
3366
  }
3367
3367
  sweepTimer = setInterval(() => {
3368
- const now = Date.now();
3368
+ const now2 = Date.now();
3369
3369
  for (const [key, record] of records) {
3370
- if (now - record.registeredAt > CARD_RUN_TTL_MS) {
3370
+ if (now2 - record.registeredAt > CARD_RUN_TTL_MS) {
3371
3371
  records.delete(key);
3372
3372
  }
3373
3373
  }
@@ -9789,10 +9789,61 @@ async function dispatchSubAgents(params) {
9789
9789
  dingtalkConfig,
9790
9790
  sessionWebhook,
9791
9791
  extractedContent,
9792
+ sessionPeer,
9793
+ onRoutesResolved,
9792
9794
  handleMessage,
9793
9795
  downloadMedia: download,
9794
9796
  log
9795
9797
  } = params;
9798
+ let helperMissingWarningSent = false;
9799
+ const sendHelperMissingWarning = async () => {
9800
+ if (helperMissingWarningSent) {
9801
+ return;
9802
+ }
9803
+ helperMissingWarningSent = true;
9804
+ try {
9805
+ const isGroup = data.conversationType !== "1";
9806
+ const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
9807
+ await sendBySession(
9808
+ dingtalkConfig,
9809
+ sessionWebhook,
9810
+ "\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",
9811
+ sendOptions
9812
+ );
9813
+ } catch (notifyError) {
9814
+ log?.debug?.(
9815
+ `[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
9816
+ );
9817
+ }
9818
+ };
9819
+ let resolvedTargets;
9820
+ try {
9821
+ const rt = getDingTalkRuntime();
9822
+ resolvedTargets = matchedAgents.map((agent) => ({
9823
+ agent,
9824
+ route: {
9825
+ agentId: agent.agentId,
9826
+ sessionKey: buildAgentSessionKey({
9827
+ rt,
9828
+ cfg,
9829
+ accountId,
9830
+ agentId: agent.agentId,
9831
+ peerKind: sessionPeer.kind,
9832
+ peerId: sessionPeer.peerId
9833
+ }),
9834
+ mainSessionKey: ""
9835
+ }
9836
+ }));
9837
+ } catch (error) {
9838
+ const message = getErrorMessage(error);
9839
+ log?.error?.(`[DingTalk] Failed to resolve sub-agent routes: ${message}`);
9840
+ if (error instanceof HostRoutingHelperUnavailableError) {
9841
+ await sendHelperMissingWarning();
9842
+ return;
9843
+ }
9844
+ throw error;
9845
+ }
9846
+ onRoutesResolved?.(resolvedTargets);
9796
9847
  let preDownloadedMedia;
9797
9848
  const robotCode = resolveRobotCode(dingtalkConfig);
9798
9849
  if (robotCode) {
@@ -9815,8 +9866,8 @@ async function dispatchSubAgents(params) {
9815
9866
  };
9816
9867
  }
9817
9868
  }
9818
- let helperMissingWarningSent = false;
9819
- for (const agentMatch of matchedAgents) {
9869
+ for (const target of resolvedTargets) {
9870
+ const agentMatch = target.agent;
9820
9871
  try {
9821
9872
  await handleMessage({
9822
9873
  cfg,
@@ -9825,6 +9876,7 @@ async function dispatchSubAgents(params) {
9825
9876
  sessionWebhook,
9826
9877
  log,
9827
9878
  dingtalkConfig,
9879
+ routeOverride: target.route,
9828
9880
  subAgentOptions: {
9829
9881
  agentId: agentMatch.agentId,
9830
9882
  responsePrefix: commandText ? "" : `> \u{1F916} **${sanitizeAgentName(agentMatch.matchedName)}**:
@@ -9838,22 +9890,8 @@ async function dispatchSubAgents(params) {
9838
9890
  } catch (error) {
9839
9891
  const message = getErrorMessage(error);
9840
9892
  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
- }
9893
+ if (error instanceof HostRoutingHelperUnavailableError) {
9894
+ await sendHelperMissingWarning();
9857
9895
  }
9858
9896
  }
9859
9897
  }
@@ -10555,6 +10593,8 @@ async function handleDingTalkMessageInner(params) {
10555
10593
  sessionWebhook,
10556
10594
  log,
10557
10595
  dingtalkConfig,
10596
+ inboundOrigin = "stream",
10597
+ routeOverride,
10558
10598
  subAgentOptions,
10559
10599
  preDownloadedMedia
10560
10600
  } = params;
@@ -10749,32 +10789,42 @@ async function handleDingTalkMessageInner(params) {
10749
10789
  config: dingtalkConfig
10750
10790
  });
10751
10791
  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 }
10792
+ const invalidateQuestionRoutes = (routes) => {
10793
+ if (inboundOrigin === "ask-user") {
10794
+ return;
10795
+ }
10796
+ const scopeKeys = new Set(
10797
+ routes.map((resolvedRoute) => `${accountId}:${resolvedRoute.sessionKey}:${senderId}`)
10798
+ );
10799
+ const invalidatedRecords = [];
10800
+ for (const questionScopeKey of scopeKeys) {
10801
+ try {
10802
+ invalidatedRecords.push(
10803
+ ...invalidateAskUserQuestionsForScope({
10804
+ storePath: accountStorePath,
10805
+ accountId,
10806
+ questionScopeKey,
10807
+ reason: "superseded_by_message",
10808
+ log
10809
+ })
10810
+ );
10811
+ } catch (err) {
10812
+ log?.warn?.(
10813
+ `[DingTalk][AskUser] Failed to invalidate pending cards before inbound dispatch scope=${questionScopeKey}: ${String(err)}`
10814
+ );
10815
+ }
10816
+ }
10817
+ if (invalidatedRecords.length === 0) {
10818
+ return;
10819
+ }
10820
+ void syncInvalidatedAskUserQuestionCards({
10821
+ records: invalidatedRecords,
10822
+ config: dingtalkConfig,
10823
+ log
10824
+ }).catch((err) => {
10825
+ log?.warn?.(`[DingTalk][AskUser] Card invalidation sync failed: ${String(err)}`);
10772
10826
  });
10773
- }
10774
- const questionContext = getDingTalkQuestionContext();
10775
- if (questionContext) {
10776
- questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
10777
- }
10827
+ };
10778
10828
  if (messageTarget && messageTarget.kind !== "default") {
10779
10829
  if (messageTarget.kind === "subagent-command") {
10780
10830
  await dispatchSubAgents({
@@ -10786,6 +10836,8 @@ async function handleDingTalkMessageInner(params) {
10786
10836
  dingtalkConfig,
10787
10837
  sessionWebhook,
10788
10838
  extractedContent,
10839
+ sessionPeer,
10840
+ onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
10789
10841
  handleMessage: handleDingTalkMessage,
10790
10842
  downloadMedia,
10791
10843
  log
@@ -10814,6 +10866,8 @@ async function handleDingTalkMessageInner(params) {
10814
10866
  dingtalkConfig,
10815
10867
  sessionWebhook,
10816
10868
  extractedContent,
10869
+ sessionPeer,
10870
+ onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
10817
10871
  handleMessage: handleDingTalkMessage,
10818
10872
  downloadMedia,
10819
10873
  log
@@ -10821,6 +10875,44 @@ async function handleDingTalkMessageInner(params) {
10821
10875
  return;
10822
10876
  }
10823
10877
  }
10878
+ let route;
10879
+ if (routeOverride) {
10880
+ route = routeOverride;
10881
+ } else if (subAgentOptions) {
10882
+ route = {
10883
+ agentId: subAgentOptions.agentId,
10884
+ sessionKey: buildAgentSessionKey({
10885
+ rt,
10886
+ cfg,
10887
+ accountId,
10888
+ agentId: subAgentOptions.agentId,
10889
+ peerKind: sessionPeer.kind,
10890
+ peerId: sessionPeer.peerId
10891
+ }),
10892
+ mainSessionKey: ""
10893
+ };
10894
+ } else {
10895
+ route = rt.channel.routing.resolveAgentRoute({
10896
+ cfg,
10897
+ channel: "dingtalk",
10898
+ accountId,
10899
+ peer: { kind: sessionPeer.kind, id: sessionPeer.peerId }
10900
+ });
10901
+ }
10902
+ const questionContext = getDingTalkQuestionContext();
10903
+ if (questionContext) {
10904
+ questionContext.resolvedRoute = route;
10905
+ questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
10906
+ questionContext.storePath = accountStorePath;
10907
+ questionContext.continuationSubAgentOptions = subAgentOptions ? {
10908
+ agentId: subAgentOptions.agentId,
10909
+ responsePrefix: subAgentOptions.responsePrefix,
10910
+ matchedName: subAgentOptions.matchedName
10911
+ } : void 0;
10912
+ }
10913
+ if (!subAgentOptions) {
10914
+ invalidateQuestionRoutes([route]);
10915
+ }
10824
10916
  const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
10825
10917
  agentId: route.agentId
10826
10918
  });
@@ -10887,7 +10979,6 @@ async function handleDingTalkMessageInner(params) {
10887
10979
  let cardFlightKey;
10888
10980
  if (questionContext) {
10889
10981
  questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
10890
- questionCardTookOver = true;
10891
10982
  if (cardFlightKey) {
10892
10983
  cardCreationInFlight.delete(cardFlightKey);
10893
10984
  cardFlightKey = void 0;
@@ -10908,23 +10999,34 @@ async function handleDingTalkMessageInner(params) {
10908
10999
  log?.warn?.(
10909
11000
  `[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`
10910
11001
  );
11002
+ return false;
10911
11003
  }
11004
+ questionCardTookOver = true;
10912
11005
  if (!currentAICard) {
10913
- return;
11006
+ return true;
10914
11007
  }
10915
11008
  if (isCardInTerminalState(currentAICard.state)) {
10916
- return;
11009
+ return true;
11010
+ }
11011
+ let recalled = false;
11012
+ try {
11013
+ recalled = await recallAICardMessage(currentAICard, log);
11014
+ } catch (err) {
11015
+ log?.warn?.(
11016
+ `[DingTalk][AskUser] Question card took over, but AI card recall errored question=${questionId} outTrackId=${outTrackId}: ${err instanceof Error ? err.message : String(err)}`
11017
+ );
11018
+ return true;
10917
11019
  }
10918
- const recalled = await recallAICardMessage(currentAICard, log);
10919
11020
  if (!recalled) {
10920
11021
  log?.warn?.(
10921
11022
  `[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`
10922
11023
  );
10923
- return;
11024
+ return true;
10924
11025
  }
10925
11026
  log?.info?.(
10926
11027
  `[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`
10927
11028
  );
11029
+ return true;
10928
11030
  };
10929
11031
  }
10930
11032
  if (useCardMode && !isBtwBypass) {
@@ -11975,6 +12077,194 @@ ${attachmentExtractedText}` : inboundBody;
11975
12077
  }
11976
12078
  }
11977
12079
 
12080
+ // src/card/ask-user-question-store.ts
12081
+ var ASK_USER_LIFECYCLE_NAMESPACE = "cards.ask-user.lifecycle";
12082
+ var ACTIVE_TTL_MS = 5 * 60 * 1e3;
12083
+ var TOMBSTONE_TTL_MS = 30 * 60 * 1e3;
12084
+ function now(options) {
12085
+ return options.now?.() ?? Date.now();
12086
+ }
12087
+ function emptyState(timestamp) {
12088
+ return { version: 1, updatedAt: timestamp, records: [] };
12089
+ }
12090
+ function isActiveState(state) {
12091
+ return state === "reserved" || state === "pending" || state === "dispatching";
12092
+ }
12093
+ function isAnswerableState(state) {
12094
+ return state === "reserved" || state === "pending";
12095
+ }
12096
+ function loadState2(options) {
12097
+ const timestamp = now(options);
12098
+ const state = readNamespaceJson(ASK_USER_LIFECYCLE_NAMESPACE, {
12099
+ storePath: options.storePath,
12100
+ scope: { accountId: options.accountId },
12101
+ fallback: emptyState(timestamp),
12102
+ log: options.log
12103
+ });
12104
+ if (state.version !== 1 || !Array.isArray(state.records)) {
12105
+ return emptyState(timestamp);
12106
+ }
12107
+ return state;
12108
+ }
12109
+ function persistState(options, state) {
12110
+ state.updatedAt = now(options);
12111
+ writeNamespaceJsonAtomic(ASK_USER_LIFECYCLE_NAMESPACE, {
12112
+ storePath: options.storePath,
12113
+ scope: { accountId: options.accountId },
12114
+ data: state,
12115
+ log: options.log
12116
+ });
12117
+ }
12118
+ function toTerminal(record, reason, timestamp) {
12119
+ record.state = "terminal";
12120
+ record.terminalReason = reason;
12121
+ record.updatedAt = timestamp;
12122
+ record.expiresAt = timestamp + TOMBSTONE_TTL_MS;
12123
+ return record;
12124
+ }
12125
+ function cleanupState(state, timestamp) {
12126
+ let changed = false;
12127
+ for (const record of state.records) {
12128
+ if (isAnswerableState(record.state) && record.expiresAt <= timestamp) {
12129
+ toTerminal(record, "expired", timestamp);
12130
+ changed = true;
12131
+ }
12132
+ }
12133
+ const retained = state.records.filter(
12134
+ (record) => record.state !== "terminal" || record.expiresAt > timestamp
12135
+ );
12136
+ if (retained.length !== state.records.length) {
12137
+ state.records = retained;
12138
+ changed = true;
12139
+ }
12140
+ return changed;
12141
+ }
12142
+ function readCleanState(options) {
12143
+ const state = loadState2(options);
12144
+ if (cleanupState(state, now(options))) {
12145
+ persistState(options, state);
12146
+ }
12147
+ return state;
12148
+ }
12149
+ function findRecord(state, identifier) {
12150
+ if (identifier.outTrackId) {
12151
+ const byTrackId = state.records.find((record) => record.outTrackId === identifier.outTrackId);
12152
+ if (byTrackId) {
12153
+ return byTrackId;
12154
+ }
12155
+ }
12156
+ if (identifier.questionId) {
12157
+ return state.records.find((record) => record.questionId === identifier.questionId);
12158
+ }
12159
+ return void 0;
12160
+ }
12161
+ function reserveAskUserQuestion(options, input) {
12162
+ const timestamp = now(options);
12163
+ const state = readCleanState(options);
12164
+ const record = {
12165
+ questionId: input.questionId,
12166
+ accountId: options.accountId,
12167
+ questionScopeKey: input.questionScopeKey,
12168
+ outTrackId: input.outTrackId,
12169
+ title: input.title,
12170
+ state: "reserved",
12171
+ createdAt: timestamp,
12172
+ updatedAt: timestamp,
12173
+ expiresAt: timestamp + ACTIVE_TTL_MS
12174
+ };
12175
+ state.records = state.records.filter(
12176
+ (item) => item.questionId !== input.questionId && item.outTrackId !== input.outTrackId
12177
+ );
12178
+ state.records.push(record);
12179
+ persistState(options, state);
12180
+ return { ...record };
12181
+ }
12182
+ function activateAskUserQuestion(options, questionId) {
12183
+ const timestamp = now(options);
12184
+ const state = readCleanState(options);
12185
+ const record = findRecord(state, { questionId });
12186
+ if (!record || record.state !== "reserved") {
12187
+ return { record: record ? { ...record } : void 0, superseded: [] };
12188
+ }
12189
+ const superseded = [];
12190
+ for (const candidate of state.records) {
12191
+ if (candidate !== record && isAnswerableState(candidate.state) && candidate.questionScopeKey === record.questionScopeKey) {
12192
+ toTerminal(candidate, "superseded_by_question", timestamp);
12193
+ superseded.push({ ...candidate });
12194
+ }
12195
+ }
12196
+ record.state = "pending";
12197
+ record.updatedAt = timestamp;
12198
+ record.expiresAt = timestamp + ACTIVE_TTL_MS;
12199
+ persistState(options, state);
12200
+ return { record: { ...record }, superseded };
12201
+ }
12202
+ function claimAskUserQuestion(options, identifier) {
12203
+ const timestamp = now(options);
12204
+ const state = readCleanState(options);
12205
+ const record = findRecord(state, identifier);
12206
+ if (!record || record.state !== "pending") {
12207
+ return void 0;
12208
+ }
12209
+ record.state = "dispatching";
12210
+ record.updatedAt = timestamp;
12211
+ record.expiresAt = timestamp + ACTIVE_TTL_MS;
12212
+ persistState(options, state);
12213
+ return { ...record };
12214
+ }
12215
+ function terminateAskUserQuestion(options, questionId, reason) {
12216
+ const timestamp = now(options);
12217
+ const state = readCleanState(options);
12218
+ const record = findRecord(state, { questionId });
12219
+ if (!record || !isActiveState(record.state)) {
12220
+ return void 0;
12221
+ }
12222
+ toTerminal(record, reason, timestamp);
12223
+ persistState(options, state);
12224
+ return { ...record };
12225
+ }
12226
+ function invalidateAskUserQuestionsInScope(options, questionScopeKey, reason) {
12227
+ const timestamp = now(options);
12228
+ const state = readCleanState(options);
12229
+ const invalidated = [];
12230
+ for (const record of state.records) {
12231
+ if (isAnswerableState(record.state) && record.questionScopeKey === questionScopeKey) {
12232
+ toTerminal(record, reason, timestamp);
12233
+ invalidated.push({ ...record });
12234
+ }
12235
+ }
12236
+ if (invalidated.length > 0) {
12237
+ persistState(options, state);
12238
+ }
12239
+ return invalidated;
12240
+ }
12241
+ function resolveAskUserQuestion(options, identifier) {
12242
+ const record = findRecord(readCleanState(options), identifier);
12243
+ return record ? { ...record } : void 0;
12244
+ }
12245
+ function recoverAskUserQuestionsAfterRestart(options) {
12246
+ const timestamp = now(options);
12247
+ const state = loadState2(options);
12248
+ const retained = state.records.filter(
12249
+ (record) => record.state !== "terminal" || record.expiresAt > timestamp
12250
+ );
12251
+ const removedExpiredTombstones = retained.length !== state.records.length;
12252
+ state.records = retained;
12253
+ const recovered = [];
12254
+ for (const record of state.records) {
12255
+ if (!isActiveState(record.state)) {
12256
+ continue;
12257
+ }
12258
+ const reason = record.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated";
12259
+ toTerminal(record, reason, timestamp);
12260
+ recovered.push({ ...record });
12261
+ }
12262
+ if (recovered.length > 0 || removedExpiredTombstones) {
12263
+ persistState(options, state);
12264
+ }
12265
+ return recovered;
12266
+ }
12267
+
11978
12268
  // src/card/ask-user-question.ts
11979
12269
  var DINGTALK_API4 = "https://api.dingtalk.com";
11980
12270
  var PENDING_QUESTION_TTL_MS = 5 * 60 * 1e3;
@@ -12244,9 +12534,11 @@ function supersedePendingQuestionsInScope(ctx) {
12244
12534
  });
12245
12535
  }
12246
12536
  }
12247
- function storePendingQuestion(ctx) {
12537
+ function storePendingQuestion(ctx, options = {}) {
12248
12538
  ctx.ownerUserId = resolvePendingQuestionOwner(ctx);
12249
- supersedePendingQuestionsInScope(ctx);
12539
+ if (options.supersedeExisting !== false) {
12540
+ supersedePendingQuestionsInScope(ctx);
12541
+ }
12250
12542
  pendingQuestionsByTrackId.set(ctx.outTrackId, ctx);
12251
12543
  pendingQuestionsByQuestionId.set(ctx.questionId, ctx);
12252
12544
  addScopeIndex(ctx);
@@ -12254,7 +12546,9 @@ function storePendingQuestion(ctx) {
12254
12546
  if (!pendingQuestionsByTrackId.has(ctx.outTrackId) || ctx.submitted) {
12255
12547
  return;
12256
12548
  }
12257
- ctx.submitted = true;
12549
+ if (!claimPendingQuestionForDispatch(ctx)) {
12550
+ return;
12551
+ }
12258
12552
  consumePendingQuestion(ctx);
12259
12553
  addHandledQuestionTombstone(ctx, "expired");
12260
12554
  void updateQuestionCardBestEffort(ctx, {
@@ -12262,14 +12556,12 @@ function storePendingQuestion(ctx) {
12262
12556
  question_desc: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
12263
12557
  form_btn_text: "\u5DF2\u5931\u6548"
12264
12558
  });
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
- );
12559
+ dispatchSyntheticAnswer({
12560
+ ctx,
12561
+ text: buildExpiredAnswerMessage(ctx),
12562
+ suffix: "expired",
12563
+ successReason: "expired",
12564
+ log: ctx.log
12273
12565
  });
12274
12566
  }, PENDING_QUESTION_TTL_MS);
12275
12567
  }
@@ -12294,6 +12586,123 @@ async function updateQuestionCardBestEffort(ctx, variables) {
12294
12586
  );
12295
12587
  }
12296
12588
  }
12589
+ function getAskUserStoreOptions(params) {
12590
+ if (!params.storePath) {
12591
+ return void 0;
12592
+ }
12593
+ return {
12594
+ storePath: params.storePath,
12595
+ accountId: params.accountId,
12596
+ log: params.log
12597
+ };
12598
+ }
12599
+ function terminalCardVariables(reason) {
12600
+ const descriptions = {
12601
+ delivery_failed: "\u95EE\u9898\u5361\u7247\u53D1\u9001\u5931\u8D25\u3002",
12602
+ superseded_by_question: "\u5DF2\u6709\u65B0\u7684\u95EE\u9898\u5361\u7247\uFF0C\u8BF7\u56DE\u7B54\u6700\u65B0\u5361\u7247\u3002",
12603
+ 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",
12604
+ expired: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
12605
+ cancelled: "\u5DF2\u53D6\u6D88\u3002",
12606
+ empty: "\u5DF2\u63D0\u4EA4\uFF0C\u672A\u586B\u5199\u4EFB\u4F55\u5185\u5BB9\u3002",
12607
+ submitted: "\u5DF2\u63D0\u4EA4\u3002",
12608
+ pause_failed: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
12609
+ restart_invalidated: "\u670D\u52A1\u5DF2\u91CD\u542F\uFF0C\u539F\u95EE\u9898\u4E0A\u4E0B\u6587\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
12610
+ 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",
12611
+ 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"
12612
+ };
12613
+ return {
12614
+ card_status: reason === "cancelled" ? "cancelled" : reason === "submitted" || reason === "empty" ? "submitted" : "expired",
12615
+ question_desc: descriptions[reason],
12616
+ form_btn_text: reason === "cancelled" ? "\u5DF2\u53D6\u6D88" : reason === "submitted" || reason === "empty" ? "\u5DF2\u63D0\u4EA4" : "\u5DF2\u5931\u6548"
12617
+ };
12618
+ }
12619
+ async function updateLifecycleRecordCardBestEffort(params) {
12620
+ const reason = params.record.terminalReason;
12621
+ if (!reason || reason === "delivery_failed") {
12622
+ return;
12623
+ }
12624
+ try {
12625
+ const token = await getAccessToken(params.config, params.log);
12626
+ await updateCardVariables(
12627
+ params.record.outTrackId,
12628
+ terminalCardVariables(reason),
12629
+ token,
12630
+ params.config
12631
+ );
12632
+ } catch (err) {
12633
+ params.log?.warn?.(
12634
+ `[DingTalk][AskUser] Failed to update lifecycle card ${params.record.questionId}: ${String(err)}`
12635
+ );
12636
+ }
12637
+ }
12638
+ function consumeLifecyclePendingContext(record) {
12639
+ const ctx = pendingQuestionsByQuestionId.get(record.questionId) ?? pendingQuestionsByTrackId.get(record.outTrackId);
12640
+ if (!ctx) {
12641
+ return void 0;
12642
+ }
12643
+ ctx.submitted = true;
12644
+ consumePendingQuestion(ctx);
12645
+ addHandledQuestionTombstone(ctx, record.terminalReason === "expired" ? "expired" : "superseded");
12646
+ return ctx;
12647
+ }
12648
+ function invalidateAskUserQuestionsForScope(params) {
12649
+ const invalidated = invalidateAskUserQuestionsInScope(
12650
+ {
12651
+ storePath: params.storePath,
12652
+ accountId: params.accountId,
12653
+ log: params.log
12654
+ },
12655
+ params.questionScopeKey,
12656
+ params.reason
12657
+ );
12658
+ for (const record of invalidated) {
12659
+ consumeLifecyclePendingContext(record);
12660
+ }
12661
+ return invalidated;
12662
+ }
12663
+ async function syncInvalidatedAskUserQuestionCards(params) {
12664
+ await Promise.allSettled(
12665
+ params.records.map(
12666
+ (record) => updateLifecycleRecordCardBestEffort({
12667
+ record,
12668
+ config: params.config,
12669
+ log: params.log
12670
+ })
12671
+ )
12672
+ );
12673
+ }
12674
+ async function recoverAskUserQuestionsForAccount(params) {
12675
+ if (!params.storePath) {
12676
+ return 0;
12677
+ }
12678
+ const recovered = recoverAskUserQuestionsAfterRestart({
12679
+ storePath: params.storePath,
12680
+ accountId: params.accountId,
12681
+ log: params.log
12682
+ });
12683
+ for (const record of recovered) {
12684
+ consumeLifecyclePendingContext(record);
12685
+ await updateLifecycleRecordCardBestEffort({
12686
+ record,
12687
+ config: params.config,
12688
+ log: params.log
12689
+ });
12690
+ }
12691
+ return recovered.length;
12692
+ }
12693
+ async function terminatePendingQuestion(params) {
12694
+ const storeOptions = getAskUserStoreOptions(params.ctx);
12695
+ if (storeOptions) {
12696
+ terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.reason);
12697
+ }
12698
+ params.ctx.submitted = true;
12699
+ consumePendingQuestion(params.ctx);
12700
+ addHandledQuestionTombstone(
12701
+ params.ctx,
12702
+ params.reason === "cancelled" ? "cancelled" : params.reason === "empty" ? "empty" : params.reason === "submitted" ? "submitted" : params.reason === "expired" ? "expired" : "superseded"
12703
+ );
12704
+ await updateQuestionCardBestEffort(params.ctx, terminalCardVariables(params.reason));
12705
+ }
12297
12706
  function parseEmbeddedJson2(value) {
12298
12707
  if (typeof value !== "string") {
12299
12708
  return value;
@@ -12413,7 +12822,42 @@ async function injectAnswerSyntheticMessage(ctx, text, suffix) {
12413
12822
  data: syntheticData,
12414
12823
  sessionWebhook: ctx.sessionWebhook,
12415
12824
  log: ctx.log,
12416
- dingtalkConfig: ctx.dingtalkConfig
12825
+ dingtalkConfig: ctx.dingtalkConfig,
12826
+ inboundOrigin: "ask-user",
12827
+ routeOverride: ctx.resolvedRoute,
12828
+ subAgentOptions: ctx.continuationSubAgentOptions
12829
+ });
12830
+ }
12831
+ function claimPendingQuestionForDispatch(ctx) {
12832
+ const storeOptions = getAskUserStoreOptions(ctx);
12833
+ if (storeOptions) {
12834
+ return Boolean(
12835
+ claimAskUserQuestion(storeOptions, {
12836
+ questionId: ctx.questionId,
12837
+ outTrackId: ctx.outTrackId
12838
+ })
12839
+ );
12840
+ }
12841
+ if (ctx.submitted) {
12842
+ return false;
12843
+ }
12844
+ ctx.submitted = true;
12845
+ return true;
12846
+ }
12847
+ function dispatchSyntheticAnswer(params) {
12848
+ const storeOptions = getAskUserStoreOptions(params.ctx);
12849
+ void injectAnswerSyntheticMessage(params.ctx, params.text, params.suffix).then(() => {
12850
+ if (storeOptions) {
12851
+ terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.successReason);
12852
+ }
12853
+ }).catch((err) => {
12854
+ if (storeOptions) {
12855
+ terminateAskUserQuestion(storeOptions, params.ctx.questionId, "dispatch_failed");
12856
+ }
12857
+ void updateQuestionCardBestEffort(params.ctx, terminalCardVariables("dispatch_failed"));
12858
+ params.log?.error?.(
12859
+ `[DingTalk][AskUser] Failed to inject ${params.suffix} answer message: ${String(err)}`
12860
+ );
12417
12861
  });
12418
12862
  }
12419
12863
  async function handleDingTalkAskUserCardCallback(params) {
@@ -12425,8 +12869,43 @@ async function handleDingTalkAskUserCardCallback(params) {
12425
12869
  );
12426
12870
  return { handled: true };
12427
12871
  }
12872
+ const storeOptions = params.storePath ? {
12873
+ storePath: params.storePath,
12874
+ accountId: params.accountId,
12875
+ log: params.log
12876
+ } : void 0;
12877
+ const lifecycleRecord = storeOptions ? resolveAskUserQuestion(storeOptions, {
12878
+ questionId: parsed.actionId,
12879
+ outTrackId: parsed.outTrackId
12880
+ }) : void 0;
12881
+ if (lifecycleRecord?.state === "terminal") {
12882
+ params.log?.debug?.(
12883
+ `[DingTalk][AskUser] Ignoring terminal callback question=${lifecycleRecord.questionId} reason=${lifecycleRecord.terminalReason ?? "unknown"}`
12884
+ );
12885
+ await updateLifecycleRecordCardBestEffort({
12886
+ record: lifecycleRecord,
12887
+ config: params.config,
12888
+ log: params.log
12889
+ });
12890
+ return { handled: true };
12891
+ }
12428
12892
  const ctx = (parsed.outTrackId ? pendingQuestionsByTrackId.get(parsed.outTrackId) : void 0) ?? (parsed.actionId ? pendingQuestionsByQuestionId.get(parsed.actionId) : void 0);
12429
12893
  if (!ctx) {
12894
+ if (lifecycleRecord && storeOptions) {
12895
+ const recovered = terminateAskUserQuestion(
12896
+ storeOptions,
12897
+ lifecycleRecord.questionId,
12898
+ lifecycleRecord.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated"
12899
+ );
12900
+ if (recovered) {
12901
+ await updateLifecycleRecordCardBestEffort({
12902
+ record: recovered,
12903
+ config: params.config,
12904
+ log: params.log
12905
+ });
12906
+ }
12907
+ return { handled: true };
12908
+ }
12430
12909
  return { handled: false };
12431
12910
  }
12432
12911
  if (!isOwnerClick(ctx, params.clickerUserId)) {
@@ -12441,13 +12920,15 @@ async function handleDingTalkAskUserCardCallback(params) {
12441
12920
  );
12442
12921
  return { handled: true };
12443
12922
  }
12444
- if (ctx.submitted) {
12923
+ if (ctx.submitted || lifecycleRecord?.state === "dispatching") {
12445
12924
  params.log?.debug?.(`[DingTalk][AskUser] Duplicate submit ignored question=${ctx.questionId}`);
12446
12925
  return { handled: true };
12447
12926
  }
12448
12927
  const isCancel = parseBooleanLike(parsed.params.user_cancel) === true;
12449
- ctx.submitted = true;
12450
12928
  if (isCancel) {
12929
+ if (!claimPendingQuestionForDispatch(ctx)) {
12930
+ return { handled: true };
12931
+ }
12451
12932
  await updateQuestionCardBestEffort(ctx, {
12452
12933
  card_status: "cancelled",
12453
12934
  question_desc: "\u5DF2\u53D6\u6D88\u3002",
@@ -12455,25 +12936,25 @@ async function handleDingTalkAskUserCardCallback(params) {
12455
12936
  });
12456
12937
  consumePendingQuestion(ctx);
12457
12938
  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
- );
12939
+ dispatchSyntheticAnswer({
12940
+ ctx,
12941
+ text: buildCancelledAnswerMessage(ctx),
12942
+ suffix: "cancelled",
12943
+ successReason: "cancelled",
12944
+ log: params.log
12466
12945
  });
12467
12946
  return { handled: true };
12468
12947
  }
12469
12948
  const form = asRecord5(parsed.params.form);
12470
12949
  if (!form) {
12471
- ctx.submitted = false;
12472
12950
  params.log?.warn?.(
12473
12951
  `[DingTalk][AskUser] Missing form payload question=${ctx.questionId} params=${JSON.stringify(parsed.params)}`
12474
12952
  );
12475
12953
  return { handled: true };
12476
12954
  }
12955
+ if (!claimPendingQuestionForDispatch(ctx)) {
12956
+ return { handled: true };
12957
+ }
12477
12958
  const answers = [];
12478
12959
  const selectedValues = [];
12479
12960
  for (const question of ctx.questions) {
@@ -12497,12 +12978,12 @@ async function handleDingTalkAskUserCardCallback(params) {
12497
12978
  });
12498
12979
  consumePendingQuestion(ctx);
12499
12980
  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
- });
12981
+ dispatchSyntheticAnswer({
12982
+ ctx,
12983
+ text: buildEmptyAnswerMessage(ctx),
12984
+ suffix: "empty",
12985
+ successReason: "empty",
12986
+ log: params.log
12506
12987
  });
12507
12988
  return { handled: true };
12508
12989
  }
@@ -12517,10 +12998,12 @@ async function handleDingTalkAskUserCardCallback(params) {
12517
12998
  consumePendingQuestion(ctx);
12518
12999
  addHandledQuestionTombstone(ctx, "submitted");
12519
13000
  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
- });
13001
+ dispatchSyntheticAnswer({
13002
+ ctx,
13003
+ text: message,
13004
+ suffix: "submitted",
13005
+ successReason: "submitted",
13006
+ log: params.log
12524
13007
  });
12525
13008
  return { handled: true };
12526
13009
  }
@@ -12687,6 +13170,16 @@ function registerDingTalkAskUserQuestionTool(api) {
12687
13170
  selected_values: "[]",
12688
13171
  form: { fields }
12689
13172
  };
13173
+ const storeOptions = getAskUserStoreOptions(context);
13174
+ const canPersistLifecycle = Boolean(storeOptions && context.questionScopeKey);
13175
+ if (storeOptions && context.questionScopeKey) {
13176
+ reserveAskUserQuestion(storeOptions, {
13177
+ questionId,
13178
+ questionScopeKey: context.questionScopeKey,
13179
+ outTrackId,
13180
+ title
13181
+ });
13182
+ }
12690
13183
  try {
12691
13184
  await createAndDeliverQuestionCard({
12692
13185
  config: context.dingtalkConfig,
@@ -12698,6 +13191,9 @@ function registerDingTalkAskUserQuestionTool(api) {
12698
13191
  log: context.log
12699
13192
  });
12700
13193
  } catch (err) {
13194
+ if (storeOptions && canPersistLifecycle) {
13195
+ terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
13196
+ }
12701
13197
  const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
12702
13198
  return jsonToolResult({
12703
13199
  status: "failed",
@@ -12708,20 +13204,75 @@ function registerDingTalkAskUserQuestionTool(api) {
12708
13204
  ...context,
12709
13205
  onQuestionCardSent: void 0
12710
13206
  };
12711
- storePendingQuestion({
13207
+ const pendingQuestion = {
12712
13208
  ...pendingContext,
12713
13209
  questionId,
12714
13210
  outTrackId,
12715
13211
  title,
12716
13212
  questions: parsed,
12717
13213
  submitted: false
13214
+ };
13215
+ storePendingQuestion(pendingQuestion, {
13216
+ supersedeExisting: !canPersistLifecycle
12718
13217
  });
13218
+ if (storeOptions && canPersistLifecycle) {
13219
+ const activation = activateAskUserQuestion(storeOptions, questionId);
13220
+ if (activation.record?.state !== "pending") {
13221
+ const terminalReason = activation.record?.terminalReason ?? "superseded_by_message";
13222
+ if (activation.record) {
13223
+ consumeLifecyclePendingContext(activation.record);
13224
+ } else {
13225
+ pendingQuestion.submitted = true;
13226
+ consumePendingQuestion(pendingQuestion);
13227
+ addHandledQuestionTombstone(pendingQuestion, "superseded");
13228
+ }
13229
+ await updateQuestionCardBestEffort(
13230
+ pendingQuestion,
13231
+ terminalCardVariables(terminalReason)
13232
+ );
13233
+ return jsonToolResult({
13234
+ status: "failed",
13235
+ questionId,
13236
+ outTrackId,
13237
+ error: "\u95EE\u9898\u5361\u7247\u5728\u53D1\u9001\u671F\u95F4\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
13238
+ });
13239
+ }
13240
+ for (const superseded of activation.superseded) {
13241
+ const supersededContext = consumeLifecyclePendingContext(superseded);
13242
+ if (supersededContext) {
13243
+ void updateQuestionCardBestEffort(
13244
+ supersededContext,
13245
+ terminalCardVariables("superseded_by_question")
13246
+ );
13247
+ } else {
13248
+ void updateLifecycleRecordCardBestEffort({
13249
+ record: superseded,
13250
+ config: context.dingtalkConfig,
13251
+ log: context.log
13252
+ });
13253
+ }
13254
+ }
13255
+ }
13256
+ let takeoverSucceeded = void 0;
12719
13257
  try {
12720
- await context.onQuestionCardSent?.({ questionId, outTrackId });
13258
+ takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
12721
13259
  } catch (err) {
12722
13260
  context.log?.warn?.(
12723
13261
  `[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`
12724
13262
  );
13263
+ takeoverSucceeded = false;
13264
+ }
13265
+ if (takeoverSucceeded === false) {
13266
+ await terminatePendingQuestion({
13267
+ ctx: pendingQuestion,
13268
+ reason: "pause_failed"
13269
+ });
13270
+ return jsonToolResult({
13271
+ status: "failed",
13272
+ questionId,
13273
+ outTrackId,
13274
+ error: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
13275
+ });
12725
13276
  }
12726
13277
  context.log?.info?.(
12727
13278
  `[DingTalk][AskUser] question card sent question=${questionId} outTrackId=${outTrackId}`
@@ -12926,6 +13477,7 @@ async function handleCardAction(params) {
12926
13477
  payload: params.payload,
12927
13478
  cfg: params.cfg,
12928
13479
  accountId: params.accountId,
13480
+ storePath: params.storePath,
12929
13481
  config: params.config,
12930
13482
  clickerUserId: params.analysis.userId,
12931
13483
  log: params.log
@@ -13066,8 +13618,8 @@ var ConnectionManager = class _ConnectionManager {
13066
13618
  `[${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
13619
  );
13068
13620
  }
13069
- recordSocketActivity(now = Date.now()) {
13070
- this.lastSocketActivityAt = now;
13621
+ recordSocketActivity(now2 = Date.now()) {
13622
+ this.lastSocketActivityAt = now2;
13071
13623
  this.consecutiveHeartbeatMisses = 0;
13072
13624
  }
13073
13625
  setupHeartbeat(socket) {
@@ -13079,13 +13631,13 @@ var ConnectionManager = class _ConnectionManager {
13079
13631
  if (socket.readyState !== 1) {
13080
13632
  return;
13081
13633
  }
13082
- const now = Date.now();
13083
- const idleMs = this.lastSocketActivityAt !== void 0 ? now - this.lastSocketActivityAt : _ConnectionManager.HEARTBEAT_INTERVAL_MS;
13634
+ const now2 = Date.now();
13635
+ const idleMs = this.lastSocketActivityAt !== void 0 ? now2 - this.lastSocketActivityAt : _ConnectionManager.HEARTBEAT_INTERVAL_MS;
13084
13636
  if (idleMs >= _ConnectionManager.HEARTBEAT_INTERVAL_MS) {
13085
13637
  this.consecutiveHeartbeatMisses += 1;
13086
13638
  this.runtimeCounters.heartbeatMisses += 1;
13087
13639
  if (this.consecutiveHeartbeatMisses >= _ConnectionManager.HEARTBEAT_MISS_THRESHOLD) {
13088
- const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now - this.lastHeartbeatPingAt : void 0;
13640
+ const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now2 - this.lastHeartbeatPingAt : void 0;
13089
13641
  this.log?.warn?.(
13090
13642
  `[${this.accountId}] Connection heartbeat missed ${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD} checks, triggering reconnection (lastPingAgoMs=${lastPingAgoMs ?? "n/a"})`
13091
13643
  );
@@ -13099,7 +13651,7 @@ var ConnectionManager = class _ConnectionManager {
13099
13651
  `[${this.accountId}] Connection heartbeat missed (${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD})`
13100
13652
  );
13101
13653
  }
13102
- this.lastHeartbeatPingAt = now;
13654
+ this.lastHeartbeatPingAt = now2;
13103
13655
  try {
13104
13656
  socket.ping("", true);
13105
13657
  } catch (err) {
@@ -13340,8 +13892,8 @@ var ConnectionManager = class _ConnectionManager {
13340
13892
  this.consecutiveUnhealthyChecks = 0;
13341
13893
  return;
13342
13894
  }
13343
- const now = Date.now();
13344
- const withinGraceWindow = this.connectedAt !== void 0 && now - this.connectedAt < _ConnectionManager.HEALTH_CHECK_GRACE_MS;
13895
+ const now2 = Date.now();
13896
+ const withinGraceWindow = this.connectedAt !== void 0 && now2 - this.connectedAt < _ConnectionManager.HEALTH_CHECK_GRACE_MS;
13345
13897
  if (withinGraceWindow) {
13346
13898
  this.consecutiveUnhealthyChecks = 0;
13347
13899
  return;
@@ -13699,12 +14251,12 @@ var MESSAGE_DEDUP_TTL = 6e4;
13699
14251
  var MESSAGE_DEDUP_MAX_SIZE = 1e3;
13700
14252
  var messageCounter = 0;
13701
14253
  function isMessageProcessed(dedupKey) {
13702
- const now = Date.now();
14254
+ const now2 = Date.now();
13703
14255
  const expiresAt = processedMessages.get(dedupKey);
13704
14256
  if (expiresAt === void 0) {
13705
14257
  return false;
13706
14258
  }
13707
- if (now >= expiresAt) {
14259
+ if (now2 >= expiresAt) {
13708
14260
  processedMessages.delete(dedupKey);
13709
14261
  return false;
13710
14262
  }
@@ -13714,9 +14266,9 @@ function markMessageProcessed(dedupKey) {
13714
14266
  const expiresAt = Date.now() + MESSAGE_DEDUP_TTL;
13715
14267
  processedMessages.set(dedupKey, expiresAt);
13716
14268
  if (processedMessages.size > MESSAGE_DEDUP_MAX_SIZE) {
13717
- const now = Date.now();
14269
+ const now2 = Date.now();
13718
14270
  for (const [key, expiry] of processedMessages.entries()) {
13719
- if (now >= expiry) {
14271
+ if (now2 >= expiry) {
13720
14272
  processedMessages.delete(key);
13721
14273
  }
13722
14274
  }
@@ -13735,9 +14287,9 @@ function markMessageProcessed(dedupKey) {
13735
14287
  messageCounter++;
13736
14288
  if (messageCounter >= 10) {
13737
14289
  messageCounter = 0;
13738
- const now = Date.now();
14290
+ const now2 = Date.now();
13739
14291
  for (const [key, expiry] of processedMessages.entries()) {
13740
- if (now >= expiry) {
14292
+ if (now2 >= expiry) {
13741
14293
  processedMessages.delete(key);
13742
14294
  }
13743
14295
  }
@@ -13864,6 +14416,23 @@ function createDingTalkGateway() {
13864
14416
  `[${account.accountId}] Failed to recover unfinished cards: ${err.message}`
13865
14417
  );
13866
14418
  }
14419
+ try {
14420
+ const recoveredQuestions = await recoverAskUserQuestionsForAccount({
14421
+ storePath: accountStorePath,
14422
+ accountId: account.accountId,
14423
+ config,
14424
+ log: pluginLog
14425
+ });
14426
+ if (recoveredQuestions > 0) {
14427
+ pluginLog?.info?.(
14428
+ `[${account.accountId}] Invalidated ${recoveredQuestions} unfinished Ask User card(s) from previous runtime`
14429
+ );
14430
+ }
14431
+ } catch (err) {
14432
+ pluginLog?.warn?.(
14433
+ `[${account.accountId}] Failed to recover Ask User cards: ${err.message}`
14434
+ );
14435
+ }
13867
14436
  const useConnectionManager = config.useConnectionManager ?? true;
13868
14437
  const applyStatusPatch = (patch) => {
13869
14438
  ctx.setStatus({
@@ -14034,6 +14603,7 @@ function createDingTalkGateway() {
14034
14603
  analysis,
14035
14604
  cfg,
14036
14605
  accountId: account.accountId,
14606
+ storePath: accountStorePath,
14037
14607
  config,
14038
14608
  log: pluginLog
14039
14609
  });
@@ -14704,9 +15274,9 @@ async function configureDingTalkAccount(params) {
14704
15274
  let lastWaitingNote = 0;
14705
15275
  const result = await session.waitForResult({
14706
15276
  onWaiting: () => {
14707
- const now = Date.now();
14708
- if (now - lastWaitingNote >= 15e3) {
14709
- lastWaitingNote = now;
15277
+ const now2 = Date.now();
15278
+ if (now2 - lastWaitingNote >= 15e3) {
15279
+ lastWaitingNote = now2;
14710
15280
  prompter.note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling").catch(() => {
14711
15281
  });
14712
15282
  }