@openclaw/feishu 2026.7.2-beta.3 → 2026.7.2-beta.5

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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/{accounts-BDoGHJDk.js → accounts-CKhCa76b.js} +2 -19
  3. package/dist/api.js +65 -43
  4. package/dist/{app-registration-BzkyrVZu.js → app-registration-cN1bVV6s.js} +5 -5
  5. package/dist/{channel-BdnXYU6g.js → channel-CBNICmK9.js} +37 -75
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/{channel.runtime-CVIzo2dV.js → channel.runtime-CSlJVqy4.js} +7 -7
  8. package/dist/{client-87BmeMpj.js → client-Dee7cKsS.js} +62 -1
  9. package/dist/contract-api.js +2 -2
  10. package/dist/doctor-contract-BiD9tyIv.js +102 -0
  11. package/dist/doctor-contract-api.js +1 -1
  12. package/dist/{drive-BHv8WW9i.js → drive-DG6pKTPE.js} +23 -20
  13. package/dist/{send-DcrXbqqA.js → media-DOwcLJ1j.js} +1050 -1021
  14. package/dist/{monitor-BVQf7-Bl.js → monitor-DEHJlyWW.js} +6 -5
  15. package/dist/{monitor.account-Blbf2T6J.js → monitor.account-DEHl8Non.js} +1556 -592
  16. package/dist/monitor.startup-CSUPT_U1.js +143 -0
  17. package/dist/{probe-BgboTHIn.js → probe-Za1kgUvx.js} +70 -10
  18. package/dist/runtime-api.js +1 -2
  19. package/dist/{security-audit-BIeA3W3Q.js → security-audit-D7WK_BHh.js} +1 -1
  20. package/dist/{security-audit-shared-BIHeF-S_.js → security-audit-shared-BgpY7AiJ.js} +6 -11
  21. package/dist/security-contract-api.js +1 -1
  22. package/dist/{send-result-DfE5Fhz6.js → send-result-Bcofg0Vv.js} +1 -1
  23. package/dist/session-binding-contract-api.js +1 -1
  24. package/dist/setup-api.js +1 -1
  25. package/dist/{subagent-hooks-BKTOxB-T.js → subagent-hooks-DL2SC5zX.js} +1 -1
  26. package/dist/subagent-hooks-api.js +1 -1
  27. package/dist/{thread-bindings-V0bwk0A1.js → thread-bindings-Dqs_A0du.js} +2 -1
  28. package/node_modules/@larksuiteoapi/node-sdk/README.md +13 -0
  29. package/node_modules/@larksuiteoapi/node-sdk/README.zh.md +12 -0
  30. package/node_modules/@larksuiteoapi/node-sdk/es/index.js +8950 -3084
  31. package/node_modules/@larksuiteoapi/node-sdk/lib/index.js +8951 -3083
  32. package/node_modules/@larksuiteoapi/node-sdk/package.json +1 -1
  33. package/node_modules/@larksuiteoapi/node-sdk/types/index.d.ts +12522 -698
  34. package/node_modules/@protobufjs/utf8/CHANGELOG.md +8 -0
  35. package/node_modules/@protobufjs/utf8/index.js +29 -18
  36. package/node_modules/@protobufjs/utf8/package.json +2 -2
  37. package/node_modules/@protobufjs/utf8/tests/index.js +5 -0
  38. package/node_modules/@types/node/README.md +1 -1
  39. package/node_modules/@types/node/fs/promises.d.ts +16 -6
  40. package/node_modules/@types/node/package.json +2 -2
  41. package/node_modules/agent-base/README.md +145 -0
  42. package/node_modules/agent-base/dist/src/index.d.ts +78 -0
  43. package/node_modules/agent-base/dist/src/index.js +203 -0
  44. package/node_modules/agent-base/dist/src/index.js.map +1 -0
  45. package/node_modules/agent-base/dist/src/promisify.d.ts +4 -0
  46. package/node_modules/agent-base/dist/src/promisify.js +18 -0
  47. package/node_modules/agent-base/dist/src/promisify.js.map +1 -0
  48. package/node_modules/agent-base/package.json +64 -0
  49. package/node_modules/agent-base/src/index.ts +345 -0
  50. package/node_modules/agent-base/src/promisify.ts +33 -0
  51. package/node_modules/axios/CHANGELOG.md +197 -1
  52. package/node_modules/axios/README.md +424 -256
  53. package/node_modules/axios/dist/axios.js +589 -204
  54. package/node_modules/axios/dist/axios.min.js +3 -3
  55. package/node_modules/axios/dist/axios.min.js.map +1 -1
  56. package/node_modules/axios/dist/browser/axios.cjs +630 -185
  57. package/node_modules/axios/dist/esm/axios.js +630 -185
  58. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  59. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  60. package/node_modules/axios/dist/node/axios.cjs +949 -301
  61. package/node_modules/axios/index.d.cts +28 -5
  62. package/node_modules/axios/index.d.ts +24 -3
  63. package/node_modules/axios/lib/adapters/adapters.js +1 -1
  64. package/node_modules/axios/lib/adapters/fetch.js +223 -49
  65. package/node_modules/axios/lib/adapters/http.js +408 -193
  66. package/node_modules/axios/lib/adapters/xhr.js +3 -1
  67. package/node_modules/axios/lib/core/Axios.js +4 -2
  68. package/node_modules/axios/lib/core/AxiosError.js +13 -1
  69. package/node_modules/axios/lib/core/AxiosHeaders.js +13 -42
  70. package/node_modules/axios/lib/core/buildFullPath.js +29 -1
  71. package/node_modules/axios/lib/core/mergeConfig.js +35 -0
  72. package/node_modules/axios/lib/defaults/transitional.js +2 -0
  73. package/node_modules/axios/lib/env/data.js +1 -1
  74. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -3
  75. package/node_modules/axios/lib/helpers/Http2Sessions.js +119 -0
  76. package/node_modules/axios/lib/helpers/buildURL.js +7 -4
  77. package/node_modules/axios/lib/helpers/composeSignals.js +48 -47
  78. package/node_modules/axios/lib/helpers/cookies.js +5 -1
  79. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +16 -11
  80. package/node_modules/axios/lib/helpers/formDataToJSON.js +26 -4
  81. package/node_modules/axios/lib/helpers/formDataToStream.js +2 -2
  82. package/node_modules/axios/lib/helpers/fromDataURI.js +20 -5
  83. package/node_modules/axios/lib/helpers/progressEventReducer.js +3 -0
  84. package/node_modules/axios/lib/helpers/resolveConfig.js +26 -13
  85. package/node_modules/axios/lib/helpers/sanitizeHeaderValue.js +60 -0
  86. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +33 -1
  87. package/node_modules/axios/lib/helpers/toFormData.js +48 -12
  88. package/node_modules/axios/lib/helpers/validator.js +1 -1
  89. package/node_modules/axios/lib/utils.js +105 -19
  90. package/node_modules/axios/package.json +31 -13
  91. package/node_modules/https-proxy-agent/README.md +137 -0
  92. package/node_modules/https-proxy-agent/dist/agent.d.ts +30 -0
  93. package/node_modules/https-proxy-agent/dist/agent.js +177 -0
  94. package/node_modules/https-proxy-agent/dist/agent.js.map +1 -0
  95. package/node_modules/https-proxy-agent/dist/index.d.ts +23 -0
  96. package/node_modules/https-proxy-agent/dist/index.js +14 -0
  97. package/node_modules/https-proxy-agent/dist/index.js.map +1 -0
  98. package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts +7 -0
  99. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js +66 -0
  100. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map +1 -0
  101. package/node_modules/https-proxy-agent/package.json +56 -0
  102. package/node_modules/protobufjs/dist/light/protobuf.js +145 -188
  103. package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
  104. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  105. package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
  106. package/node_modules/protobufjs/dist/minimal/protobuf.js +33 -76
  107. package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
  108. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  109. package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
  110. package/node_modules/protobufjs/dist/protobuf.js +168 -208
  111. package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
  112. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  113. package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
  114. package/node_modules/protobufjs/index.d.ts +0 -8
  115. package/node_modules/protobufjs/package.json +2 -3
  116. package/node_modules/protobufjs/src/parse.js +3 -0
  117. package/node_modules/protobufjs/src/util/minimal.js +0 -3
  118. package/node_modules/qs/CHANGELOG.md +11 -0
  119. package/node_modules/qs/dist/qs.js +17 -17
  120. package/node_modules/qs/lib/parse.js +19 -8
  121. package/node_modules/qs/lib/utils.js +47 -8
  122. package/node_modules/qs/package.json +6 -5
  123. package/node_modules/qs/test/parse.js +230 -0
  124. package/node_modules/qs/test/utils.js +194 -0
  125. package/node_modules/typebox/build/compile/validator.d.mts +1 -4
  126. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  127. package/node_modules/typebox/build/guard/guard.mjs +5 -2
  128. package/node_modules/typebox/build/guard/string.mjs +16 -4
  129. package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
  130. package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
  131. package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
  132. package/node_modules/typebox/build/typebox.d.mts +1 -1
  133. package/node_modules/typebox/build/typebox.mjs +1 -1
  134. package/node_modules/typebox/package.json +1 -1
  135. package/node_modules/typebox/readme.md +29 -32
  136. package/node_modules/ws/lib/receiver.js +15 -32
  137. package/node_modules/ws/lib/websocket-server.js +4 -4
  138. package/node_modules/ws/lib/websocket.js +4 -4
  139. package/node_modules/ws/package.json +5 -1
  140. package/openclaw.plugin.json +2 -16
  141. package/package.json +20 -8
  142. package/dist/doctor-contract-DXH5hux1.js +0 -97
  143. package/dist/monitor.startup-Cy8NPGAX.js +0 -43
  144. package/node_modules/@protobufjs/inquire/CHANGELOG.md +0 -8
  145. package/node_modules/@protobufjs/inquire/LICENSE +0 -26
  146. package/node_modules/@protobufjs/inquire/README.md +0 -13
  147. package/node_modules/@protobufjs/inquire/index.d.ts +0 -10
  148. package/node_modules/@protobufjs/inquire/index.js +0 -38
  149. package/node_modules/@protobufjs/inquire/package.json +0 -21
  150. package/node_modules/@protobufjs/inquire/tests/data/array.js +0 -1
  151. package/node_modules/@protobufjs/inquire/tests/data/emptyArray.js +0 -1
  152. package/node_modules/@protobufjs/inquire/tests/data/emptyObject.js +0 -1
  153. package/node_modules/@protobufjs/inquire/tests/data/object.js +0 -1
  154. package/node_modules/@protobufjs/inquire/tests/index.js +0 -20
  155. package/node_modules/axios/dist/axios.js.map +0 -1
  156. package/node_modules/axios/dist/browser/axios.cjs.map +0 -1
  157. package/node_modules/axios/dist/esm/axios.js.map +0 -1
  158. package/node_modules/axios/dist/node/axios.cjs.map +0 -1
  159. package/npm-shrinkwrap.json +0 -1354
@@ -1,44 +1,46 @@
1
- import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$1, f as normalizeString, g as buildFeishuCommentTarget, h as requestFeishuApi, l as extractReplyText, m as readString, o as resolveFeishuAccount, p as parseCommentContentElements, s as resolveFeishuRuntimeAccount } from "./accounts-BDoGHJDk.js";
1
+ import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$2, f as normalizeString, g as buildFeishuCommentTarget, h as requestFeishuApi, l as extractReplyText, m as readString$1, o as resolveFeishuAccount, p as parseCommentContentElements, s as resolveFeishuRuntimeAccount } from "./accounts-CKhCa76b.js";
2
2
  import { i as resolveReceiveIdType } from "./targets-BUjQ1TcA.js";
3
- import { A as resolveFeishuChatType, C as resolveFeishuDmIngressAccess, E as resolveFeishuGroupSenderActivationIngressAccess, M as buildFeishuCardActionTextFallback, N as createFeishuCardInteractionEnvelope, O as resolveFeishuReplyPolicy, P as decodeFeishuCardAction, S as normalizeFeishuAllowEntry, T as resolveFeishuGroupConversationIngressAccess, d as chunkFeishuPostMarkdown, f as materializeFeishuPostMarkdownSoftBreaks, k as normalizeFeishuChatType$2, o as resolveFeishuCardTemplate, w as resolveFeishuGroupConfig, x as hasExplicitFeishuGroupConfig } from "./send-result-DfE5Fhz6.js";
3
+ import { A as resolveFeishuChatType, C as resolveFeishuDmIngressAccess, E as resolveFeishuGroupSenderActivationIngressAccess, M as buildFeishuCardActionTextFallback, N as createFeishuCardInteractionEnvelope, O as resolveFeishuReplyPolicy, P as decodeFeishuCardAction, S as normalizeFeishuAllowEntry, T as resolveFeishuGroupConversationIngressAccess, d as chunkFeishuPostMarkdown, f as materializeFeishuPostMarkdownSoftBreaks, k as normalizeFeishuChatType$2, o as resolveFeishuCardTemplate, w as resolveFeishuGroupConfig, x as hasExplicitFeishuGroupConfig } from "./send-result-Bcofg0Vv.js";
4
4
  import { o as resolveConfiguredFeishuGroupSessionScope, t as buildFeishuConversationId } from "./conversation-id-BeJL-wq7.js";
5
- import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-87BmeMpj.js";
5
+ import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-Dee7cKsS.js";
6
6
  import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
7
- import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-BHv8WW9i.js";
8
- import { t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
7
+ import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-DG6pKTPE.js";
8
+ import { t as createFeishuThreadBindingManager } from "./thread-bindings-Dqs_A0du.js";
9
9
  import { createReplyPrefixContext, evaluateSupplementalContextVisibility, getSessionEntry, normalizeAgentId as normalizeAgentId$2, resolveChannelContextVisibilityMode } from "./runtime-api.js";
10
10
  import { t as readFeishuJsonResponse } from "./json-response-CheljwGr.js";
11
- import { _ as buildFeishuMediaFallbackText, c as parsePostContent, d as isMentionForwardRequest, f as isFeishuGroupChatType, g as normalizeFeishuExternalKey, h as shouldSuppressFeishuTextForVoiceMedia, i as sendCardFeishu, l as extractMentionTargets, m as sendMediaFeishu, n as getMessageFeishu, o as sendMessageFeishu, p as saveMessageResourceFeishu, r as listFeishuThreadMessages, s as sendStructuredCardFeishu, u as isFeishuBroadcastMention, v as resolveFeishuIdentityEmoji } from "./send-DcrXbqqA.js";
12
- import { i as waitForAbortableDelay, r as raceWithTimeoutAndAbort } from "./probe-BgboTHIn.js";
13
- import { t as fetchBotIdentityForMonitor } from "./monitor.startup-Cy8NPGAX.js";
14
- import { createChannelMessageReplyPipeline, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
11
+ import { _ as buildFeishuMediaFallbackText, a as getMessageFeishu, d as parsePostContent, f as extractMentionTargets, g as normalizeFeishuExternalKey, h as isFeishuGroupChatType, l as sendMessageFeishu, m as isMentionForwardRequest, n as sendMediaFeishu, o as listFeishuThreadMessages, p as isFeishuBroadcastMention, r as shouldSuppressFeishuTextForVoiceMedia, s as sendCardFeishu, t as saveMessageResourceFeishu, u as sendStructuredCardFeishu, v as resolveFeishuIdentityEmoji } from "./media-DOwcLJ1j.js";
12
+ import { a as waitForAbortableDelay, i as raceWithTimeoutAndAbort } from "./probe-Za1kgUvx.js";
13
+ import { t as fetchBotIdentityForMonitor } from "./monitor.startup-CSUPT_U1.js";
14
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
15
15
  import { createChannelPairingController, createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
16
16
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
17
17
  import { asDateTimestampMs, isFutureDateTimestampMs, parseStrictNonNegativeInteger, resolveDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveExpiresAtMsFromDurationSeconds, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
18
18
  import { asBoolean, asNullableRecord, isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, readStringValue, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
19
19
  import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-chunking";
20
20
  import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
21
+ import { fanInChannelIngressLifecycles } from "openclaw/plugin-sdk/channel-ingress-runtime";
21
22
  import fs from "node:fs";
22
23
  import os from "node:os";
23
24
  import path from "node:path";
24
25
  import { normalizeAccountId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
25
26
  import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
26
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
27
+ import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
27
28
  import * as Lark from "@larksuiteoapi/node-sdk";
29
+ import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
28
30
  import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
29
31
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
30
32
  import { getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
31
33
  import { resolvePinnedMainDmOwnerFromAllowlist, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
32
34
  import * as crypto$1 from "node:crypto";
33
35
  import crypto, { createHash } from "node:crypto";
34
- import { buildChannelInboundEventContext, formatAgentEnvelope, formatInboundMediaUnavailableText, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
36
+ import { buildChannelInboundEventContext, createChannelPartialDeliveryError, formatAgentEnvelope, formatInboundMediaUnavailableText, isChannelPartialDeliveryError, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
35
37
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
36
38
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
37
39
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
38
40
  import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
39
41
  import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
40
- import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
41
42
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
43
+ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
42
44
  import * as http from "node:http";
43
45
  import { installRequestBodyLimitGuard, readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
44
46
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
@@ -74,6 +76,167 @@ function buildFeishuAgentBody(params) {
74
76
  }
75
77
  //#endregion
76
78
  //#region extensions/feishu/src/bot-broadcast.ts
79
+ function createFeishuBroadcastIngressSettlement(params) {
80
+ const lanes = /* @__PURE__ */ new Set();
81
+ const failures = [];
82
+ const fallbackAbortSignal = new AbortController().signal;
83
+ let fanoutSettled = false;
84
+ let terminal;
85
+ let adoption;
86
+ let abandonment;
87
+ let finalizing = false;
88
+ let deferred = false;
89
+ let replayReleased = false;
90
+ const beginFinalizing = () => {
91
+ if (finalizing) return;
92
+ finalizing = true;
93
+ params.lifecycle?.onAdoptionFinalizing();
94
+ };
95
+ const defer = () => {
96
+ if (deferred) return;
97
+ deferred = true;
98
+ params.lifecycle?.onDeferred();
99
+ };
100
+ const reportReplayCommitError = (error) => {
101
+ try {
102
+ params.onReplayCommitError?.(error);
103
+ } catch {}
104
+ };
105
+ const releaseReplayClaim = (error) => {
106
+ if (replayReleased || terminal === "adopted") return;
107
+ replayReleased = true;
108
+ params.replayClaim?.release({ error });
109
+ };
110
+ const runAbandonment = async (error) => {
111
+ if (terminal) return;
112
+ releaseReplayClaim(error);
113
+ try {
114
+ await params.lifecycle?.onAbandoned();
115
+ } finally {
116
+ terminal = "abandoned";
117
+ }
118
+ };
119
+ const abandon = async (error) => {
120
+ if (terminal) return;
121
+ if (adoption) {
122
+ await adoption.catch(() => void 0);
123
+ if (terminal) return;
124
+ }
125
+ const activeAbandonment = abandonment ?? runAbandonment(error);
126
+ abandonment = activeAbandonment;
127
+ await activeAbandonment;
128
+ };
129
+ const runAdoption = async () => {
130
+ beginFinalizing();
131
+ try {
132
+ await params.lifecycle?.onAdopted();
133
+ terminal = "adopted";
134
+ try {
135
+ params.onAdopted?.();
136
+ } catch {}
137
+ try {
138
+ await params.replayClaim?.commit();
139
+ } catch (error) {
140
+ reportReplayCommitError(error);
141
+ }
142
+ } catch (error) {
143
+ await runAbandonment(error).catch(() => void 0);
144
+ throw error;
145
+ }
146
+ };
147
+ const adopt = async () => {
148
+ if (terminal) return;
149
+ if (abandonment) {
150
+ await abandonment.catch(() => void 0);
151
+ if (terminal) return;
152
+ }
153
+ const activeAdoption = adoption ?? runAdoption();
154
+ adoption = activeAdoption;
155
+ await activeAdoption;
156
+ };
157
+ const maybeSettle = async () => {
158
+ if (!fanoutSettled || terminal) return;
159
+ if (failures.length > 0 || [...lanes].some((lane) => lane.status === "failed" || lane.status === "abandoned")) {
160
+ await abandon(failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed"));
161
+ return;
162
+ }
163
+ if ([...lanes].some((lane) => lane.status === "pending" || lane.status === "deferred" || lane.status === "adopted")) return;
164
+ await adopt();
165
+ };
166
+ return {
167
+ createLane: (replayClaim) => {
168
+ const lane = {
169
+ replayClaim,
170
+ status: "pending"
171
+ };
172
+ lanes.add(lane);
173
+ const releaseLane = (error) => {
174
+ lane.replayClaim?.release({ error });
175
+ };
176
+ return {
177
+ lifecycle: {
178
+ abortSignal: params.lifecycle?.abortSignal ?? fallbackAbortSignal,
179
+ onAdopted: async () => {
180
+ if (lane.status === "adopted" || lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
181
+ lane.status = "adopted";
182
+ beginFinalizing();
183
+ try {
184
+ await lane.replayClaim?.commit();
185
+ } catch (error) {
186
+ reportReplayCommitError(error);
187
+ }
188
+ lane.status = "completed";
189
+ await maybeSettle();
190
+ },
191
+ onDeferred: () => {
192
+ if (lane.status !== "pending") return;
193
+ lane.status = "deferred";
194
+ defer();
195
+ },
196
+ onAdoptionFinalizing: beginFinalizing,
197
+ onAbandoned: async () => {
198
+ if (lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
199
+ lane.status = "abandoned";
200
+ releaseLane(/* @__PURE__ */ new Error("feishu-broadcast-turn-abandoned"));
201
+ await maybeSettle();
202
+ }
203
+ },
204
+ onDispatchComplete: async (dispatched) => {
205
+ if (!dispatched && lane.status === "pending") {
206
+ const error = /* @__PURE__ */ new Error("feishu broadcast lane was not dispatched");
207
+ lane.status = "failed";
208
+ failures.push(error);
209
+ releaseLane(error);
210
+ return;
211
+ }
212
+ if (lane.status !== "pending") return;
213
+ const error = /* @__PURE__ */ new Error("feishu broadcast dispatch returned before turn adoption");
214
+ lane.status = "failed";
215
+ failures.push(error);
216
+ releaseLane(error);
217
+ },
218
+ onDispatchFailed: async (error) => {
219
+ failures.push(error);
220
+ if (lane.status !== "completed") {
221
+ lane.status = "failed";
222
+ releaseLane(error);
223
+ }
224
+ await maybeSettle();
225
+ }
226
+ };
227
+ },
228
+ onLanePending: defer,
229
+ onDispatchComplete: async () => {
230
+ fanoutSettled = true;
231
+ await maybeSettle();
232
+ },
233
+ onDispatchFailed: async (error) => {
234
+ failures.push(error);
235
+ fanoutSettled = true;
236
+ await maybeSettle();
237
+ }
238
+ };
239
+ }
77
240
  function resolveBroadcastAgents(cfg, peerId) {
78
241
  const broadcast = cfg.broadcast;
79
242
  if (!broadcast || typeof broadcast !== "object") return null;
@@ -139,11 +302,14 @@ function resolveFeishuGroupSession(params) {
139
302
  };
140
303
  }
141
304
  function parseMessageContent(content, messageType) {
142
- if (messageType === "post") return parsePostContent(content).textContent;
305
+ if (messageType === "post") return parsePostContent(content, {
306
+ renderMediaPlaceholders: false,
307
+ emptyTextFallback: ""
308
+ }).textContent;
143
309
  try {
144
310
  const parsed = JSON.parse(content);
145
311
  if (messageType === "text") return parsed.text || "";
146
- if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType).body;
312
+ if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType);
147
313
  if (messageType === "share_chat") {
148
314
  if (parsed && typeof parsed === "object") {
149
315
  const share = parsed;
@@ -156,7 +322,7 @@ function parseMessageContent(content, messageType) {
156
322
  if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
157
323
  return content;
158
324
  } catch {
159
- return content;
325
+ return FEISHU_MEDIA_MESSAGE_TYPES.has(messageType) ? "" : content;
160
326
  }
161
327
  }
162
328
  const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
@@ -169,32 +335,8 @@ const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
169
335
  ]);
170
336
  function formatFeishuMediaContent(parsed, messageType) {
171
337
  const speechToText = messageType === "audio" && typeof parsed.speech_to_text === "string" ? parsed.speech_to_text.trim() : "";
172
- if (speechToText) return { body: speechToText };
173
- const placeholder = inferPlaceholder(messageType);
174
- const fileName = typeof parsed.file_name === "string" ? parsed.file_name.trim() : "";
175
- return {
176
- body: fileName ? `${placeholder} (${fileName})` : placeholder,
177
- mediaPlaceholder: placeholder,
178
- unavailableBody: fileName || void 0
179
- };
180
- }
181
- function resolveFeishuMediaFailurePresentation(content, messageType) {
182
- if (messageType === "post") return { unavailableBody: parsePostContent(content, {
183
- renderMediaPlaceholders: false,
184
- emptyTextFallback: ""
185
- }).textContent };
186
- if (!FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return {};
187
- try {
188
- const parsed = JSON.parse(content);
189
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
190
- const presentation = formatFeishuMediaContent(parsed, messageType);
191
- return {
192
- mediaPlaceholder: presentation.mediaPlaceholder,
193
- unavailableBody: presentation.unavailableBody
194
- };
195
- } catch {
196
- return {};
197
- }
338
+ if (speechToText) return speechToText;
339
+ return "";
198
340
  }
199
341
  function formatSubMessageContent(content, contentType) {
200
342
  try {
@@ -295,15 +437,15 @@ async function resolveSavedFeishuMedia(params) {
295
437
  const contentType = params.result.contentType ?? await core.media.detectMime({ buffer: params.result.buffer });
296
438
  return await core.channel.media.saveMediaBuffer(params.result.buffer, contentType, "inbound", params.maxBytes, params.result.fileName ?? params.originalFilename);
297
439
  }
298
- function inferPlaceholder(messageType) {
440
+ function resolveFeishuMediaKind(messageType) {
299
441
  switch (messageType) {
300
- case "image": return "<media:image>";
301
- case "file": return "<media:document>";
302
- case "audio": return "<media:audio>";
442
+ case "image": return "image";
443
+ case "file": return "document";
444
+ case "audio": return "audio";
303
445
  case "video":
304
- case "media": return "<media:video>";
305
- case "sticker": return "<media:sticker>";
306
- default: return "<media:document>";
446
+ case "media": return "video";
447
+ case "sticker": return "sticker";
448
+ default: return "document";
307
449
  }
308
450
  }
309
451
  async function resolveFeishuMediaList(params) {
@@ -316,18 +458,11 @@ async function resolveFeishuMediaList(params) {
316
458
  "media",
317
459
  "sticker",
318
460
  "post"
319
- ].includes(messageType)) return {
320
- media: [],
321
- unavailableCount: 0
322
- };
461
+ ].includes(messageType)) return [];
323
462
  const out = [];
324
- let unavailableCount = 0;
325
463
  if (messageType === "post") {
326
464
  const { imageKeys, mediaKeys } = parsePostContent(content);
327
- if (imageKeys.length === 0 && mediaKeys.length === 0) return {
328
- media: [],
329
- unavailableCount: 0
330
- };
465
+ if (imageKeys.length === 0 && mediaKeys.length === 0) return [];
331
466
  if (imageKeys.length > 0) log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
332
467
  if (mediaKeys.length > 0) log?.(`feishu: post message contains ${mediaKeys.length} embedded media file(s)`);
333
468
  for (const imageKey of imageKeys) try {
@@ -345,11 +480,11 @@ async function resolveFeishuMediaList(params) {
345
480
  out.push({
346
481
  path: saved.path,
347
482
  contentType: saved.contentType,
348
- placeholder: "<media:image>"
483
+ kind: "image"
349
484
  });
350
485
  log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
351
486
  } catch (err) {
352
- unavailableCount += 1;
487
+ out.push({ kind: "image" });
353
488
  log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
354
489
  }
355
490
  for (const media of mediaKeys) try {
@@ -369,29 +504,20 @@ async function resolveFeishuMediaList(params) {
369
504
  out.push({
370
505
  path: saved.path,
371
506
  contentType: saved.contentType,
372
- placeholder: "<media:video>"
507
+ kind: "video"
373
508
  });
374
509
  log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
375
510
  } catch (err) {
376
- unavailableCount += 1;
511
+ out.push({ kind: "video" });
377
512
  log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
378
513
  }
379
- return {
380
- media: out,
381
- unavailableCount
382
- };
514
+ return out;
383
515
  }
384
516
  const mediaKeys = parseMediaKeys(content, messageType);
385
- if (!mediaKeys.imageKey && !mediaKeys.fileKey) return {
386
- media: [],
387
- unavailableCount: 1
388
- };
517
+ if (!mediaKeys.imageKey && !mediaKeys.fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
389
518
  try {
390
519
  const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
391
- if (!fileKey) return {
392
- media: [],
393
- unavailableCount: 1
394
- };
520
+ if (!fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
395
521
  const saved = await resolveSavedFeishuMedia({
396
522
  result: await saveMessageResourceFeishu({
397
523
  cfg,
@@ -408,17 +534,14 @@ async function resolveFeishuMediaList(params) {
408
534
  out.push({
409
535
  path: saved.path,
410
536
  contentType: saved.contentType,
411
- placeholder: inferPlaceholder(messageType)
537
+ kind: resolveFeishuMediaKind(messageType)
412
538
  });
413
539
  log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
414
540
  } catch (err) {
415
- unavailableCount += 1;
541
+ out.push({ kind: resolveFeishuMediaKind(messageType) });
416
542
  log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
417
543
  }
418
- return {
419
- media: out,
420
- unavailableCount
421
- };
544
+ return out;
422
545
  }
423
546
  //#endregion
424
547
  //#region extensions/feishu/src/bot-group-name-state.ts
@@ -742,9 +865,7 @@ function dedupeOptions(namespace, log) {
742
865
  * unidentifiable event is never suppressed.
743
866
  */
744
867
  async function claimUnprocessedFeishuMessage(params) {
745
- const claim = await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
746
- if (claim.kind === "inflight") return { kind: "inflight" };
747
- return claim;
868
+ return await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
748
869
  }
749
870
  /**
750
871
  * Claims (unless the caller already holds the claim) and commits a message.
@@ -759,11 +880,6 @@ async function finalizeFeishuMessageProcessing(params) {
759
880
  if ("kind" in claim && claim.kind !== "claimed") return false;
760
881
  return await ("kind" in claim ? claim.handle : claim).commit();
761
882
  }
762
- /** Records a handled message so restart/replay cannot dispatch it again; false when already recorded. */
763
- async function recordProcessedFeishuMessage(messageId, namespace = "global", log) {
764
- const claim = await feishuDedupeState.guard.claim(messageId, dedupeOptions(namespace, log));
765
- return claim.kind === "claimed" ? await claim.handle.commit() : false;
766
- }
767
883
  /** Forgets a recorded message so a retryable synthetic event can be handled on redelivery. */
768
884
  async function forgetProcessedFeishuMessage(messageId, namespace = "global", log) {
769
885
  return await feishuDedupeState.guard.forget(messageId, dedupeOptions(namespace, log));
@@ -1021,6 +1137,47 @@ function resolveFeishuReasoningPreviewEnabled(params) {
1021
1137
  return configDefault === "stream";
1022
1138
  }
1023
1139
  //#endregion
1140
+ //#region extensions/feishu/src/reply-delivery-result.ts
1141
+ const noVisibleFeishuReplyDelivery = { visibleReplySent: false };
1142
+ function hasProviderIdentity(result) {
1143
+ return Boolean(result && (result.messageId?.trim() || result.receipt?.primaryPlatformMessageId?.trim() || result.receipt?.platformMessageIds.length));
1144
+ }
1145
+ /** Normalizes every physical Lark send behind one logical reply payload. */
1146
+ function createFeishuReplyDeliveryResult(params) {
1147
+ const results = params.visibleReplySent ? (params.results ?? []).filter(hasProviderIdentity) : [];
1148
+ const receipt = results.length > 0 ? createMessageReceiptFromOutboundResults({
1149
+ results,
1150
+ ...params.kind ? { kind: params.kind } : {}
1151
+ }) : void 0;
1152
+ if (!receipt) return {
1153
+ visibleReplySent: params.visibleReplySent,
1154
+ ...params.content === void 0 ? {} : { content: params.content }
1155
+ };
1156
+ return {
1157
+ messageIds: [...receipt.platformMessageIds],
1158
+ receipt,
1159
+ visibleReplySent: params.visibleReplySent,
1160
+ ...params.content === void 0 ? {} : { content: params.content }
1161
+ };
1162
+ }
1163
+ /** Preserves the first result's provider identity while retaining supplemental ids. */
1164
+ function mergeFeishuReplyDeliveryResults(results, content) {
1165
+ const visible = results.filter((result) => result.visibleReplySent === true);
1166
+ return createFeishuReplyDeliveryResult({
1167
+ results: visible,
1168
+ visibleReplySent: visible.length > 0,
1169
+ content: content === void 0 ? results.find((result) => result.content !== void 0)?.content : content
1170
+ });
1171
+ }
1172
+ /** Keeps a failed outcome while exposing the provider-visible subset to core lifecycle hooks. */
1173
+ function createFeishuPartialReplyDeliveryError(cause, result) {
1174
+ if (result.visibleReplySent !== true) return cause instanceof Error ? cause : new Error(formatErrorMessage(cause), { cause });
1175
+ return createChannelPartialDeliveryError(cause, {
1176
+ ...result,
1177
+ visibleReplySent: true
1178
+ });
1179
+ }
1180
+ //#endregion
1024
1181
  //#region extensions/feishu/src/reply-dispatcher-state.ts
1025
1182
  const streamingStartBackoffUntilByAccount = /* @__PURE__ */ new Map();
1026
1183
  //#endregion
@@ -1032,6 +1189,14 @@ function resolveStreamingCardSendMode(options) {
1032
1189
  }
1033
1190
  //#endregion
1034
1191
  //#region extensions/feishu/src/streaming-card.ts
1192
+ /** Provider finalization failed after a streaming card may already be visible. */
1193
+ var FeishuStreamingFinalizationError = class extends Error {
1194
+ constructor(cause, result) {
1195
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
1196
+ this.name = "FeishuStreamingFinalizationError";
1197
+ this.result = result;
1198
+ }
1199
+ };
1035
1200
  const STREAMING_UPDATE_THROTTLE_MS = 160;
1036
1201
  const STREAMING_SIGNIFICANT_DELTA_CHARS = 18;
1037
1202
  const FEISHU_STREAMING_TOKEN_DEFAULT_LIFETIME_SECONDS = 7200;
@@ -1106,23 +1271,17 @@ function truncateSummary(text, max = 50) {
1106
1271
  const clean = text.replace(/\n/g, " ").trim();
1107
1272
  return clean.length <= max ? clean : sliceUtf16Safe(clean, 0, max - 3) + "...";
1108
1273
  }
1109
- function hasNaturalStreamingBoundary(text) {
1110
- return /[\n。!?!?;;::]$/.test(text);
1111
- }
1112
1274
  function shouldPushStreamingUpdate(previousText, nextText) {
1113
- if (!previousText) return true;
1114
- if (hasNaturalStreamingBoundary(nextText)) return true;
1115
- return nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
1275
+ return !previousText || /[\n。!?!?;;::]$/.test(nextText) || nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
1116
1276
  }
1277
+ /** Merges cumulative or overlapping streaming snapshots without duplicating content. */
1117
1278
  function mergeStreamingText(previousText, nextText) {
1118
1279
  const previous = typeof previousText === "string" ? previousText : "";
1119
1280
  const next = typeof nextText === "string" ? nextText : "";
1120
1281
  if (!next) return previous;
1121
1282
  if (!previous || next === previous) return next;
1122
- if (next.startsWith(previous)) return next;
1123
- if (previous.startsWith(next)) return previous;
1124
- if (next.includes(previous)) return next;
1125
- if (previous.includes(next)) return previous;
1283
+ if (next.startsWith(previous) || next.includes(previous)) return next;
1284
+ if (previous.startsWith(next) || previous.includes(next)) return previous;
1126
1285
  const maxOverlap = Math.min(previous.length, next.length);
1127
1286
  for (let overlap = maxOverlap; overlap > 0; overlap -= 1) if (previous.slice(-overlap) === next.slice(0, overlap)) return `${previous}${next.slice(overlap)}`;
1128
1287
  return `${previous}${next}`;
@@ -1412,16 +1571,23 @@ var FeishuStreamingSession = class {
1412
1571
  }
1413
1572
  }).catch((e) => this.log?.(`Note update failed: ${String(e)}`));
1414
1573
  }
1415
- async close(finalText, options) {
1416
- if (!this.state || this.closed) return false;
1574
+ async closeWithResult(finalText, options) {
1575
+ if (!this.state || this.closed) return { visibleReplySent: false };
1417
1576
  this.closed = true;
1418
1577
  this.clearFlushTimer();
1419
1578
  await this.queue;
1420
1579
  const text = finalText ?? this.pendingText ?? this.state.currentText;
1421
1580
  const apiBase = resolveApiBase(this.creds.domain);
1422
1581
  let visibleContentSent = Boolean(this.state.sentText.trim());
1582
+ let finalWriteError;
1423
1583
  if ((text || finalText !== void 0) && text !== this.state.sentText) {
1424
- const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) => this.log?.(`Final update failed: ${String(e)}`)) : await this.replaceCardContent(text, (e) => this.log?.(`Final replace failed: ${String(e)}`));
1584
+ const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) => {
1585
+ finalWriteError = e;
1586
+ this.log?.(`Final update failed: ${String(e)}`);
1587
+ }) : await this.replaceCardContent(text, (e) => {
1588
+ finalWriteError = e;
1589
+ this.log?.(`Final replace failed: ${String(e)}`);
1590
+ });
1425
1591
  this.state.currentText = text;
1426
1592
  if (sent) {
1427
1593
  this.state.sentText = text;
@@ -1431,44 +1597,63 @@ var FeishuStreamingSession = class {
1431
1597
  if (options?.note) await this.updateNoteContent(options.note);
1432
1598
  const acceptedText = this.state.sentText;
1433
1599
  this.state.sequence += 1;
1434
- await fetchWithSsrFGuard({
1435
- url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
1436
- init: {
1437
- method: "PATCH",
1438
- headers: {
1439
- Authorization: `Bearer ${await getToken(this.creds, {
1440
- fetchImpl: this.fetchImpl,
1441
- lookupFn: this.lookupFn
1442
- })}`,
1443
- "Content-Type": "application/json; charset=utf-8",
1444
- "User-Agent": getFeishuUserAgent()
1600
+ let closeError;
1601
+ try {
1602
+ const { response, release } = await fetchWithSsrFGuard({
1603
+ url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
1604
+ init: {
1605
+ method: "PATCH",
1606
+ headers: {
1607
+ Authorization: `Bearer ${await getToken(this.creds, {
1608
+ fetchImpl: this.fetchImpl,
1609
+ lookupFn: this.lookupFn
1610
+ })}`,
1611
+ "Content-Type": "application/json; charset=utf-8",
1612
+ "User-Agent": getFeishuUserAgent()
1613
+ },
1614
+ body: JSON.stringify({
1615
+ settings: JSON.stringify({ config: {
1616
+ streaming_mode: false,
1617
+ summary: { content: truncateSummary(acceptedText) }
1618
+ } }),
1619
+ sequence: this.state.sequence,
1620
+ uuid: `c_${this.state.cardId}_${this.state.sequence}`
1621
+ })
1445
1622
  },
1446
- body: JSON.stringify({
1447
- settings: JSON.stringify({ config: {
1448
- streaming_mode: false,
1449
- summary: { content: truncateSummary(acceptedText) }
1450
- } }),
1451
- sequence: this.state.sequence,
1452
- uuid: `c_${this.state.cardId}_${this.state.sequence}`
1453
- })
1454
- },
1455
- fetchImpl: this.fetchImpl,
1456
- lookupFn: this.lookupFn,
1457
- policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
1458
- auditContext: "feishu.streaming-card.close",
1459
- timeoutMs: this.creds.httpTimeoutMs ?? 3e4
1460
- }).then(async ({ response, release }) => {
1623
+ fetchImpl: this.fetchImpl,
1624
+ lookupFn: this.lookupFn,
1625
+ policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
1626
+ auditContext: "feishu.streaming-card.close",
1627
+ timeoutMs: this.creds.httpTimeoutMs ?? 3e4
1628
+ });
1461
1629
  try {
1462
1630
  await assertSuccessfulCardKitResponse(response, "feishu.streaming-card.close", "Close streaming card");
1463
1631
  } finally {
1464
1632
  await release();
1465
1633
  }
1466
- }).catch((e) => this.log?.(`Close failed: ${String(e)}`));
1634
+ } catch (error) {
1635
+ closeError = error;
1636
+ this.log?.(`Close failed: ${String(error)}`);
1637
+ }
1467
1638
  const finalState = this.state;
1468
1639
  this.state = null;
1469
1640
  this.pendingText = null;
1470
1641
  this.log?.(`Closed streaming: cardId=${finalState.cardId}`);
1471
- return visibleContentSent;
1642
+ const result = {
1643
+ visibleReplySent: visibleContentSent,
1644
+ ...visibleContentSent ? { content: finalState.sentText } : {},
1645
+ messageId: finalState.messageId
1646
+ };
1647
+ if (finalWriteError !== void 0 || closeError !== void 0) throw new FeishuStreamingFinalizationError(finalWriteError !== void 0 && closeError !== void 0 ? new AggregateError([finalWriteError, closeError], "Feishu streaming card finalization failed") : finalWriteError ?? closeError, result);
1648
+ return result;
1649
+ }
1650
+ async close(finalText, options) {
1651
+ try {
1652
+ return (await this.closeWithResult(finalText, options)).visibleReplySent;
1653
+ } catch (error) {
1654
+ if (error instanceof FeishuStreamingFinalizationError) return error.result.visibleReplySent;
1655
+ throw error;
1656
+ }
1472
1657
  }
1473
1658
  async discard() {
1474
1659
  if (!this.state || this.closed) return;
@@ -1614,6 +1799,9 @@ function mergeStreamingFinalText(previousText, nextText, appendError) {
1614
1799
  if (previousText.endsWith(`\n\n${nextText}`)) return previousText;
1615
1800
  return `${previousText}\n\n${nextText}`;
1616
1801
  }
1802
+ function toError(error) {
1803
+ return error instanceof Error ? error : new Error(String(error));
1804
+ }
1617
1805
  /** Maximum age (ms) for a message to receive a typing indicator reaction.
1618
1806
  * Messages older than this are likely replays after context compaction (#30418). */
1619
1807
  const TYPING_INDICATOR_MAX_AGE_MS = 2 * 6e4;
@@ -1724,9 +1912,12 @@ function createFeishuReplyDispatcher(params) {
1724
1912
  });
1725
1913
  const renderMode = account.config?.renderMode ?? "auto";
1726
1914
  const streamingEnabled = !requiredMentionTargets?.length && resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1915
+ const hookRunner = getGlobalHookRunner();
1916
+ const modifyingHooksRegistered = (hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false);
1917
+ const previewStreamingEnabled = streamingEnabled && !modifyingHooksRegistered;
1727
1918
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1728
1919
  const coreBlockStreamingEnabled = blockStreamingEnabled === true;
1729
- const reasoningPreviewEnabled = streamingEnabled && params.allowReasoningPreview === true;
1920
+ const reasoningPreviewEnabled = previewStreamingEnabled && params.allowReasoningPreview === true;
1730
1921
  let streaming = null;
1731
1922
  let streamText = "";
1732
1923
  let lastPartial = "";
@@ -1736,15 +1927,20 @@ function createFeishuReplyDispatcher(params) {
1736
1927
  let lastSnapshotTextLength = 0;
1737
1928
  let hasStreamingFinalText = false;
1738
1929
  const deliveredFinalTexts = /* @__PURE__ */ new Set();
1930
+ const closedStreamingSettlements = /* @__PURE__ */ new Map();
1739
1931
  let sentIndependentBlockText = false;
1740
1932
  let partialUpdateQueue = Promise.resolve();
1741
1933
  let streamingStartPromise = null;
1742
- let streamingClosedForReply = false;
1743
- let streamingCloseErroredForReply = false;
1934
+ let streamingGeneration = 0;
1935
+ let activeStreamingGeneration;
1936
+ let inFlightStreamingClose;
1744
1937
  let visibleReplySent = false;
1745
1938
  let skippedFinalReason = null;
1746
1939
  let idleSideEffectsPromise = Promise.resolve();
1940
+ let activeIdleSideEffectsPromise = null;
1941
+ let idleRequestedForReply = false;
1747
1942
  let replyLifecycleStateInitialized = false;
1943
+ const pendingStreamingDeliveries = [];
1748
1944
  const markVisibleReplySent = () => {
1749
1945
  visibleReplySent = true;
1750
1946
  };
@@ -1761,9 +1957,12 @@ function createFeishuReplyDispatcher(params) {
1761
1957
  return parts.join("");
1762
1958
  };
1763
1959
  const flushStreamingCardUpdate = (combined) => {
1960
+ const session = streaming;
1961
+ const generation = activeStreamingGeneration;
1962
+ const startPromise = streamingStartPromise;
1764
1963
  partialUpdateQueue = partialUpdateQueue.then(async () => {
1765
- if (streamingStartPromise) await streamingStartPromise;
1766
- if (streaming?.isActive()) await streaming.update(combined);
1964
+ if (startPromise) await startPromise;
1965
+ if (generation !== void 0 && session?.isActive()) await session.update(combined);
1767
1966
  });
1768
1967
  };
1769
1968
  const queueStreamingUpdate = (nextText, options) => {
@@ -1796,12 +1995,15 @@ function createFeishuReplyDispatcher(params) {
1796
1995
  httpTimeoutMs: resolveConfiguredHttpTimeoutMs(account)
1797
1996
  } : null;
1798
1997
  if (!creds) return;
1799
- streaming = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
1998
+ const session = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
1999
+ const generation = ++streamingGeneration;
2000
+ streaming = session;
2001
+ activeStreamingGeneration = generation;
1800
2002
  try {
1801
2003
  const cardHeader = resolveCardHeader(agentId, identity);
1802
2004
  const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1803
2005
  const streamingTarget = sendTarget.replace(/^(feishu|lark):/i, "").replace(/^(chat|user|group|dm|open_id):/i, "").trim();
1804
- await streaming.start(streamingTarget, resolveReceiveIdType(sendTarget), {
2006
+ await session.start(streamingTarget, resolveReceiveIdType(sendTarget), {
1805
2007
  replyToMessageId: sendReplyToMessageId,
1806
2008
  replyInThread: effectiveReplyInThread,
1807
2009
  rootId,
@@ -1812,14 +2014,18 @@ function createFeishuReplyDispatcher(params) {
1812
2014
  } catch (error) {
1813
2015
  rememberStreamingStartFailure(account.accountId);
1814
2016
  params.runtime.error?.(`feishu[${account.accountId}]: streaming start failed; using non-streaming card fallback for ${STREAMING_START_FAILURE_BACKOFF_MS / 1e3}s: ${String(error)}`);
1815
- streaming = null;
1816
- streamingStartPromise = null;
2017
+ if (streaming === session) {
2018
+ streaming = null;
2019
+ streamingStartPromise = null;
2020
+ activeStreamingGeneration = void 0;
2021
+ }
1817
2022
  }
1818
2023
  })();
1819
2024
  };
1820
2025
  const resetStreamingState = () => {
1821
2026
  streaming = null;
1822
2027
  streamingStartPromise = null;
2028
+ activeStreamingGeneration = void 0;
1823
2029
  partialUpdateQueue = Promise.resolve();
1824
2030
  streamText = "";
1825
2031
  lastPartial = "";
@@ -1829,24 +2035,126 @@ function createFeishuReplyDispatcher(params) {
1829
2035
  lastSnapshotTextLength = 0;
1830
2036
  hasStreamingFinalText = false;
1831
2037
  };
1832
- const closeStreaming = async (options) => {
2038
+ const rememberClosedStreamingSettlement = (generation, content, result, error) => {
2039
+ if (generation === void 0 || !content) return;
2040
+ closedStreamingSettlements.set(generation, {
2041
+ result,
2042
+ content,
2043
+ ...error === void 0 ? {} : { error }
2044
+ });
2045
+ };
2046
+ const performStreamingClose = async () => {
2047
+ const streamingToClose = streaming;
2048
+ const generationToClose = activeStreamingGeneration;
2049
+ const startPromiseToClose = streamingStartPromise;
2050
+ const updateQueueToClose = partialUpdateQueue;
2051
+ const finalizedAnswerText = streamText;
2052
+ const finalizedReasoningText = reasoningText;
2053
+ if (generationToClose !== void 0 && activeStreamingGeneration === generationToClose) activeStreamingGeneration = void 0;
1833
2054
  try {
1834
- if (streamingStartPromise) await streamingStartPromise;
1835
- await partialUpdateQueue;
1836
- if (streaming?.isActive()) {
2055
+ if (startPromiseToClose) await startPromiseToClose;
2056
+ await updateQueueToClose;
2057
+ if (streamingToClose?.isActive()) {
1837
2058
  statusLine = "";
1838
- const text = buildCombinedStreamText(reasoningText, streamText);
2059
+ const text = buildCombinedStreamText(finalizedReasoningText, finalizedAnswerText);
1839
2060
  const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1840
- const contentVisible = await streaming.close(text, { note: finalNote });
1841
- if (contentVisible) markVisibleReplySent();
1842
- if (contentVisible && streamText) {
1843
- deliveredFinalTexts.add(streamText);
1844
- if (options?.markClosedForReply !== false && !streamingCloseErroredForReply) streamingClosedForReply = true;
2061
+ let closed;
2062
+ try {
2063
+ closed = await streamingToClose.closeWithResult(text, { note: finalNote });
2064
+ } catch (error) {
2065
+ if (!(error instanceof FeishuStreamingFinalizationError)) throw error;
2066
+ const failedResult = createFeishuReplyDeliveryResult({
2067
+ results: [error.result],
2068
+ visibleReplySent: error.result.visibleReplySent,
2069
+ content: error.result.content,
2070
+ kind: "card"
2071
+ });
2072
+ if (failedResult.visibleReplySent) markVisibleReplySent();
2073
+ if (failedResult.visibleReplySent && finalizedAnswerText) {
2074
+ const partialError = createFeishuPartialReplyDeliveryError(error.cause ?? error, failedResult);
2075
+ if (failedResult.content === text) deliveredFinalTexts.add(finalizedAnswerText);
2076
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, partialError);
2077
+ return {
2078
+ result: failedResult,
2079
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2080
+ error: partialError
2081
+ };
2082
+ }
2083
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, error);
2084
+ return {
2085
+ result: failedResult,
2086
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2087
+ error
2088
+ };
2089
+ }
2090
+ const result = createFeishuReplyDeliveryResult({
2091
+ results: [closed],
2092
+ visibleReplySent: closed.visibleReplySent,
2093
+ content: closed.content,
2094
+ kind: "card"
2095
+ });
2096
+ if (result.visibleReplySent) markVisibleReplySent();
2097
+ if (result.visibleReplySent && finalizedAnswerText) {
2098
+ deliveredFinalTexts.add(finalizedAnswerText);
2099
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, result);
1845
2100
  }
2101
+ return {
2102
+ result,
2103
+ ...generationToClose === void 0 ? {} : { generation: generationToClose }
2104
+ };
1846
2105
  }
2106
+ return {
2107
+ result: noVisibleFeishuReplyDelivery,
2108
+ ...generationToClose === void 0 ? {} : { generation: generationToClose }
2109
+ };
2110
+ } catch (error) {
2111
+ return {
2112
+ result: noVisibleFeishuReplyDelivery,
2113
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2114
+ error
2115
+ };
1847
2116
  } finally {
1848
- resetStreamingState();
2117
+ if (streaming === streamingToClose) resetStreamingState();
2118
+ }
2119
+ };
2120
+ const closeStreaming = () => {
2121
+ const generation = activeStreamingGeneration;
2122
+ if (generation !== void 0 && inFlightStreamingClose?.generation === generation) return inFlightStreamingClose.promise;
2123
+ const content = streamText;
2124
+ const closePromise = performStreamingClose();
2125
+ if (generation !== void 0) {
2126
+ const closing = {
2127
+ generation,
2128
+ content,
2129
+ promise: closePromise
2130
+ };
2131
+ inFlightStreamingClose = closing;
2132
+ const clear = () => {
2133
+ if (inFlightStreamingClose === closing) inFlightStreamingClose = void 0;
2134
+ };
2135
+ closePromise.then(clear, clear);
1849
2136
  }
2137
+ return closePromise;
2138
+ };
2139
+ const deferStreamingDelivery = (result, infoKind, ownerGeneration) => {
2140
+ let resolveFinalization;
2141
+ let rejectFinalization;
2142
+ const finalization = new Promise((resolve, reject) => {
2143
+ resolveFinalization = resolve;
2144
+ rejectFinalization = reject;
2145
+ });
2146
+ pendingStreamingDeliveries.push({
2147
+ result,
2148
+ ...infoKind ? { infoKind } : {},
2149
+ ...ownerGeneration === void 0 ? {} : { streamingGeneration: ownerGeneration },
2150
+ resolve: resolveFinalization,
2151
+ reject: rejectFinalization
2152
+ });
2153
+ if (idleRequestedForReply) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] late reply finalization failed: ${String(error)}`));
2154
+ return {
2155
+ ...noVisibleFeishuReplyDelivery,
2156
+ finalization
2157
+ };
1850
2158
  };
1851
2159
  const discardStreamingPreview = async () => {
1852
2160
  try {
@@ -1874,82 +2182,98 @@ function createFeishuReplyDispatcher(params) {
1874
2182
  chunkMentions: paramsLocal.chunkMentions,
1875
2183
  initialChunks
1876
2184
  }));
2185
+ const results = [];
2186
+ const acceptedChunks = [];
1877
2187
  for (const [index, chunk] of chunks.entries()) {
1878
2188
  const mentions = [...paramsLocal.chunkMentions ?? [], ...index === 0 ? paramsLocal.firstChunkMentions ?? [] : []];
1879
- await paramsLocal.sendChunk({
1880
- chunk,
1881
- isFirst: index === 0,
1882
- mentions: mentions.length > 0 ? mentions : void 0
1883
- });
1884
- markVisibleReplySent();
2189
+ try {
2190
+ const result = await paramsLocal.sendChunk({
2191
+ chunk,
2192
+ isFirst: index === 0,
2193
+ mentions: mentions.length > 0 ? mentions : void 0
2194
+ });
2195
+ results.push(result);
2196
+ acceptedChunks.push(chunk);
2197
+ markVisibleReplySent();
2198
+ } catch (error) {
2199
+ throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
2200
+ results,
2201
+ visibleReplySent: results.length > 0,
2202
+ content: acceptedChunks.join(""),
2203
+ kind: paramsLocal.useCard ? "card" : "text"
2204
+ }));
2205
+ }
1885
2206
  }
1886
2207
  if (paramsLocal.infoKind === "final") deliveredFinalTexts.add(paramsLocal.text);
2208
+ return createFeishuReplyDeliveryResult({
2209
+ results,
2210
+ visibleReplySent: results.length > 0,
2211
+ content: paramsLocal.text,
2212
+ kind: paramsLocal.useCard ? "card" : "text"
2213
+ });
1887
2214
  };
1888
2215
  const sendMediaReplies = async (payload, options) => {
1889
2216
  const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
1890
2217
  let sentFallbackText = false;
1891
- await sendMediaWithLeadingCaption({
1892
- mediaUrls,
1893
- caption: "",
1894
- send: async ({ mediaUrl }) => {
1895
- const result = await sendMediaFeishu({
1896
- cfg,
1897
- to: sendTarget,
1898
- mediaUrl,
1899
- replyToMessageId: sendReplyToMessageId,
1900
- replyInThread: effectiveReplyInThread,
1901
- accountId,
1902
- ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
1903
- });
1904
- markVisibleReplySent();
1905
- if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) {
1906
- sentFallbackText = true;
1907
- await sendChunkedTextReply({
1908
- text: options.fallbackText,
1909
- useCard: false,
1910
- infoKind: "final",
1911
- chunkMentions: requiredMentionTargets,
1912
- sendChunk: async ({ chunk, mentions }) => {
1913
- await sendMessageFeishu({
1914
- cfg,
1915
- to: sendTarget,
1916
- text: chunk,
1917
- replyToMessageId: sendReplyToMessageId,
1918
- replyInThread: effectiveReplyInThread,
1919
- allowTopLevelReplyFallback,
1920
- accountId,
1921
- ...mentions ? { mentions } : {}
1922
- });
1923
- }
2218
+ let degradedVoiceFallbackText;
2219
+ const results = [];
2220
+ const sendFallbackText = async (text) => await sendChunkedTextReply({
2221
+ text,
2222
+ useCard: false,
2223
+ infoKind: "final",
2224
+ chunkMentions: requiredMentionTargets,
2225
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2226
+ cfg,
2227
+ to: sendTarget,
2228
+ text: chunk,
2229
+ replyToMessageId: sendReplyToMessageId,
2230
+ replyInThread: effectiveReplyInThread,
2231
+ allowTopLevelReplyFallback,
2232
+ accountId,
2233
+ ...mentions ? { mentions } : {}
2234
+ })
2235
+ });
2236
+ try {
2237
+ await sendMediaWithLeadingCaption({
2238
+ mediaUrls,
2239
+ caption: "",
2240
+ send: async ({ mediaUrl }) => {
2241
+ const result = await sendMediaFeishu({
2242
+ cfg,
2243
+ to: sendTarget,
2244
+ mediaUrl,
2245
+ replyToMessageId: sendReplyToMessageId,
2246
+ replyInThread: effectiveReplyInThread,
2247
+ allowTopLevelReplyFallback,
2248
+ accountId,
2249
+ ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
2250
+ });
2251
+ results.push(createFeishuReplyDeliveryResult({
2252
+ results: [result],
2253
+ visibleReplySent: true,
2254
+ kind: result?.voiceIntentDegradedToFile ? "media" : void 0
2255
+ }));
2256
+ markVisibleReplySent();
2257
+ if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) degradedVoiceFallbackText = options.fallbackText;
2258
+ },
2259
+ onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
2260
+ const fallbackText = await buildFeishuMediaFallbackText({
2261
+ text: sentFallbackText ? void 0 : options.fallbackText,
2262
+ mediaUrl
1924
2263
  });
2264
+ sentFallbackText = true;
2265
+ results.push(await sendFallbackText(fallbackText));
1925
2266
  }
1926
- },
1927
- onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
1928
- const fallbackText = await buildFeishuMediaFallbackText({
1929
- text: sentFallbackText ? void 0 : options.fallbackText,
1930
- mediaUrl
1931
- });
2267
+ });
2268
+ if (degradedVoiceFallbackText && !sentFallbackText) {
1932
2269
  sentFallbackText = true;
1933
- await sendChunkedTextReply({
1934
- text: fallbackText,
1935
- useCard: false,
1936
- infoKind: "final",
1937
- chunkMentions: requiredMentionTargets,
1938
- sendChunk: async ({ chunk, mentions }) => {
1939
- await sendMessageFeishu({
1940
- cfg,
1941
- to: sendTarget,
1942
- text: chunk,
1943
- replyToMessageId: sendReplyToMessageId,
1944
- replyInThread: effectiveReplyInThread,
1945
- allowTopLevelReplyFallback,
1946
- accountId,
1947
- ...mentions ? { mentions } : {}
1948
- });
1949
- }
1950
- });
2270
+ results.push(await sendFallbackText(degradedVoiceFallbackText));
1951
2271
  }
1952
- });
2272
+ } catch (error) {
2273
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2274
+ throw createFeishuPartialReplyDeliveryError(error, mergeFeishuReplyDeliveryResults([...results, ...partial ? [partial] : []]));
2275
+ }
2276
+ return mergeFeishuReplyDeliveryResults(results);
1953
2277
  };
1954
2278
  const ensureNoVisibleReplyFallback = async (reason) => {
1955
2279
  await idleSideEffectsPromise;
@@ -1972,13 +2296,130 @@ function createFeishuReplyDispatcher(params) {
1972
2296
  params.runtime.error?.(`feishu[${account.accountId}]: sent no-visible-reply fallback (${reason})`);
1973
2297
  return true;
1974
2298
  };
1975
- const queueIdleSideEffects = (options) => {
2299
+ const claimClosedStreamingResult = (generation, content) => {
2300
+ if (generation !== void 0) {
2301
+ const settlement = closedStreamingSettlements.get(generation);
2302
+ if (settlement) settlement.contentClaimed = true;
2303
+ return settlement;
2304
+ }
2305
+ let latestKey;
2306
+ for (const [key, settlement] of closedStreamingSettlements) if (settlement.contentClaimed !== true && (content === void 0 || settlement.content === content)) latestKey = key;
2307
+ if (latestKey === void 0) return;
2308
+ const result = closedStreamingSettlements.get(latestKey);
2309
+ if (result) result.contentClaimed = true;
2310
+ return result;
2311
+ };
2312
+ const markClosedStreamingContentClaimed = (generation) => {
2313
+ if (generation !== void 0) {
2314
+ const settlement = closedStreamingSettlements.get(generation);
2315
+ if (settlement) settlement.contentClaimed = true;
2316
+ }
2317
+ };
2318
+ const ensureVisibleStreamingDelivery = async (result, content, infoKind) => {
2319
+ if (result?.visibleReplySent === true || !content?.trim()) return result;
2320
+ const cardHeader = resolveCardHeader(agentId, identity);
2321
+ const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2322
+ return await sendChunkedTextReply({
2323
+ text: content,
2324
+ useCard: true,
2325
+ infoKind,
2326
+ chunkMentions: requiredMentionTargets,
2327
+ sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
2328
+ cfg,
2329
+ to: sendTarget,
2330
+ text: chunk,
2331
+ replyToMessageId: sendReplyToMessageId,
2332
+ replyInThread: effectiveReplyInThread,
2333
+ allowTopLevelReplyFallback,
2334
+ accountId,
2335
+ header: cardHeader,
2336
+ note: cardNote,
2337
+ ...mentions ? { mentions } : {}
2338
+ })
2339
+ });
2340
+ };
2341
+ function queueIdleSideEffects() {
2342
+ idleRequestedForReply = true;
2343
+ if (activeIdleSideEffectsPromise) return activeIdleSideEffectsPromise;
1976
2344
  const nextIdleSideEffects = idleSideEffectsPromise.then(async () => {
1977
- await closeStreaming(options);
1978
- typingCallbacks?.onIdle?.();
2345
+ try {
2346
+ do {
2347
+ const completions = pendingStreamingDeliveries.splice(0);
2348
+ const closeOutcome = await closeStreaming();
2349
+ const finalized = closeOutcome.result;
2350
+ const ownsCurrentClose = (completion) => closeOutcome.generation !== void 0 && completion.streamingGeneration === closeOutcome.generation;
2351
+ if (completions.some((completion) => ownsCurrentClose(completion))) markClosedStreamingContentClaimed(closeOutcome.generation);
2352
+ for (const completion of completions) {
2353
+ const claimedSettlement = ownsCurrentClose(completion) ? {
2354
+ result: finalized,
2355
+ ...closeOutcome.error === void 0 ? {} : { error: closeOutcome.error }
2356
+ } : claimClosedStreamingResult(completion.streamingGeneration, completion.result.content);
2357
+ const deliveryError = claimedSettlement?.error;
2358
+ let providerFinalized = claimedSettlement?.result;
2359
+ try {
2360
+ providerFinalized = await ensureVisibleStreamingDelivery(providerFinalized, completion.result.content, completion.infoKind);
2361
+ } catch (fallbackError) {
2362
+ const fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
2363
+ const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
2364
+ completion.reject(createFeishuPartialReplyDeliveryError(deliveryError === void 0 ? fallbackCause : new AggregateError([deliveryError, fallbackCause], "Feishu streaming finalization and static fallback failed"), mergeFeishuReplyDeliveryResults([
2365
+ ...providerFinalized ? [providerFinalized] : [],
2366
+ ...fallbackPartial ? [fallbackPartial] : [],
2367
+ completion.result
2368
+ ], fallbackPartial?.content ?? providerFinalized?.content ?? completion.result.content)));
2369
+ continue;
2370
+ }
2371
+ const settledResult = mergeFeishuReplyDeliveryResults([...providerFinalized ? [providerFinalized] : [], completion.result], deliveryError === void 0 ? completion.result.content ?? providerFinalized?.content : providerFinalized?.content ?? completion.result.content);
2372
+ if (deliveryError !== void 0) completion.reject(createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(deliveryError) && deliveryError instanceof Error ? deliveryError.cause ?? deliveryError : deliveryError instanceof FeishuStreamingFinalizationError ? deliveryError.cause ?? deliveryError : deliveryError, settledResult));
2373
+ else completion.resolve(settledResult);
2374
+ }
2375
+ if (closeOutcome.error !== void 0) throw toError(closeOutcome.error);
2376
+ } while (pendingStreamingDeliveries.length > 0);
2377
+ } finally {
2378
+ typingCallbacks?.onIdle?.();
2379
+ }
1979
2380
  });
2381
+ activeIdleSideEffectsPromise = nextIdleSideEffects;
1980
2382
  idleSideEffectsPromise = nextIdleSideEffects.catch(() => {});
2383
+ const finishIdleSideEffects = () => {
2384
+ if (activeIdleSideEffectsPromise === nextIdleSideEffects) activeIdleSideEffectsPromise = null;
2385
+ if (pendingStreamingDeliveries.length > 0) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] queued reply finalization failed: ${String(error)}`));
2386
+ };
2387
+ nextIdleSideEffects.then(finishIdleSideEffects, finishIdleSideEffects);
1981
2388
  return nextIdleSideEffects;
2389
+ }
2390
+ const throwStreamingDeliveryFailure = async (paramsLocal) => {
2391
+ let finalized = noVisibleFeishuReplyDelivery;
2392
+ let finalizationError;
2393
+ let fallbackPartial;
2394
+ const claimedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
2395
+ if (claimedSettlement) {
2396
+ finalized = claimedSettlement.result;
2397
+ finalizationError = claimedSettlement.error;
2398
+ } else if (paramsLocal.ownerGeneration !== void 0 && inFlightStreamingClose?.generation === paramsLocal.ownerGeneration) {
2399
+ const closeOutcome = await inFlightStreamingClose.promise;
2400
+ const completedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
2401
+ finalized = completedSettlement?.result ?? closeOutcome?.result ?? finalized;
2402
+ finalizationError = completedSettlement?.error ?? closeOutcome?.error;
2403
+ } else if (paramsLocal.ownerGeneration !== void 0 && activeStreamingGeneration === paramsLocal.ownerGeneration) {
2404
+ const closeOutcome = await closeStreaming();
2405
+ finalized = closeOutcome.result;
2406
+ finalizationError = closeOutcome.error;
2407
+ }
2408
+ try {
2409
+ finalized = await ensureVisibleStreamingDelivery(finalized, paramsLocal.content, paramsLocal.infoKind) ?? finalized;
2410
+ } catch (fallbackError) {
2411
+ fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
2412
+ const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
2413
+ finalizationError = finalizationError ? new AggregateError([finalizationError, fallbackCause], "Feishu streaming finalization and static fallback failed") : fallbackCause;
2414
+ }
2415
+ const mediaPartial = isChannelPartialDeliveryError(paramsLocal.error) ? paramsLocal.error.deliveryResult : void 0;
2416
+ const accepted = mergeFeishuReplyDeliveryResults([
2417
+ finalized,
2418
+ ...fallbackPartial ? [fallbackPartial] : [],
2419
+ ...mediaPartial ? [mediaPartial] : []
2420
+ ], fallbackPartial?.visibleReplySent === true ? fallbackPartial.content : finalized.visibleReplySent === true ? finalized.content : paramsLocal.content);
2421
+ const mediaCause = mediaPartial && paramsLocal.error instanceof Error ? paramsLocal.error.cause ?? paramsLocal.error : paramsLocal.error;
2422
+ throw createFeishuPartialReplyDeliveryError(finalizationError ? new AggregateError([mediaCause, finalizationError instanceof Error ? finalizationError.cause ?? finalizationError : finalizationError], "Feishu reply delivery and streaming finalization failed") : mediaCause, accepted);
1982
2423
  };
1983
2424
  const dispatcherOptions = {
1984
2425
  responsePrefix: prefixContext.responsePrefix,
@@ -1997,13 +2438,13 @@ function createFeishuReplyDispatcher(params) {
1997
2438
  if (!replyLifecycleStateInitialized) {
1998
2439
  replyLifecycleStateInitialized = true;
1999
2440
  deliveredFinalTexts.clear();
2441
+ closedStreamingSettlements.clear();
2000
2442
  sentIndependentBlockText = false;
2001
- streamingClosedForReply = false;
2002
- streamingCloseErroredForReply = false;
2443
+ idleRequestedForReply = false;
2003
2444
  visibleReplySent = false;
2004
2445
  skippedFinalReason = null;
2005
2446
  }
2006
- if (streamingEnabled && renderMode === "card") startStreaming();
2447
+ if (previewStreamingEnabled && renderMode === "card") startStreaming();
2007
2448
  await Promise.resolve(typingCallbacks?.onReplyStart?.());
2008
2449
  },
2009
2450
  onIdle: () => queueIdleSideEffects(),
@@ -2012,14 +2453,13 @@ function createFeishuReplyDispatcher(params) {
2012
2453
  }
2013
2454
  };
2014
2455
  const handleDeliveryError = async (error, info) => {
2015
- streamingCloseErroredForReply = true;
2016
- streamingClosedForReply = false;
2017
2456
  params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
2018
- await queueIdleSideEffects({ markClosedForReply: false }).catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2457
+ await queueIdleSideEffects().catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2019
2458
  };
2020
2459
  return {
2021
2460
  dispatcherOptions,
2022
2461
  delivery: {
2462
+ observeMessageSent: true,
2023
2463
  deliver: async (payload, info) => {
2024
2464
  if (info?.kind === "final") skippedFinalReason = null;
2025
2465
  const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
@@ -2040,42 +2480,52 @@ function createFeishuReplyDispatcher(params) {
2040
2480
  const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
2041
2481
  const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
2042
2482
  const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
2043
- const finalTextWouldUseStreamingCard = info?.kind === "final" && hasText && streamingEnabled;
2044
2483
  const useCard = useStaticCard || useStreamingCard;
2045
2484
  const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
2046
- const skipTextForClosedStreamingFinal = info?.kind === "final" && hasText && streamingClosedForReply && !streamingCloseErroredForReply && finalTextWouldUseStreamingCard;
2047
- const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
2485
+ const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal;
2048
2486
  const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText && !ttsTextAlreadyVisible || skipTextForDuplicateFinal));
2049
- if (!shouldDeliverText && !hasMedia) return;
2487
+ const priorClosedStreamingSettlement = info?.kind === "final" && hasText && skipTextForDuplicateFinal ? claimClosedStreamingResult(void 0, text) : void 0;
2488
+ if (!shouldDeliverText && !hasMedia) {
2489
+ if (priorClosedStreamingSettlement?.error !== void 0) throw toError(priorClosedStreamingSettlement.error);
2490
+ return priorClosedStreamingSettlement?.result ?? noVisibleFeishuReplyDelivery;
2491
+ }
2492
+ const deliveredResults = priorClosedStreamingSettlement ? [priorClosedStreamingSettlement.result] : [];
2493
+ const collectMediaDelivery = async (mediaPayload, mediaOptions) => {
2494
+ try {
2495
+ deliveredResults.push(await sendMediaReplies(mediaPayload, mediaOptions));
2496
+ } catch (error) {
2497
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2498
+ const accumulated = mergeFeishuReplyDeliveryResults([...deliveredResults, ...partial ? [partial] : []]);
2499
+ throw createFeishuPartialReplyDeliveryError(partial && error instanceof Error ? error.cause ?? error : error, accumulated);
2500
+ }
2501
+ };
2050
2502
  if (shouldDiscardStreamingPreview) await discardStreamingPreview();
2051
2503
  if (shouldDeliverText) {
2052
2504
  if (info?.kind === "block") {
2053
2505
  if (!useStreamingCard) {
2054
2506
  if (coreBlockStreamingEnabled) {
2055
2507
  const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
2056
- await sendChunkedTextReply({
2508
+ deliveredResults.push(await sendChunkedTextReply({
2057
2509
  text,
2058
2510
  useCard: false,
2059
2511
  infoKind: "block",
2060
2512
  firstChunkMentions,
2061
2513
  chunkMentions: requiredMentionTargets,
2062
- sendChunk: async ({ chunk, mentions }) => {
2063
- await sendMessageFeishu({
2064
- cfg,
2065
- to: sendTarget,
2066
- text: chunk,
2067
- replyToMessageId: sendReplyToMessageId,
2068
- replyInThread: effectiveReplyInThread,
2069
- allowTopLevelReplyFallback,
2070
- accountId,
2071
- ...mentions ? { mentions } : {}
2072
- });
2073
- }
2074
- });
2514
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2515
+ cfg,
2516
+ to: sendTarget,
2517
+ text: chunk,
2518
+ replyToMessageId: sendReplyToMessageId,
2519
+ replyInThread: effectiveReplyInThread,
2520
+ allowTopLevelReplyFallback,
2521
+ accountId,
2522
+ ...mentions ? { mentions } : {}
2523
+ })
2524
+ }));
2075
2525
  sentIndependentBlockText = true;
2076
- if (hasMedia) await sendMediaReplies(payload);
2526
+ if (hasMedia) await collectMediaDelivery(payload);
2077
2527
  }
2078
- return;
2528
+ return mergeFeishuReplyDeliveryResults(deliveredResults, text);
2079
2529
  }
2080
2530
  startStreaming();
2081
2531
  if (streamingStartPromise) await streamingStartPromise;
@@ -2085,75 +2535,87 @@ function createFeishuReplyDispatcher(params) {
2085
2535
  if (streamingStartPromise) await streamingStartPromise;
2086
2536
  }
2087
2537
  const shouldStreamText = info?.kind === "block" || info?.kind === "final";
2088
- if (streaming?.isActive() && shouldStreamText) {
2089
- if (info?.kind === "block") queueStreamingUpdate(text, {
2090
- mode: "delta",
2091
- dedupeWithLastPartial: true
2092
- });
2093
- if (info?.kind === "final") {
2094
- streamText = text;
2095
- hasStreamingFinalText = true;
2096
- snapshotBaseText = "";
2097
- lastSnapshotTextLength = text.length;
2098
- flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2538
+ const matchingInFlightClose = info?.kind === "final" && inFlightStreamingClose?.content === text ? inFlightStreamingClose : void 0;
2539
+ const ownerGeneration = activeStreamingGeneration ?? matchingInFlightClose?.generation;
2540
+ if (shouldStreamText && ownerGeneration !== void 0 && (streaming?.isActive() || matchingInFlightClose !== void 0)) {
2541
+ if (activeStreamingGeneration !== void 0) {
2542
+ if (info?.kind === "block") queueStreamingUpdate(text, {
2543
+ mode: "delta",
2544
+ dedupeWithLastPartial: true
2545
+ });
2546
+ if (info?.kind === "final") {
2547
+ streamText = text;
2548
+ hasStreamingFinalText = true;
2549
+ snapshotBaseText = "";
2550
+ lastSnapshotTextLength = text.length;
2551
+ flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2552
+ }
2099
2553
  }
2100
- if (hasMedia) await sendMediaReplies(payload);
2101
- return;
2554
+ if (hasMedia) try {
2555
+ await collectMediaDelivery(payload);
2556
+ } catch (error) {
2557
+ await throwStreamingDeliveryFailure({
2558
+ error,
2559
+ content: text,
2560
+ infoKind: info?.kind,
2561
+ ownerGeneration
2562
+ });
2563
+ }
2564
+ return deferStreamingDelivery(mergeFeishuReplyDeliveryResults(deliveredResults, text), info?.kind, ownerGeneration);
2102
2565
  }
2103
2566
  if (useCard) {
2104
2567
  const cardHeader = resolveCardHeader(agentId, identity);
2105
2568
  const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2106
- await sendChunkedTextReply({
2569
+ deliveredResults.push(await sendChunkedTextReply({
2107
2570
  text,
2108
2571
  useCard: true,
2109
2572
  infoKind: info?.kind,
2110
2573
  chunkMentions: requiredMentionTargets,
2111
- sendChunk: async ({ chunk, mentions }) => {
2112
- await sendStructuredCardFeishu({
2113
- cfg,
2114
- to: sendTarget,
2115
- text: chunk,
2116
- replyToMessageId: sendReplyToMessageId,
2117
- replyInThread: effectiveReplyInThread,
2118
- allowTopLevelReplyFallback,
2119
- accountId,
2120
- header: cardHeader,
2121
- note: cardNote,
2122
- ...mentions ? { mentions } : {}
2123
- });
2124
- }
2125
- });
2574
+ sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
2575
+ cfg,
2576
+ to: sendTarget,
2577
+ text: chunk,
2578
+ replyToMessageId: sendReplyToMessageId,
2579
+ replyInThread: effectiveReplyInThread,
2580
+ allowTopLevelReplyFallback,
2581
+ accountId,
2582
+ header: cardHeader,
2583
+ note: cardNote,
2584
+ ...mentions ? { mentions } : {}
2585
+ })
2586
+ }));
2126
2587
  } else {
2127
2588
  const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
2128
- await sendChunkedTextReply({
2589
+ deliveredResults.push(await sendChunkedTextReply({
2129
2590
  text,
2130
2591
  useCard: false,
2131
2592
  infoKind: info?.kind,
2132
2593
  firstChunkMentions,
2133
2594
  chunkMentions: requiredMentionTargets,
2134
- sendChunk: async ({ chunk, mentions }) => {
2135
- await sendMessageFeishu({
2136
- cfg,
2137
- to: sendTarget,
2138
- text: chunk,
2139
- replyToMessageId: sendReplyToMessageId,
2140
- replyInThread: effectiveReplyInThread,
2141
- allowTopLevelReplyFallback,
2142
- accountId,
2143
- ...mentions ? { mentions } : {}
2144
- });
2145
- }
2146
- });
2595
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2596
+ cfg,
2597
+ to: sendTarget,
2598
+ text: chunk,
2599
+ replyToMessageId: sendReplyToMessageId,
2600
+ replyInThread: effectiveReplyInThread,
2601
+ allowTopLevelReplyFallback,
2602
+ accountId,
2603
+ ...mentions ? { mentions } : {}
2604
+ })
2605
+ }));
2147
2606
  }
2148
2607
  }
2149
- if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2608
+ if (hasMedia) await collectMediaDelivery(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2609
+ const result = mergeFeishuReplyDeliveryResults(deliveredResults, text);
2610
+ if (priorClosedStreamingSettlement?.error !== void 0) throw createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(priorClosedStreamingSettlement.error) && priorClosedStreamingSettlement.error instanceof Error ? priorClosedStreamingSettlement.error.cause ?? priorClosedStreamingSettlement.error : priorClosedStreamingSettlement.error, result);
2611
+ return result;
2150
2612
  },
2151
2613
  onError: handleDeliveryError
2152
2614
  },
2153
2615
  replyOptions: {
2154
2616
  onModelSelected: prefixContext.onModelSelected,
2155
2617
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
2156
- onPartialReply: streamingEnabled ? (payload) => {
2618
+ onPartialReply: previewStreamingEnabled ? (payload) => {
2157
2619
  if (!payload.text) return;
2158
2620
  const cleaned = stripReasoningTagsFromText(payload.text, {
2159
2621
  mode: "strict",
@@ -2172,7 +2634,7 @@ function createFeishuReplyDispatcher(params) {
2172
2634
  queueReasoningUpdate(formatReasoningMessage(payload.text));
2173
2635
  } : void 0,
2174
2636
  onReasoningEnd: reasoningPreviewEnabled ? () => {} : void 0,
2175
- onToolStart: streamingEnabled ? (payload) => {
2637
+ onToolStart: previewStreamingEnabled ? (payload) => {
2176
2638
  if (!isChannelProgressDraftWorkToolName(payload.name)) return;
2177
2639
  const statusLineLocal = formatChannelProgressDraftLineForEntry(account.config, {
2178
2640
  event: "tool",
@@ -2182,13 +2644,13 @@ function createFeishuReplyDispatcher(params) {
2182
2644
  }, { detailMode: payload.detailMode });
2183
2645
  if (statusLineLocal) updateStreamingStatusLine(statusLineLocal);
2184
2646
  } : void 0,
2185
- onAssistantMessageStart: streamingEnabled ? () => {
2647
+ onAssistantMessageStart: previewStreamingEnabled ? () => {
2186
2648
  updateStreamingStatusLine("", { startIfNeeded: false });
2187
2649
  } : void 0,
2188
- onCompactionStart: streamingEnabled ? () => {
2650
+ onCompactionStart: previewStreamingEnabled ? () => {
2189
2651
  updateStreamingStatusLine("📦 **Compacting context...**");
2190
2652
  } : void 0,
2191
- onCompactionEnd: streamingEnabled ? () => {
2653
+ onCompactionEnd: previewStreamingEnabled ? () => {
2192
2654
  updateStreamingStatusLine("");
2193
2655
  } : void 0
2194
2656
  },
@@ -2225,15 +2687,14 @@ function isFeishuTopicSessionScope(scope) {
2225
2687
  return scope === "group_topic" || scope === "group_topic_sender";
2226
2688
  }
2227
2689
  async function resolveFeishuAudioPreflightTranscript(params) {
2228
- if (params.content.trim() !== "<media:audio>") return;
2229
- const audioMedia = params.mediaList.filter((media) => media.contentType?.startsWith("audio/"));
2690
+ if (params.messageType !== "audio" || params.content.trim()) return;
2691
+ const audioMedia = params.mediaList.filter((media) => Boolean(media.path) && (media.kind === "audio" || media.contentType?.startsWith("audio/")));
2230
2692
  if (audioMedia.length === 0) return;
2231
2693
  try {
2232
2694
  const { transcribeFirstAudio } = await import("./audio-preflight.runtime-BBfC9elv.js");
2233
2695
  return await transcribeFirstAudio({
2234
2696
  ctx: {
2235
- MediaPaths: audioMedia.map((media) => media.path),
2236
- MediaTypes: audioMedia.map((media) => media.contentType).filter(Boolean),
2697
+ media: audioMedia,
2237
2698
  ChatType: params.chatType
2238
2699
  },
2239
2700
  cfg: params.cfg
@@ -2244,8 +2705,7 @@ async function resolveFeishuAudioPreflightTranscript(params) {
2244
2705
  }
2245
2706
  }
2246
2707
  /**
2247
- * Build media payload for inbound context.
2248
- * Similar to Discord's buildDiscordMediaPayload().
2708
+ * Parse an inbound Feishu event into its caption and routing metadata.
2249
2709
  */
2250
2710
  function parseFeishuMessageEvent(event, botOpenId, _botName) {
2251
2711
  const rawContent = parseMessageContent(event.message.content, event.message.message_type);
@@ -2313,7 +2773,7 @@ async function filterFetchedGroupContextMessages(messages, params) {
2313
2773
  }) ? message : void 0))).filter((message) => message !== void 0);
2314
2774
  }
2315
2775
  async function handleFeishuMessage(params) {
2316
- const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride } = params;
2776
+ const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride, turnAdoptionLifecycle } = params;
2317
2777
  const account = resolveFeishuRuntimeAccount({
2318
2778
  cfg,
2319
2779
  accountId
@@ -2323,7 +2783,7 @@ async function handleFeishuMessage(params) {
2323
2783
  const error = runtime?.error ?? console.error;
2324
2784
  const messageId = event.message.message_id;
2325
2785
  const messageDedupeKey = messageDedupeKeyOverride ?? resolveFeishuMessageDedupeKey(event);
2326
- if (!await finalizeFeishuMessageProcessing({
2786
+ if (!turnAdoptionLifecycle && !await finalizeFeishuMessageProcessing({
2327
2787
  messageId: messageDedupeKey,
2328
2788
  namespace: account.accountId,
2329
2789
  log,
@@ -2787,7 +3247,7 @@ async function handleFeishuMessage(params) {
2787
3247
  });
2788
3248
  log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
2789
3249
  const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024;
2790
- const mediaResolution = await resolveFeishuMediaList({
3250
+ const mediaList = await resolveFeishuMediaList({
2791
3251
  cfg,
2792
3252
  messageId: ctx.messageId,
2793
3253
  messageType: event.message.message_type,
@@ -2796,12 +3256,10 @@ async function handleFeishuMessage(params) {
2796
3256
  log,
2797
3257
  accountId: account.accountId
2798
3258
  });
2799
- const mediaList = mediaResolution.media;
2800
- const mediaFailurePresentation = resolveFeishuMediaFailurePresentation(event.message.content, event.message.message_type);
2801
- const mediaFailureContent = mediaResolution.unavailableCount > 0 ? formatInboundMediaUnavailableText({
2802
- body: mediaFailurePresentation.unavailableBody ?? ctx.content,
2803
- mediaPlaceholder: mediaFailurePresentation.mediaPlaceholder,
2804
- notice: `[feishu ${mediaResolution.unavailableCount > 1 ? `${mediaResolution.unavailableCount} attachments` : "attachment"} unavailable]`
3259
+ const unavailableMediaCount = mediaList.filter((media) => !media.path).length;
3260
+ const mediaFailureContent = unavailableMediaCount > 0 ? formatInboundMediaUnavailableText({
3261
+ body: ctx.content,
3262
+ notice: `[feishu ${unavailableMediaCount > 1 ? `${unavailableMediaCount} attachments` : "attachment"} unavailable]`
2805
3263
  }) : ctx.content;
2806
3264
  let quotedMessageInfo = null;
2807
3265
  let quotedContent;
@@ -2836,10 +3294,11 @@ async function handleFeishuMessage(params) {
2836
3294
  cfg: effectiveCfg,
2837
3295
  mediaList,
2838
3296
  content: ctx.content,
3297
+ messageType: event.message.message_type,
2839
3298
  chatType: isGroup ? "group" : "direct",
2840
3299
  log
2841
3300
  });
2842
- const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
3301
+ const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.kind === "audio" || media.contentType?.startsWith("audio/"));
2843
3302
  const inboundMedia = toInboundMediaFacts(mediaList, { transcribed: (_media, index) => index === preflightAudioIndex });
2844
3303
  const requiredMentionTargets = isGroup && ctx.senderType === "bot" && ctx.senderOpenId ? [{
2845
3304
  openId: ctx.senderOpenId,
@@ -3074,6 +3533,7 @@ async function handleFeishuMessage(params) {
3074
3533
  rawBody: commandFacingContent,
3075
3534
  commandBody: commandFacingContent
3076
3535
  },
3536
+ sessionTranscript: { historyLimit: isGroup ? historyLimit : 0 },
3077
3537
  access: {
3078
3538
  mentions: {
3079
3539
  canDetectMention: isGroup,
@@ -3123,144 +3583,202 @@ async function handleFeishuMessage(params) {
3123
3583
  };
3124
3584
  };
3125
3585
  if (broadcastAgents) {
3126
- if (!await recordProcessedFeishuMessage(messageDedupeKey ?? ctx.messageId, "broadcast", log)) {
3586
+ const broadcastDedupeKey = messageDedupeKey ?? ctx.messageId;
3587
+ const broadcastClaim = await claimUnprocessedFeishuMessage({
3588
+ messageId: broadcastDedupeKey,
3589
+ namespace: "broadcast",
3590
+ log
3591
+ });
3592
+ if (broadcastClaim.kind === "duplicate" || broadcastClaim.kind === "inflight") {
3127
3593
  log(`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`);
3128
3594
  return;
3129
3595
  }
3130
- const strategy = cfg.broadcast?.strategy === "sequential" ? "sequential" : "parallel";
3131
- const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$2(route.agentId) : null;
3132
- const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$2(a.id));
3133
- const hasKnownAgents = agentIds.length > 0;
3596
+ const broadcastSettlement = createFeishuBroadcastIngressSettlement({
3597
+ lifecycle: turnAdoptionLifecycle,
3598
+ replayClaim: broadcastClaim.kind === "claimed" ? broadcastClaim.handle : void 0,
3599
+ onReplayCommitError: (err) => error(`feishu[${account.accountId}]: failed to commit broadcast replay guard: ${String(err)}`),
3600
+ onAdopted: () => {
3601
+ if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
3602
+ historyKey,
3603
+ limit: historyLimit
3604
+ });
3605
+ }
3606
+ });
3607
+ const abandonBroadcast = async (err) => {
3608
+ try {
3609
+ await broadcastSettlement.onDispatchFailed(err);
3610
+ } catch (abandonError) {
3611
+ error(`feishu[${account.accountId}]: failed to abandon broadcast ingress: ${String(abandonError)}`);
3612
+ }
3613
+ };
3614
+ const strategy = cfg.broadcast?.strategy === "sequential" ? "sequential" : "parallel";
3615
+ const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$2(route.agentId) : null;
3616
+ const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$2(a.id));
3617
+ const hasKnownAgents = agentIds.length > 0;
3134
3618
  log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
3619
+ const createBroadcastInboundAdapter = (paramsLocal) => ({
3620
+ ingest: () => ({
3621
+ id: ctx.messageId,
3622
+ timestamp: messageCreateTimeMs,
3623
+ rawText: ctx.content,
3624
+ textForAgent: paramsLocal.ctxPayload.BodyForAgent,
3625
+ textForCommands: paramsLocal.ctxPayload.CommandBody,
3626
+ raw: ctx
3627
+ }),
3628
+ resolveTurn: () => ({
3629
+ cfg,
3630
+ channel: "feishu",
3631
+ accountId: route.accountId,
3632
+ route: {
3633
+ agentId: paramsLocal.agentId,
3634
+ sessionKey: paramsLocal.sessionKey
3635
+ },
3636
+ ctxPayload: paramsLocal.ctxPayload,
3637
+ record: paramsLocal.record,
3638
+ ...paramsLocal.variant.kind === "observeOnly" ? {
3639
+ admission: {
3640
+ kind: "observeOnly",
3641
+ reason: "broadcast-observer"
3642
+ },
3643
+ delivery: { deliver: async () => ({ visibleReplySent: false }) },
3644
+ replyOptions: bindIngressLifecycleToReplyOptions(paramsLocal.lifecycle)
3645
+ } : {
3646
+ dispatcherOptions: paramsLocal.variant.dispatcher.dispatcherOptions,
3647
+ delivery: paramsLocal.variant.dispatcher.delivery,
3648
+ replyOptions: {
3649
+ ...paramsLocal.variant.dispatcher.replyOptions,
3650
+ ...bindIngressLifecycleToReplyOptions(paramsLocal.lifecycle)
3651
+ }
3652
+ }
3653
+ })
3654
+ });
3135
3655
  const dispatchForAgent = async (agentId) => {
3136
- if (hasKnownAgents && !agentIds.includes(normalizeAgentId$2(agentId))) {
3656
+ const normalizedAgentId = normalizeAgentId$2(agentId);
3657
+ if (hasKnownAgents && !agentIds.includes(normalizedAgentId)) {
3137
3658
  log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
3138
3659
  return;
3139
3660
  }
3140
- const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
3141
- const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
3142
- const agentRecord = {
3143
- updateLastRoute: buildFeishuInboundLastRouteUpdate({
3144
- sessionKey: agentSessionKey,
3145
- accountId: route.accountId
3146
- }),
3147
- onRecordError: (err) => {
3148
- log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
3661
+ let agentClaim;
3662
+ for (let attempt = 0; attempt < 2; attempt += 1) {
3663
+ agentClaim = await claimUnprocessedFeishuMessage({
3664
+ messageId: broadcastDedupeKey,
3665
+ namespace: `broadcast:${normalizedAgentId}`,
3666
+ log
3667
+ });
3668
+ if (agentClaim.kind === "duplicate") return;
3669
+ if (agentClaim.kind !== "inflight") break;
3670
+ broadcastSettlement.onLanePending();
3671
+ try {
3672
+ await agentClaim.pending;
3673
+ return;
3674
+ } catch (err) {
3675
+ if (attempt === 1) throw err;
3149
3676
  }
3150
- };
3151
- const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3152
- cfg,
3153
- agentId,
3154
- storePath: agentStorePath,
3155
- sessionKey: agentSessionKey
3156
- });
3157
- const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
3158
- if (agentId === activeAgentId) {
3159
- const identity = resolveAgentOutboundIdentity(cfg, agentId);
3160
- const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3677
+ }
3678
+ const lane = broadcastSettlement.createLane(agentClaim?.kind === "claimed" ? agentClaim.handle : void 0);
3679
+ try {
3680
+ const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
3681
+ const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
3682
+ const agentRecord = {
3683
+ updateLastRoute: buildFeishuInboundLastRouteUpdate({
3684
+ sessionKey: agentSessionKey,
3685
+ accountId: route.accountId
3686
+ }),
3687
+ onRecordError: (err) => {
3688
+ log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
3689
+ }
3690
+ };
3691
+ const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3161
3692
  cfg,
3162
3693
  agentId,
3163
- runtime,
3164
- chatId: ctx.chatId,
3165
- sendTarget: feishuTo,
3166
- allowReasoningPreview,
3167
- replyToMessageId: replyTargetMessageId,
3168
- typingTargetMessageId,
3169
- skipReplyToInMessages: !isGroup && !directThreadReply,
3170
- replyInThread,
3171
- rootId: ctx.rootId,
3172
- threadReply,
3173
- accountId: account.accountId,
3174
- identity,
3175
- mentionTargets: ctx.mentionTargets,
3176
- requiredMentionTargets,
3177
- messageCreateTimeMs,
3694
+ storePath: agentStorePath,
3178
3695
  sessionKey: agentSessionKey
3179
3696
  });
3180
- log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
3181
- const turnResult = await core.channel.inbound.run({
3182
- channel: "feishu",
3183
- accountId: route.accountId,
3184
- raw: ctx,
3185
- adapter: {
3186
- ingest: () => ({
3187
- id: ctx.messageId,
3188
- timestamp: messageCreateTimeMs,
3189
- rawText: ctx.content,
3190
- textForAgent: agentCtx.BodyForAgent,
3191
- textForCommands: agentCtx.CommandBody,
3192
- raw: ctx
3193
- }),
3194
- resolveTurn: () => ({
3697
+ const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
3698
+ let variant;
3699
+ if (agentId === activeAgentId) {
3700
+ const identity = resolveAgentOutboundIdentity(cfg, agentId);
3701
+ variant = {
3702
+ kind: "active",
3703
+ dispatcher: createFeishuReplyDispatcher({
3195
3704
  cfg,
3196
- channel: "feishu",
3197
- accountId: route.accountId,
3198
- route: {
3199
- agentId,
3200
- sessionKey: agentSessionKey
3201
- },
3202
- ctxPayload: agentCtx,
3203
- record: agentRecord,
3204
- dispatcherOptions,
3205
- delivery,
3206
- replyOptions
3705
+ agentId,
3706
+ runtime,
3707
+ chatId: ctx.chatId,
3708
+ sendTarget: feishuTo,
3709
+ allowReasoningPreview,
3710
+ replyToMessageId: replyTargetMessageId,
3711
+ typingTargetMessageId,
3712
+ skipReplyToInMessages: !isGroup && !directThreadReply,
3713
+ replyInThread,
3714
+ rootId: ctx.rootId,
3715
+ threadReply,
3716
+ accountId: account.accountId,
3717
+ identity,
3718
+ mentionTargets: ctx.mentionTargets,
3719
+ requiredMentionTargets,
3720
+ messageCreateTimeMs,
3721
+ sessionKey: agentSessionKey
3207
3722
  })
3208
- }
3209
- });
3210
- if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3211
- } else {
3212
- delete agentCtx.CommandAuthorized;
3213
- log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
3214
- await core.channel.inbound.run({
3723
+ };
3724
+ log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
3725
+ } else {
3726
+ delete agentCtx.CommandAuthorized;
3727
+ variant = { kind: "observeOnly" };
3728
+ log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
3729
+ }
3730
+ const turnResult = await core.channel.inbound.run({
3215
3731
  channel: "feishu",
3216
3732
  accountId: route.accountId,
3217
3733
  raw: ctx,
3218
- adapter: {
3219
- ingest: () => ({
3220
- id: ctx.messageId,
3221
- timestamp: messageCreateTimeMs,
3222
- rawText: ctx.content,
3223
- textForAgent: agentCtx.BodyForAgent,
3224
- textForCommands: agentCtx.CommandBody,
3225
- raw: ctx
3226
- }),
3227
- resolveTurn: () => ({
3228
- cfg,
3229
- channel: "feishu",
3230
- accountId: route.accountId,
3231
- route: {
3232
- agentId,
3233
- sessionKey: agentSessionKey
3234
- },
3235
- ctxPayload: agentCtx,
3236
- record: agentRecord,
3237
- admission: {
3238
- kind: "observeOnly",
3239
- reason: "broadcast-observer"
3240
- },
3241
- delivery: { deliver: async () => ({ visibleReplySent: false }) }
3242
- })
3243
- }
3734
+ adapter: createBroadcastInboundAdapter({
3735
+ agentId,
3736
+ sessionKey: agentSessionKey,
3737
+ ctxPayload: agentCtx,
3738
+ record: agentRecord,
3739
+ lifecycle: lane.lifecycle,
3740
+ variant
3741
+ })
3244
3742
  });
3743
+ if (variant.kind === "active" && turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await variant.dispatcher.ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3744
+ await lane.onDispatchComplete(turnResult.dispatched);
3745
+ } catch (err) {
3746
+ await lane.onDispatchFailed(err);
3747
+ throw err;
3245
3748
  }
3246
3749
  };
3750
+ const results = [];
3247
3751
  if (strategy === "sequential") for (const agentId of broadcastAgents) try {
3248
3752
  await dispatchForAgent(agentId);
3249
- } catch (err) {
3250
- log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`);
3753
+ results.push({
3754
+ status: "fulfilled",
3755
+ value: void 0
3756
+ });
3757
+ } catch (reason) {
3758
+ results.push({
3759
+ status: "rejected",
3760
+ reason
3761
+ });
3251
3762
  }
3252
- else {
3253
- const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
3254
- for (const [i, result] of results.entries()) if (result.status === "rejected") {
3255
- const agentId = broadcastAgents.at(i);
3256
- if (agentId === void 0) continue;
3257
- log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
3258
- }
3763
+ else results.push(...await Promise.allSettled(broadcastAgents.map(dispatchForAgent)));
3764
+ const failures = [];
3765
+ for (const [i, result] of results.entries()) if (result.status === "rejected") {
3766
+ const agentId = broadcastAgents.at(i);
3767
+ if (agentId === void 0) continue;
3768
+ log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
3769
+ failures.push(result.reason);
3770
+ }
3771
+ if (failures.length > 0) {
3772
+ const failure = failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed");
3773
+ await abandonBroadcast(failure);
3774
+ throw failure;
3775
+ }
3776
+ try {
3777
+ await broadcastSettlement.onDispatchComplete();
3778
+ } catch (err) {
3779
+ await abandonBroadcast(err);
3780
+ throw err;
3259
3781
  }
3260
- if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
3261
- historyKey,
3262
- limit: historyLimit
3263
- });
3264
3782
  log(`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`);
3265
3783
  } else {
3266
3784
  const ctxPayload = await buildCtxPayloadForAgent(route.agentId, route.sessionKey, route.accountId, ctx.mentionedBot);
@@ -3332,7 +3850,10 @@ async function handleFeishuMessage(params) {
3332
3850
  },
3333
3851
  dispatcherOptions,
3334
3852
  delivery,
3335
- replyOptions
3853
+ replyOptions: {
3854
+ ...replyOptions,
3855
+ ...turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(turnAdoptionLifecycle) : {}
3856
+ }
3336
3857
  })
3337
3858
  }
3338
3859
  });
@@ -3344,6 +3865,7 @@ async function handleFeishuMessage(params) {
3344
3865
  }
3345
3866
  } catch (err) {
3346
3867
  error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
3868
+ if (turnAdoptionLifecycle) throw err;
3347
3869
  }
3348
3870
  }
3349
3871
  //#endregion
@@ -3721,6 +4243,350 @@ async function handleFeishuCardAction(params) {
3721
4243
  }
3722
4244
  }
3723
4245
  //#endregion
4246
+ //#region extensions/feishu/src/feishu-ingress.ts
4247
+ const FEISHU_INGRESS_PAYLOAD_VERSION = 1;
4248
+ const FEISHU_INGRESS_POLL_INTERVAL_MS = 500;
4249
+ const FEISHU_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
4250
+ const FEISHU_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
4251
+ const FEISHU_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
4252
+ const FEISHU_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
4253
+ const FEISHU_INGRESS_FAILED_MAX_ENTRIES = 2e4;
4254
+ const FEISHU_DURABLE_EVENT_TYPES = /* @__PURE__ */ new Set(["drive.notice.comment_add_v1", "im.message.receive_v1"]);
4255
+ var FeishuIngressPermanentError = class extends Error {
4256
+ constructor(reason, message, options) {
4257
+ super(message, options);
4258
+ this.reason = reason;
4259
+ this.name = "FeishuIngressPermanentError";
4260
+ }
4261
+ };
4262
+ function isRecord$1(value) {
4263
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4264
+ }
4265
+ function readString(value) {
4266
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4267
+ }
4268
+ function parseRawEnvelope(rawEnvelope) {
4269
+ let parsed;
4270
+ try {
4271
+ parsed = JSON.parse(rawEnvelope);
4272
+ } catch (error) {
4273
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope contains invalid JSON.", { cause: error });
4274
+ }
4275
+ if (!isRecord$1(parsed)) throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope must be a JSON object.");
4276
+ return parsed;
4277
+ }
4278
+ function decryptEnvelope(envelope, encryptKey) {
4279
+ const encrypted = readString(envelope.encrypt);
4280
+ if (!encrypted) return envelope;
4281
+ const key = encryptKey?.trim();
4282
+ if (!key) throw new FeishuIngressPermanentError("authentication-failed", "Feishu encrypted ingress envelope has no configured encrypt key.");
4283
+ try {
4284
+ return parseRawEnvelope(new Lark.AESCipher(key).decrypt(encrypted));
4285
+ } catch (error) {
4286
+ if (error instanceof FeishuIngressPermanentError) throw error;
4287
+ throw new FeishuIngressPermanentError("authentication-failed", "Feishu ingress envelope decryption failed.", { cause: error });
4288
+ }
4289
+ }
4290
+ function inspectFeishuIngressEnvelope(rawEnvelope, encryptKey, allowInvalidLane = false) {
4291
+ const envelope = decryptEnvelope(parseRawEnvelope(rawEnvelope), encryptKey);
4292
+ const nestedHeader = isRecord$1(envelope.header) ? envelope.header : null;
4293
+ const nestedEvent = isRecord$1(envelope.event) ? envelope.event : null;
4294
+ const eventType = readString(nestedHeader?.event_type) ?? readString(envelope.event_type);
4295
+ if (!eventType || !FEISHU_DURABLE_EVENT_TYPES.has(eventType)) return null;
4296
+ const eventId = readString(nestedHeader?.event_id) ?? readString(envelope.event_id);
4297
+ if (!eventId) throw new FeishuIngressPermanentError("invalid-event", `Feishu ${eventType} envelope is missing header.event_id.`);
4298
+ const event = nestedEvent ?? envelope;
4299
+ if (eventType === "im.message.receive_v1") {
4300
+ const chatId = readString((isRecord$1(event.message) ? event.message : null)?.chat_id);
4301
+ if (!chatId) {
4302
+ if (allowInvalidLane) return {
4303
+ eventId,
4304
+ eventType,
4305
+ laneKey: `invalid:${eventType}:${eventId}`
4306
+ };
4307
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu message ingress envelope is missing event.message.chat_id.");
4308
+ }
4309
+ return {
4310
+ eventId,
4311
+ eventType,
4312
+ laneKey: `chat:${chatId}`
4313
+ };
4314
+ }
4315
+ const noticeMeta = isRecord$1(event.notice_meta) ? event.notice_meta : null;
4316
+ const fileType = readString(noticeMeta?.file_type);
4317
+ const documentId = readString(noticeMeta?.file_token);
4318
+ if (!fileType || !documentId) {
4319
+ if (allowInvalidLane) return {
4320
+ eventId,
4321
+ eventType,
4322
+ laneKey: `invalid:${eventType}:${eventId}`
4323
+ };
4324
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu comment ingress envelope is missing its document identity.");
4325
+ }
4326
+ return {
4327
+ eventId,
4328
+ eventType,
4329
+ laneKey: `comment-doc:${fileType}:${documentId}`
4330
+ };
4331
+ }
4332
+ function resolveFeishuIngressNonRetryableFailure(error) {
4333
+ if (error instanceof FeishuIngressPermanentError) return {
4334
+ reason: error.reason,
4335
+ message: error.message
4336
+ };
4337
+ for (const candidate of collectErrorGraphCandidates(error, (current) => [current.cause, current.response])) {
4338
+ const status = isRecord$1(candidate) ? candidate.status : void 0;
4339
+ if (status === 401 || status === 403) return {
4340
+ reason: "authentication-failed",
4341
+ message: formatErrorMessage(error)
4342
+ };
4343
+ }
4344
+ return null;
4345
+ }
4346
+ /** Fan one merged Feishu turn's adoption across every transport and logical claim. */
4347
+ function buildFeishuFlushIngressLifecycle(sources, options) {
4348
+ const durableSources = sources.filter((source) => source.lifecycle !== void 0);
4349
+ const lifecycles = durableSources.map((source) => source.lifecycle);
4350
+ const replayClaims = durableSources.map((source) => source.replayClaim).filter((claim) => claim !== void 0);
4351
+ const transportLifecycle = fanInChannelIngressLifecycles(lifecycles).lifecycle;
4352
+ if (!transportLifecycle) return {
4353
+ lifecycle: void 0,
4354
+ settle: async () => {}
4355
+ };
4356
+ let handedOff = false;
4357
+ let terminal;
4358
+ let adopting;
4359
+ let abandoning;
4360
+ const releaseReplayClaims = () => {
4361
+ for (const claim of replayClaims) claim.release({ error: /* @__PURE__ */ new Error("feishu-ingress-not-adopted") });
4362
+ };
4363
+ const runAbandon = async () => {
4364
+ if (terminal) return;
4365
+ releaseReplayClaims();
4366
+ await transportLifecycle.onAbandoned();
4367
+ terminal = "abandoned";
4368
+ };
4369
+ const ensureAbandoned = async () => {
4370
+ if (terminal) return;
4371
+ const activeAbandonment = abandoning ?? runAbandon();
4372
+ abandoning = activeAbandonment;
4373
+ try {
4374
+ await activeAbandonment;
4375
+ } finally {
4376
+ if (abandoning === activeAbandonment && terminal !== "abandoned") abandoning = void 0;
4377
+ }
4378
+ };
4379
+ const abandonAll = async () => {
4380
+ if (terminal) return;
4381
+ if (adopting) {
4382
+ await adopting.catch(() => void 0);
4383
+ if (terminal) return;
4384
+ }
4385
+ await ensureAbandoned();
4386
+ };
4387
+ const adoptAll = async () => {
4388
+ if (terminal) return;
4389
+ if (abandoning) {
4390
+ await abandoning.catch(() => void 0);
4391
+ if (terminal) return;
4392
+ }
4393
+ const activeAdoption = adopting ?? (async () => {
4394
+ try {
4395
+ await transportLifecycle.onAdopted();
4396
+ terminal = "adopted";
4397
+ const results = await Promise.allSettled(replayClaims.map(async (claim) => claim.commit()));
4398
+ for (const result of results) if (result.status === "rejected") try {
4399
+ options?.onReplayCommitError?.(result.reason);
4400
+ } catch {}
4401
+ } catch (error) {
4402
+ await ensureAbandoned().catch(() => void 0);
4403
+ throw error;
4404
+ }
4405
+ })();
4406
+ adopting = activeAdoption;
4407
+ try {
4408
+ await activeAdoption;
4409
+ } finally {
4410
+ if (adopting === activeAdoption && terminal !== "adopted") adopting = void 0;
4411
+ }
4412
+ };
4413
+ return {
4414
+ lifecycle: {
4415
+ abortSignal: transportLifecycle.abortSignal,
4416
+ onAdopted: async () => {
4417
+ handedOff = true;
4418
+ await adoptAll();
4419
+ },
4420
+ onDeferred: () => {
4421
+ handedOff = true;
4422
+ transportLifecycle.onDeferred();
4423
+ },
4424
+ onAdoptionFinalizing: () => {
4425
+ transportLifecycle.onAdoptionFinalizing();
4426
+ },
4427
+ onAbandoned: async () => {
4428
+ handedOff = true;
4429
+ await abandonAll();
4430
+ }
4431
+ },
4432
+ settle: async () => {
4433
+ if (handedOff) return;
4434
+ handedOff = true;
4435
+ releaseReplayClaims();
4436
+ try {
4437
+ transportLifecycle.onAdoptionFinalizing();
4438
+ await transportLifecycle.onAdopted();
4439
+ terminal = "adopted";
4440
+ } catch (error) {
4441
+ await ensureAbandoned().catch(() => void 0);
4442
+ throw error;
4443
+ }
4444
+ }
4445
+ };
4446
+ }
4447
+ function createFeishuDurableIngress(options) {
4448
+ let socketTerminator;
4449
+ const activeLifecycles = /* @__PURE__ */ new Map();
4450
+ const deferredClaims = /* @__PURE__ */ new Map();
4451
+ const monitor = createChannelIngressMonitor({
4452
+ queue: options.queue ?? (() => getFeishuRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
4453
+ inspect: (rawEnvelope, context) => {
4454
+ const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, context.phase === "admission");
4455
+ return facts ? {
4456
+ eventId: facts.eventId,
4457
+ laneKey: facts.laneKey
4458
+ } : null;
4459
+ },
4460
+ payload: {
4461
+ version: FEISHU_INGRESS_PAYLOAD_VERSION,
4462
+ serialize: (rawEnvelope, { receivedAt }) => ({
4463
+ receivedAt,
4464
+ rawEnvelope
4465
+ }),
4466
+ deserialize: (body) => body.rawEnvelope,
4467
+ encode: ({ body }) => ({
4468
+ version: FEISHU_INGRESS_PAYLOAD_VERSION,
4469
+ ...body
4470
+ }),
4471
+ decode: (payload) => ({
4472
+ version: payload.version,
4473
+ body: {
4474
+ receivedAt: payload.receivedAt,
4475
+ rawEnvelope: payload.rawEnvelope
4476
+ }
4477
+ }),
4478
+ createClaimError: (kind, claim) => new FeishuIngressPermanentError("invalid-event", kind === "invalid-version" ? `Feishu ingress row ${claim.id} has an unsupported version.` : `Feishu ingress row ${claim.id} has invalid delivery identity.`)
4479
+ },
4480
+ deliver: async (rawEnvelope, lifecycle, claim) => {
4481
+ let resolveDeferred;
4482
+ const deferred = new Promise((resolve) => {
4483
+ resolveDeferred = resolve;
4484
+ });
4485
+ let deferredSettled = false;
4486
+ const settleDeferred = () => {
4487
+ if (deferredSettled) return;
4488
+ deferredSettled = true;
4489
+ if (deferredClaims.get(claim.id) === deferred) deferredClaims.delete(claim.id);
4490
+ resolveDeferred();
4491
+ };
4492
+ const abandonHandlers = /* @__PURE__ */ new Set();
4493
+ const wrappedLifecycle = {
4494
+ ...lifecycle,
4495
+ onAdopted: async () => {
4496
+ try {
4497
+ await lifecycle.onAdopted();
4498
+ } finally {
4499
+ settleDeferred();
4500
+ }
4501
+ },
4502
+ onAbandoned: async () => {
4503
+ try {
4504
+ await Promise.allSettled([...abandonHandlers].map(async (handler) => await handler()));
4505
+ await lifecycle.onAbandoned();
4506
+ } finally {
4507
+ settleDeferred();
4508
+ }
4509
+ },
4510
+ registerAbandonHandler: (handler) => {
4511
+ abandonHandlers.add(handler);
4512
+ return () => abandonHandlers.delete(handler);
4513
+ }
4514
+ };
4515
+ activeLifecycles.set(claim.id, wrappedLifecycle);
4516
+ try {
4517
+ const result = await options.dispatcher.invoke(parseRawEnvelope(rawEnvelope), { needCheck: false });
4518
+ if (!isRecord$1(result)) return;
4519
+ if (result.kind === "deferred") {
4520
+ if (!deferredSettled) deferredClaims.set(claim.id, deferred);
4521
+ return { kind: "deferred" };
4522
+ }
4523
+ if (result.kind === "completed") return { kind: "completed" };
4524
+ if (result.kind === "failed-retryable") return {
4525
+ kind: "failed-retryable",
4526
+ error: result.error
4527
+ };
4528
+ return;
4529
+ } finally {
4530
+ if (activeLifecycles.get(claim.id) === wrappedLifecycle) activeLifecycles.delete(claim.id);
4531
+ }
4532
+ },
4533
+ pollIntervalMs: options.pollIntervalMs ?? FEISHU_INGRESS_POLL_INTERVAL_MS,
4534
+ retention: {
4535
+ pruneIntervalMs: FEISHU_INGRESS_PRUNE_INTERVAL_MS,
4536
+ completedTtlMs: FEISHU_INGRESS_COMPLETED_TTL_MS,
4537
+ completedMaxEntries: FEISHU_INGRESS_COMPLETED_MAX_ENTRIES,
4538
+ failedTtlMs: FEISHU_INGRESS_FAILED_TTL_MS,
4539
+ failedMaxEntries: FEISHU_INGRESS_FAILED_MAX_ENTRIES
4540
+ },
4541
+ drain: {
4542
+ adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
4543
+ resolveNonRetryableFailure: resolveFeishuIngressNonRetryableFailure,
4544
+ onLog: (message) => options.runtime.error?.(`feishu ingress: ${message}`)
4545
+ },
4546
+ createStoppedError: () => /* @__PURE__ */ new Error("Feishu ingress is stopped."),
4547
+ onError: (error) => options.runtime.error?.(`feishu ingress drain failed: ${formatErrorMessage(error)}`)
4548
+ });
4549
+ const invoke = async (data, params) => {
4550
+ let rawEnvelope;
4551
+ try {
4552
+ const serialized = JSON.stringify(data);
4553
+ if (serialized === void 0) throw new TypeError("Feishu ingress envelope has no JSON representation.");
4554
+ rawEnvelope = serialized;
4555
+ } catch (error) {
4556
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope is not serializable.", { cause: error });
4557
+ }
4558
+ const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, true);
4559
+ if (!facts) return await options.dispatcher.invoke(data, params);
4560
+ try {
4561
+ await monitor.admit(rawEnvelope, { facts: {
4562
+ eventId: facts.eventId,
4563
+ laneKey: facts.laneKey
4564
+ } });
4565
+ } catch (error) {
4566
+ socketTerminator?.();
4567
+ throw error;
4568
+ }
4569
+ };
4570
+ return {
4571
+ invoke,
4572
+ resolveLifecycle: (data) => {
4573
+ const eventId = isRecord$1(data) ? readString(data.event_id) : null;
4574
+ return eventId ? activeLifecycles.get(eventId) : void 0;
4575
+ },
4576
+ setSocketTerminator: (terminate) => {
4577
+ socketTerminator = terminate;
4578
+ },
4579
+ start: monitor.start,
4580
+ stop: async () => {
4581
+ await monitor.stop();
4582
+ await Promise.allSettled(deferredClaims.values());
4583
+ activeLifecycles.clear();
4584
+ socketTerminator = void 0;
4585
+ },
4586
+ waitForIdle: monitor.waitForIdle
4587
+ };
4588
+ }
4589
+ //#endregion
3724
4590
  //#region extensions/feishu/src/monitor-defaults.ts
3725
4591
  const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS = {
3726
4592
  windowMs: 6e4,
@@ -3871,7 +4737,8 @@ function applyBotIdentityState(accountId, identity) {
3871
4737
  });
3872
4738
  return {
3873
4739
  botOpenId,
3874
- botName
4740
+ botName,
4741
+ source: botOpenId ? identity.source : void 0
3875
4742
  };
3876
4743
  }
3877
4744
  async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
@@ -3881,12 +4748,13 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
3881
4748
  for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) {
3882
4749
  if (abortSignal?.aborted) return;
3883
4750
  if (!await waitForAbortableDelay(delayMs, abortSignal)) return;
3884
- const resolved = applyBotIdentityState(accountId, await fetchBotIdentityForMonitor(account, {
4751
+ const identity = await fetchBotIdentityForMonitor(account, {
3885
4752
  runtime,
3886
- abortSignal
3887
- }));
3888
- if (resolved.botOpenId) {
3889
- log(`feishu[${accountId}]: bot open_id recovered via background retry: ${resolved.botOpenId}`);
4753
+ abortSignal,
4754
+ allowCachedFallback: false
4755
+ });
4756
+ if (normalizeOptionalString(identity.botOpenId) && identity.source === "provider") {
4757
+ log(`feishu[${accountId}]: bot open_id recovered via background retry: ${applyBotIdentityState(accountId, identity).botOpenId}`);
3890
4758
  return;
3891
4759
  }
3892
4760
  const nextDelayResult = nextDelays.next();
@@ -3896,10 +4764,10 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
3896
4764
  error(`feishu[${accountId}]: bot identity background retry exhausted; requireMention group messages may be skipped until restart`);
3897
4765
  }
3898
4766
  function startBotIdentityRecovery(params) {
3899
- const { account, accountId, runtime, abortSignal } = params;
4767
+ const { account, accountId, runtime, abortSignal, currentSource } = params;
3900
4768
  const log = runtime?.log ?? console.log;
3901
- log(`feishu[${accountId}]: bot open_id unknown; starting background retry (delays: ${BOT_IDENTITY_RETRY_DELAYS_MS.map((delay) => `${delay / 1e3}s`).join(", ")})`);
3902
- log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
4769
+ log(`feishu[${accountId}]: bot open_id ${currentSource === "cache" ? "loaded from cache" : "unknown"}; starting background provider refresh (delays: ${BOT_IDENTITY_RETRY_DELAYS_MS.map((delay) => `${delay / 1e3}s`).join(", ")})`);
4770
+ if (currentSource !== "cache") log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
3903
4771
  retryBotIdentityProbe(account, accountId, runtime, abortSignal);
3904
4772
  }
3905
4773
  //#endregion
@@ -4153,20 +5021,40 @@ function createFeishuCommentReplyDispatcher(params) {
4153
5021
  }
4154
5022
  },
4155
5023
  delivery: {
5024
+ observeMessageSent: true,
4156
5025
  deliver: async (payload, info) => {
4157
- if (info.kind !== "final") return;
5026
+ if (info.kind !== "final") return noVisibleFeishuReplyDelivery;
4158
5027
  const reply = resolveSendableOutboundReplyParts(payload);
4159
5028
  if (!reply.hasText) {
4160
5029
  if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
4161
- return;
5030
+ return noVisibleFeishuReplyDelivery;
4162
5031
  }
4163
5032
  const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
4164
- for (const chunk of chunks) await deliverCommentThreadText(client, {
4165
- file_token: params.fileToken,
4166
- file_type: params.fileType,
4167
- comment_id: params.commentId,
4168
- content: chunk,
4169
- is_whole_comment: params.isWholeComment
5033
+ const results = [];
5034
+ const acceptedChunks = [];
5035
+ for (const chunk of chunks) try {
5036
+ const result = await deliverCommentThreadText(client, {
5037
+ file_token: params.fileToken,
5038
+ file_type: params.fileType,
5039
+ comment_id: params.commentId,
5040
+ content: chunk,
5041
+ is_whole_comment: params.isWholeComment
5042
+ });
5043
+ results.push({ messageId: result.delivery_mode === "reply_comment" ? result.reply_id : result.comment_id });
5044
+ acceptedChunks.push(chunk);
5045
+ } catch (error) {
5046
+ throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
5047
+ results,
5048
+ visibleReplySent: results.length > 0,
5049
+ content: acceptedChunks.join(""),
5050
+ kind: "text"
5051
+ }));
5052
+ }
5053
+ return createFeishuReplyDeliveryResult({
5054
+ results,
5055
+ visibleReplySent: results.length > 0,
5056
+ content: reply.text,
5057
+ kind: "text"
4170
5058
  });
4171
5059
  },
4172
5060
  onError: (err, info) => {
@@ -4332,18 +5220,18 @@ async function fetchDriveComments(params) {
4332
5220
  }
4333
5221
  async function requestFeishuOpenApi(params) {
4334
5222
  const formatErrorDetails = (error) => {
4335
- if (!isRecord$1(error)) return typeof error === "string" ? error : JSON.stringify(error);
4336
- const response = isRecord$1(error.response) ? error.response : void 0;
4337
- const responseData = isRecord$1(response?.data) ? response?.data : void 0;
5223
+ if (!isRecord$2(error)) return typeof error === "string" ? error : JSON.stringify(error);
5224
+ const response = isRecord$2(error.response) ? error.response : void 0;
5225
+ const responseData = isRecord$2(response?.data) ? response?.data : void 0;
4338
5226
  return safeJsonStringify({
4339
5227
  message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
4340
- code: readString(error.code),
4341
- method: readString(isRecord$1(error.config) ? error.config.method : void 0),
4342
- url: readString(isRecord$1(error.config) ? error.config.url : void 0),
5228
+ code: readString$1(error.code),
5229
+ method: readString$1(isRecord$2(error.config) ? error.config.method : void 0),
5230
+ url: readString$1(isRecord$2(error.config) ? error.config.url : void 0),
4343
5231
  http_status: typeof response?.status === "number" ? response.status : void 0,
4344
- feishu_code: typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
4345
- feishu_msg: readString(responseData?.msg),
4346
- feishu_log_id: readString(responseData?.log_id)
5232
+ feishu_code: typeof responseData?.code === "number" ? responseData.code : readString$1(responseData?.code),
5233
+ feishu_msg: readString$1(responseData?.msg),
5234
+ feishu_log_id: readString$1(responseData?.log_id)
4347
5235
  });
4348
5236
  };
4349
5237
  const result = await raceWithTimeoutAndAbort(params.client.request({
@@ -4399,7 +5287,7 @@ async function resolveCommentReplyContext(params) {
4399
5287
  createTime: typeof params.reply.create_time === "number" ? params.reply.create_time : void 0,
4400
5288
  isBotAuthored: typeof userId === "string" && normalizedBotOpenIds.has(userId),
4401
5289
  content: await resolveParsedCommentContent({
4402
- elements: isRecord$1(params.reply.content) ? params.reply.content.elements : void 0,
5290
+ elements: isRecord$2(params.reply.content) ? params.reply.content.elements : void 0,
4403
5291
  botOpenIds: params.botOpenIds,
4404
5292
  currentDocument: params.currentDocument,
4405
5293
  client: params.client,
@@ -4571,7 +5459,7 @@ async function fetchDriveCommentContext(params) {
4571
5459
  const rootWholeReply = comment.reply_list?.replies?.[0];
4572
5460
  const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeString(botId)).filter((botId) => Boolean(botId)));
4573
5461
  const content = await resolveParsedCommentContent({
4574
- elements: isRecord$1(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
5462
+ elements: isRecord$2(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
4575
5463
  botOpenIds: params.botOpenIds,
4576
5464
  currentDocument,
4577
5465
  client: params.client,
@@ -4678,16 +5566,19 @@ async function resolveDriveCommentEventCore(params) {
4678
5566
  logger?.(`feishu[${accountId}]: unsupported drive comment notice type ${noticeType}`);
4679
5567
  return null;
4680
5568
  }
4681
- if (!botOpenId) {
5569
+ const configuredBotOpenId = botOpenId?.trim();
5570
+ const eventRecipientBotOpenId = event.notice_meta?.to_user_id?.open_id?.trim();
5571
+ const effectiveBotOpenId = configuredBotOpenId || (event.is_mentioned === true ? eventRecipientBotOpenId : void 0);
5572
+ if (!effectiveBotOpenId) {
4682
5573
  logger?.(`feishu[${accountId}]: skipping drive comment notice because bot open_id is unavailable event=${eventId}`);
4683
5574
  return null;
4684
5575
  }
4685
- if (senderId === botOpenId) {
5576
+ if (senderId === effectiveBotOpenId) {
4686
5577
  logger?.(`feishu[${accountId}]: ignoring self-authored drive comment notice event=${eventId} sender=${senderId}`);
4687
5578
  return null;
4688
5579
  }
4689
5580
  const context = await fetchDriveCommentContext({
4690
- client: createClient ? createClient(account ?? { accountId }) : createFeishuClient((await import("./accounts-BDoGHJDk.js").then((n) => n.t)).resolveFeishuAccount({
5581
+ client: createClient ? createClient(account ?? { accountId }) : createFeishuClient((await import("./accounts-CKhCa76b.js").then((n) => n.t)).resolveFeishuAccount({
4691
5582
  cfg,
4692
5583
  accountId
4693
5584
  })),
@@ -4695,7 +5586,7 @@ async function resolveDriveCommentEventCore(params) {
4695
5586
  fileType,
4696
5587
  commentId,
4697
5588
  replyId,
4698
- botOpenIds: [botOpenId, event.notice_meta?.to_user_id?.open_id],
5589
+ botOpenIds: [effectiveBotOpenId, event.notice_meta?.to_user_id?.open_id],
4699
5590
  timeoutMs: verificationTimeoutMs,
4700
5591
  logger,
4701
5592
  accountId,
@@ -4717,32 +5608,32 @@ async function resolveDriveCommentEventCore(params) {
4717
5608
  };
4718
5609
  }
4719
5610
  function parseFeishuDriveCommentNoticeEventPayload(value) {
4720
- if (!isRecord$1(value) || !isRecord$1(value.notice_meta)) return null;
5611
+ if (!isRecord$2(value) || !isRecord$2(value.notice_meta)) return null;
4721
5612
  const noticeMeta = value.notice_meta;
4722
- const fromUserId = isRecord$1(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
4723
- const toUserId = isRecord$1(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
5613
+ const fromUserId = isRecord$2(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
5614
+ const toUserId = isRecord$2(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
4724
5615
  return {
4725
- comment_id: readString(value.comment_id),
4726
- event_id: readString(value.event_id),
5616
+ comment_id: readString$1(value.comment_id),
5617
+ event_id: readString$1(value.event_id),
4727
5618
  is_mentioned: asBoolean(value.is_mentioned),
4728
5619
  notice_meta: {
4729
- file_token: readString(noticeMeta.file_token),
4730
- file_type: readString(noticeMeta.file_type),
5620
+ file_token: readString$1(noticeMeta.file_token),
5621
+ file_type: readString$1(noticeMeta.file_type),
4731
5622
  from_user_id: fromUserId ? {
4732
- open_id: readString(fromUserId.open_id),
4733
- user_id: readString(fromUserId.user_id),
4734
- union_id: readString(fromUserId.union_id)
5623
+ open_id: readString$1(fromUserId.open_id),
5624
+ user_id: readString$1(fromUserId.user_id),
5625
+ union_id: readString$1(fromUserId.union_id)
4735
5626
  } : void 0,
4736
- notice_type: readString(noticeMeta.notice_type),
5627
+ notice_type: readString$1(noticeMeta.notice_type),
4737
5628
  to_user_id: toUserId ? {
4738
- open_id: readString(toUserId.open_id),
4739
- user_id: readString(toUserId.user_id),
4740
- union_id: readString(toUserId.union_id)
5629
+ open_id: readString$1(toUserId.open_id),
5630
+ user_id: readString$1(toUserId.user_id),
5631
+ union_id: readString$1(toUserId.union_id)
4741
5632
  } : void 0
4742
5633
  },
4743
- reply_id: readString(value.reply_id),
4744
- timestamp: readString(value.timestamp),
4745
- type: readString(value.type)
5634
+ reply_id: readString$1(value.reply_id),
5635
+ timestamp: readString$1(value.timestamp),
5636
+ type: readString$1(value.type)
4746
5637
  };
4747
5638
  }
4748
5639
  async function resolveDriveCommentEventTurn(params) {
@@ -5034,7 +5925,8 @@ async function handleFeishuCommentEvent(params) {
5034
5925
  error(`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`);
5035
5926
  } },
5036
5927
  dispatcherOptions,
5037
- delivery
5928
+ delivery,
5929
+ ...params.turnAdoptionLifecycle ? { replyOptions: bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) } : {}
5038
5930
  })
5039
5931
  }
5040
5932
  });
@@ -5121,52 +6013,40 @@ function createFeishuDriveCommentNoticeHandler(params) {
5121
6013
  });
5122
6014
  if (!fireAndForget) await promise;
5123
6015
  };
5124
- return async (data) => {
5125
- await runFeishuHandler(async () => {
5126
- const event = parseFeishuDriveCommentNoticeEventPayload(data);
5127
- if (!event) {
5128
- error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
6016
+ const handleNotice = async (data, turnAdoptionLifecycle) => {
6017
+ const event = parseFeishuDriveCommentNoticeEventPayload(data);
6018
+ if (!event) {
6019
+ if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable comment event payload is malformed.");
6020
+ error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
6021
+ return;
6022
+ }
6023
+ log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
6024
+ await enqueue(buildCommentNoticeQueueKey(event), async () => {
6025
+ if (turnAdoptionLifecycle?.abortSignal.aborted) {
6026
+ await turnAdoptionLifecycle.onAbandoned();
5129
6027
  return;
5130
6028
  }
5131
- const eventId = event.event_id?.trim();
5132
- const syntheticMessageId = eventId ? `drive-comment:${eventId}` : void 0;
5133
- let processingClaim;
5134
- if (syntheticMessageId) {
5135
- const claim = await claimUnprocessedFeishuMessage({
5136
- messageId: syntheticMessageId,
5137
- namespace: accountId,
5138
- log
5139
- });
5140
- if (claim.kind === "duplicate") {
5141
- log(`feishu[${accountId}]: dropping duplicate comment event ${syntheticMessageId}`);
5142
- return;
5143
- }
5144
- if (claim.kind === "inflight") {
5145
- log(`feishu[${accountId}]: dropping in-flight comment event ${syntheticMessageId}`);
5146
- return;
5147
- }
5148
- processingClaim = claim.kind === "claimed" ? claim.handle : void 0;
5149
- }
5150
- log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
5151
- try {
5152
- await enqueue(buildCommentNoticeQueueKey(event), async () => {
5153
- await handleFeishuCommentEvent({
5154
- cfg,
5155
- accountId,
5156
- event,
5157
- botOpenId: getBotOpenId(accountId),
5158
- runtime,
5159
- abortSignal
5160
- });
5161
- });
5162
- await processingClaim?.commit();
5163
- } catch (err) {
5164
- if (isFeishuRetryableSyntheticEventError(err)) processingClaim?.release({ error: err });
5165
- else await processingClaim?.commit();
5166
- throw err;
5167
- }
6029
+ await handleFeishuCommentEvent({
6030
+ cfg,
6031
+ accountId,
6032
+ event,
6033
+ botOpenId: getBotOpenId(accountId),
6034
+ runtime,
6035
+ abortSignal,
6036
+ turnAdoptionLifecycle
6037
+ });
5168
6038
  });
5169
6039
  };
6040
+ return async (data) => {
6041
+ const ingressLifecycle = params.resolveIngressLifecycle?.(data);
6042
+ if (!ingressLifecycle) {
6043
+ await runFeishuHandler(async () => await handleNotice(data));
6044
+ return;
6045
+ }
6046
+ const { lifecycle, settle } = buildFeishuFlushIngressLifecycle([{ lifecycle: ingressLifecycle }]);
6047
+ await handleNotice(data, lifecycle);
6048
+ await settle();
6049
+ };
5170
6050
  }
5171
6051
  //#endregion
5172
6052
  //#region extensions/feishu/src/monitor.message-handler.ts
@@ -5227,7 +6107,7 @@ function resolveFeishuDebounceMentions(params) {
5227
6107
  const botMentions = merged.filter((mention) => mention.id.open_id?.trim() === normalizedBotOpenId);
5228
6108
  return botMentions.length > 0 ? botMentions : void 0;
5229
6109
  }
5230
- function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink }) {
6110
+ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink, resolveIngressLifecycle }) {
5231
6111
  const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
5232
6112
  cfg,
5233
6113
  channel: "feishu"
@@ -5237,25 +6117,32 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5237
6117
  const enqueue = createSequentialQueue({ onTaskTimeout: (key, timeoutMs) => {
5238
6118
  log(`feishu[${accountId}]: per-chat task exceeded ${timeoutMs}ms cap (key=${key}); evicting from queue so later same-key messages can proceed (#70133)`);
5239
6119
  } });
5240
- const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim) => {
6120
+ const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim, turnAdoptionLifecycle) => {
5241
6121
  const sequentialKey = resolveSequentialKey({
5242
6122
  accountId,
5243
6123
  event,
5244
6124
  botOpenId: getBotOpenId(accountId),
5245
6125
  botName: getBotName(accountId)
5246
6126
  });
5247
- const task = () => handleMessage({
5248
- cfg,
5249
- event,
5250
- botOpenId: getBotOpenId(accountId),
5251
- botName: getBotName(accountId),
5252
- runtime,
5253
- channelRuntime,
5254
- chatHistories,
5255
- accountId,
5256
- processingClaim,
5257
- messageDedupeKey
5258
- });
6127
+ const task = async () => {
6128
+ if (turnAdoptionLifecycle?.abortSignal.aborted) {
6129
+ await turnAdoptionLifecycle.onAbandoned();
6130
+ return;
6131
+ }
6132
+ await handleMessage({
6133
+ cfg,
6134
+ event,
6135
+ botOpenId: getBotOpenId(accountId),
6136
+ botName: getBotName(accountId),
6137
+ runtime,
6138
+ channelRuntime,
6139
+ chatHistories,
6140
+ accountId,
6141
+ processingClaim,
6142
+ messageDedupeKey,
6143
+ turnAdoptionLifecycle
6144
+ });
6145
+ };
5259
6146
  await enqueue(sequentialKey, task);
5260
6147
  };
5261
6148
  const resolveSenderDebounceId = (event) => {
@@ -5295,45 +6182,80 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5295
6182
  return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
5296
6183
  },
5297
6184
  onFlush: async (entries) => {
5298
- const last = entries.at(-1);
6185
+ const activeEntries = entries.filter((entry) => !entry.abandoned);
6186
+ const last = activeEntries.at(-1);
5299
6187
  if (!last) return;
5300
- if (entries.length === 1) {
5301
- await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim);
6188
+ const { lifecycle, settle } = buildFeishuFlushIngressLifecycle(activeEntries.map((entry) => ({
6189
+ lifecycle: entry.turnAdoptionLifecycle,
6190
+ replayClaim: entry.processingClaim
6191
+ })), { onReplayCommitError: (err) => error(`feishu[${accountId}]: failed to commit logical replay guard: ${String(err)}`) });
6192
+ if (lifecycle?.abortSignal.aborted) {
6193
+ await lifecycle.onAbandoned();
5302
6194
  return;
5303
6195
  }
5304
- const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(entries);
5305
- const freshEntries = [];
5306
- for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
5307
- const dispatchEntry = freshEntries.at(-1);
5308
- if (!dispatchEntry) return;
5309
- const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
5310
- await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
5311
- const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
5312
- const mergedMentions = resolveFeishuDebounceMentions({
5313
- entries: freshEntries.map((entry) => entry.event),
5314
- botOpenId: getBotOpenId(accountId)
5315
- });
5316
- await dispatchFeishuMessage({
5317
- ...dispatchEntry.event,
5318
- message: {
5319
- ...dispatchEntry.event.message,
5320
- ...combinedText.trim() ? {
5321
- message_type: "text",
5322
- content: JSON.stringify({ text: combinedText })
5323
- } : {},
5324
- mentions: mergedMentions ?? dispatchEntry.event.message.mentions
6196
+ try {
6197
+ if (activeEntries.length === 1) {
6198
+ await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim, lifecycle);
6199
+ await settle();
6200
+ return;
6201
+ }
6202
+ const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(activeEntries);
6203
+ const freshEntries = [];
6204
+ for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
6205
+ const dispatchEntry = freshEntries.at(-1);
6206
+ if (!dispatchEntry) {
6207
+ await settle();
6208
+ return;
5325
6209
  }
5326
- }, dispatchDedupeKey, dispatchEntry.processingClaim);
6210
+ const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
6211
+ if (!lifecycle) await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
6212
+ const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
6213
+ const mergedMentions = resolveFeishuDebounceMentions({
6214
+ entries: freshEntries.map((entry) => entry.event),
6215
+ botOpenId: getBotOpenId(accountId)
6216
+ });
6217
+ await dispatchFeishuMessage({
6218
+ ...dispatchEntry.event,
6219
+ message: {
6220
+ ...dispatchEntry.event.message,
6221
+ ...combinedText.trim() ? {
6222
+ message_type: "text",
6223
+ content: JSON.stringify({ text: combinedText })
6224
+ } : {},
6225
+ mentions: mergedMentions ?? dispatchEntry.event.message.mentions
6226
+ }
6227
+ }, dispatchDedupeKey, dispatchEntry.processingClaim, lifecycle);
6228
+ await settle();
6229
+ } catch (err) {
6230
+ await lifecycle?.onAbandoned();
6231
+ throw err;
6232
+ }
5327
6233
  },
5328
6234
  onError: (err, entries) => {
5329
- for (const entry of entries) entry.processingClaim?.release({ error: err });
6235
+ for (const entry of entries) {
6236
+ entry.processingClaim?.release({ error: err });
6237
+ try {
6238
+ Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned()).catch((abandonError) => {
6239
+ error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
6240
+ });
6241
+ } catch (abandonError) {
6242
+ error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
6243
+ }
6244
+ }
5330
6245
  error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
5331
6246
  }
5332
6247
  });
5333
6248
  return async (data) => {
6249
+ const turnAdoptionLifecycle = resolveIngressLifecycle?.(data);
6250
+ const completeSuppressedIngress = async () => {
6251
+ if (!turnAdoptionLifecycle) return;
6252
+ turnAdoptionLifecycle.onAdoptionFinalizing();
6253
+ await turnAdoptionLifecycle.onAdopted();
6254
+ };
5334
6255
  statusSink?.({ lastEventAt: Date.now() });
5335
6256
  const event = parseFeishuMessageEventPayload(data);
5336
6257
  if (!event) {
6258
+ if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable message event payload is malformed.");
5337
6259
  error(`feishu[${accountId}]: ignoring malformed message event payload`);
5338
6260
  return;
5339
6261
  }
@@ -5342,6 +6264,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5342
6264
  const senderOpenId = event.sender.sender_id.open_id?.trim();
5343
6265
  if (botOpenId && senderOpenId === botOpenId) {
5344
6266
  log(`feishu[${accountId}]: dropping self-authored message ${messageId ?? "unknown"}`);
6267
+ await completeSuppressedIngress();
5345
6268
  return;
5346
6269
  }
5347
6270
  const claim = await claimUnprocessedFeishuMessage({
@@ -5351,14 +6274,31 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5351
6274
  });
5352
6275
  if (claim.kind === "duplicate" || claim.kind === "inflight") {
5353
6276
  log(`feishu[${accountId}]: dropping ${claim.kind} event for message ${messageId}`);
6277
+ await completeSuppressedIngress();
5354
6278
  return;
5355
6279
  }
6280
+ const debounceEntry = {
6281
+ event,
6282
+ ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {},
6283
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {}
6284
+ };
6285
+ if (claim.kind === "claimed" && turnAdoptionLifecycle) turnAdoptionLifecycle.registerAbandonHandler?.(() => {
6286
+ debounceEntry.abandoned = true;
6287
+ claim.handle.release({ error: /* @__PURE__ */ new Error("feishu-ingress-abandoned-before-flush") });
6288
+ });
5356
6289
  const processMessage = async () => {
5357
- await inboundDebouncer.enqueue({
5358
- event,
5359
- ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {}
5360
- });
6290
+ await inboundDebouncer.enqueue(debounceEntry);
5361
6291
  };
6292
+ if (turnAdoptionLifecycle) try {
6293
+ await processMessage();
6294
+ return { kind: "deferred" };
6295
+ } catch (err) {
6296
+ if (claim.kind === "claimed") claim.handle.release({ error: err });
6297
+ return {
6298
+ kind: "failed-retryable",
6299
+ error: err
6300
+ };
6301
+ }
5362
6302
  if (fireAndForget) {
5363
6303
  processMessage().catch((err) => {
5364
6304
  if (claim.kind === "claimed") claim.handle.release({ error: err });
@@ -5468,7 +6408,7 @@ function waitForFeishuWsCycleEnd(params) {
5468
6408
  params.terminalError.then(finish);
5469
6409
  });
5470
6410
  }
5471
- async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
6411
+ async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, setSocketTerminator, statusSink }) {
5472
6412
  const log = runtime?.log ?? console.log;
5473
6413
  const error = runtime?.error ?? console.error;
5474
6414
  let attempt = 0;
@@ -5509,6 +6449,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5509
6449
  onReconnected: publishWsConnected,
5510
6450
  onReconnecting: publishWsReconnecting
5511
6451
  });
6452
+ setSocketTerminator?.(() => wsClient?.close({ force: true }));
5512
6453
  if (abortSignal?.aborted) {
5513
6454
  cleanupFeishuWsClient({
5514
6455
  accountId,
@@ -5534,6 +6475,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5534
6475
  error,
5535
6476
  clearIdentity: true
5536
6477
  });
6478
+ setSocketTerminator?.(void 0);
5537
6479
  return;
5538
6480
  }
5539
6481
  cleanupFeishuWsClient({
@@ -5542,6 +6484,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5542
6484
  error,
5543
6485
  clearIdentity: false
5544
6486
  });
6487
+ setSocketTerminator?.(void 0);
5545
6488
  if (abortSignal?.aborted) break;
5546
6489
  statusSink?.({
5547
6490
  connected: false,
@@ -5558,6 +6501,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5558
6501
  error,
5559
6502
  clearIdentity: false
5560
6503
  });
6504
+ setSocketTerminator?.(void 0);
5561
6505
  if (abortSignal?.aborted) break;
5562
6506
  statusSink?.({
5563
6507
  connected: false,
@@ -5575,6 +6519,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5575
6519
  error,
5576
6520
  clearIdentity: true
5577
6521
  });
6522
+ setSocketTerminator?.(void 0);
5578
6523
  }
5579
6524
  async function monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
5580
6525
  const log = runtime?.log ?? console.log;
@@ -5829,7 +6774,7 @@ async function resolveReactionSyntheticEvent(params) {
5829
6774
  const senderId = event.user_id?.open_id;
5830
6775
  const senderUserId = event.user_id?.user_id;
5831
6776
  if (!emoji || !messageId || !senderId) return null;
5832
- const { resolveFeishuAccount } = await import("./accounts-BDoGHJDk.js").then((n) => n.t);
6777
+ const { resolveFeishuAccount } = await import("./accounts-CKhCa76b.js").then((n) => n.t);
5833
6778
  const reactionNotifications = resolveFeishuAccount({
5834
6779
  cfg,
5835
6780
  accountId
@@ -5882,41 +6827,41 @@ function normalizeFeishuChatType(value) {
5882
6827
  return value === "group" || value === "topic_group" || value === "private" || value === "p2p" ? value : void 0;
5883
6828
  }
5884
6829
  function parseFeishuBotAddedEventPayload(value) {
5885
- if (!isRecord$1(value) || !readString(value.chat_id) || !isRecord$1(value.operator_id)) return null;
6830
+ if (!isRecord$2(value) || !readString$1(value.chat_id) || !isRecord$2(value.operator_id)) return null;
5886
6831
  return value;
5887
6832
  }
5888
6833
  function parseFeishuBotRemovedChatId(value) {
5889
- if (!isRecord$1(value)) return null;
5890
- return readString(value.chat_id) ?? null;
6834
+ if (!isRecord$2(value)) return null;
6835
+ return readString$1(value.chat_id) ?? null;
5891
6836
  }
5892
6837
  function firstString(...values) {
5893
6838
  for (const value of values) {
5894
- const trimmed = readString(value)?.trim();
6839
+ const trimmed = readString$1(value)?.trim();
5895
6840
  if (trimmed) return trimmed;
5896
6841
  }
5897
6842
  }
5898
6843
  function readFeishuIdentityField(value, field) {
5899
- if (!isRecord$1(value)) return;
6844
+ if (!isRecord$2(value)) return;
5900
6845
  return firstString(value[field]);
5901
6846
  }
5902
6847
  function parseFeishuCardActionEventPayload(value) {
5903
- if (!isRecord$1(value)) return null;
5904
- const operator = isRecord$1(value.operator) ? value.operator : {};
6848
+ if (!isRecord$2(value)) return null;
6849
+ const operator = isRecord$2(value.operator) ? value.operator : {};
5905
6850
  const action = value.action;
5906
- const context = isRecord$1(value.context) ? value.context : {};
5907
- if (!isRecord$1(action)) return null;
6851
+ const context = isRecord$2(value.context) ? value.context : {};
6852
+ if (!isRecord$2(action)) return null;
5908
6853
  const operatorUserId = operator.user_id;
5909
- const token = readString(value.token);
6854
+ const token = readString$1(value.token);
5910
6855
  const openId = firstString(operator.open_id, readFeishuIdentityField(operatorUserId, "open_id"), value.open_id, context.open_id);
5911
6856
  const userId = firstString(operator.user_id, readFeishuIdentityField(operatorUserId, "user_id"), value.user_id, context.user_id);
5912
6857
  const unionId = firstString(operator.union_id, readFeishuIdentityField(operatorUserId, "union_id"));
5913
- const tag = readString(action.tag);
6858
+ const tag = readString$1(action.tag);
5914
6859
  const actionValue = action.value;
5915
6860
  const openMessageId = firstString(context.open_message_id, value.open_message_id);
5916
6861
  const contextOpenId = firstString(context.open_id, openId);
5917
6862
  const contextUserId = firstString(context.user_id, userId);
5918
6863
  const chatId = firstString(context.chat_id, context.open_chat_id);
5919
- if (!token || !openId || !tag || !isRecord$1(actionValue)) return null;
6864
+ if (!token || !openId || !tag || !isRecord$2(actionValue)) return null;
5920
6865
  return {
5921
6866
  operator: {
5922
6867
  open_id: openId,
@@ -5968,6 +6913,7 @@ function registerEventHandlers(eventDispatcher, context) {
5968
6913
  getBotOpenId: (id) => botOpenIds.get(id),
5969
6914
  getBotName: (id) => botNames.get(id),
5970
6915
  resolveSequentialKey: getFeishuSequentialKey,
6916
+ resolveIngressLifecycle: context.resolveIngressLifecycle,
5971
6917
  ...context.statusSink ? { statusSink: context.statusSink } : {}
5972
6918
  }),
5973
6919
  "im.message.message_read_v1": async () => {},
@@ -5995,7 +6941,8 @@ function registerEventHandlers(eventDispatcher, context) {
5995
6941
  accountId,
5996
6942
  runtime,
5997
6943
  fireAndForget,
5998
- abortSignal
6944
+ abortSignal,
6945
+ resolveIngressLifecycle: context.resolveIngressLifecycle
5999
6946
  }),
6000
6947
  "vc.bot.meeting_invited_v1": createFeishuVcMeetingInvitedHandler({
6001
6948
  cfg,
@@ -6098,19 +7045,22 @@ async function monitorSingleAccount(params) {
6098
7045
  const { accountId } = account;
6099
7046
  const log = runtime?.log ?? console.log;
6100
7047
  const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
6101
- const { botOpenId } = applyBotIdentityState(accountId, botOpenIdSource.kind === "prefetched" ? {
7048
+ const botIdentity = botOpenIdSource.kind === "prefetched" ? {
6102
7049
  botOpenId: botOpenIdSource.botOpenId,
6103
- botName: botOpenIdSource.botName
7050
+ botName: botOpenIdSource.botName,
7051
+ source: botOpenIdSource.source
6104
7052
  } : await fetchBotIdentityForMonitor(account, {
6105
7053
  runtime,
6106
7054
  abortSignal
6107
- }));
7055
+ });
7056
+ const { botOpenId } = applyBotIdentityState(accountId, botIdentity);
6108
7057
  log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
6109
- if (!botOpenId && !abortSignal?.aborted) startBotIdentityRecovery({
7058
+ if ((!botOpenId || botIdentity.source === "cache") && !abortSignal?.aborted) startBotIdentityRecovery({
6110
7059
  account,
6111
7060
  accountId,
6112
7061
  runtime,
6113
- abortSignal
7062
+ abortSignal,
7063
+ currentSource: botIdentity.source
6114
7064
  });
6115
7065
  const connectionMode = account.config.connectionMode ?? "websocket";
6116
7066
  if (connectionMode === "webhook" && !account.verificationToken?.trim()) throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
@@ -6120,6 +7070,13 @@ async function monitorSingleAccount(params) {
6120
7070
  let threadBindingManager;
6121
7071
  try {
6122
7072
  const eventDispatcher = createEventDispatcher(account);
7073
+ const durableIngress = typeof eventDispatcher.invoke === "function" ? createFeishuDurableIngress({
7074
+ accountId,
7075
+ dispatcher: eventDispatcher,
7076
+ ...account.encryptKey ? { encryptKey: account.encryptKey } : {},
7077
+ runtime: runtime ?? {}
7078
+ }) : void 0;
7079
+ const durableEventDispatcher = durableIngress ? Object.assign(Object.create(eventDispatcher), { invoke: durableIngress.invoke }) : eventDispatcher;
6123
7080
  const chatHistories = /* @__PURE__ */ new Map();
6124
7081
  threadBindingManager = createFeishuThreadBindingManager({
6125
7082
  accountId,
@@ -6134,24 +7091,31 @@ async function monitorSingleAccount(params) {
6134
7091
  fireAndForget: params.fireAndForget ?? true,
6135
7092
  vcAutoJoin: account.config.vcAutoJoin === true,
6136
7093
  abortSignal,
7094
+ ...durableIngress ? { resolveIngressLifecycle: durableIngress.resolveLifecycle } : {},
6137
7095
  ...params.statusSink ? { statusSink: params.statusSink } : {}
6138
7096
  });
6139
- if (connectionMode === "webhook") return await monitorWebhook({
6140
- account,
6141
- accountId,
6142
- runtime,
6143
- abortSignal,
6144
- eventDispatcher,
6145
- ...params.statusSink ? { statusSink: params.statusSink } : {}
6146
- });
6147
- return await monitorWebSocket({
6148
- account,
6149
- accountId,
6150
- runtime,
6151
- abortSignal,
6152
- eventDispatcher,
6153
- ...params.statusSink ? { statusSink: params.statusSink } : {}
6154
- });
7097
+ durableIngress?.start();
7098
+ try {
7099
+ if (connectionMode === "webhook") return await monitorWebhook({
7100
+ account,
7101
+ accountId,
7102
+ runtime,
7103
+ abortSignal,
7104
+ eventDispatcher: durableEventDispatcher,
7105
+ ...params.statusSink ? { statusSink: params.statusSink } : {}
7106
+ });
7107
+ return await monitorWebSocket({
7108
+ account,
7109
+ accountId,
7110
+ runtime,
7111
+ abortSignal,
7112
+ eventDispatcher: durableEventDispatcher,
7113
+ ...durableIngress ? { setSocketTerminator: durableIngress.setSocketTerminator } : {},
7114
+ ...params.statusSink ? { statusSink: params.statusSink } : {}
7115
+ });
7116
+ } finally {
7117
+ await durableIngress?.stop();
7118
+ }
6155
7119
  } finally {
6156
7120
  threadBindingManager?.stop();
6157
7121
  }