@openclaw/feishu 2026.7.2-beta.2 → 2026.7.2-beta.4

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 (149) hide show
  1. package/dist/api.js +4 -4
  2. package/dist/{app-registration-BzkyrVZu.js → app-registration-cN1bVV6s.js} +5 -5
  3. package/dist/{channel-X30GQVwc.js → channel-DB1x6UJX.js} +33 -24
  4. package/dist/channel-plugin-api.js +1 -1
  5. package/dist/{channel.runtime-By2uqa3Q.js → channel.runtime-B5yAAvbT.js} +102 -17
  6. package/dist/{client-87BmeMpj.js → client-Dee7cKsS.js} +62 -1
  7. package/dist/{doctor-contract-DXH5hux1.js → doctor-contract-Cw5sdjOm.js} +29 -41
  8. package/dist/doctor-contract-api.js +1 -1
  9. package/dist/{drive-B4E8OXZ0.js → drive-_zrcOxzS.js} +2 -2
  10. package/dist/{send-DrWM_Ilf.js → media-CRJunSRr.js} +1075 -1017
  11. package/dist/{monitor-CHl0bWzb.js → monitor-8CLu6myv.js} +5 -4
  12. package/dist/{monitor.account-BSRV2hIF.js → monitor.account-CkYj45PC.js} +2113 -809
  13. package/dist/monitor.startup-CSUPT_U1.js +143 -0
  14. package/dist/{probe-BgboTHIn.js → probe-Za1kgUvx.js} +70 -10
  15. package/dist/{secret-contract-DnlMcrn5.js → secret-contract-BCpDLdg9.js} +17 -26
  16. package/dist/secret-contract-api.js +1 -1
  17. package/dist/{send-result-DsqTk27v.js → send-result-DfE5Fhz6.js} +22 -11
  18. package/dist/setup-api.js +1 -1
  19. package/node_modules/@larksuiteoapi/node-sdk/README.md +13 -0
  20. package/node_modules/@larksuiteoapi/node-sdk/README.zh.md +12 -0
  21. package/node_modules/@larksuiteoapi/node-sdk/es/index.js +8950 -3084
  22. package/node_modules/@larksuiteoapi/node-sdk/lib/index.js +8951 -3083
  23. package/node_modules/@larksuiteoapi/node-sdk/package.json +1 -1
  24. package/node_modules/@larksuiteoapi/node-sdk/types/index.d.ts +12522 -698
  25. package/node_modules/@protobufjs/utf8/CHANGELOG.md +8 -0
  26. package/node_modules/@protobufjs/utf8/index.js +29 -18
  27. package/node_modules/@protobufjs/utf8/package.json +2 -2
  28. package/node_modules/@protobufjs/utf8/tests/index.js +5 -0
  29. package/node_modules/@types/node/README.md +1 -1
  30. package/node_modules/@types/node/fs/promises.d.ts +16 -6
  31. package/node_modules/@types/node/package.json +2 -2
  32. package/node_modules/agent-base/README.md +145 -0
  33. package/node_modules/agent-base/dist/src/index.d.ts +78 -0
  34. package/node_modules/agent-base/dist/src/index.js +203 -0
  35. package/node_modules/agent-base/dist/src/index.js.map +1 -0
  36. package/node_modules/agent-base/dist/src/promisify.d.ts +4 -0
  37. package/node_modules/agent-base/dist/src/promisify.js +18 -0
  38. package/node_modules/agent-base/dist/src/promisify.js.map +1 -0
  39. package/node_modules/agent-base/package.json +64 -0
  40. package/node_modules/agent-base/src/index.ts +345 -0
  41. package/node_modules/agent-base/src/promisify.ts +33 -0
  42. package/node_modules/axios/CHANGELOG.md +197 -1
  43. package/node_modules/axios/README.md +424 -256
  44. package/node_modules/axios/dist/axios.js +589 -204
  45. package/node_modules/axios/dist/axios.min.js +3 -3
  46. package/node_modules/axios/dist/axios.min.js.map +1 -1
  47. package/node_modules/axios/dist/browser/axios.cjs +630 -185
  48. package/node_modules/axios/dist/esm/axios.js +630 -185
  49. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  50. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  51. package/node_modules/axios/dist/node/axios.cjs +949 -301
  52. package/node_modules/axios/index.d.cts +28 -5
  53. package/node_modules/axios/index.d.ts +24 -3
  54. package/node_modules/axios/lib/adapters/adapters.js +1 -1
  55. package/node_modules/axios/lib/adapters/fetch.js +223 -49
  56. package/node_modules/axios/lib/adapters/http.js +408 -193
  57. package/node_modules/axios/lib/adapters/xhr.js +3 -1
  58. package/node_modules/axios/lib/core/Axios.js +4 -2
  59. package/node_modules/axios/lib/core/AxiosError.js +13 -1
  60. package/node_modules/axios/lib/core/AxiosHeaders.js +13 -42
  61. package/node_modules/axios/lib/core/buildFullPath.js +29 -1
  62. package/node_modules/axios/lib/core/mergeConfig.js +35 -0
  63. package/node_modules/axios/lib/defaults/transitional.js +2 -0
  64. package/node_modules/axios/lib/env/data.js +1 -1
  65. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -3
  66. package/node_modules/axios/lib/helpers/Http2Sessions.js +119 -0
  67. package/node_modules/axios/lib/helpers/buildURL.js +7 -4
  68. package/node_modules/axios/lib/helpers/composeSignals.js +48 -47
  69. package/node_modules/axios/lib/helpers/cookies.js +5 -1
  70. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +16 -11
  71. package/node_modules/axios/lib/helpers/formDataToJSON.js +26 -4
  72. package/node_modules/axios/lib/helpers/formDataToStream.js +2 -2
  73. package/node_modules/axios/lib/helpers/fromDataURI.js +20 -5
  74. package/node_modules/axios/lib/helpers/progressEventReducer.js +3 -0
  75. package/node_modules/axios/lib/helpers/resolveConfig.js +26 -13
  76. package/node_modules/axios/lib/helpers/sanitizeHeaderValue.js +60 -0
  77. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +33 -1
  78. package/node_modules/axios/lib/helpers/toFormData.js +48 -12
  79. package/node_modules/axios/lib/helpers/validator.js +1 -1
  80. package/node_modules/axios/lib/utils.js +105 -19
  81. package/node_modules/axios/package.json +31 -13
  82. package/node_modules/https-proxy-agent/README.md +137 -0
  83. package/node_modules/https-proxy-agent/dist/agent.d.ts +30 -0
  84. package/node_modules/https-proxy-agent/dist/agent.js +177 -0
  85. package/node_modules/https-proxy-agent/dist/agent.js.map +1 -0
  86. package/node_modules/https-proxy-agent/dist/index.d.ts +23 -0
  87. package/node_modules/https-proxy-agent/dist/index.js +14 -0
  88. package/node_modules/https-proxy-agent/dist/index.js.map +1 -0
  89. package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts +7 -0
  90. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js +66 -0
  91. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map +1 -0
  92. package/node_modules/https-proxy-agent/package.json +56 -0
  93. package/node_modules/protobufjs/dist/light/protobuf.js +145 -188
  94. package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
  95. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  96. package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
  97. package/node_modules/protobufjs/dist/minimal/protobuf.js +33 -76
  98. package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
  99. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  100. package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
  101. package/node_modules/protobufjs/dist/protobuf.js +168 -208
  102. package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
  103. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  104. package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
  105. package/node_modules/protobufjs/index.d.ts +0 -8
  106. package/node_modules/protobufjs/package.json +2 -3
  107. package/node_modules/protobufjs/src/parse.js +3 -0
  108. package/node_modules/protobufjs/src/util/minimal.js +0 -3
  109. package/node_modules/qs/CHANGELOG.md +11 -0
  110. package/node_modules/qs/dist/qs.js +17 -17
  111. package/node_modules/qs/lib/parse.js +19 -8
  112. package/node_modules/qs/lib/utils.js +47 -8
  113. package/node_modules/qs/package.json +6 -5
  114. package/node_modules/qs/test/parse.js +230 -0
  115. package/node_modules/qs/test/utils.js +194 -0
  116. package/node_modules/typebox/build/compile/validator.d.mts +1 -4
  117. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  118. package/node_modules/typebox/build/guard/guard.mjs +5 -2
  119. package/node_modules/typebox/build/guard/string.mjs +16 -4
  120. package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
  121. package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
  122. package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
  123. package/node_modules/typebox/build/typebox.d.mts +1 -1
  124. package/node_modules/typebox/build/typebox.mjs +1 -1
  125. package/node_modules/typebox/package.json +1 -1
  126. package/node_modules/typebox/readme.md +29 -32
  127. package/node_modules/ws/lib/receiver.js +15 -32
  128. package/node_modules/ws/lib/websocket-server.js +4 -4
  129. package/node_modules/ws/lib/websocket.js +4 -4
  130. package/node_modules/ws/package.json +5 -1
  131. package/npm-shrinkwrap.json +58 -38
  132. package/openclaw.plugin.json +26 -10
  133. package/package.json +20 -7
  134. package/dist/monitor.startup-Cy8NPGAX.js +0 -43
  135. package/node_modules/@protobufjs/inquire/CHANGELOG.md +0 -8
  136. package/node_modules/@protobufjs/inquire/LICENSE +0 -26
  137. package/node_modules/@protobufjs/inquire/README.md +0 -13
  138. package/node_modules/@protobufjs/inquire/index.d.ts +0 -10
  139. package/node_modules/@protobufjs/inquire/index.js +0 -38
  140. package/node_modules/@protobufjs/inquire/package.json +0 -21
  141. package/node_modules/@protobufjs/inquire/tests/data/array.js +0 -1
  142. package/node_modules/@protobufjs/inquire/tests/data/emptyArray.js +0 -1
  143. package/node_modules/@protobufjs/inquire/tests/data/emptyObject.js +0 -1
  144. package/node_modules/@protobufjs/inquire/tests/data/object.js +0 -1
  145. package/node_modules/@protobufjs/inquire/tests/index.js +0 -20
  146. package/node_modules/axios/dist/axios.js.map +0 -1
  147. package/node_modules/axios/dist/browser/axios.cjs.map +0 -1
  148. package/node_modules/axios/dist/esm/axios.js.map +0 -1
  149. package/node_modules/axios/dist/node/axios.cjs.map +0 -1
@@ -1,17 +1,17 @@
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-BDoGHJDk.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-DsqTk27v.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";
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-B4E8OXZ0.js";
7
+ import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-_zrcOxzS.js";
8
8
  import { t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.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 resolveFeishuIdentityEmoji, 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 } from "./send-DrWM_Ilf.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-CRJunSRr.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";
@@ -22,21 +22,24 @@ import fs from "node:fs";
22
22
  import os from "node:os";
23
23
  import path from "node:path";
24
24
  import { normalizeAccountId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
25
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
25
+ import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
26
+ import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
26
27
  import * as Lark from "@larksuiteoapi/node-sdk";
27
28
  import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
28
29
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
29
- import { resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
30
+ import { getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
30
31
  import { resolvePinnedMainDmOwnerFromAllowlist, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
31
32
  import * as crypto$1 from "node:crypto";
32
33
  import crypto, { createHash } from "node:crypto";
33
- import { buildChannelInboundEventContext, formatInboundMediaUnavailableText, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
34
+ import { buildChannelInboundEventContext, createChannelPartialDeliveryError, formatAgentEnvelope, formatInboundMediaUnavailableText, isChannelPartialDeliveryError, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
34
35
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
35
36
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
37
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
36
38
  import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
37
39
  import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
38
- import { formatReasoningMessage } from "openclaw/plugin-sdk/agent-runtime";
40
+ import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
39
41
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
42
+ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
40
43
  import * as http from "node:http";
41
44
  import { installRequestBodyLimitGuard, readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
42
45
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
@@ -72,6 +75,167 @@ function buildFeishuAgentBody(params) {
72
75
  }
73
76
  //#endregion
74
77
  //#region extensions/feishu/src/bot-broadcast.ts
78
+ function createFeishuBroadcastIngressSettlement(params) {
79
+ const lanes = /* @__PURE__ */ new Set();
80
+ const failures = [];
81
+ const fallbackAbortSignal = new AbortController().signal;
82
+ let fanoutSettled = false;
83
+ let terminal;
84
+ let adoption;
85
+ let abandonment;
86
+ let finalizing = false;
87
+ let deferred = false;
88
+ let replayReleased = false;
89
+ const beginFinalizing = () => {
90
+ if (finalizing) return;
91
+ finalizing = true;
92
+ params.lifecycle?.onAdoptionFinalizing();
93
+ };
94
+ const defer = () => {
95
+ if (deferred) return;
96
+ deferred = true;
97
+ params.lifecycle?.onDeferred();
98
+ };
99
+ const reportReplayCommitError = (error) => {
100
+ try {
101
+ params.onReplayCommitError?.(error);
102
+ } catch {}
103
+ };
104
+ const releaseReplayClaim = (error) => {
105
+ if (replayReleased || terminal === "adopted") return;
106
+ replayReleased = true;
107
+ params.replayClaim?.release({ error });
108
+ };
109
+ const runAbandonment = async (error) => {
110
+ if (terminal) return;
111
+ releaseReplayClaim(error);
112
+ try {
113
+ await params.lifecycle?.onAbandoned();
114
+ } finally {
115
+ terminal = "abandoned";
116
+ }
117
+ };
118
+ const abandon = async (error) => {
119
+ if (terminal) return;
120
+ if (adoption) {
121
+ await adoption.catch(() => void 0);
122
+ if (terminal) return;
123
+ }
124
+ const activeAbandonment = abandonment ?? runAbandonment(error);
125
+ abandonment = activeAbandonment;
126
+ await activeAbandonment;
127
+ };
128
+ const runAdoption = async () => {
129
+ beginFinalizing();
130
+ try {
131
+ await params.lifecycle?.onAdopted();
132
+ terminal = "adopted";
133
+ try {
134
+ params.onAdopted?.();
135
+ } catch {}
136
+ try {
137
+ await params.replayClaim?.commit();
138
+ } catch (error) {
139
+ reportReplayCommitError(error);
140
+ }
141
+ } catch (error) {
142
+ await runAbandonment(error).catch(() => void 0);
143
+ throw error;
144
+ }
145
+ };
146
+ const adopt = async () => {
147
+ if (terminal) return;
148
+ if (abandonment) {
149
+ await abandonment.catch(() => void 0);
150
+ if (terminal) return;
151
+ }
152
+ const activeAdoption = adoption ?? runAdoption();
153
+ adoption = activeAdoption;
154
+ await activeAdoption;
155
+ };
156
+ const maybeSettle = async () => {
157
+ if (!fanoutSettled || terminal) return;
158
+ if (failures.length > 0 || [...lanes].some((lane) => lane.status === "failed" || lane.status === "abandoned")) {
159
+ await abandon(failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed"));
160
+ return;
161
+ }
162
+ if ([...lanes].some((lane) => lane.status === "pending" || lane.status === "deferred" || lane.status === "adopted")) return;
163
+ await adopt();
164
+ };
165
+ return {
166
+ createLane: (replayClaim) => {
167
+ const lane = {
168
+ replayClaim,
169
+ status: "pending"
170
+ };
171
+ lanes.add(lane);
172
+ const releaseLane = (error) => {
173
+ lane.replayClaim?.release({ error });
174
+ };
175
+ return {
176
+ lifecycle: {
177
+ abortSignal: params.lifecycle?.abortSignal ?? fallbackAbortSignal,
178
+ onAdopted: async () => {
179
+ if (lane.status === "adopted" || lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
180
+ lane.status = "adopted";
181
+ beginFinalizing();
182
+ try {
183
+ await lane.replayClaim?.commit();
184
+ } catch (error) {
185
+ reportReplayCommitError(error);
186
+ }
187
+ lane.status = "completed";
188
+ await maybeSettle();
189
+ },
190
+ onDeferred: () => {
191
+ if (lane.status !== "pending") return;
192
+ lane.status = "deferred";
193
+ defer();
194
+ },
195
+ onAdoptionFinalizing: beginFinalizing,
196
+ onAbandoned: async () => {
197
+ if (lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
198
+ lane.status = "abandoned";
199
+ releaseLane(/* @__PURE__ */ new Error("feishu-broadcast-turn-abandoned"));
200
+ await maybeSettle();
201
+ }
202
+ },
203
+ onDispatchComplete: async (dispatched) => {
204
+ if (!dispatched && lane.status === "pending") {
205
+ const error = /* @__PURE__ */ new Error("feishu broadcast lane was not dispatched");
206
+ lane.status = "failed";
207
+ failures.push(error);
208
+ releaseLane(error);
209
+ return;
210
+ }
211
+ if (lane.status !== "pending") return;
212
+ const error = /* @__PURE__ */ new Error("feishu broadcast dispatch returned before turn adoption");
213
+ lane.status = "failed";
214
+ failures.push(error);
215
+ releaseLane(error);
216
+ },
217
+ onDispatchFailed: async (error) => {
218
+ failures.push(error);
219
+ if (lane.status !== "completed") {
220
+ lane.status = "failed";
221
+ releaseLane(error);
222
+ }
223
+ await maybeSettle();
224
+ }
225
+ };
226
+ },
227
+ onLanePending: defer,
228
+ onDispatchComplete: async () => {
229
+ fanoutSettled = true;
230
+ await maybeSettle();
231
+ },
232
+ onDispatchFailed: async (error) => {
233
+ failures.push(error);
234
+ fanoutSettled = true;
235
+ await maybeSettle();
236
+ }
237
+ };
238
+ }
75
239
  function resolveBroadcastAgents(cfg, peerId) {
76
240
  const broadcast = cfg.broadcast;
77
241
  if (!broadcast || typeof broadcast !== "object") return null;
@@ -137,11 +301,14 @@ function resolveFeishuGroupSession(params) {
137
301
  };
138
302
  }
139
303
  function parseMessageContent(content, messageType) {
140
- if (messageType === "post") return parsePostContent(content).textContent;
304
+ if (messageType === "post") return parsePostContent(content, {
305
+ renderMediaPlaceholders: false,
306
+ emptyTextFallback: ""
307
+ }).textContent;
141
308
  try {
142
309
  const parsed = JSON.parse(content);
143
310
  if (messageType === "text") return parsed.text || "";
144
- if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType).body;
311
+ if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType);
145
312
  if (messageType === "share_chat") {
146
313
  if (parsed && typeof parsed === "object") {
147
314
  const share = parsed;
@@ -154,7 +321,7 @@ function parseMessageContent(content, messageType) {
154
321
  if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
155
322
  return content;
156
323
  } catch {
157
- return content;
324
+ return FEISHU_MEDIA_MESSAGE_TYPES.has(messageType) ? "" : content;
158
325
  }
159
326
  }
160
327
  const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
@@ -167,32 +334,8 @@ const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
167
334
  ]);
168
335
  function formatFeishuMediaContent(parsed, messageType) {
169
336
  const speechToText = messageType === "audio" && typeof parsed.speech_to_text === "string" ? parsed.speech_to_text.trim() : "";
170
- if (speechToText) return { body: speechToText };
171
- const placeholder = inferPlaceholder(messageType);
172
- const fileName = typeof parsed.file_name === "string" ? parsed.file_name.trim() : "";
173
- return {
174
- body: fileName ? `${placeholder} (${fileName})` : placeholder,
175
- mediaPlaceholder: placeholder,
176
- unavailableBody: fileName || void 0
177
- };
178
- }
179
- function resolveFeishuMediaFailurePresentation(content, messageType) {
180
- if (messageType === "post") return { unavailableBody: parsePostContent(content, {
181
- renderMediaPlaceholders: false,
182
- emptyTextFallback: ""
183
- }).textContent };
184
- if (!FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return {};
185
- try {
186
- const parsed = JSON.parse(content);
187
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
188
- const presentation = formatFeishuMediaContent(parsed, messageType);
189
- return {
190
- mediaPlaceholder: presentation.mediaPlaceholder,
191
- unavailableBody: presentation.unavailableBody
192
- };
193
- } catch {
194
- return {};
195
- }
337
+ if (speechToText) return speechToText;
338
+ return "";
196
339
  }
197
340
  function formatSubMessageContent(content, contentType) {
198
341
  try {
@@ -293,15 +436,15 @@ async function resolveSavedFeishuMedia(params) {
293
436
  const contentType = params.result.contentType ?? await core.media.detectMime({ buffer: params.result.buffer });
294
437
  return await core.channel.media.saveMediaBuffer(params.result.buffer, contentType, "inbound", params.maxBytes, params.result.fileName ?? params.originalFilename);
295
438
  }
296
- function inferPlaceholder(messageType) {
439
+ function resolveFeishuMediaKind(messageType) {
297
440
  switch (messageType) {
298
- case "image": return "<media:image>";
299
- case "file": return "<media:document>";
300
- case "audio": return "<media:audio>";
441
+ case "image": return "image";
442
+ case "file": return "document";
443
+ case "audio": return "audio";
301
444
  case "video":
302
- case "media": return "<media:video>";
303
- case "sticker": return "<media:sticker>";
304
- default: return "<media:document>";
445
+ case "media": return "video";
446
+ case "sticker": return "sticker";
447
+ default: return "document";
305
448
  }
306
449
  }
307
450
  async function resolveFeishuMediaList(params) {
@@ -314,18 +457,11 @@ async function resolveFeishuMediaList(params) {
314
457
  "media",
315
458
  "sticker",
316
459
  "post"
317
- ].includes(messageType)) return {
318
- media: [],
319
- unavailableCount: 0
320
- };
460
+ ].includes(messageType)) return [];
321
461
  const out = [];
322
- let unavailableCount = 0;
323
462
  if (messageType === "post") {
324
463
  const { imageKeys, mediaKeys } = parsePostContent(content);
325
- if (imageKeys.length === 0 && mediaKeys.length === 0) return {
326
- media: [],
327
- unavailableCount: 0
328
- };
464
+ if (imageKeys.length === 0 && mediaKeys.length === 0) return [];
329
465
  if (imageKeys.length > 0) log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
330
466
  if (mediaKeys.length > 0) log?.(`feishu: post message contains ${mediaKeys.length} embedded media file(s)`);
331
467
  for (const imageKey of imageKeys) try {
@@ -343,11 +479,11 @@ async function resolveFeishuMediaList(params) {
343
479
  out.push({
344
480
  path: saved.path,
345
481
  contentType: saved.contentType,
346
- placeholder: "<media:image>"
482
+ kind: "image"
347
483
  });
348
484
  log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
349
485
  } catch (err) {
350
- unavailableCount += 1;
486
+ out.push({ kind: "image" });
351
487
  log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
352
488
  }
353
489
  for (const media of mediaKeys) try {
@@ -367,29 +503,20 @@ async function resolveFeishuMediaList(params) {
367
503
  out.push({
368
504
  path: saved.path,
369
505
  contentType: saved.contentType,
370
- placeholder: "<media:video>"
506
+ kind: "video"
371
507
  });
372
508
  log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
373
509
  } catch (err) {
374
- unavailableCount += 1;
510
+ out.push({ kind: "video" });
375
511
  log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
376
512
  }
377
- return {
378
- media: out,
379
- unavailableCount
380
- };
513
+ return out;
381
514
  }
382
515
  const mediaKeys = parseMediaKeys(content, messageType);
383
- if (!mediaKeys.imageKey && !mediaKeys.fileKey) return {
384
- media: [],
385
- unavailableCount: 1
386
- };
516
+ if (!mediaKeys.imageKey && !mediaKeys.fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
387
517
  try {
388
518
  const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
389
- if (!fileKey) return {
390
- media: [],
391
- unavailableCount: 1
392
- };
519
+ if (!fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
393
520
  const saved = await resolveSavedFeishuMedia({
394
521
  result: await saveMessageResourceFeishu({
395
522
  cfg,
@@ -406,17 +533,14 @@ async function resolveFeishuMediaList(params) {
406
533
  out.push({
407
534
  path: saved.path,
408
535
  contentType: saved.contentType,
409
- placeholder: inferPlaceholder(messageType)
536
+ kind: resolveFeishuMediaKind(messageType)
410
537
  });
411
538
  log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
412
539
  } catch (err) {
413
- unavailableCount += 1;
540
+ out.push({ kind: resolveFeishuMediaKind(messageType) });
414
541
  log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
415
542
  }
416
- return {
417
- media: out,
418
- unavailableCount
419
- };
543
+ return out;
420
544
  }
421
545
  //#endregion
422
546
  //#region extensions/feishu/src/bot-group-name-state.ts
@@ -478,10 +602,150 @@ async function resolveGroupName(params) {
478
602
  return resolvedName;
479
603
  }
480
604
  //#endregion
605
+ //#region extensions/feishu/src/bot-name.ts
606
+ const POSITIVE_TTL_MS = 10 * 6e4;
607
+ const NEGATIVE_TTL_MS = 6e4;
608
+ const MAX_CACHE_ENTRIES = 5e3;
609
+ const BREAKER_FAILURE_THRESHOLD = 10;
610
+ const BREAKER_OPEN_MS = 60 * 6e4;
611
+ const PERMISSION_BACKOFF_MS = 6e4;
612
+ const REQUEST_TIMEOUT_MS = 1500;
613
+ const cache = /* @__PURE__ */ new Map();
614
+ const breakerByAccount = /* @__PURE__ */ new Map();
615
+ const permissionBackoffUntilByAccount = /* @__PURE__ */ new Map();
616
+ const inflight = /* @__PURE__ */ new Map();
617
+ function resolveCacheKey(accountId, openId) {
618
+ return `${accountId}::${openId}`;
619
+ }
620
+ function readCache(key) {
621
+ const entry = cache.get(key);
622
+ if (!entry) return;
623
+ if (entry.expiresAt <= Date.now()) {
624
+ cache.delete(key);
625
+ return;
626
+ }
627
+ cache.delete(key);
628
+ cache.set(key, entry);
629
+ return entry;
630
+ }
631
+ function writeCache(key, entry) {
632
+ cache.delete(key);
633
+ cache.set(key, entry);
634
+ pruneMapToMaxSize(cache, MAX_CACHE_ENTRIES);
635
+ }
636
+ function isBreakerOpen(accountId) {
637
+ const state = breakerByAccount.get(accountId);
638
+ if (!state) return false;
639
+ if (state.openUntil > Date.now()) return true;
640
+ if (state.openUntil > 0) breakerByAccount.delete(accountId);
641
+ return false;
642
+ }
643
+ function recordFailure(accountId) {
644
+ const state = breakerByAccount.get(accountId) ?? {
645
+ failures: 0,
646
+ openUntil: 0
647
+ };
648
+ state.failures += 1;
649
+ if (state.failures >= BREAKER_FAILURE_THRESHOLD) {
650
+ state.failures = 0;
651
+ state.openUntil = Date.now() + BREAKER_OPEN_MS;
652
+ }
653
+ breakerByAccount.set(accountId, state);
654
+ }
655
+ function recordSuccess(accountId) {
656
+ breakerByAccount.delete(accountId);
657
+ }
658
+ function readFeishuErrorCode(error) {
659
+ if (!error || typeof error !== "object") return;
660
+ const data = error.response?.data;
661
+ if (!data || typeof data !== "object") return;
662
+ const code = data.code;
663
+ return typeof code === "number" ? code : void 0;
664
+ }
665
+ async function requestBotName(params) {
666
+ const { account, openId, log } = params;
667
+ const query = new URLSearchParams({ bot_ids: openId });
668
+ try {
669
+ const response = await createFeishuClient(account).request({
670
+ method: "GET",
671
+ url: `/open-apis/bot/v3/bots/basic_batch?${query.toString()}`,
672
+ timeout: REQUEST_TIMEOUT_MS
673
+ });
674
+ const code = response.code ?? 0;
675
+ if (code === 0) return response;
676
+ if (code === 99991672) {
677
+ log(`feishu[${account.accountId}]: bot.basic_info scope not granted`);
678
+ return "permission";
679
+ }
680
+ log(`feishu[${account.accountId}]: bot name lookup failed (code=${code})`);
681
+ return "failure";
682
+ } catch (error) {
683
+ if (readFeishuErrorCode(error) === 99991672) {
684
+ log(`feishu[${account.accountId}]: bot.basic_info scope not granted`);
685
+ return "permission";
686
+ }
687
+ log(`feishu[${account.accountId}]: bot name lookup failed: ${String(error)}`);
688
+ return "failure";
689
+ }
690
+ }
691
+ async function resolveUncachedBotName(params) {
692
+ const { account, openId, cacheKey, log } = params;
693
+ if (isBreakerOpen(account.accountId)) {
694
+ log(`feishu[${account.accountId}]: bot name lookup skipped (breaker open)`);
695
+ return;
696
+ }
697
+ const result = await requestBotName({
698
+ account,
699
+ openId,
700
+ log
701
+ });
702
+ if (result === "permission") {
703
+ permissionBackoffUntilByAccount.set(account.accountId, Date.now() + PERMISSION_BACKOFF_MS);
704
+ return;
705
+ }
706
+ if (result === "failure") {
707
+ recordFailure(account.accountId);
708
+ return;
709
+ }
710
+ recordSuccess(account.accountId);
711
+ const name = result.data?.bots?.[openId]?.name?.trim();
712
+ writeCache(cacheKey, {
713
+ ...name ? { name } : {},
714
+ expiresAt: Date.now() + (name ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS)
715
+ });
716
+ return name;
717
+ }
718
+ async function resolveFeishuBotName(params) {
719
+ const openId = params.openId.trim();
720
+ if (!params.account.configured || !openId) return;
721
+ const permissionBackoffUntil = permissionBackoffUntilByAccount.get(params.account.accountId);
722
+ if (permissionBackoffUntil !== void 0) {
723
+ if (permissionBackoffUntil > Date.now()) return;
724
+ permissionBackoffUntilByAccount.delete(params.account.accountId);
725
+ }
726
+ const key = resolveCacheKey(params.account.accountId, openId);
727
+ const cached = readCache(key);
728
+ if (cached) return cached.name;
729
+ const pending = inflight.get(key);
730
+ if (pending) return pending;
731
+ const lookup = resolveUncachedBotName({
732
+ ...params,
733
+ openId,
734
+ cacheKey: key
735
+ });
736
+ inflight.set(key, lookup);
737
+ try {
738
+ return await lookup;
739
+ } finally {
740
+ inflight.delete(key);
741
+ }
742
+ }
743
+ //#endregion
481
744
  //#region extensions/feishu/src/bot-sender-name.ts
482
745
  const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"];
483
746
  const FEISHU_SCOPE_CORRECTIONS = { "contact:contact.base:readonly": "contact:user.base:readonly" };
484
747
  const SENDER_NAME_TTL_MS = 600 * 1e3;
748
+ const SENDER_NAME_CACHE_MAX_SIZE = 500;
485
749
  const senderNameCache = /* @__PURE__ */ new Map();
486
750
  function correctFeishuScopeInUrl(url) {
487
751
  let corrected = url;
@@ -535,10 +799,13 @@ async function resolveFeishuSenderName(params) {
535
799
  const name = user?.name ?? user?.nickname ?? user?.en_name;
536
800
  if (name) {
537
801
  const expireAt = resolveExpiresAtMsFromDurationMs(SENDER_NAME_TTL_MS);
538
- if (expireAt !== void 0) senderNameCache.set(normalizedSenderId, {
539
- name,
540
- expireAt
541
- });
802
+ if (expireAt !== void 0) {
803
+ senderNameCache.set(normalizedSenderId, {
804
+ name,
805
+ expireAt
806
+ });
807
+ pruneMapToMaxSize(senderNameCache, SENDER_NAME_CACHE_MAX_SIZE);
808
+ }
542
809
  return { name };
543
810
  }
544
811
  return {};
@@ -597,9 +864,7 @@ function dedupeOptions(namespace, log) {
597
864
  * unidentifiable event is never suppressed.
598
865
  */
599
866
  async function claimUnprocessedFeishuMessage(params) {
600
- const claim = await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
601
- if (claim.kind === "inflight") return { kind: "inflight" };
602
- return claim;
867
+ return await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
603
868
  }
604
869
  /**
605
870
  * Claims (unless the caller already holds the claim) and commits a message.
@@ -614,11 +879,6 @@ async function finalizeFeishuMessageProcessing(params) {
614
879
  if ("kind" in claim && claim.kind !== "claimed") return false;
615
880
  return await ("kind" in claim ? claim.handle : claim).commit();
616
881
  }
617
- /** Records a handled message so restart/replay cannot dispatch it again; false when already recorded. */
618
- async function recordProcessedFeishuMessage(messageId, namespace = "global", log) {
619
- const claim = await feishuDedupeState.guard.claim(messageId, dedupeOptions(namespace, log));
620
- return claim.kind === "claimed" ? await claim.handle.commit() : false;
621
- }
622
882
  /** Forgets a recorded message so a retryable synthetic event can be handled on redelivery. */
623
883
  async function forgetProcessedFeishuMessage(messageId, namespace = "global", log) {
624
884
  return await feishuDedupeState.guard.forget(messageId, dedupeOptions(namespace, log));
@@ -876,6 +1136,47 @@ function resolveFeishuReasoningPreviewEnabled(params) {
876
1136
  return configDefault === "stream";
877
1137
  }
878
1138
  //#endregion
1139
+ //#region extensions/feishu/src/reply-delivery-result.ts
1140
+ const noVisibleFeishuReplyDelivery = { visibleReplySent: false };
1141
+ function hasProviderIdentity(result) {
1142
+ return Boolean(result && (result.messageId?.trim() || result.receipt?.primaryPlatformMessageId?.trim() || result.receipt?.platformMessageIds.length));
1143
+ }
1144
+ /** Normalizes every physical Lark send behind one logical reply payload. */
1145
+ function createFeishuReplyDeliveryResult(params) {
1146
+ const results = params.visibleReplySent ? (params.results ?? []).filter(hasProviderIdentity) : [];
1147
+ const receipt = results.length > 0 ? createMessageReceiptFromOutboundResults({
1148
+ results,
1149
+ ...params.kind ? { kind: params.kind } : {}
1150
+ }) : void 0;
1151
+ if (!receipt) return {
1152
+ visibleReplySent: params.visibleReplySent,
1153
+ ...params.content === void 0 ? {} : { content: params.content }
1154
+ };
1155
+ return {
1156
+ messageIds: [...receipt.platformMessageIds],
1157
+ receipt,
1158
+ visibleReplySent: params.visibleReplySent,
1159
+ ...params.content === void 0 ? {} : { content: params.content }
1160
+ };
1161
+ }
1162
+ /** Preserves the first result's provider identity while retaining supplemental ids. */
1163
+ function mergeFeishuReplyDeliveryResults(results, content) {
1164
+ const visible = results.filter((result) => result.visibleReplySent === true);
1165
+ return createFeishuReplyDeliveryResult({
1166
+ results: visible,
1167
+ visibleReplySent: visible.length > 0,
1168
+ content: content === void 0 ? results.find((result) => result.content !== void 0)?.content : content
1169
+ });
1170
+ }
1171
+ /** Keeps a failed outcome while exposing the provider-visible subset to core lifecycle hooks. */
1172
+ function createFeishuPartialReplyDeliveryError(cause, result) {
1173
+ if (result.visibleReplySent !== true) return cause instanceof Error ? cause : new Error(formatErrorMessage(cause), { cause });
1174
+ return createChannelPartialDeliveryError(cause, {
1175
+ ...result,
1176
+ visibleReplySent: true
1177
+ });
1178
+ }
1179
+ //#endregion
879
1180
  //#region extensions/feishu/src/reply-dispatcher-state.ts
880
1181
  const streamingStartBackoffUntilByAccount = /* @__PURE__ */ new Map();
881
1182
  //#endregion
@@ -887,6 +1188,14 @@ function resolveStreamingCardSendMode(options) {
887
1188
  }
888
1189
  //#endregion
889
1190
  //#region extensions/feishu/src/streaming-card.ts
1191
+ /** Provider finalization failed after a streaming card may already be visible. */
1192
+ var FeishuStreamingFinalizationError = class extends Error {
1193
+ constructor(cause, result) {
1194
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
1195
+ this.name = "FeishuStreamingFinalizationError";
1196
+ this.result = result;
1197
+ }
1198
+ };
890
1199
  const STREAMING_UPDATE_THROTTLE_MS = 160;
891
1200
  const STREAMING_SIGNIFICANT_DELTA_CHARS = 18;
892
1201
  const FEISHU_STREAMING_TOKEN_DEFAULT_LIFETIME_SECONDS = 7200;
@@ -961,23 +1270,17 @@ function truncateSummary(text, max = 50) {
961
1270
  const clean = text.replace(/\n/g, " ").trim();
962
1271
  return clean.length <= max ? clean : sliceUtf16Safe(clean, 0, max - 3) + "...";
963
1272
  }
964
- function hasNaturalStreamingBoundary(text) {
965
- return /[\n。!?!?;;::]$/.test(text);
966
- }
967
1273
  function shouldPushStreamingUpdate(previousText, nextText) {
968
- if (!previousText) return true;
969
- if (hasNaturalStreamingBoundary(nextText)) return true;
970
- return nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
1274
+ return !previousText || /[\n。!?!?;;::]$/.test(nextText) || nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
971
1275
  }
1276
+ /** Merges cumulative or overlapping streaming snapshots without duplicating content. */
972
1277
  function mergeStreamingText(previousText, nextText) {
973
1278
  const previous = typeof previousText === "string" ? previousText : "";
974
1279
  const next = typeof nextText === "string" ? nextText : "";
975
1280
  if (!next) return previous;
976
1281
  if (!previous || next === previous) return next;
977
- if (next.startsWith(previous)) return next;
978
- if (previous.startsWith(next)) return previous;
979
- if (next.includes(previous)) return next;
980
- if (previous.includes(next)) return previous;
1282
+ if (next.startsWith(previous) || next.includes(previous)) return next;
1283
+ if (previous.startsWith(next) || previous.includes(next)) return previous;
981
1284
  const maxOverlap = Math.min(previous.length, next.length);
982
1285
  for (let overlap = maxOverlap; overlap > 0; overlap -= 1) if (previous.slice(-overlap) === next.slice(0, overlap)) return `${previous}${next.slice(overlap)}`;
983
1286
  return `${previous}${next}`;
@@ -1267,16 +1570,23 @@ var FeishuStreamingSession = class {
1267
1570
  }
1268
1571
  }).catch((e) => this.log?.(`Note update failed: ${String(e)}`));
1269
1572
  }
1270
- async close(finalText, options) {
1271
- if (!this.state || this.closed) return false;
1573
+ async closeWithResult(finalText, options) {
1574
+ if (!this.state || this.closed) return { visibleReplySent: false };
1272
1575
  this.closed = true;
1273
1576
  this.clearFlushTimer();
1274
1577
  await this.queue;
1275
1578
  const text = finalText ?? this.pendingText ?? this.state.currentText;
1276
1579
  const apiBase = resolveApiBase(this.creds.domain);
1277
1580
  let visibleContentSent = Boolean(this.state.sentText.trim());
1581
+ let finalWriteError;
1278
1582
  if ((text || finalText !== void 0) && text !== this.state.sentText) {
1279
- 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)}`));
1583
+ const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) => {
1584
+ finalWriteError = e;
1585
+ this.log?.(`Final update failed: ${String(e)}`);
1586
+ }) : await this.replaceCardContent(text, (e) => {
1587
+ finalWriteError = e;
1588
+ this.log?.(`Final replace failed: ${String(e)}`);
1589
+ });
1280
1590
  this.state.currentText = text;
1281
1591
  if (sent) {
1282
1592
  this.state.sentText = text;
@@ -1286,44 +1596,63 @@ var FeishuStreamingSession = class {
1286
1596
  if (options?.note) await this.updateNoteContent(options.note);
1287
1597
  const acceptedText = this.state.sentText;
1288
1598
  this.state.sequence += 1;
1289
- await fetchWithSsrFGuard({
1290
- url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
1291
- init: {
1292
- method: "PATCH",
1293
- headers: {
1294
- Authorization: `Bearer ${await getToken(this.creds, {
1295
- fetchImpl: this.fetchImpl,
1296
- lookupFn: this.lookupFn
1297
- })}`,
1298
- "Content-Type": "application/json; charset=utf-8",
1299
- "User-Agent": getFeishuUserAgent()
1599
+ let closeError;
1600
+ try {
1601
+ const { response, release } = await fetchWithSsrFGuard({
1602
+ url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
1603
+ init: {
1604
+ method: "PATCH",
1605
+ headers: {
1606
+ Authorization: `Bearer ${await getToken(this.creds, {
1607
+ fetchImpl: this.fetchImpl,
1608
+ lookupFn: this.lookupFn
1609
+ })}`,
1610
+ "Content-Type": "application/json; charset=utf-8",
1611
+ "User-Agent": getFeishuUserAgent()
1612
+ },
1613
+ body: JSON.stringify({
1614
+ settings: JSON.stringify({ config: {
1615
+ streaming_mode: false,
1616
+ summary: { content: truncateSummary(acceptedText) }
1617
+ } }),
1618
+ sequence: this.state.sequence,
1619
+ uuid: `c_${this.state.cardId}_${this.state.sequence}`
1620
+ })
1300
1621
  },
1301
- body: JSON.stringify({
1302
- settings: JSON.stringify({ config: {
1303
- streaming_mode: false,
1304
- summary: { content: truncateSummary(acceptedText) }
1305
- } }),
1306
- sequence: this.state.sequence,
1307
- uuid: `c_${this.state.cardId}_${this.state.sequence}`
1308
- })
1309
- },
1310
- fetchImpl: this.fetchImpl,
1311
- lookupFn: this.lookupFn,
1312
- policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
1313
- auditContext: "feishu.streaming-card.close",
1314
- timeoutMs: this.creds.httpTimeoutMs ?? 3e4
1315
- }).then(async ({ response, release }) => {
1622
+ fetchImpl: this.fetchImpl,
1623
+ lookupFn: this.lookupFn,
1624
+ policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
1625
+ auditContext: "feishu.streaming-card.close",
1626
+ timeoutMs: this.creds.httpTimeoutMs ?? 3e4
1627
+ });
1316
1628
  try {
1317
1629
  await assertSuccessfulCardKitResponse(response, "feishu.streaming-card.close", "Close streaming card");
1318
1630
  } finally {
1319
1631
  await release();
1320
1632
  }
1321
- }).catch((e) => this.log?.(`Close failed: ${String(e)}`));
1633
+ } catch (error) {
1634
+ closeError = error;
1635
+ this.log?.(`Close failed: ${String(error)}`);
1636
+ }
1322
1637
  const finalState = this.state;
1323
1638
  this.state = null;
1324
1639
  this.pendingText = null;
1325
1640
  this.log?.(`Closed streaming: cardId=${finalState.cardId}`);
1326
- return visibleContentSent;
1641
+ const result = {
1642
+ visibleReplySent: visibleContentSent,
1643
+ ...visibleContentSent ? { content: finalState.sentText } : {},
1644
+ messageId: finalState.messageId
1645
+ };
1646
+ 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);
1647
+ return result;
1648
+ }
1649
+ async close(finalText, options) {
1650
+ try {
1651
+ return (await this.closeWithResult(finalText, options)).visibleReplySent;
1652
+ } catch (error) {
1653
+ if (error instanceof FeishuStreamingFinalizationError) return error.result.visibleReplySent;
1654
+ throw error;
1655
+ }
1327
1656
  }
1328
1657
  async discard() {
1329
1658
  if (!this.state || this.closed) return;
@@ -1469,6 +1798,9 @@ function mergeStreamingFinalText(previousText, nextText, appendError) {
1469
1798
  if (previousText.endsWith(`\n\n${nextText}`)) return previousText;
1470
1799
  return `${previousText}\n\n${nextText}`;
1471
1800
  }
1801
+ function toError(error) {
1802
+ return error instanceof Error ? error : new Error(String(error));
1803
+ }
1472
1804
  /** Maximum age (ms) for a message to receive a typing indicator reaction.
1473
1805
  * Messages older than this are likely replays after context compaction (#30418). */
1474
1806
  const TYPING_INDICATOR_MAX_AGE_MS = 2 * 6e4;
@@ -1489,11 +1821,6 @@ function rememberStreamingStartFailure(accountId, now = Date.now()) {
1489
1821
  streamingStartBackoffUntilByAccount.set(accountId, backoffUntil);
1490
1822
  return backoffUntil;
1491
1823
  }
1492
- function formatMediaFallbackText(text, mediaUrl) {
1493
- const trimmedText = text?.trim() ?? "";
1494
- const attachmentText = `📎 ${mediaUrl}`;
1495
- return trimmedText ? `${trimmedText}\n\n${attachmentText}` : attachmentText;
1496
- }
1497
1824
  function normalizeEpochMs(timestamp) {
1498
1825
  if (!Number.isFinite(timestamp) || timestamp === void 0 || timestamp <= 0) return;
1499
1826
  return timestamp < MS_EPOCH_MIN ? timestamp * 1e3 : timestamp;
@@ -1518,7 +1845,7 @@ function resolveCardNote(agentId, identity, prefixCtx) {
1518
1845
  }
1519
1846
  function createFeishuReplyDispatcher(params) {
1520
1847
  const core = getFeishuRuntime();
1521
- const { cfg, agentId, chatId, sendTarget, replyToMessageId, typingTargetMessageId: explicitTypingTargetMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, accountId, identity, mentionTargets } = params;
1848
+ const { cfg, agentId, chatId, sendTarget, replyToMessageId, typingTargetMessageId: explicitTypingTargetMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, accountId, identity, mentionTargets, requiredMentionTargets } = params;
1522
1849
  const sendReplyToMessageId = skipReplyToInMessages ? void 0 : replyToMessageId;
1523
1850
  const typingTargetMessageId = explicitTypingTargetMessageId?.trim() || replyToMessageId;
1524
1851
  const threadReplyMode = threadReply === true;
@@ -1583,10 +1910,13 @@ function createFeishuReplyDispatcher(params) {
1583
1910
  channel: "feishu"
1584
1911
  });
1585
1912
  const renderMode = account.config?.renderMode ?? "auto";
1586
- const streamingEnabled = resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1913
+ const streamingEnabled = !requiredMentionTargets?.length && resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1914
+ const hookRunner = getGlobalHookRunner();
1915
+ const modifyingHooksRegistered = (hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false);
1916
+ const previewStreamingEnabled = streamingEnabled && !modifyingHooksRegistered;
1587
1917
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1588
1918
  const coreBlockStreamingEnabled = blockStreamingEnabled === true;
1589
- const reasoningPreviewEnabled = streamingEnabled && params.allowReasoningPreview === true;
1919
+ const reasoningPreviewEnabled = previewStreamingEnabled && params.allowReasoningPreview === true;
1590
1920
  let streaming = null;
1591
1921
  let streamText = "";
1592
1922
  let lastPartial = "";
@@ -1596,15 +1926,20 @@ function createFeishuReplyDispatcher(params) {
1596
1926
  let lastSnapshotTextLength = 0;
1597
1927
  let hasStreamingFinalText = false;
1598
1928
  const deliveredFinalTexts = /* @__PURE__ */ new Set();
1929
+ const closedStreamingSettlements = /* @__PURE__ */ new Map();
1599
1930
  let sentIndependentBlockText = false;
1600
1931
  let partialUpdateQueue = Promise.resolve();
1601
1932
  let streamingStartPromise = null;
1602
- let streamingClosedForReply = false;
1603
- let streamingCloseErroredForReply = false;
1933
+ let streamingGeneration = 0;
1934
+ let activeStreamingGeneration;
1935
+ let inFlightStreamingClose;
1604
1936
  let visibleReplySent = false;
1605
1937
  let skippedFinalReason = null;
1606
1938
  let idleSideEffectsPromise = Promise.resolve();
1939
+ let activeIdleSideEffectsPromise = null;
1940
+ let idleRequestedForReply = false;
1607
1941
  let replyLifecycleStateInitialized = false;
1942
+ const pendingStreamingDeliveries = [];
1608
1943
  const markVisibleReplySent = () => {
1609
1944
  visibleReplySent = true;
1610
1945
  };
@@ -1621,9 +1956,12 @@ function createFeishuReplyDispatcher(params) {
1621
1956
  return parts.join("");
1622
1957
  };
1623
1958
  const flushStreamingCardUpdate = (combined) => {
1959
+ const session = streaming;
1960
+ const generation = activeStreamingGeneration;
1961
+ const startPromise = streamingStartPromise;
1624
1962
  partialUpdateQueue = partialUpdateQueue.then(async () => {
1625
- if (streamingStartPromise) await streamingStartPromise;
1626
- if (streaming?.isActive()) await streaming.update(combined);
1963
+ if (startPromise) await startPromise;
1964
+ if (generation !== void 0 && session?.isActive()) await session.update(combined);
1627
1965
  });
1628
1966
  };
1629
1967
  const queueStreamingUpdate = (nextText, options) => {
@@ -1656,12 +1994,15 @@ function createFeishuReplyDispatcher(params) {
1656
1994
  httpTimeoutMs: resolveConfiguredHttpTimeoutMs(account)
1657
1995
  } : null;
1658
1996
  if (!creds) return;
1659
- streaming = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
1997
+ const session = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
1998
+ const generation = ++streamingGeneration;
1999
+ streaming = session;
2000
+ activeStreamingGeneration = generation;
1660
2001
  try {
1661
2002
  const cardHeader = resolveCardHeader(agentId, identity);
1662
2003
  const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1663
2004
  const streamingTarget = sendTarget.replace(/^(feishu|lark):/i, "").replace(/^(chat|user|group|dm|open_id):/i, "").trim();
1664
- await streaming.start(streamingTarget, resolveReceiveIdType(sendTarget), {
2005
+ await session.start(streamingTarget, resolveReceiveIdType(sendTarget), {
1665
2006
  replyToMessageId: sendReplyToMessageId,
1666
2007
  replyInThread: effectiveReplyInThread,
1667
2008
  rootId,
@@ -1672,14 +2013,18 @@ function createFeishuReplyDispatcher(params) {
1672
2013
  } catch (error) {
1673
2014
  rememberStreamingStartFailure(account.accountId);
1674
2015
  params.runtime.error?.(`feishu[${account.accountId}]: streaming start failed; using non-streaming card fallback for ${STREAMING_START_FAILURE_BACKOFF_MS / 1e3}s: ${String(error)}`);
1675
- streaming = null;
1676
- streamingStartPromise = null;
2016
+ if (streaming === session) {
2017
+ streaming = null;
2018
+ streamingStartPromise = null;
2019
+ activeStreamingGeneration = void 0;
2020
+ }
1677
2021
  }
1678
2022
  })();
1679
2023
  };
1680
2024
  const resetStreamingState = () => {
1681
2025
  streaming = null;
1682
2026
  streamingStartPromise = null;
2027
+ activeStreamingGeneration = void 0;
1683
2028
  partialUpdateQueue = Promise.resolve();
1684
2029
  streamText = "";
1685
2030
  lastPartial = "";
@@ -1689,24 +2034,126 @@ function createFeishuReplyDispatcher(params) {
1689
2034
  lastSnapshotTextLength = 0;
1690
2035
  hasStreamingFinalText = false;
1691
2036
  };
1692
- const closeStreaming = async (options) => {
2037
+ const rememberClosedStreamingSettlement = (generation, content, result, error) => {
2038
+ if (generation === void 0 || !content) return;
2039
+ closedStreamingSettlements.set(generation, {
2040
+ result,
2041
+ content,
2042
+ ...error === void 0 ? {} : { error }
2043
+ });
2044
+ };
2045
+ const performStreamingClose = async () => {
2046
+ const streamingToClose = streaming;
2047
+ const generationToClose = activeStreamingGeneration;
2048
+ const startPromiseToClose = streamingStartPromise;
2049
+ const updateQueueToClose = partialUpdateQueue;
2050
+ const finalizedAnswerText = streamText;
2051
+ const finalizedReasoningText = reasoningText;
2052
+ if (generationToClose !== void 0 && activeStreamingGeneration === generationToClose) activeStreamingGeneration = void 0;
1693
2053
  try {
1694
- if (streamingStartPromise) await streamingStartPromise;
1695
- await partialUpdateQueue;
1696
- if (streaming?.isActive()) {
2054
+ if (startPromiseToClose) await startPromiseToClose;
2055
+ await updateQueueToClose;
2056
+ if (streamingToClose?.isActive()) {
1697
2057
  statusLine = "";
1698
- const text = buildCombinedStreamText(reasoningText, streamText);
2058
+ const text = buildCombinedStreamText(finalizedReasoningText, finalizedAnswerText);
1699
2059
  const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1700
- const contentVisible = await streaming.close(text, { note: finalNote });
1701
- if (contentVisible) markVisibleReplySent();
1702
- if (contentVisible && streamText) {
1703
- deliveredFinalTexts.add(streamText);
1704
- if (options?.markClosedForReply !== false && !streamingCloseErroredForReply) streamingClosedForReply = true;
2060
+ let closed;
2061
+ try {
2062
+ closed = await streamingToClose.closeWithResult(text, { note: finalNote });
2063
+ } catch (error) {
2064
+ if (!(error instanceof FeishuStreamingFinalizationError)) throw error;
2065
+ const failedResult = createFeishuReplyDeliveryResult({
2066
+ results: [error.result],
2067
+ visibleReplySent: error.result.visibleReplySent,
2068
+ content: error.result.content,
2069
+ kind: "card"
2070
+ });
2071
+ if (failedResult.visibleReplySent) markVisibleReplySent();
2072
+ if (failedResult.visibleReplySent && finalizedAnswerText) {
2073
+ const partialError = createFeishuPartialReplyDeliveryError(error.cause ?? error, failedResult);
2074
+ if (failedResult.content === text) deliveredFinalTexts.add(finalizedAnswerText);
2075
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, partialError);
2076
+ return {
2077
+ result: failedResult,
2078
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2079
+ error: partialError
2080
+ };
2081
+ }
2082
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, error);
2083
+ return {
2084
+ result: failedResult,
2085
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2086
+ error
2087
+ };
2088
+ }
2089
+ const result = createFeishuReplyDeliveryResult({
2090
+ results: [closed],
2091
+ visibleReplySent: closed.visibleReplySent,
2092
+ content: closed.content,
2093
+ kind: "card"
2094
+ });
2095
+ if (result.visibleReplySent) markVisibleReplySent();
2096
+ if (result.visibleReplySent && finalizedAnswerText) {
2097
+ deliveredFinalTexts.add(finalizedAnswerText);
2098
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, result);
1705
2099
  }
2100
+ return {
2101
+ result,
2102
+ ...generationToClose === void 0 ? {} : { generation: generationToClose }
2103
+ };
1706
2104
  }
2105
+ return {
2106
+ result: noVisibleFeishuReplyDelivery,
2107
+ ...generationToClose === void 0 ? {} : { generation: generationToClose }
2108
+ };
2109
+ } catch (error) {
2110
+ return {
2111
+ result: noVisibleFeishuReplyDelivery,
2112
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2113
+ error
2114
+ };
1707
2115
  } finally {
1708
- resetStreamingState();
2116
+ if (streaming === streamingToClose) resetStreamingState();
2117
+ }
2118
+ };
2119
+ const closeStreaming = () => {
2120
+ const generation = activeStreamingGeneration;
2121
+ if (generation !== void 0 && inFlightStreamingClose?.generation === generation) return inFlightStreamingClose.promise;
2122
+ const content = streamText;
2123
+ const closePromise = performStreamingClose();
2124
+ if (generation !== void 0) {
2125
+ const closing = {
2126
+ generation,
2127
+ content,
2128
+ promise: closePromise
2129
+ };
2130
+ inFlightStreamingClose = closing;
2131
+ const clear = () => {
2132
+ if (inFlightStreamingClose === closing) inFlightStreamingClose = void 0;
2133
+ };
2134
+ closePromise.then(clear, clear);
1709
2135
  }
2136
+ return closePromise;
2137
+ };
2138
+ const deferStreamingDelivery = (result, infoKind, ownerGeneration) => {
2139
+ let resolveFinalization;
2140
+ let rejectFinalization;
2141
+ const finalization = new Promise((resolve, reject) => {
2142
+ resolveFinalization = resolve;
2143
+ rejectFinalization = reject;
2144
+ });
2145
+ pendingStreamingDeliveries.push({
2146
+ result,
2147
+ ...infoKind ? { infoKind } : {},
2148
+ ...ownerGeneration === void 0 ? {} : { streamingGeneration: ownerGeneration },
2149
+ resolve: resolveFinalization,
2150
+ reject: rejectFinalization
2151
+ });
2152
+ if (idleRequestedForReply) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] late reply finalization failed: ${String(error)}`));
2153
+ return {
2154
+ ...noVisibleFeishuReplyDelivery,
2155
+ finalization
2156
+ };
1710
2157
  };
1711
2158
  const discardStreamingPreview = async () => {
1712
2159
  try {
@@ -1731,75 +2178,101 @@ function createFeishuReplyDispatcher(params) {
1731
2178
  limit: textChunkLimit,
1732
2179
  mode: chunkMode,
1733
2180
  firstChunkMentions: paramsLocal.firstChunkMentions,
2181
+ chunkMentions: paramsLocal.chunkMentions,
1734
2182
  initialChunks
1735
2183
  }));
2184
+ const results = [];
2185
+ const acceptedChunks = [];
1736
2186
  for (const [index, chunk] of chunks.entries()) {
1737
- await paramsLocal.sendChunk({
1738
- chunk,
1739
- isFirst: index === 0
1740
- });
1741
- markVisibleReplySent();
2187
+ const mentions = [...paramsLocal.chunkMentions ?? [], ...index === 0 ? paramsLocal.firstChunkMentions ?? [] : []];
2188
+ try {
2189
+ const result = await paramsLocal.sendChunk({
2190
+ chunk,
2191
+ isFirst: index === 0,
2192
+ mentions: mentions.length > 0 ? mentions : void 0
2193
+ });
2194
+ results.push(result);
2195
+ acceptedChunks.push(chunk);
2196
+ markVisibleReplySent();
2197
+ } catch (error) {
2198
+ throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
2199
+ results,
2200
+ visibleReplySent: results.length > 0,
2201
+ content: acceptedChunks.join(""),
2202
+ kind: paramsLocal.useCard ? "card" : "text"
2203
+ }));
2204
+ }
1742
2205
  }
1743
2206
  if (paramsLocal.infoKind === "final") deliveredFinalTexts.add(paramsLocal.text);
2207
+ return createFeishuReplyDeliveryResult({
2208
+ results,
2209
+ visibleReplySent: results.length > 0,
2210
+ content: paramsLocal.text,
2211
+ kind: paramsLocal.useCard ? "card" : "text"
2212
+ });
1744
2213
  };
1745
2214
  const sendMediaReplies = async (payload, options) => {
1746
2215
  const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
1747
2216
  let sentFallbackText = false;
1748
- await sendMediaWithLeadingCaption({
1749
- mediaUrls,
1750
- caption: "",
1751
- send: async ({ mediaUrl }) => {
1752
- const result = await sendMediaFeishu({
1753
- cfg,
1754
- to: sendTarget,
1755
- mediaUrl,
1756
- replyToMessageId: sendReplyToMessageId,
1757
- replyInThread: effectiveReplyInThread,
1758
- accountId,
1759
- ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
1760
- });
1761
- markVisibleReplySent();
1762
- if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) {
1763
- sentFallbackText = true;
1764
- await sendChunkedTextReply({
1765
- text: options.fallbackText,
1766
- useCard: false,
1767
- infoKind: "final",
1768
- sendChunk: async ({ chunk }) => {
1769
- await sendMessageFeishu({
1770
- cfg,
1771
- to: sendTarget,
1772
- text: chunk,
1773
- replyToMessageId: sendReplyToMessageId,
1774
- replyInThread: effectiveReplyInThread,
1775
- allowTopLevelReplyFallback,
1776
- accountId
1777
- });
1778
- }
2217
+ let degradedVoiceFallbackText;
2218
+ const results = [];
2219
+ const sendFallbackText = async (text) => await sendChunkedTextReply({
2220
+ text,
2221
+ useCard: false,
2222
+ infoKind: "final",
2223
+ chunkMentions: requiredMentionTargets,
2224
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2225
+ cfg,
2226
+ to: sendTarget,
2227
+ text: chunk,
2228
+ replyToMessageId: sendReplyToMessageId,
2229
+ replyInThread: effectiveReplyInThread,
2230
+ allowTopLevelReplyFallback,
2231
+ accountId,
2232
+ ...mentions ? { mentions } : {}
2233
+ })
2234
+ });
2235
+ try {
2236
+ await sendMediaWithLeadingCaption({
2237
+ mediaUrls,
2238
+ caption: "",
2239
+ send: async ({ mediaUrl }) => {
2240
+ const result = await sendMediaFeishu({
2241
+ cfg,
2242
+ to: sendTarget,
2243
+ mediaUrl,
2244
+ replyToMessageId: sendReplyToMessageId,
2245
+ replyInThread: effectiveReplyInThread,
2246
+ allowTopLevelReplyFallback,
2247
+ accountId,
2248
+ ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
2249
+ });
2250
+ results.push(createFeishuReplyDeliveryResult({
2251
+ results: [result],
2252
+ visibleReplySent: true,
2253
+ kind: result?.voiceIntentDegradedToFile ? "media" : void 0
2254
+ }));
2255
+ markVisibleReplySent();
2256
+ if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) degradedVoiceFallbackText = options.fallbackText;
2257
+ },
2258
+ onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
2259
+ const fallbackText = await buildFeishuMediaFallbackText({
2260
+ text: sentFallbackText ? void 0 : options.fallbackText,
2261
+ mediaUrl
1779
2262
  });
2263
+ sentFallbackText = true;
2264
+ results.push(await sendFallbackText(fallbackText));
1780
2265
  }
1781
- },
1782
- onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
1783
- const fallbackText = formatMediaFallbackText(sentFallbackText ? void 0 : options.fallbackText, mediaUrl);
2266
+ });
2267
+ if (degradedVoiceFallbackText && !sentFallbackText) {
1784
2268
  sentFallbackText = true;
1785
- await sendChunkedTextReply({
1786
- text: fallbackText,
1787
- useCard: false,
1788
- infoKind: "final",
1789
- sendChunk: async ({ chunk }) => {
1790
- await sendMessageFeishu({
1791
- cfg,
1792
- to: sendTarget,
1793
- text: chunk,
1794
- replyToMessageId: sendReplyToMessageId,
1795
- replyInThread: effectiveReplyInThread,
1796
- allowTopLevelReplyFallback,
1797
- accountId
1798
- });
1799
- }
1800
- });
2269
+ results.push(await sendFallbackText(degradedVoiceFallbackText));
1801
2270
  }
1802
- });
2271
+ } catch (error) {
2272
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2273
+ throw createFeishuPartialReplyDeliveryError(error, mergeFeishuReplyDeliveryResults([...results, ...partial ? [partial] : []]));
2274
+ }
2275
+ return mergeFeishuReplyDeliveryResults(results);
1803
2276
  };
1804
2277
  const ensureNoVisibleReplyFallback = async (reason) => {
1805
2278
  await idleSideEffectsPromise;
@@ -1815,24 +2288,142 @@ function createFeishuReplyDispatcher(params) {
1815
2288
  replyToMessageId: sendReplyToMessageId,
1816
2289
  replyInThread: effectiveReplyInThread,
1817
2290
  allowTopLevelReplyFallback,
1818
- accountId
2291
+ accountId,
2292
+ ...requiredMentionTargets?.length ? { mentions: requiredMentionTargets } : {}
1819
2293
  });
1820
2294
  markVisibleReplySent();
1821
2295
  params.runtime.error?.(`feishu[${account.accountId}]: sent no-visible-reply fallback (${reason})`);
1822
2296
  return true;
1823
2297
  };
1824
- const queueIdleSideEffects = (options) => {
2298
+ const claimClosedStreamingResult = (generation, content) => {
2299
+ if (generation !== void 0) {
2300
+ const settlement = closedStreamingSettlements.get(generation);
2301
+ if (settlement) settlement.contentClaimed = true;
2302
+ return settlement;
2303
+ }
2304
+ let latestKey;
2305
+ for (const [key, settlement] of closedStreamingSettlements) if (settlement.contentClaimed !== true && (content === void 0 || settlement.content === content)) latestKey = key;
2306
+ if (latestKey === void 0) return;
2307
+ const result = closedStreamingSettlements.get(latestKey);
2308
+ if (result) result.contentClaimed = true;
2309
+ return result;
2310
+ };
2311
+ const markClosedStreamingContentClaimed = (generation) => {
2312
+ if (generation !== void 0) {
2313
+ const settlement = closedStreamingSettlements.get(generation);
2314
+ if (settlement) settlement.contentClaimed = true;
2315
+ }
2316
+ };
2317
+ const ensureVisibleStreamingDelivery = async (result, content, infoKind) => {
2318
+ if (result?.visibleReplySent === true || !content?.trim()) return result;
2319
+ const cardHeader = resolveCardHeader(agentId, identity);
2320
+ const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2321
+ return await sendChunkedTextReply({
2322
+ text: content,
2323
+ useCard: true,
2324
+ infoKind,
2325
+ chunkMentions: requiredMentionTargets,
2326
+ sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
2327
+ cfg,
2328
+ to: sendTarget,
2329
+ text: chunk,
2330
+ replyToMessageId: sendReplyToMessageId,
2331
+ replyInThread: effectiveReplyInThread,
2332
+ allowTopLevelReplyFallback,
2333
+ accountId,
2334
+ header: cardHeader,
2335
+ note: cardNote,
2336
+ ...mentions ? { mentions } : {}
2337
+ })
2338
+ });
2339
+ };
2340
+ function queueIdleSideEffects() {
2341
+ idleRequestedForReply = true;
2342
+ if (activeIdleSideEffectsPromise) return activeIdleSideEffectsPromise;
1825
2343
  const nextIdleSideEffects = idleSideEffectsPromise.then(async () => {
1826
- await closeStreaming(options);
1827
- typingCallbacks?.onIdle?.();
2344
+ try {
2345
+ do {
2346
+ const completions = pendingStreamingDeliveries.splice(0);
2347
+ const closeOutcome = await closeStreaming();
2348
+ const finalized = closeOutcome.result;
2349
+ const ownsCurrentClose = (completion) => closeOutcome.generation !== void 0 && completion.streamingGeneration === closeOutcome.generation;
2350
+ if (completions.some((completion) => ownsCurrentClose(completion))) markClosedStreamingContentClaimed(closeOutcome.generation);
2351
+ for (const completion of completions) {
2352
+ const claimedSettlement = ownsCurrentClose(completion) ? {
2353
+ result: finalized,
2354
+ ...closeOutcome.error === void 0 ? {} : { error: closeOutcome.error }
2355
+ } : claimClosedStreamingResult(completion.streamingGeneration, completion.result.content);
2356
+ const deliveryError = claimedSettlement?.error;
2357
+ let providerFinalized = claimedSettlement?.result;
2358
+ try {
2359
+ providerFinalized = await ensureVisibleStreamingDelivery(providerFinalized, completion.result.content, completion.infoKind);
2360
+ } catch (fallbackError) {
2361
+ const fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
2362
+ const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
2363
+ completion.reject(createFeishuPartialReplyDeliveryError(deliveryError === void 0 ? fallbackCause : new AggregateError([deliveryError, fallbackCause], "Feishu streaming finalization and static fallback failed"), mergeFeishuReplyDeliveryResults([
2364
+ ...providerFinalized ? [providerFinalized] : [],
2365
+ ...fallbackPartial ? [fallbackPartial] : [],
2366
+ completion.result
2367
+ ], fallbackPartial?.content ?? providerFinalized?.content ?? completion.result.content)));
2368
+ continue;
2369
+ }
2370
+ const settledResult = mergeFeishuReplyDeliveryResults([...providerFinalized ? [providerFinalized] : [], completion.result], deliveryError === void 0 ? completion.result.content ?? providerFinalized?.content : providerFinalized?.content ?? completion.result.content);
2371
+ if (deliveryError !== void 0) completion.reject(createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(deliveryError) && deliveryError instanceof Error ? deliveryError.cause ?? deliveryError : deliveryError instanceof FeishuStreamingFinalizationError ? deliveryError.cause ?? deliveryError : deliveryError, settledResult));
2372
+ else completion.resolve(settledResult);
2373
+ }
2374
+ if (closeOutcome.error !== void 0) throw toError(closeOutcome.error);
2375
+ } while (pendingStreamingDeliveries.length > 0);
2376
+ } finally {
2377
+ typingCallbacks?.onIdle?.();
2378
+ }
1828
2379
  });
2380
+ activeIdleSideEffectsPromise = nextIdleSideEffects;
1829
2381
  idleSideEffectsPromise = nextIdleSideEffects.catch(() => {});
2382
+ const finishIdleSideEffects = () => {
2383
+ if (activeIdleSideEffectsPromise === nextIdleSideEffects) activeIdleSideEffectsPromise = null;
2384
+ if (pendingStreamingDeliveries.length > 0) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] queued reply finalization failed: ${String(error)}`));
2385
+ };
2386
+ nextIdleSideEffects.then(finishIdleSideEffects, finishIdleSideEffects);
1830
2387
  return nextIdleSideEffects;
2388
+ }
2389
+ const throwStreamingDeliveryFailure = async (paramsLocal) => {
2390
+ let finalized = noVisibleFeishuReplyDelivery;
2391
+ let finalizationError;
2392
+ let fallbackPartial;
2393
+ const claimedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
2394
+ if (claimedSettlement) {
2395
+ finalized = claimedSettlement.result;
2396
+ finalizationError = claimedSettlement.error;
2397
+ } else if (paramsLocal.ownerGeneration !== void 0 && inFlightStreamingClose?.generation === paramsLocal.ownerGeneration) {
2398
+ const closeOutcome = await inFlightStreamingClose.promise;
2399
+ const completedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
2400
+ finalized = completedSettlement?.result ?? closeOutcome?.result ?? finalized;
2401
+ finalizationError = completedSettlement?.error ?? closeOutcome?.error;
2402
+ } else if (paramsLocal.ownerGeneration !== void 0 && activeStreamingGeneration === paramsLocal.ownerGeneration) {
2403
+ const closeOutcome = await closeStreaming();
2404
+ finalized = closeOutcome.result;
2405
+ finalizationError = closeOutcome.error;
2406
+ }
2407
+ try {
2408
+ finalized = await ensureVisibleStreamingDelivery(finalized, paramsLocal.content, paramsLocal.infoKind) ?? finalized;
2409
+ } catch (fallbackError) {
2410
+ fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
2411
+ const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
2412
+ finalizationError = finalizationError ? new AggregateError([finalizationError, fallbackCause], "Feishu streaming finalization and static fallback failed") : fallbackCause;
2413
+ }
2414
+ const mediaPartial = isChannelPartialDeliveryError(paramsLocal.error) ? paramsLocal.error.deliveryResult : void 0;
2415
+ const accepted = mergeFeishuReplyDeliveryResults([
2416
+ finalized,
2417
+ ...fallbackPartial ? [fallbackPartial] : [],
2418
+ ...mediaPartial ? [mediaPartial] : []
2419
+ ], fallbackPartial?.visibleReplySent === true ? fallbackPartial.content : finalized.visibleReplySent === true ? finalized.content : paramsLocal.content);
2420
+ const mediaCause = mediaPartial && paramsLocal.error instanceof Error ? paramsLocal.error.cause ?? paramsLocal.error : paramsLocal.error;
2421
+ throw createFeishuPartialReplyDeliveryError(finalizationError ? new AggregateError([mediaCause, finalizationError instanceof Error ? finalizationError.cause ?? finalizationError : finalizationError], "Feishu reply delivery and streaming finalization failed") : mediaCause, accepted);
1831
2422
  };
1832
- const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
2423
+ const dispatcherOptions = {
1833
2424
  responsePrefix: prefixContext.responsePrefix,
1834
2425
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
1835
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
2426
+ humanDelay: resolveHumanDelayConfig(cfg, agentId),
1836
2427
  silentReplyContext: {
1837
2428
  cfg,
1838
2429
  sessionKey: params.sessionKey,
@@ -1846,52 +2437,80 @@ function createFeishuReplyDispatcher(params) {
1846
2437
  if (!replyLifecycleStateInitialized) {
1847
2438
  replyLifecycleStateInitialized = true;
1848
2439
  deliveredFinalTexts.clear();
2440
+ closedStreamingSettlements.clear();
1849
2441
  sentIndependentBlockText = false;
1850
- streamingClosedForReply = false;
1851
- streamingCloseErroredForReply = false;
2442
+ idleRequestedForReply = false;
1852
2443
  visibleReplySent = false;
1853
2444
  skippedFinalReason = null;
1854
2445
  }
1855
- if (streamingEnabled && renderMode === "card") startStreaming();
2446
+ if (previewStreamingEnabled && renderMode === "card") startStreaming();
1856
2447
  await Promise.resolve(typingCallbacks?.onReplyStart?.());
1857
2448
  },
1858
- deliver: async (payload, info) => {
1859
- if (info?.kind === "final") skippedFinalReason = null;
1860
- const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
1861
- const reply = resolveSendableOutboundReplyParts({
1862
- ...payload,
1863
- text: payloadText
1864
- });
1865
- const text = info?.kind === "final" ? mergeStreamingFinalText(streamText, reply.text, payload.isError === true && hasStreamingFinalText) : reply.text;
1866
- const hasText = reply.hasText;
1867
- const hasMedia = reply.hasMedia;
1868
- const hasVoiceMedia = hasMedia && reply.mediaUrls.some((mediaUrl) => shouldSuppressFeishuTextForVoiceMedia({
1869
- mediaUrl,
1870
- ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
1871
- }));
1872
- const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
1873
- const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
1874
- const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
1875
- const finalTextWouldUseStreamingCard = info?.kind === "final" && hasText && streamingEnabled;
1876
- const useCard = useStaticCard || useStreamingCard;
1877
- const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
1878
- const skipTextForClosedStreamingFinal = info?.kind === "final" && hasText && streamingClosedForReply && !streamingCloseErroredForReply && finalTextWouldUseStreamingCard;
1879
- const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
1880
- const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText || skipTextForDuplicateFinal));
1881
- if (!shouldDeliverText && !hasMedia) return;
1882
- if (shouldDiscardStreamingPreview) await discardStreamingPreview();
1883
- if (shouldDeliverText) {
1884
- if (info?.kind === "block") {
1885
- if (!useStreamingCard) {
1886
- if (coreBlockStreamingEnabled) {
1887
- const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
1888
- await sendChunkedTextReply({
1889
- text,
1890
- useCard: false,
1891
- infoKind: "block",
1892
- firstChunkMentions,
1893
- sendChunk: async ({ chunk, isFirst }) => {
1894
- await sendMessageFeishu({
2449
+ onIdle: () => queueIdleSideEffects(),
2450
+ onCleanup: () => {
2451
+ typingCallbacks?.onCleanup?.();
2452
+ }
2453
+ };
2454
+ const handleDeliveryError = async (error, info) => {
2455
+ params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
2456
+ await queueIdleSideEffects().catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2457
+ };
2458
+ return {
2459
+ dispatcherOptions,
2460
+ delivery: {
2461
+ observeMessageSent: true,
2462
+ deliver: async (payload, info) => {
2463
+ if (info?.kind === "final") skippedFinalReason = null;
2464
+ const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
2465
+ const reply = resolveSendableOutboundReplyParts({
2466
+ ...payload,
2467
+ text: payloadText
2468
+ });
2469
+ const text = info?.kind === "final" ? mergeStreamingFinalText(streamText, reply.text, payload.isError === true && hasStreamingFinalText) : reply.text;
2470
+ const hasText = reply.hasText;
2471
+ const hasMedia = reply.hasMedia;
2472
+ const ttsSupplement = getReplyPayloadTtsSupplement(payload);
2473
+ const ttsTextAlreadyVisible = ttsSupplement?.visibleTextAlreadyDelivered === true;
2474
+ const hasVoiceMedia = hasMedia && reply.mediaUrls.some((mediaUrl) => shouldSuppressFeishuTextForVoiceMedia({
2475
+ mediaUrl,
2476
+ ...payload.audioAsVoice === true ? { audioAsVoice: true } : {},
2477
+ ttsSupplement
2478
+ }));
2479
+ const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
2480
+ const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
2481
+ const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
2482
+ const useCard = useStaticCard || useStreamingCard;
2483
+ const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
2484
+ const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal;
2485
+ const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText && !ttsTextAlreadyVisible || skipTextForDuplicateFinal));
2486
+ const priorClosedStreamingSettlement = info?.kind === "final" && hasText && skipTextForDuplicateFinal ? claimClosedStreamingResult(void 0, text) : void 0;
2487
+ if (!shouldDeliverText && !hasMedia) {
2488
+ if (priorClosedStreamingSettlement?.error !== void 0) throw toError(priorClosedStreamingSettlement.error);
2489
+ return priorClosedStreamingSettlement?.result ?? noVisibleFeishuReplyDelivery;
2490
+ }
2491
+ const deliveredResults = priorClosedStreamingSettlement ? [priorClosedStreamingSettlement.result] : [];
2492
+ const collectMediaDelivery = async (mediaPayload, mediaOptions) => {
2493
+ try {
2494
+ deliveredResults.push(await sendMediaReplies(mediaPayload, mediaOptions));
2495
+ } catch (error) {
2496
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2497
+ const accumulated = mergeFeishuReplyDeliveryResults([...deliveredResults, ...partial ? [partial] : []]);
2498
+ throw createFeishuPartialReplyDeliveryError(partial && error instanceof Error ? error.cause ?? error : error, accumulated);
2499
+ }
2500
+ };
2501
+ if (shouldDiscardStreamingPreview) await discardStreamingPreview();
2502
+ if (shouldDeliverText) {
2503
+ if (info?.kind === "block") {
2504
+ if (!useStreamingCard) {
2505
+ if (coreBlockStreamingEnabled) {
2506
+ const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
2507
+ deliveredResults.push(await sendChunkedTextReply({
2508
+ text,
2509
+ useCard: false,
2510
+ infoKind: "block",
2511
+ firstChunkMentions,
2512
+ chunkMentions: requiredMentionTargets,
2513
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
1895
2514
  cfg,
1896
2515
  to: sendTarget,
1897
2516
  text: chunk,
@@ -1899,47 +2518,59 @@ function createFeishuReplyDispatcher(params) {
1899
2518
  replyInThread: effectiveReplyInThread,
1900
2519
  allowTopLevelReplyFallback,
1901
2520
  accountId,
1902
- ...isFirst && firstChunkMentions ? { mentions: firstChunkMentions } : {}
1903
- });
1904
- }
1905
- });
1906
- sentIndependentBlockText = true;
1907
- if (hasMedia) await sendMediaReplies(payload);
2521
+ ...mentions ? { mentions } : {}
2522
+ })
2523
+ }));
2524
+ sentIndependentBlockText = true;
2525
+ if (hasMedia) await collectMediaDelivery(payload);
2526
+ }
2527
+ return mergeFeishuReplyDeliveryResults(deliveredResults, text);
1908
2528
  }
1909
- return;
2529
+ startStreaming();
2530
+ if (streamingStartPromise) await streamingStartPromise;
1910
2531
  }
1911
- startStreaming();
1912
- if (streamingStartPromise) await streamingStartPromise;
1913
- }
1914
- if (info?.kind === "final" && useStreamingCard) {
1915
- startStreaming();
1916
- if (streamingStartPromise) await streamingStartPromise;
1917
- }
1918
- const shouldStreamText = info?.kind === "block" || info?.kind === "final";
1919
- if (streaming?.isActive() && shouldStreamText) {
1920
- if (info?.kind === "block") queueStreamingUpdate(text, {
1921
- mode: "delta",
1922
- dedupeWithLastPartial: true
1923
- });
1924
- if (info?.kind === "final") {
1925
- streamText = text;
1926
- hasStreamingFinalText = true;
1927
- snapshotBaseText = "";
1928
- lastSnapshotTextLength = text.length;
1929
- flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2532
+ if (info?.kind === "final" && useStreamingCard) {
2533
+ startStreaming();
2534
+ if (streamingStartPromise) await streamingStartPromise;
1930
2535
  }
1931
- if (hasMedia) await sendMediaReplies(payload);
1932
- return;
1933
- }
1934
- if (useCard) {
1935
- const cardHeader = resolveCardHeader(agentId, identity);
1936
- const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1937
- await sendChunkedTextReply({
1938
- text,
1939
- useCard: true,
1940
- infoKind: info?.kind,
1941
- sendChunk: async ({ chunk }) => {
1942
- await sendStructuredCardFeishu({
2536
+ const shouldStreamText = info?.kind === "block" || info?.kind === "final";
2537
+ const matchingInFlightClose = info?.kind === "final" && inFlightStreamingClose?.content === text ? inFlightStreamingClose : void 0;
2538
+ const ownerGeneration = activeStreamingGeneration ?? matchingInFlightClose?.generation;
2539
+ if (shouldStreamText && ownerGeneration !== void 0 && (streaming?.isActive() || matchingInFlightClose !== void 0)) {
2540
+ if (activeStreamingGeneration !== void 0) {
2541
+ if (info?.kind === "block") queueStreamingUpdate(text, {
2542
+ mode: "delta",
2543
+ dedupeWithLastPartial: true
2544
+ });
2545
+ if (info?.kind === "final") {
2546
+ streamText = text;
2547
+ hasStreamingFinalText = true;
2548
+ snapshotBaseText = "";
2549
+ lastSnapshotTextLength = text.length;
2550
+ flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2551
+ }
2552
+ }
2553
+ if (hasMedia) try {
2554
+ await collectMediaDelivery(payload);
2555
+ } catch (error) {
2556
+ await throwStreamingDeliveryFailure({
2557
+ error,
2558
+ content: text,
2559
+ infoKind: info?.kind,
2560
+ ownerGeneration
2561
+ });
2562
+ }
2563
+ return deferStreamingDelivery(mergeFeishuReplyDeliveryResults(deliveredResults, text), info?.kind, ownerGeneration);
2564
+ }
2565
+ if (useCard) {
2566
+ const cardHeader = resolveCardHeader(agentId, identity);
2567
+ const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2568
+ deliveredResults.push(await sendChunkedTextReply({
2569
+ text,
2570
+ useCard: true,
2571
+ infoKind: info?.kind,
2572
+ chunkMentions: requiredMentionTargets,
2573
+ sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
1943
2574
  cfg,
1944
2575
  to: sendTarget,
1945
2576
  text: chunk,
@@ -1948,19 +2579,19 @@ function createFeishuReplyDispatcher(params) {
1948
2579
  allowTopLevelReplyFallback,
1949
2580
  accountId,
1950
2581
  header: cardHeader,
1951
- note: cardNote
1952
- });
1953
- }
1954
- });
1955
- } else {
1956
- const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
1957
- await sendChunkedTextReply({
1958
- text,
1959
- useCard: false,
1960
- infoKind: info?.kind,
1961
- firstChunkMentions,
1962
- sendChunk: async ({ chunk, isFirst }) => {
1963
- await sendMessageFeishu({
2582
+ note: cardNote,
2583
+ ...mentions ? { mentions } : {}
2584
+ })
2585
+ }));
2586
+ } else {
2587
+ const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
2588
+ deliveredResults.push(await sendChunkedTextReply({
2589
+ text,
2590
+ useCard: false,
2591
+ infoKind: info?.kind,
2592
+ firstChunkMentions,
2593
+ chunkMentions: requiredMentionTargets,
2594
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
1964
2595
  cfg,
1965
2596
  to: sendTarget,
1966
2597
  text: chunk,
@@ -1968,32 +2599,22 @@ function createFeishuReplyDispatcher(params) {
1968
2599
  replyInThread: effectiveReplyInThread,
1969
2600
  allowTopLevelReplyFallback,
1970
2601
  accountId,
1971
- ...isFirst && firstChunkMentions ? { mentions: firstChunkMentions } : {}
1972
- });
1973
- }
1974
- });
2602
+ ...mentions ? { mentions } : {}
2603
+ })
2604
+ }));
2605
+ }
1975
2606
  }
1976
- }
1977
- if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
1978
- },
1979
- onError: async (error, info) => {
1980
- streamingCloseErroredForReply = true;
1981
- streamingClosedForReply = false;
1982
- params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
1983
- await queueIdleSideEffects({ markClosedForReply: false });
2607
+ if (hasMedia) await collectMediaDelivery(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2608
+ const result = mergeFeishuReplyDeliveryResults(deliveredResults, text);
2609
+ if (priorClosedStreamingSettlement?.error !== void 0) throw createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(priorClosedStreamingSettlement.error) && priorClosedStreamingSettlement.error instanceof Error ? priorClosedStreamingSettlement.error.cause ?? priorClosedStreamingSettlement.error : priorClosedStreamingSettlement.error, result);
2610
+ return result;
2611
+ },
2612
+ onError: handleDeliveryError
1984
2613
  },
1985
- onIdle: () => queueIdleSideEffects(),
1986
- onCleanup: () => {
1987
- typingCallbacks?.onCleanup?.();
1988
- }
1989
- });
1990
- return {
1991
- dispatcher,
1992
2614
  replyOptions: {
1993
- ...replyOptions,
1994
2615
  onModelSelected: prefixContext.onModelSelected,
1995
2616
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
1996
- onPartialReply: streamingEnabled ? (payload) => {
2617
+ onPartialReply: previewStreamingEnabled ? (payload) => {
1997
2618
  if (!payload.text) return;
1998
2619
  const cleaned = stripReasoningTagsFromText(payload.text, {
1999
2620
  mode: "strict",
@@ -2012,7 +2633,7 @@ function createFeishuReplyDispatcher(params) {
2012
2633
  queueReasoningUpdate(formatReasoningMessage(payload.text));
2013
2634
  } : void 0,
2014
2635
  onReasoningEnd: reasoningPreviewEnabled ? () => {} : void 0,
2015
- onToolStart: streamingEnabled ? (payload) => {
2636
+ onToolStart: previewStreamingEnabled ? (payload) => {
2016
2637
  if (!isChannelProgressDraftWorkToolName(payload.name)) return;
2017
2638
  const statusLineLocal = formatChannelProgressDraftLineForEntry(account.config, {
2018
2639
  event: "tool",
@@ -2022,17 +2643,16 @@ function createFeishuReplyDispatcher(params) {
2022
2643
  }, { detailMode: payload.detailMode });
2023
2644
  if (statusLineLocal) updateStreamingStatusLine(statusLineLocal);
2024
2645
  } : void 0,
2025
- onAssistantMessageStart: streamingEnabled ? () => {
2646
+ onAssistantMessageStart: previewStreamingEnabled ? () => {
2026
2647
  updateStreamingStatusLine("", { startIfNeeded: false });
2027
2648
  } : void 0,
2028
- onCompactionStart: streamingEnabled ? () => {
2649
+ onCompactionStart: previewStreamingEnabled ? () => {
2029
2650
  updateStreamingStatusLine("📦 **Compacting context...**");
2030
2651
  } : void 0,
2031
- onCompactionEnd: streamingEnabled ? () => {
2652
+ onCompactionEnd: previewStreamingEnabled ? () => {
2032
2653
  updateStreamingStatusLine("");
2033
2654
  } : void 0
2034
2655
  },
2035
- markDispatchIdle,
2036
2656
  ensureNoVisibleReplyFallback,
2037
2657
  getVisibleReplyState: () => ({
2038
2658
  visibleReplySent,
@@ -2041,6 +2661,17 @@ function createFeishuReplyDispatcher(params) {
2041
2661
  };
2042
2662
  }
2043
2663
  //#endregion
2664
+ //#region extensions/feishu/src/synthetic-event-target.ts
2665
+ const directPreDispatchTargets = /* @__PURE__ */ new WeakMap();
2666
+ /** Keep synthetic-only routing metadata outside the public Feishu event shape. */
2667
+ function setFeishuSyntheticDirectPreDispatchTarget(event, target) {
2668
+ directPreDispatchTargets.set(event, target);
2669
+ return event;
2670
+ }
2671
+ function getFeishuSyntheticDirectPreDispatchTarget(event) {
2672
+ return directPreDispatchTargets.get(event);
2673
+ }
2674
+ //#endregion
2044
2675
  //#region extensions/feishu/src/bot.ts
2045
2676
  const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
2046
2677
  const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
@@ -2055,15 +2686,14 @@ function isFeishuTopicSessionScope(scope) {
2055
2686
  return scope === "group_topic" || scope === "group_topic_sender";
2056
2687
  }
2057
2688
  async function resolveFeishuAudioPreflightTranscript(params) {
2058
- if (params.content.trim() !== "<media:audio>") return;
2059
- const audioMedia = params.mediaList.filter((media) => media.contentType?.startsWith("audio/"));
2689
+ if (params.messageType !== "audio" || params.content.trim()) return;
2690
+ const audioMedia = params.mediaList.filter((media) => Boolean(media.path) && (media.kind === "audio" || media.contentType?.startsWith("audio/")));
2060
2691
  if (audioMedia.length === 0) return;
2061
2692
  try {
2062
2693
  const { transcribeFirstAudio } = await import("./audio-preflight.runtime-BBfC9elv.js");
2063
2694
  return await transcribeFirstAudio({
2064
2695
  ctx: {
2065
- MediaPaths: audioMedia.map((media) => media.path),
2066
- MediaTypes: audioMedia.map((media) => media.contentType).filter(Boolean),
2696
+ media: audioMedia,
2067
2697
  ChatType: params.chatType
2068
2698
  },
2069
2699
  cfg: params.cfg
@@ -2074,8 +2704,7 @@ async function resolveFeishuAudioPreflightTranscript(params) {
2074
2704
  }
2075
2705
  }
2076
2706
  /**
2077
- * Build media payload for inbound context.
2078
- * Similar to Discord's buildDiscordMediaPayload().
2707
+ * Parse an inbound Feishu event into its caption and routing metadata.
2079
2708
  */
2080
2709
  function parseFeishuMessageEvent(event, botOpenId, _botName) {
2081
2710
  const rawContent = parseMessageContent(event.message.content, event.message.message_type);
@@ -2093,6 +2722,7 @@ function parseFeishuMessageEvent(event, botOpenId, _botName) {
2093
2722
  suppressReplyTarget: event.message.suppress_reply_target === true,
2094
2723
  senderId: senderUserId || senderOpenId || "",
2095
2724
  senderOpenId: senderFallbackId,
2725
+ senderType: event.sender.sender_type === "bot" ? "bot" : "user",
2096
2726
  chatType: event.message.chat_type,
2097
2727
  mentionedBot,
2098
2728
  hasAnyMention,
@@ -2142,7 +2772,7 @@ async function filterFetchedGroupContextMessages(messages, params) {
2142
2772
  }) ? message : void 0))).filter((message) => message !== void 0);
2143
2773
  }
2144
2774
  async function handleFeishuMessage(params) {
2145
- const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride } = params;
2775
+ const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride, turnAdoptionLifecycle } = params;
2146
2776
  const account = resolveFeishuRuntimeAccount({
2147
2777
  cfg,
2148
2778
  accountId
@@ -2152,7 +2782,7 @@ async function handleFeishuMessage(params) {
2152
2782
  const error = runtime?.error ?? console.error;
2153
2783
  const messageId = event.message.message_id;
2154
2784
  const messageDedupeKey = messageDedupeKeyOverride ?? resolveFeishuMessageDedupeKey(event);
2155
- if (!await finalizeFeishuMessageProcessing({
2785
+ if (!turnAdoptionLifecycle && !await finalizeFeishuMessageProcessing({
2156
2786
  messageId: messageDedupeKey,
2157
2787
  namespace: account.accountId,
2158
2788
  log,
@@ -2164,7 +2794,62 @@ async function handleFeishuMessage(params) {
2164
2794
  let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
2165
2795
  const isGroup = isFeishuGroupChatType(ctx.chatType);
2166
2796
  const isDirect = !isGroup;
2797
+ const directPreDispatchTarget = isDirect ? getFeishuSyntheticDirectPreDispatchTarget(event) : void 0;
2167
2798
  const senderUserId = normalizeOptionalString(event.sender.sender_id.user_id);
2799
+ const localBotOpenId = botOpenId?.trim();
2800
+ if (ctx.senderType === "bot") {
2801
+ if (!localBotOpenId) {
2802
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (local bot identity unavailable)`);
2803
+ return;
2804
+ }
2805
+ if (ctx.senderOpenId === localBotOpenId) {
2806
+ log(`feishu[${account.accountId}]: dropping self-authored bot message ${ctx.messageId}`);
2807
+ return;
2808
+ }
2809
+ if (feishuCfg?.allowBots !== true) {
2810
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (allowBots=false)`);
2811
+ return;
2812
+ }
2813
+ if (isGroup && !ctx.mentionedBot) {
2814
+ let verifiedEvent;
2815
+ try {
2816
+ const response = await createFeishuClient(account).im.message.get({
2817
+ params: { user_id_type: "open_id" },
2818
+ path: { message_id: ctx.messageId }
2819
+ });
2820
+ const verifiedKey = (response.data?.items?.[0]?.mentions?.find((mention) => mention.id_type === "open_id" && mention.id === localBotOpenId && Boolean(mention.key?.trim())))?.key?.trim();
2821
+ if (response.code === 0 && verifiedKey) {
2822
+ const eventMention = event.message.mentions?.find((mention) => mention.key === verifiedKey && !isFeishuBroadcastMention(mention));
2823
+ if (eventMention) verifiedEvent = {
2824
+ ...event,
2825
+ message: {
2826
+ ...event.message,
2827
+ mentions: event.message.mentions?.map((mention) => mention === eventMention ? {
2828
+ ...mention,
2829
+ id: {
2830
+ ...mention.id,
2831
+ open_id: localBotOpenId
2832
+ }
2833
+ } : mention)
2834
+ }
2835
+ };
2836
+ }
2837
+ } catch (err) {
2838
+ log(`feishu[${account.accountId}]: failed to verify bot mention for ${ctx.messageId}: ${String(err)}`);
2839
+ }
2840
+ if (!verifiedEvent) {
2841
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (local mention not verifiable)`);
2842
+ return;
2843
+ }
2844
+ const deliveredCtx = parseFeishuMessageEvent(verifiedEvent, localBotOpenId, botName);
2845
+ ctx = {
2846
+ ...deliveredCtx,
2847
+ mentionedBot: true,
2848
+ content: normalizeFeishuCommandProbeBody(deliveredCtx.content),
2849
+ mentionTargets: void 0
2850
+ };
2851
+ }
2852
+ }
2168
2853
  if (event.message.message_type === "merge_forward") {
2169
2854
  log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
2170
2855
  try {
@@ -2195,7 +2880,17 @@ async function handleFeishuMessage(params) {
2195
2880
  }
2196
2881
  }
2197
2882
  let permissionErrorForAgent;
2198
- if (feishuCfg?.resolveSenderNames ?? true) {
2883
+ if (feishuCfg?.resolveSenderNames ?? true) if (ctx.senderType === "bot") {
2884
+ const senderName = await resolveFeishuBotName({
2885
+ account,
2886
+ openId: ctx.senderOpenId,
2887
+ log
2888
+ });
2889
+ if (senderName) ctx = {
2890
+ ...ctx,
2891
+ senderName
2892
+ };
2893
+ } else {
2199
2894
  const senderResult = await resolveFeishuSenderName({
2200
2895
  account,
2201
2896
  senderId: ctx.senderOpenId,
@@ -2332,6 +3027,23 @@ async function handleFeishuMessage(params) {
2332
3027
  });
2333
3028
  return;
2334
3029
  }
3030
+ if (ctx.senderType === "bot") {
3031
+ if (!localBotOpenId || !ctx.senderOpenId) {
3032
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (loop identity unavailable)`);
3033
+ return;
3034
+ }
3035
+ if (recordChannelBotPairLoopAndCheckSuppression({
3036
+ scopeId: account.accountId,
3037
+ conversationId: ctx.chatId,
3038
+ senderId: ctx.senderOpenId,
3039
+ receiverId: localBotOpenId,
3040
+ defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
3041
+ defaultEnabled: true
3042
+ }).suppressed) {
3043
+ log(`feishu[${account.accountId}]: bot-pair loop suppressed for ${ctx.senderOpenId} in ${ctx.chatId}`);
3044
+ return;
3045
+ }
3046
+ }
2335
3047
  }
2336
3048
  try {
2337
3049
  const core = { channel: channelRuntime?.inbound ? channelRuntime : getFeishuRuntime().channel };
@@ -2379,7 +3091,7 @@ async function handleFeishuMessage(params) {
2379
3091
  sendPairingReply: async (text) => {
2380
3092
  await sendMessageFeishu({
2381
3093
  cfg: authorization.cfg,
2382
- to: `chat:${ctx.chatId}`,
3094
+ to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
2383
3095
  text,
2384
3096
  accountId: account.accountId
2385
3097
  });
@@ -2514,7 +3226,7 @@ async function handleFeishuMessage(params) {
2514
3226
  const replyTargetMessageId = directThreadReply ? directThreadReplyTargetMessageId : acpTopicReply ? ctx.rootId ?? ctx.messageId : ctx.messageId;
2515
3227
  await sendMessageFeishu({
2516
3228
  cfg: effectiveCfg,
2517
- to: `chat:${ctx.chatId}`,
3229
+ to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
2518
3230
  text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
2519
3231
  replyToMessageId: replyTargetMessageId,
2520
3232
  replyInThread,
@@ -2534,7 +3246,7 @@ async function handleFeishuMessage(params) {
2534
3246
  });
2535
3247
  log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
2536
3248
  const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024;
2537
- const mediaResolution = await resolveFeishuMediaList({
3249
+ const mediaList = await resolveFeishuMediaList({
2538
3250
  cfg,
2539
3251
  messageId: ctx.messageId,
2540
3252
  messageType: event.message.message_type,
@@ -2543,12 +3255,10 @@ async function handleFeishuMessage(params) {
2543
3255
  log,
2544
3256
  accountId: account.accountId
2545
3257
  });
2546
- const mediaList = mediaResolution.media;
2547
- const mediaFailurePresentation = resolveFeishuMediaFailurePresentation(event.message.content, event.message.message_type);
2548
- const mediaFailureContent = mediaResolution.unavailableCount > 0 ? formatInboundMediaUnavailableText({
2549
- body: mediaFailurePresentation.unavailableBody ?? ctx.content,
2550
- mediaPlaceholder: mediaFailurePresentation.mediaPlaceholder,
2551
- notice: `[feishu ${mediaResolution.unavailableCount > 1 ? `${mediaResolution.unavailableCount} attachments` : "attachment"} unavailable]`
3258
+ const unavailableMediaCount = mediaList.filter((media) => !media.path).length;
3259
+ const mediaFailureContent = unavailableMediaCount > 0 ? formatInboundMediaUnavailableText({
3260
+ body: ctx.content,
3261
+ notice: `[feishu ${unavailableMediaCount > 1 ? `${unavailableMediaCount} attachments` : "attachment"} unavailable]`
2552
3262
  }) : ctx.content;
2553
3263
  let quotedMessageInfo = null;
2554
3264
  let quotedContent;
@@ -2583,11 +3293,17 @@ async function handleFeishuMessage(params) {
2583
3293
  cfg: effectiveCfg,
2584
3294
  mediaList,
2585
3295
  content: ctx.content,
3296
+ messageType: event.message.message_type,
2586
3297
  chatType: isGroup ? "group" : "direct",
2587
3298
  log
2588
3299
  });
2589
- const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
3300
+ const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.kind === "audio" || media.contentType?.startsWith("audio/"));
2590
3301
  const inboundMedia = toInboundMediaFacts(mediaList, { transcribed: (_media, index) => index === preflightAudioIndex });
3302
+ const requiredMentionTargets = isGroup && ctx.senderType === "bot" && ctx.senderOpenId ? [{
3303
+ openId: ctx.senderOpenId,
3304
+ name: ctx.senderName ?? ctx.senderOpenId,
3305
+ key: ""
3306
+ }] : void 0;
2591
3307
  const agentFacingContent = audioTranscript ?? mediaFailureContent;
2592
3308
  const commandFacingContent = audioTranscript ?? ctx.content;
2593
3309
  const agentFacingCtx = agentFacingContent === ctx.content ? ctx : {
@@ -2618,7 +3334,7 @@ async function handleFeishuMessage(params) {
2618
3334
  command: { hasControlCommand: true }
2619
3335
  })).commandAccess.authorized : void 0;
2620
3336
  const isTopicSessionForThread = isGroup && (groupSession?.groupSessionScope === "group_topic" || groupSession?.groupSessionScope === "group_topic_sender");
2621
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
3337
+ const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
2622
3338
  const messageBody = buildFeishuAgentBody({
2623
3339
  ctx: agentFacingCtx,
2624
3340
  quotedContent,
@@ -2627,7 +3343,7 @@ async function handleFeishuMessage(params) {
2627
3343
  });
2628
3344
  const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
2629
3345
  if (permissionErrorForAgent) log(`feishu[${account.accountId}]: appending permission error notice to message body`);
2630
- let combinedBody = core.channel.reply.formatAgentEnvelope({
3346
+ let combinedBody = formatAgentEnvelope({
2631
3347
  channel: "Feishu",
2632
3348
  from: envelopeFrom,
2633
3349
  timestamp: /* @__PURE__ */ new Date(),
@@ -2639,7 +3355,7 @@ async function handleFeishuMessage(params) {
2639
3355
  historyKey,
2640
3356
  limit: historyLimit,
2641
3357
  currentMessage: combinedBody,
2642
- formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
3358
+ formatEntry: (entry) => formatAgentEnvelope({
2643
3359
  channel: "Feishu",
2644
3360
  from: `${ctx.chatId}:${entry.sender}`,
2645
3361
  timestamp: entry.timestamp,
@@ -2706,7 +3422,7 @@ async function handleFeishuMessage(params) {
2706
3422
  threadContextBySessionKey.set(agentSessionKey, threadContext);
2707
3423
  return threadContext;
2708
3424
  }
2709
- const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
3425
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
2710
3426
  if (core.channel.session.readSessionUpdatedAt({
2711
3427
  storePath,
2712
3428
  sessionKey: agentSessionKey
@@ -2748,7 +3464,7 @@ async function handleFeishuMessage(params) {
2748
3464
  const historyMessages = Boolean(rootMsg?.content || ctx.rootId) ? relevantMessages : relevantMessages.slice(1);
2749
3465
  const historyParts = historyMessages.map((msg) => {
2750
3466
  const role = msg.senderType === "app" ? "assistant" : "user";
2751
- return core.channel.reply.formatAgentEnvelope({
3467
+ return formatAgentEnvelope({
2752
3468
  channel: "Feishu",
2753
3469
  from: `${msg.senderId ?? "Unknown"} (${role})`,
2754
3470
  timestamp: msg.createTime,
@@ -2770,7 +3486,6 @@ async function handleFeishuMessage(params) {
2770
3486
  const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
2771
3487
  return buildChannelInboundEventContext({
2772
3488
  channel: "feishu",
2773
- finalize: core.channel.reply.finalizeInboundContext,
2774
3489
  supplemental: {
2775
3490
  quote: quotedContent ? {
2776
3491
  id: ctx.parentId,
@@ -2789,7 +3504,8 @@ async function handleFeishuMessage(params) {
2789
3504
  from: feishuFrom,
2790
3505
  sender: {
2791
3506
  id: ctx.senderOpenId,
2792
- name: ctx.senderName ?? ctx.senderOpenId
3507
+ name: ctx.senderName ?? ctx.senderOpenId,
3508
+ isBot: ctx.senderType === "bot"
2793
3509
  },
2794
3510
  conversation: {
2795
3511
  kind: isGroup ? "group" : "direct",
@@ -2800,6 +3516,7 @@ async function handleFeishuMessage(params) {
2800
3516
  },
2801
3517
  route: {
2802
3518
  agentId,
3519
+ dmScope: route.dmScope,
2803
3520
  accountId: agentAccountId,
2804
3521
  routeSessionKey: agentSessionKey
2805
3522
  },
@@ -2815,6 +3532,7 @@ async function handleFeishuMessage(params) {
2815
3532
  rawBody: commandFacingContent,
2816
3533
  commandBody: commandFacingContent
2817
3534
  },
3535
+ sessionTranscript: { historyLimit: isGroup ? historyLimit : 0 },
2818
3536
  access: {
2819
3537
  mentions: {
2820
3538
  canDetectMention: isGroup,
@@ -2864,189 +3582,225 @@ async function handleFeishuMessage(params) {
2864
3582
  };
2865
3583
  };
2866
3584
  if (broadcastAgents) {
2867
- if (!await recordProcessedFeishuMessage(messageDedupeKey ?? ctx.messageId, "broadcast", log)) {
3585
+ const broadcastDedupeKey = messageDedupeKey ?? ctx.messageId;
3586
+ const broadcastClaim = await claimUnprocessedFeishuMessage({
3587
+ messageId: broadcastDedupeKey,
3588
+ namespace: "broadcast",
3589
+ log
3590
+ });
3591
+ if (broadcastClaim.kind === "duplicate" || broadcastClaim.kind === "inflight") {
2868
3592
  log(`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`);
2869
3593
  return;
2870
3594
  }
3595
+ const broadcastSettlement = createFeishuBroadcastIngressSettlement({
3596
+ lifecycle: turnAdoptionLifecycle,
3597
+ replayClaim: broadcastClaim.kind === "claimed" ? broadcastClaim.handle : void 0,
3598
+ onReplayCommitError: (err) => error(`feishu[${account.accountId}]: failed to commit broadcast replay guard: ${String(err)}`),
3599
+ onAdopted: () => {
3600
+ if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
3601
+ historyKey,
3602
+ limit: historyLimit
3603
+ });
3604
+ }
3605
+ });
3606
+ const abandonBroadcast = async (err) => {
3607
+ try {
3608
+ await broadcastSettlement.onDispatchFailed(err);
3609
+ } catch (abandonError) {
3610
+ error(`feishu[${account.accountId}]: failed to abandon broadcast ingress: ${String(abandonError)}`);
3611
+ }
3612
+ };
2871
3613
  const strategy = cfg.broadcast?.strategy === "sequential" ? "sequential" : "parallel";
2872
3614
  const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$2(route.agentId) : null;
2873
3615
  const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$2(a.id));
2874
3616
  const hasKnownAgents = agentIds.length > 0;
2875
3617
  log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
2876
3618
  const dispatchForAgent = async (agentId) => {
2877
- if (hasKnownAgents && !agentIds.includes(normalizeAgentId$2(agentId))) {
3619
+ const normalizedAgentId = normalizeAgentId$2(agentId);
3620
+ if (hasKnownAgents && !agentIds.includes(normalizedAgentId)) {
2878
3621
  log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
2879
3622
  return;
2880
3623
  }
2881
- const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
2882
- const agentStorePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
2883
- const agentRecord = {
2884
- updateLastRoute: buildFeishuInboundLastRouteUpdate({
2885
- sessionKey: agentSessionKey,
2886
- accountId: route.accountId
2887
- }),
2888
- onRecordError: (err) => {
2889
- log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
3624
+ let agentClaim;
3625
+ for (let attempt = 0; attempt < 2; attempt += 1) {
3626
+ agentClaim = await claimUnprocessedFeishuMessage({
3627
+ messageId: broadcastDedupeKey,
3628
+ namespace: `broadcast:${normalizedAgentId}`,
3629
+ log
3630
+ });
3631
+ if (agentClaim.kind === "duplicate") return;
3632
+ if (agentClaim.kind !== "inflight") break;
3633
+ broadcastSettlement.onLanePending();
3634
+ try {
3635
+ await agentClaim.pending;
3636
+ return;
3637
+ } catch (err) {
3638
+ if (attempt === 1) throw err;
2890
3639
  }
2891
- };
2892
- const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
2893
- cfg,
2894
- agentId,
2895
- storePath: agentStorePath,
2896
- sessionKey: agentSessionKey
2897
- });
2898
- const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
2899
- if (agentId === activeAgentId) {
2900
- const identity = resolveAgentOutboundIdentity(cfg, agentId);
2901
- const { dispatcher, replyOptions, markDispatchIdle, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3640
+ }
3641
+ const lane = broadcastSettlement.createLane(agentClaim?.kind === "claimed" ? agentClaim.handle : void 0);
3642
+ try {
3643
+ const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
3644
+ const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
3645
+ const agentRecord = {
3646
+ updateLastRoute: buildFeishuInboundLastRouteUpdate({
3647
+ sessionKey: agentSessionKey,
3648
+ accountId: route.accountId
3649
+ }),
3650
+ onRecordError: (err) => {
3651
+ log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
3652
+ }
3653
+ };
3654
+ const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
2902
3655
  cfg,
2903
3656
  agentId,
2904
- runtime,
2905
- chatId: ctx.chatId,
2906
- sendTarget: feishuTo,
2907
- allowReasoningPreview,
2908
- replyToMessageId: replyTargetMessageId,
2909
- typingTargetMessageId,
2910
- skipReplyToInMessages: !isGroup && !directThreadReply,
2911
- replyInThread,
2912
- rootId: ctx.rootId,
2913
- threadReply,
2914
- accountId: account.accountId,
2915
- identity,
2916
- mentionTargets: ctx.mentionTargets,
2917
- messageCreateTimeMs,
3657
+ storePath: agentStorePath,
2918
3658
  sessionKey: agentSessionKey
2919
3659
  });
2920
- log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
2921
- const turnResult = await core.channel.inbound.run({
2922
- channel: "feishu",
2923
- accountId: route.accountId,
2924
- raw: ctx,
2925
- adapter: {
2926
- ingest: () => ({
2927
- id: ctx.messageId,
2928
- timestamp: messageCreateTimeMs,
2929
- rawText: ctx.content,
2930
- textForAgent: agentCtx.BodyForAgent,
2931
- textForCommands: agentCtx.CommandBody,
2932
- raw: ctx
2933
- }),
2934
- resolveTurn: () => ({
2935
- channel: "feishu",
2936
- accountId: route.accountId,
2937
- routeSessionKey: agentSessionKey,
2938
- storePath: agentStorePath,
2939
- ctxPayload: agentCtx,
2940
- recordInboundSession: core.channel.session.recordInboundSession,
2941
- record: agentRecord,
2942
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
2943
- dispatcher,
2944
- onSettled: () => markDispatchIdle()
3660
+ const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
3661
+ if (agentId === activeAgentId) {
3662
+ const identity = resolveAgentOutboundIdentity(cfg, agentId);
3663
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3664
+ cfg,
3665
+ agentId,
3666
+ runtime,
3667
+ chatId: ctx.chatId,
3668
+ sendTarget: feishuTo,
3669
+ allowReasoningPreview,
3670
+ replyToMessageId: replyTargetMessageId,
3671
+ typingTargetMessageId,
3672
+ skipReplyToInMessages: !isGroup && !directThreadReply,
3673
+ replyInThread,
3674
+ rootId: ctx.rootId,
3675
+ threadReply,
3676
+ accountId: account.accountId,
3677
+ identity,
3678
+ mentionTargets: ctx.mentionTargets,
3679
+ requiredMentionTargets,
3680
+ messageCreateTimeMs,
3681
+ sessionKey: agentSessionKey
3682
+ });
3683
+ log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
3684
+ const turnResult = await core.channel.inbound.run({
3685
+ channel: "feishu",
3686
+ accountId: route.accountId,
3687
+ raw: ctx,
3688
+ adapter: {
3689
+ ingest: () => ({
3690
+ id: ctx.messageId,
3691
+ timestamp: messageCreateTimeMs,
3692
+ rawText: ctx.content,
3693
+ textForAgent: agentCtx.BodyForAgent,
3694
+ textForCommands: agentCtx.CommandBody,
3695
+ raw: ctx
2945
3696
  }),
2946
- runDispatch: () => core.channel.reply.withReplyDispatcher({
2947
- dispatcher,
2948
- onSettled: () => markDispatchIdle(),
2949
- run: () => core.channel.reply.dispatchReplyFromConfig({
2950
- ctx: agentCtx,
2951
- cfg,
2952
- dispatcher,
2953
- replyOptions
2954
- })
3697
+ resolveTurn: () => ({
3698
+ cfg,
3699
+ channel: "feishu",
3700
+ accountId: route.accountId,
3701
+ route: {
3702
+ agentId,
3703
+ sessionKey: agentSessionKey
3704
+ },
3705
+ ctxPayload: agentCtx,
3706
+ record: agentRecord,
3707
+ dispatcherOptions,
3708
+ delivery,
3709
+ replyOptions: {
3710
+ ...replyOptions,
3711
+ ...bindIngressLifecycleToReplyOptions(lane.lifecycle)
3712
+ }
2955
3713
  })
2956
- })
2957
- }
2958
- });
2959
- if (turnResult.dispatched && shouldSendNoVisibleReplyFallback({
2960
- ...turnResult.dispatchResult,
2961
- failedCounts: dispatcher.getFailedCounts?.() ?? {
2962
- tool: 0,
2963
- block: 0,
2964
- final: 0
2965
- }
2966
- })) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
2967
- } else {
2968
- delete agentCtx.CommandAuthorized;
2969
- const noopDispatcher = {
2970
- sendToolResult: () => false,
2971
- sendBlockReply: () => false,
2972
- sendFinalReply: () => false,
2973
- waitForIdle: async () => {},
2974
- getQueuedCounts: () => ({
2975
- tool: 0,
2976
- block: 0,
2977
- final: 0
2978
- }),
2979
- getFailedCounts: () => ({
2980
- tool: 0,
2981
- block: 0,
2982
- final: 0
2983
- }),
2984
- markComplete: () => {}
2985
- };
2986
- log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
2987
- await core.channel.inbound.run({
2988
- channel: "feishu",
2989
- accountId: route.accountId,
2990
- raw: ctx,
2991
- adapter: {
2992
- ingest: () => ({
2993
- id: ctx.messageId,
2994
- timestamp: messageCreateTimeMs,
2995
- rawText: ctx.content,
2996
- textForAgent: agentCtx.BodyForAgent,
2997
- textForCommands: agentCtx.CommandBody,
2998
- raw: ctx
2999
- }),
3000
- resolveTurn: () => ({
3001
- channel: "feishu",
3002
- accountId: route.accountId,
3003
- routeSessionKey: agentSessionKey,
3004
- storePath: agentStorePath,
3005
- ctxPayload: agentCtx,
3006
- recordInboundSession: core.channel.session.recordInboundSession,
3007
- record: agentRecord,
3008
- runDispatch: () => core.channel.reply.withReplyDispatcher({
3009
- dispatcher: noopDispatcher,
3010
- run: () => core.channel.reply.dispatchReplyFromConfig({
3011
- ctx: agentCtx,
3012
- cfg,
3013
- dispatcher: noopDispatcher
3014
- })
3714
+ }
3715
+ });
3716
+ if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3717
+ await lane.onDispatchComplete(turnResult.dispatched);
3718
+ } else {
3719
+ delete agentCtx.CommandAuthorized;
3720
+ log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
3721
+ const turnResult = await core.channel.inbound.run({
3722
+ channel: "feishu",
3723
+ accountId: route.accountId,
3724
+ raw: ctx,
3725
+ adapter: {
3726
+ ingest: () => ({
3727
+ id: ctx.messageId,
3728
+ timestamp: messageCreateTimeMs,
3729
+ rawText: ctx.content,
3730
+ textForAgent: agentCtx.BodyForAgent,
3731
+ textForCommands: agentCtx.CommandBody,
3732
+ raw: ctx
3733
+ }),
3734
+ resolveTurn: () => ({
3735
+ cfg,
3736
+ channel: "feishu",
3737
+ accountId: route.accountId,
3738
+ route: {
3739
+ agentId,
3740
+ sessionKey: agentSessionKey
3741
+ },
3742
+ ctxPayload: agentCtx,
3743
+ record: agentRecord,
3744
+ admission: {
3745
+ kind: "observeOnly",
3746
+ reason: "broadcast-observer"
3747
+ },
3748
+ delivery: { deliver: async () => ({ visibleReplySent: false }) },
3749
+ replyOptions: bindIngressLifecycleToReplyOptions(lane.lifecycle)
3015
3750
  })
3016
- })
3017
- }
3018
- });
3751
+ }
3752
+ });
3753
+ await lane.onDispatchComplete(turnResult.dispatched);
3754
+ }
3755
+ } catch (err) {
3756
+ await lane.onDispatchFailed(err);
3757
+ throw err;
3019
3758
  }
3020
3759
  };
3760
+ const results = [];
3021
3761
  if (strategy === "sequential") for (const agentId of broadcastAgents) try {
3022
3762
  await dispatchForAgent(agentId);
3023
- } catch (err) {
3024
- log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`);
3763
+ results.push({
3764
+ status: "fulfilled",
3765
+ value: void 0
3766
+ });
3767
+ } catch (reason) {
3768
+ results.push({
3769
+ status: "rejected",
3770
+ reason
3771
+ });
3025
3772
  }
3026
- else {
3027
- const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
3028
- for (const [i, result] of results.entries()) if (result.status === "rejected") {
3029
- const agentId = broadcastAgents.at(i);
3030
- if (agentId === void 0) continue;
3031
- log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
3032
- }
3773
+ else results.push(...await Promise.allSettled(broadcastAgents.map(dispatchForAgent)));
3774
+ const failures = [];
3775
+ for (const [i, result] of results.entries()) if (result.status === "rejected") {
3776
+ const agentId = broadcastAgents.at(i);
3777
+ if (agentId === void 0) continue;
3778
+ log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
3779
+ failures.push(result.reason);
3780
+ }
3781
+ if (failures.length > 0) {
3782
+ const failure = failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed");
3783
+ await abandonBroadcast(failure);
3784
+ throw failure;
3785
+ }
3786
+ try {
3787
+ await broadcastSettlement.onDispatchComplete();
3788
+ } catch (err) {
3789
+ await abandonBroadcast(err);
3790
+ throw err;
3033
3791
  }
3034
- if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
3035
- historyKey,
3036
- limit: historyLimit
3037
- });
3038
3792
  log(`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`);
3039
3793
  } else {
3040
3794
  const ctxPayload = await buildCtxPayloadForAgent(route.agentId, route.sessionKey, route.accountId, ctx.mentionedBot);
3041
3795
  const identity = resolveAgentOutboundIdentity(effectiveCfg, route.agentId);
3042
- const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
3796
+ const storePath = resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
3043
3797
  const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3044
3798
  cfg: effectiveCfg,
3045
3799
  agentId: route.agentId,
3046
3800
  storePath,
3047
3801
  sessionKey: route.sessionKey
3048
3802
  });
3049
- const { dispatcher, replyOptions, markDispatchIdle, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3803
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3050
3804
  cfg: effectiveCfg,
3051
3805
  agentId: route.agentId,
3052
3806
  runtime,
@@ -3062,6 +3816,7 @@ async function handleFeishuMessage(params) {
3062
3816
  accountId: account.accountId,
3063
3817
  identity,
3064
3818
  mentionTargets: ctx.mentionTargets,
3819
+ requiredMentionTargets,
3065
3820
  messageCreateTimeMs,
3066
3821
  sessionKey: route.sessionKey
3067
3822
  });
@@ -3080,12 +3835,14 @@ async function handleFeishuMessage(params) {
3080
3835
  raw: ctx
3081
3836
  }),
3082
3837
  resolveTurn: () => ({
3838
+ cfg: effectiveCfg,
3083
3839
  channel: "feishu",
3084
3840
  accountId: route.accountId,
3085
- routeSessionKey: route.sessionKey,
3086
- storePath,
3841
+ route: {
3842
+ agentId: route.agentId,
3843
+ sessionKey: route.sessionKey
3844
+ },
3087
3845
  ctxPayload,
3088
- recordInboundSession: core.channel.session.recordInboundSession,
3089
3846
  record: {
3090
3847
  updateLastRoute: buildFeishuInboundLastRouteUpdate({
3091
3848
  sessionKey: route.sessionKey,
@@ -3101,40 +3858,24 @@ async function handleFeishuMessage(params) {
3101
3858
  historyMap: chatHistories,
3102
3859
  limit: historyLimit
3103
3860
  },
3104
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
3105
- dispatcher,
3106
- onSettled: () => markDispatchIdle()
3107
- }),
3108
- runDispatch: () => core.channel.reply.withReplyDispatcher({
3109
- dispatcher,
3110
- onSettled: () => {
3111
- markDispatchIdle();
3112
- },
3113
- run: () => core.channel.reply.dispatchReplyFromConfig({
3114
- ctx: ctxPayload,
3115
- cfg: effectiveCfg,
3116
- dispatcher,
3117
- replyOptions
3118
- })
3119
- })
3861
+ dispatcherOptions,
3862
+ delivery,
3863
+ replyOptions: {
3864
+ ...replyOptions,
3865
+ ...turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(turnAdoptionLifecycle) : {}
3866
+ }
3120
3867
  })
3121
3868
  }
3122
3869
  });
3123
3870
  if (!turnResult.dispatched) return;
3124
3871
  const { dispatchResult } = turnResult;
3125
3872
  const { queuedFinal, counts } = dispatchResult;
3126
- if (shouldSendNoVisibleReplyFallback({
3127
- ...dispatchResult,
3128
- failedCounts: dispatcher.getFailedCounts?.() ?? {
3129
- tool: 0,
3130
- block: 0,
3131
- final: 0
3132
- }
3133
- })) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
3873
+ if (shouldSendNoVisibleReplyFallback(dispatchResult)) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
3134
3874
  log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
3135
3875
  }
3136
3876
  } catch (err) {
3137
3877
  error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
3878
+ if (turnAdoptionLifecycle) throw err;
3138
3879
  }
3139
3880
  }
3140
3881
  //#endregion
@@ -3250,7 +3991,7 @@ function completeFeishuCardAction(actionId, accountId, now = Date.now()) {
3250
3991
  expiresAt
3251
3992
  });
3252
3993
  }
3253
- function buildSyntheticMessageEvent(event, content, chatType) {
3994
+ function buildSyntheticMessageEvent$1(event, content, chatType) {
3254
3995
  const replyTargetMessageId = event.context.open_message_id ?? event.open_message_id;
3255
3996
  const isTemporaryCardActionId = replyTargetMessageId?.startsWith("card-action-c-");
3256
3997
  const validReplyTargetId = replyTargetMessageId && !isTemporaryCardActionId ? replyTargetMessageId : void 0;
@@ -3286,7 +4027,7 @@ async function dispatchSyntheticCommand(params) {
3286
4027
  });
3287
4028
  await handleFeishuMessage({
3288
4029
  cfg: params.cfg,
3289
- event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
4030
+ event: buildSyntheticMessageEvent$1(params.event, params.command, resolvedChatType),
3290
4031
  botOpenId: params.botOpenId,
3291
4032
  runtime: params.runtime,
3292
4033
  channelRuntime: params.channelRuntime,
@@ -3484,32 +4225,376 @@ async function handleFeishuCardAction(params) {
3484
4225
  completeFeishuCardAction(event.token, account.accountId);
3485
4226
  return;
3486
4227
  }
3487
- await sendInvalidInteractionNotice({
3488
- cfg,
3489
- event,
3490
- reason: "malformed",
3491
- accountId
3492
- });
3493
- completeFeishuCardAction(event.token, account.accountId);
3494
- return;
4228
+ await sendInvalidInteractionNotice({
4229
+ cfg,
4230
+ event,
4231
+ reason: "malformed",
4232
+ accountId
4233
+ });
4234
+ completeFeishuCardAction(event.token, account.accountId);
4235
+ return;
4236
+ }
4237
+ const content = buildFeishuCardActionTextFallback(event);
4238
+ log(`feishu[${account.accountId}]: handling card action from ${event.operator.open_id}: ${content}`);
4239
+ await dispatchSyntheticCommand({
4240
+ cfg,
4241
+ event,
4242
+ command: content,
4243
+ account,
4244
+ botOpenId: params.botOpenId,
4245
+ runtime,
4246
+ channelRuntime: params.channelRuntime,
4247
+ accountId
4248
+ });
4249
+ completeFeishuCardAction(event.token, account.accountId);
4250
+ } catch (err) {
4251
+ completeFeishuCardAction(event.token, account.accountId);
4252
+ throw err;
4253
+ }
4254
+ }
4255
+ //#endregion
4256
+ //#region extensions/feishu/src/feishu-ingress.ts
4257
+ const FEISHU_INGRESS_PAYLOAD_VERSION = 1;
4258
+ const FEISHU_INGRESS_POLL_INTERVAL_MS = 500;
4259
+ const FEISHU_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
4260
+ const FEISHU_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
4261
+ const FEISHU_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
4262
+ const FEISHU_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
4263
+ const FEISHU_INGRESS_FAILED_MAX_ENTRIES = 2e4;
4264
+ const FEISHU_DURABLE_EVENT_TYPES = /* @__PURE__ */ new Set(["drive.notice.comment_add_v1", "im.message.receive_v1"]);
4265
+ var FeishuIngressPermanentError = class extends Error {
4266
+ constructor(reason, message, options) {
4267
+ super(message, options);
4268
+ this.reason = reason;
4269
+ this.name = "FeishuIngressPermanentError";
4270
+ }
4271
+ };
4272
+ function isRecord$1(value) {
4273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4274
+ }
4275
+ function readString(value) {
4276
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4277
+ }
4278
+ function parseRawEnvelope(rawEnvelope) {
4279
+ let parsed;
4280
+ try {
4281
+ parsed = JSON.parse(rawEnvelope);
4282
+ } catch (error) {
4283
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope contains invalid JSON.", { cause: error });
4284
+ }
4285
+ if (!isRecord$1(parsed)) throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope must be a JSON object.");
4286
+ return parsed;
4287
+ }
4288
+ function decryptEnvelope(envelope, encryptKey) {
4289
+ const encrypted = readString(envelope.encrypt);
4290
+ if (!encrypted) return envelope;
4291
+ const key = encryptKey?.trim();
4292
+ if (!key) throw new FeishuIngressPermanentError("authentication-failed", "Feishu encrypted ingress envelope has no configured encrypt key.");
4293
+ try {
4294
+ return parseRawEnvelope(new Lark.AESCipher(key).decrypt(encrypted));
4295
+ } catch (error) {
4296
+ if (error instanceof FeishuIngressPermanentError) throw error;
4297
+ throw new FeishuIngressPermanentError("authentication-failed", "Feishu ingress envelope decryption failed.", { cause: error });
4298
+ }
4299
+ }
4300
+ function inspectFeishuIngressEnvelope(rawEnvelope, encryptKey, allowInvalidLane = false) {
4301
+ const envelope = decryptEnvelope(parseRawEnvelope(rawEnvelope), encryptKey);
4302
+ const nestedHeader = isRecord$1(envelope.header) ? envelope.header : null;
4303
+ const nestedEvent = isRecord$1(envelope.event) ? envelope.event : null;
4304
+ const eventType = readString(nestedHeader?.event_type) ?? readString(envelope.event_type);
4305
+ if (!eventType || !FEISHU_DURABLE_EVENT_TYPES.has(eventType)) return null;
4306
+ const eventId = readString(nestedHeader?.event_id) ?? readString(envelope.event_id);
4307
+ if (!eventId) throw new FeishuIngressPermanentError("invalid-event", `Feishu ${eventType} envelope is missing header.event_id.`);
4308
+ const event = nestedEvent ?? envelope;
4309
+ if (eventType === "im.message.receive_v1") {
4310
+ const chatId = readString((isRecord$1(event.message) ? event.message : null)?.chat_id);
4311
+ if (!chatId) {
4312
+ if (allowInvalidLane) return {
4313
+ eventId,
4314
+ eventType,
4315
+ laneKey: `invalid:${eventType}:${eventId}`
4316
+ };
4317
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu message ingress envelope is missing event.message.chat_id.");
4318
+ }
4319
+ return {
4320
+ eventId,
4321
+ eventType,
4322
+ laneKey: `chat:${chatId}`
4323
+ };
4324
+ }
4325
+ const noticeMeta = isRecord$1(event.notice_meta) ? event.notice_meta : null;
4326
+ const fileType = readString(noticeMeta?.file_type);
4327
+ const documentId = readString(noticeMeta?.file_token);
4328
+ if (!fileType || !documentId) {
4329
+ if (allowInvalidLane) return {
4330
+ eventId,
4331
+ eventType,
4332
+ laneKey: `invalid:${eventType}:${eventId}`
4333
+ };
4334
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu comment ingress envelope is missing its document identity.");
4335
+ }
4336
+ return {
4337
+ eventId,
4338
+ eventType,
4339
+ laneKey: `comment-doc:${fileType}:${documentId}`
4340
+ };
4341
+ }
4342
+ function resolveFeishuIngressNonRetryableFailure(error) {
4343
+ if (error instanceof FeishuIngressPermanentError) return {
4344
+ reason: error.reason,
4345
+ message: error.message
4346
+ };
4347
+ for (const candidate of collectErrorGraphCandidates(error, (current) => [current.cause, current.response])) {
4348
+ const status = isRecord$1(candidate) ? candidate.status : void 0;
4349
+ if (status === 401 || status === 403) return {
4350
+ reason: "authentication-failed",
4351
+ message: formatErrorMessage(error)
4352
+ };
4353
+ }
4354
+ return null;
4355
+ }
4356
+ /** Fan one merged Feishu turn's adoption across every transport and logical claim. */
4357
+ function buildFeishuFlushIngressLifecycle(sources, options) {
4358
+ const durableSources = sources.filter((source) => source.lifecycle !== void 0);
4359
+ const lifecycles = durableSources.map((source) => source.lifecycle);
4360
+ const replayClaims = durableSources.map((source) => source.replayClaim).filter((claim) => claim !== void 0);
4361
+ const [firstLifecycle] = lifecycles;
4362
+ if (!firstLifecycle) return {
4363
+ lifecycle: void 0,
4364
+ settle: async () => {}
4365
+ };
4366
+ let handedOff = false;
4367
+ let terminal;
4368
+ let adopting;
4369
+ let abandoning;
4370
+ const releaseReplayClaims = () => {
4371
+ for (const claim of replayClaims) claim.release({ error: /* @__PURE__ */ new Error("feishu-ingress-not-adopted") });
4372
+ };
4373
+ const runAbandon = async () => {
4374
+ if (terminal) return;
4375
+ releaseReplayClaims();
4376
+ await Promise.all(lifecycles.map(async (lifecycle) => await lifecycle.onAbandoned()));
4377
+ terminal = "abandoned";
4378
+ };
4379
+ const ensureAbandoned = async () => {
4380
+ if (terminal) return;
4381
+ const activeAbandonment = abandoning ?? runAbandon();
4382
+ abandoning = activeAbandonment;
4383
+ try {
4384
+ await activeAbandonment;
4385
+ } finally {
4386
+ if (abandoning === activeAbandonment && terminal !== "abandoned") abandoning = void 0;
4387
+ }
4388
+ };
4389
+ const abandonAll = async () => {
4390
+ if (terminal) return;
4391
+ if (adopting) {
4392
+ await adopting.catch(() => void 0);
4393
+ if (terminal) return;
4394
+ }
4395
+ await ensureAbandoned();
4396
+ };
4397
+ const adoptAll = async () => {
4398
+ if (terminal) return;
4399
+ if (abandoning) {
4400
+ await abandoning.catch(() => void 0);
4401
+ if (terminal) return;
4402
+ }
4403
+ const activeAdoption = adopting ?? (async () => {
4404
+ try {
4405
+ for (const lifecycle of lifecycles) await lifecycle.onAdopted();
4406
+ terminal = "adopted";
4407
+ const results = await Promise.allSettled(replayClaims.map(async (claim) => claim.commit()));
4408
+ for (const result of results) if (result.status === "rejected") try {
4409
+ options?.onReplayCommitError?.(result.reason);
4410
+ } catch {}
4411
+ } catch (error) {
4412
+ await ensureAbandoned().catch(() => void 0);
4413
+ throw error;
4414
+ }
4415
+ })();
4416
+ adopting = activeAdoption;
4417
+ try {
4418
+ await activeAdoption;
4419
+ } finally {
4420
+ if (adopting === activeAdoption && terminal !== "adopted") adopting = void 0;
4421
+ }
4422
+ };
4423
+ return {
4424
+ lifecycle: {
4425
+ abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
4426
+ onAdopted: async () => {
4427
+ handedOff = true;
4428
+ await adoptAll();
4429
+ },
4430
+ onDeferred: () => {
4431
+ handedOff = true;
4432
+ for (const lifecycle of lifecycles) lifecycle.onDeferred();
4433
+ },
4434
+ onAdoptionFinalizing: () => {
4435
+ for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
4436
+ },
4437
+ onAbandoned: async () => {
4438
+ handedOff = true;
4439
+ await abandonAll();
4440
+ }
4441
+ },
4442
+ settle: async () => {
4443
+ if (handedOff) return;
4444
+ handedOff = true;
4445
+ releaseReplayClaims();
4446
+ try {
4447
+ for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
4448
+ for (const lifecycle of lifecycles) await lifecycle.onAdopted();
4449
+ terminal = "adopted";
4450
+ } catch (error) {
4451
+ await ensureAbandoned().catch(() => void 0);
4452
+ throw error;
4453
+ }
4454
+ }
4455
+ };
4456
+ }
4457
+ function createFeishuDurableIngress(options) {
4458
+ let socketTerminator;
4459
+ const activeLifecycles = /* @__PURE__ */ new Map();
4460
+ const deferredClaims = /* @__PURE__ */ new Map();
4461
+ const monitor = createChannelIngressMonitor({
4462
+ queue: options.queue ?? (() => getFeishuRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
4463
+ inspect: (rawEnvelope, context) => {
4464
+ const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, context.phase === "admission");
4465
+ return facts ? {
4466
+ eventId: facts.eventId,
4467
+ laneKey: facts.laneKey
4468
+ } : null;
4469
+ },
4470
+ payload: {
4471
+ version: FEISHU_INGRESS_PAYLOAD_VERSION,
4472
+ serialize: (rawEnvelope, { receivedAt }) => ({
4473
+ receivedAt,
4474
+ rawEnvelope
4475
+ }),
4476
+ deserialize: (body) => body.rawEnvelope,
4477
+ encode: ({ body }) => ({
4478
+ version: FEISHU_INGRESS_PAYLOAD_VERSION,
4479
+ ...body
4480
+ }),
4481
+ decode: (payload) => ({
4482
+ version: payload.version,
4483
+ body: {
4484
+ receivedAt: payload.receivedAt,
4485
+ rawEnvelope: payload.rawEnvelope
4486
+ }
4487
+ }),
4488
+ 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.`)
4489
+ },
4490
+ deliver: async (rawEnvelope, lifecycle, claim) => {
4491
+ let resolveDeferred;
4492
+ const deferred = new Promise((resolve) => {
4493
+ resolveDeferred = resolve;
4494
+ });
4495
+ let deferredSettled = false;
4496
+ const settleDeferred = () => {
4497
+ if (deferredSettled) return;
4498
+ deferredSettled = true;
4499
+ if (deferredClaims.get(claim.id) === deferred) deferredClaims.delete(claim.id);
4500
+ resolveDeferred();
4501
+ };
4502
+ const abandonHandlers = /* @__PURE__ */ new Set();
4503
+ const wrappedLifecycle = {
4504
+ ...lifecycle,
4505
+ onAdopted: async () => {
4506
+ try {
4507
+ await lifecycle.onAdopted();
4508
+ } finally {
4509
+ settleDeferred();
4510
+ }
4511
+ },
4512
+ onAbandoned: async () => {
4513
+ try {
4514
+ await Promise.allSettled([...abandonHandlers].map(async (handler) => await handler()));
4515
+ await lifecycle.onAbandoned();
4516
+ } finally {
4517
+ settleDeferred();
4518
+ }
4519
+ },
4520
+ registerAbandonHandler: (handler) => {
4521
+ abandonHandlers.add(handler);
4522
+ return () => abandonHandlers.delete(handler);
4523
+ }
4524
+ };
4525
+ activeLifecycles.set(claim.id, wrappedLifecycle);
4526
+ try {
4527
+ const result = await options.dispatcher.invoke(parseRawEnvelope(rawEnvelope), { needCheck: false });
4528
+ if (!isRecord$1(result)) return;
4529
+ if (result.kind === "deferred") {
4530
+ if (!deferredSettled) deferredClaims.set(claim.id, deferred);
4531
+ return { kind: "deferred" };
4532
+ }
4533
+ if (result.kind === "completed") return { kind: "completed" };
4534
+ if (result.kind === "failed-retryable") return {
4535
+ kind: "failed-retryable",
4536
+ error: result.error
4537
+ };
4538
+ return;
4539
+ } finally {
4540
+ if (activeLifecycles.get(claim.id) === wrappedLifecycle) activeLifecycles.delete(claim.id);
4541
+ }
4542
+ },
4543
+ pollIntervalMs: options.pollIntervalMs ?? FEISHU_INGRESS_POLL_INTERVAL_MS,
4544
+ retention: {
4545
+ pruneIntervalMs: FEISHU_INGRESS_PRUNE_INTERVAL_MS,
4546
+ completedTtlMs: FEISHU_INGRESS_COMPLETED_TTL_MS,
4547
+ completedMaxEntries: FEISHU_INGRESS_COMPLETED_MAX_ENTRIES,
4548
+ failedTtlMs: FEISHU_INGRESS_FAILED_TTL_MS,
4549
+ failedMaxEntries: FEISHU_INGRESS_FAILED_MAX_ENTRIES
4550
+ },
4551
+ drain: {
4552
+ adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
4553
+ resolveNonRetryableFailure: resolveFeishuIngressNonRetryableFailure,
4554
+ onLog: (message) => options.runtime.error?.(`feishu ingress: ${message}`)
4555
+ },
4556
+ createStoppedError: () => /* @__PURE__ */ new Error("Feishu ingress is stopped."),
4557
+ onError: (error) => options.runtime.error?.(`feishu ingress drain failed: ${formatErrorMessage(error)}`)
4558
+ });
4559
+ const invoke = async (data, params) => {
4560
+ let rawEnvelope;
4561
+ try {
4562
+ const serialized = JSON.stringify(data);
4563
+ if (serialized === void 0) throw new TypeError("Feishu ingress envelope has no JSON representation.");
4564
+ rawEnvelope = serialized;
4565
+ } catch (error) {
4566
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope is not serializable.", { cause: error });
3495
4567
  }
3496
- const content = buildFeishuCardActionTextFallback(event);
3497
- log(`feishu[${account.accountId}]: handling card action from ${event.operator.open_id}: ${content}`);
3498
- await dispatchSyntheticCommand({
3499
- cfg,
3500
- event,
3501
- command: content,
3502
- account,
3503
- botOpenId: params.botOpenId,
3504
- runtime,
3505
- channelRuntime: params.channelRuntime,
3506
- accountId
3507
- });
3508
- completeFeishuCardAction(event.token, account.accountId);
3509
- } catch (err) {
3510
- completeFeishuCardAction(event.token, account.accountId);
3511
- throw err;
3512
- }
4568
+ const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, true);
4569
+ if (!facts) return await options.dispatcher.invoke(data, params);
4570
+ try {
4571
+ await monitor.admit(rawEnvelope, { facts: {
4572
+ eventId: facts.eventId,
4573
+ laneKey: facts.laneKey
4574
+ } });
4575
+ } catch (error) {
4576
+ socketTerminator?.();
4577
+ throw error;
4578
+ }
4579
+ };
4580
+ return {
4581
+ invoke,
4582
+ resolveLifecycle: (data) => {
4583
+ const eventId = isRecord$1(data) ? readString(data.event_id) : null;
4584
+ return eventId ? activeLifecycles.get(eventId) : void 0;
4585
+ },
4586
+ setSocketTerminator: (terminate) => {
4587
+ socketTerminator = terminate;
4588
+ },
4589
+ start: monitor.start,
4590
+ stop: async () => {
4591
+ await monitor.stop();
4592
+ await Promise.allSettled(deferredClaims.values());
4593
+ activeLifecycles.clear();
4594
+ socketTerminator = void 0;
4595
+ },
4596
+ waitForIdle: monitor.waitForIdle
4597
+ };
3513
4598
  }
3514
4599
  //#endregion
3515
4600
  //#region extensions/feishu/src/monitor-defaults.ts
@@ -3662,7 +4747,8 @@ function applyBotIdentityState(accountId, identity) {
3662
4747
  });
3663
4748
  return {
3664
4749
  botOpenId,
3665
- botName
4750
+ botName,
4751
+ source: botOpenId ? identity.source : void 0
3666
4752
  };
3667
4753
  }
3668
4754
  async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
@@ -3672,12 +4758,13 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
3672
4758
  for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) {
3673
4759
  if (abortSignal?.aborted) return;
3674
4760
  if (!await waitForAbortableDelay(delayMs, abortSignal)) return;
3675
- const resolved = applyBotIdentityState(accountId, await fetchBotIdentityForMonitor(account, {
4761
+ const identity = await fetchBotIdentityForMonitor(account, {
3676
4762
  runtime,
3677
- abortSignal
3678
- }));
3679
- if (resolved.botOpenId) {
3680
- log(`feishu[${accountId}]: bot open_id recovered via background retry: ${resolved.botOpenId}`);
4763
+ abortSignal,
4764
+ allowCachedFallback: false
4765
+ });
4766
+ if (normalizeOptionalString(identity.botOpenId) && identity.source === "provider") {
4767
+ log(`feishu[${accountId}]: bot open_id recovered via background retry: ${applyBotIdentityState(accountId, identity).botOpenId}`);
3681
4768
  return;
3682
4769
  }
3683
4770
  const nextDelayResult = nextDelays.next();
@@ -3687,10 +4774,10 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
3687
4774
  error(`feishu[${accountId}]: bot identity background retry exhausted; requireMention group messages may be skipped until restart`);
3688
4775
  }
3689
4776
  function startBotIdentityRecovery(params) {
3690
- const { account, accountId, runtime, abortSignal } = params;
4777
+ const { account, accountId, runtime, abortSignal, currentSource } = params;
3691
4778
  const log = runtime?.log ?? console.log;
3692
- log(`feishu[${accountId}]: bot open_id unknown; starting background retry (delays: ${BOT_IDENTITY_RETRY_DELAYS_MS.map((delay) => `${delay / 1e3}s`).join(", ")})`);
3693
- log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
4779
+ 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(", ")})`);
4780
+ if (currentSource !== "cache") log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
3694
4781
  retryBotIdentityProbe(account, accountId, runtime, abortSignal);
3695
4782
  }
3696
4783
  //#endregion
@@ -3931,41 +5018,59 @@ function createFeishuCommentReplyDispatcher(params) {
3931
5018
  accountId: params.accountId,
3932
5019
  runtime: params.runtime
3933
5020
  });
3934
- const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
3935
- responsePrefix: prefixContext.responsePrefix,
3936
- responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
3937
- humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
3938
- onReplyStart: async () => {
3939
- await typingReaction.start();
3940
- },
3941
- deliver: async (payload, info) => {
3942
- if (info.kind !== "final") return;
3943
- const reply = resolveSendableOutboundReplyParts(payload);
3944
- if (!reply.hasText) {
3945
- if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
3946
- return;
5021
+ return {
5022
+ dispatcherOptions: {
5023
+ responsePrefix: prefixContext.responsePrefix,
5024
+ responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
5025
+ humanDelay: resolveHumanDelayConfig(params.cfg, params.agentId),
5026
+ onReplyStart: async () => {
5027
+ await typingReaction.start();
5028
+ },
5029
+ onCleanup: () => {
5030
+ typingReaction.cleanup();
3947
5031
  }
3948
- const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
3949
- for (const chunk of chunks) await deliverCommentThreadText(client, {
3950
- file_token: params.fileToken,
3951
- file_type: params.fileType,
3952
- comment_id: params.commentId,
3953
- content: chunk,
3954
- is_whole_comment: params.isWholeComment
3955
- });
3956
5032
  },
3957
- onError: (err, info) => {
3958
- params.runtime.error?.(`feishu[${params.accountId ?? "default"}]: comment dispatcher failed kind=${info.kind} comment=${params.commentId}: ${String(err)}`);
5033
+ delivery: {
5034
+ observeMessageSent: true,
5035
+ deliver: async (payload, info) => {
5036
+ if (info.kind !== "final") return noVisibleFeishuReplyDelivery;
5037
+ const reply = resolveSendableOutboundReplyParts(payload);
5038
+ if (!reply.hasText) {
5039
+ if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
5040
+ return noVisibleFeishuReplyDelivery;
5041
+ }
5042
+ const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
5043
+ const results = [];
5044
+ const acceptedChunks = [];
5045
+ for (const chunk of chunks) try {
5046
+ const result = await deliverCommentThreadText(client, {
5047
+ file_token: params.fileToken,
5048
+ file_type: params.fileType,
5049
+ comment_id: params.commentId,
5050
+ content: chunk,
5051
+ is_whole_comment: params.isWholeComment
5052
+ });
5053
+ results.push({ messageId: result.delivery_mode === "reply_comment" ? result.reply_id : result.comment_id });
5054
+ acceptedChunks.push(chunk);
5055
+ } catch (error) {
5056
+ throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
5057
+ results,
5058
+ visibleReplySent: results.length > 0,
5059
+ content: acceptedChunks.join(""),
5060
+ kind: "text"
5061
+ }));
5062
+ }
5063
+ return createFeishuReplyDeliveryResult({
5064
+ results,
5065
+ visibleReplySent: results.length > 0,
5066
+ content: reply.text,
5067
+ kind: "text"
5068
+ });
5069
+ },
5070
+ onError: (err, info) => {
5071
+ params.runtime.error?.(`feishu[${params.accountId ?? "default"}]: comment dispatcher failed kind=${info.kind} comment=${params.commentId}: ${String(err)}`);
5072
+ }
3959
5073
  },
3960
- onCleanup: () => {
3961
- typingReaction.cleanup();
3962
- }
3963
- });
3964
- return {
3965
- dispatcher,
3966
- replyOptions,
3967
- markDispatchIdle,
3968
- markRunComplete,
3969
5074
  startTypingReaction: typingReaction.start,
3970
5075
  cleanupTypingReaction: typingReaction.cleanup
3971
5076
  };
@@ -4125,18 +5230,18 @@ async function fetchDriveComments(params) {
4125
5230
  }
4126
5231
  async function requestFeishuOpenApi(params) {
4127
5232
  const formatErrorDetails = (error) => {
4128
- if (!isRecord$1(error)) return typeof error === "string" ? error : JSON.stringify(error);
4129
- const response = isRecord$1(error.response) ? error.response : void 0;
4130
- const responseData = isRecord$1(response?.data) ? response?.data : void 0;
5233
+ if (!isRecord$2(error)) return typeof error === "string" ? error : JSON.stringify(error);
5234
+ const response = isRecord$2(error.response) ? error.response : void 0;
5235
+ const responseData = isRecord$2(response?.data) ? response?.data : void 0;
4131
5236
  return safeJsonStringify({
4132
5237
  message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
4133
- code: readString(error.code),
4134
- method: readString(isRecord$1(error.config) ? error.config.method : void 0),
4135
- url: readString(isRecord$1(error.config) ? error.config.url : void 0),
5238
+ code: readString$1(error.code),
5239
+ method: readString$1(isRecord$2(error.config) ? error.config.method : void 0),
5240
+ url: readString$1(isRecord$2(error.config) ? error.config.url : void 0),
4136
5241
  http_status: typeof response?.status === "number" ? response.status : void 0,
4137
- feishu_code: typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
4138
- feishu_msg: readString(responseData?.msg),
4139
- feishu_log_id: readString(responseData?.log_id)
5242
+ feishu_code: typeof responseData?.code === "number" ? responseData.code : readString$1(responseData?.code),
5243
+ feishu_msg: readString$1(responseData?.msg),
5244
+ feishu_log_id: readString$1(responseData?.log_id)
4140
5245
  });
4141
5246
  };
4142
5247
  const result = await raceWithTimeoutAndAbort(params.client.request({
@@ -4192,7 +5297,7 @@ async function resolveCommentReplyContext(params) {
4192
5297
  createTime: typeof params.reply.create_time === "number" ? params.reply.create_time : void 0,
4193
5298
  isBotAuthored: typeof userId === "string" && normalizedBotOpenIds.has(userId),
4194
5299
  content: await resolveParsedCommentContent({
4195
- elements: isRecord$1(params.reply.content) ? params.reply.content.elements : void 0,
5300
+ elements: isRecord$2(params.reply.content) ? params.reply.content.elements : void 0,
4196
5301
  botOpenIds: params.botOpenIds,
4197
5302
  currentDocument: params.currentDocument,
4198
5303
  client: params.client,
@@ -4364,7 +5469,7 @@ async function fetchDriveCommentContext(params) {
4364
5469
  const rootWholeReply = comment.reply_list?.replies?.[0];
4365
5470
  const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeString(botId)).filter((botId) => Boolean(botId)));
4366
5471
  const content = await resolveParsedCommentContent({
4367
- elements: isRecord$1(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
5472
+ elements: isRecord$2(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
4368
5473
  botOpenIds: params.botOpenIds,
4369
5474
  currentDocument,
4370
5475
  client: params.client,
@@ -4471,11 +5576,14 @@ async function resolveDriveCommentEventCore(params) {
4471
5576
  logger?.(`feishu[${accountId}]: unsupported drive comment notice type ${noticeType}`);
4472
5577
  return null;
4473
5578
  }
4474
- if (!botOpenId) {
5579
+ const configuredBotOpenId = botOpenId?.trim();
5580
+ const eventRecipientBotOpenId = event.notice_meta?.to_user_id?.open_id?.trim();
5581
+ const effectiveBotOpenId = configuredBotOpenId || (event.is_mentioned === true ? eventRecipientBotOpenId : void 0);
5582
+ if (!effectiveBotOpenId) {
4475
5583
  logger?.(`feishu[${accountId}]: skipping drive comment notice because bot open_id is unavailable event=${eventId}`);
4476
5584
  return null;
4477
5585
  }
4478
- if (senderId === botOpenId) {
5586
+ if (senderId === effectiveBotOpenId) {
4479
5587
  logger?.(`feishu[${accountId}]: ignoring self-authored drive comment notice event=${eventId} sender=${senderId}`);
4480
5588
  return null;
4481
5589
  }
@@ -4488,7 +5596,7 @@ async function resolveDriveCommentEventCore(params) {
4488
5596
  fileType,
4489
5597
  commentId,
4490
5598
  replyId,
4491
- botOpenIds: [botOpenId, event.notice_meta?.to_user_id?.open_id],
5599
+ botOpenIds: [effectiveBotOpenId, event.notice_meta?.to_user_id?.open_id],
4492
5600
  timeoutMs: verificationTimeoutMs,
4493
5601
  logger,
4494
5602
  accountId,
@@ -4510,32 +5618,32 @@ async function resolveDriveCommentEventCore(params) {
4510
5618
  };
4511
5619
  }
4512
5620
  function parseFeishuDriveCommentNoticeEventPayload(value) {
4513
- if (!isRecord$1(value) || !isRecord$1(value.notice_meta)) return null;
5621
+ if (!isRecord$2(value) || !isRecord$2(value.notice_meta)) return null;
4514
5622
  const noticeMeta = value.notice_meta;
4515
- const fromUserId = isRecord$1(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
4516
- const toUserId = isRecord$1(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
5623
+ const fromUserId = isRecord$2(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
5624
+ const toUserId = isRecord$2(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
4517
5625
  return {
4518
- comment_id: readString(value.comment_id),
4519
- event_id: readString(value.event_id),
5626
+ comment_id: readString$1(value.comment_id),
5627
+ event_id: readString$1(value.event_id),
4520
5628
  is_mentioned: asBoolean(value.is_mentioned),
4521
5629
  notice_meta: {
4522
- file_token: readString(noticeMeta.file_token),
4523
- file_type: readString(noticeMeta.file_type),
5630
+ file_token: readString$1(noticeMeta.file_token),
5631
+ file_type: readString$1(noticeMeta.file_type),
4524
5632
  from_user_id: fromUserId ? {
4525
- open_id: readString(fromUserId.open_id),
4526
- user_id: readString(fromUserId.user_id),
4527
- union_id: readString(fromUserId.union_id)
5633
+ open_id: readString$1(fromUserId.open_id),
5634
+ user_id: readString$1(fromUserId.user_id),
5635
+ union_id: readString$1(fromUserId.union_id)
4528
5636
  } : void 0,
4529
- notice_type: readString(noticeMeta.notice_type),
5637
+ notice_type: readString$1(noticeMeta.notice_type),
4530
5638
  to_user_id: toUserId ? {
4531
- open_id: readString(toUserId.open_id),
4532
- user_id: readString(toUserId.user_id),
4533
- union_id: readString(toUserId.union_id)
5639
+ open_id: readString$1(toUserId.open_id),
5640
+ user_id: readString$1(toUserId.user_id),
5641
+ union_id: readString$1(toUserId.union_id)
4534
5642
  } : void 0
4535
5643
  },
4536
- reply_id: readString(value.reply_id),
4537
- timestamp: readString(value.timestamp),
4538
- type: readString(value.type)
5644
+ reply_id: readString$1(value.reply_id),
5645
+ timestamp: readString$1(value.timestamp),
5646
+ type: readString$1(value.type)
4539
5647
  };
4540
5648
  }
4541
5649
  async function resolveDriveCommentEventTurn(params) {
@@ -4744,31 +5852,51 @@ async function handleFeishuCommentEvent(params) {
4744
5852
  fileToken: turn.fileToken
4745
5853
  });
4746
5854
  const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
4747
- const ctxPayload = core.channel.reply.finalizeInboundContext({
4748
- Body: bodyForAgent,
4749
- BodyForAgent: bodyForAgent,
4750
- RawBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
4751
- CommandBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
4752
- From: `feishu:${turn.senderId}`,
4753
- To: commentTarget,
4754
- SessionKey: commentSessionKey,
4755
- AccountId: route.accountId,
4756
- ChatType: "direct",
4757
- ConversationLabel: turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment",
4758
- SenderName: turn.senderId,
4759
- SenderId: turn.senderId,
4760
- Provider: "feishu",
4761
- Surface: "feishu-comment",
4762
- MessageSid: turn.messageId,
4763
- MessageThreadId: turn.replyId,
4764
- Timestamp: parseTimestampMs(turn.timestamp),
4765
- WasMentioned: turn.isMentioned,
4766
- CommandAuthorized: false,
4767
- OriginatingChannel: "feishu",
4768
- OriginatingTo: commentTarget
5855
+ const rawBody = turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt;
5856
+ const conversationLabel = turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment";
5857
+ const ctxPayload = buildChannelInboundEventContext({
5858
+ channel: "feishu",
5859
+ accountId: route.accountId,
5860
+ surface: "feishu-comment",
5861
+ messageId: turn.messageId,
5862
+ timestamp: parseTimestampMs(turn.timestamp),
5863
+ from: `feishu:${turn.senderId}`,
5864
+ sender: {
5865
+ id: turn.senderId,
5866
+ name: turn.senderId
5867
+ },
5868
+ conversation: {
5869
+ kind: "direct",
5870
+ id: commentTarget,
5871
+ label: conversationLabel
5872
+ },
5873
+ route: {
5874
+ agentId: route.agentId,
5875
+ dmScope: route.dmScope,
5876
+ accountId: route.accountId,
5877
+ routeSessionKey: commentSessionKey,
5878
+ dispatchSessionKey: commentSessionKey
5879
+ },
5880
+ reply: {
5881
+ to: commentTarget,
5882
+ originatingTo: commentTarget,
5883
+ messageThreadId: turn.replyId
5884
+ },
5885
+ message: {
5886
+ body: bodyForAgent,
5887
+ bodyForAgent,
5888
+ rawBody,
5889
+ commandBody: rawBody
5890
+ },
5891
+ access: {
5892
+ commands: { authorized: false },
5893
+ mentions: {
5894
+ canDetectMention: true,
5895
+ wasMentioned: turn.isMentioned ?? false
5896
+ }
5897
+ }
4769
5898
  });
4770
- const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
4771
- const { dispatcher, replyOptions, markDispatchIdle, markRunComplete, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
5899
+ const { dispatcherOptions, delivery, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
4772
5900
  cfg: effectiveCfg,
4773
5901
  agentId: route.agentId,
4774
5902
  runtime,
@@ -4779,7 +5907,6 @@ async function handleFeishuCommentEvent(params) {
4779
5907
  replyId: turn.replyId,
4780
5908
  isWholeComment: turn.isWholeComment
4781
5909
  });
4782
- let dispatchSettledBeforeStart = false;
4783
5910
  try {
4784
5911
  log(`feishu[${account.accountId}]: dispatching drive comment to agent (session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`);
4785
5912
  const turnResult = await core.channel.inbound.run({
@@ -4796,34 +5923,20 @@ async function handleFeishuCommentEvent(params) {
4796
5923
  raw: turn
4797
5924
  }),
4798
5925
  resolveTurn: () => ({
5926
+ cfg: effectiveCfg,
4799
5927
  channel: "feishu",
4800
5928
  accountId: route.accountId,
4801
- routeSessionKey: commentSessionKey,
4802
- storePath,
5929
+ route: {
5930
+ agentId: route.agentId,
5931
+ sessionKey: commentSessionKey
5932
+ },
4803
5933
  ctxPayload,
4804
- recordInboundSession: core.channel.session.recordInboundSession,
4805
5934
  record: { onRecordError: (err) => {
4806
5935
  error(`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`);
4807
5936
  } },
4808
- onPreDispatchFailure: async () => {
4809
- dispatchSettledBeforeStart = true;
4810
- await core.channel.reply.settleReplyDispatcher({
4811
- dispatcher,
4812
- onSettled: () => {
4813
- markRunComplete();
4814
- markDispatchIdle();
4815
- }
4816
- });
4817
- },
4818
- runDispatch: () => core.channel.reply.withReplyDispatcher({
4819
- dispatcher,
4820
- run: () => core.channel.reply.dispatchReplyFromConfig({
4821
- ctx: ctxPayload,
4822
- cfg: effectiveCfg,
4823
- dispatcher,
4824
- replyOptions
4825
- })
4826
- })
5937
+ dispatcherOptions,
5938
+ delivery,
5939
+ ...params.turnAdoptionLifecycle ? { replyOptions: bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) } : {}
4827
5940
  })
4828
5941
  }
4829
5942
  });
@@ -4836,10 +5949,6 @@ async function handleFeishuCommentEvent(params) {
4836
5949
  };
4837
5950
  log(`feishu[${account.accountId}]: drive comment dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`);
4838
5951
  } finally {
4839
- if (!dispatchSettledBeforeStart) {
4840
- markRunComplete();
4841
- markDispatchIdle();
4842
- }
4843
5952
  cleanupTypingReaction();
4844
5953
  }
4845
5954
  }
@@ -4914,52 +6023,40 @@ function createFeishuDriveCommentNoticeHandler(params) {
4914
6023
  });
4915
6024
  if (!fireAndForget) await promise;
4916
6025
  };
4917
- return async (data) => {
4918
- await runFeishuHandler(async () => {
4919
- const event = parseFeishuDriveCommentNoticeEventPayload(data);
4920
- if (!event) {
4921
- error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
6026
+ const handleNotice = async (data, turnAdoptionLifecycle) => {
6027
+ const event = parseFeishuDriveCommentNoticeEventPayload(data);
6028
+ if (!event) {
6029
+ if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable comment event payload is malformed.");
6030
+ error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
6031
+ return;
6032
+ }
6033
+ 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"}`);
6034
+ await enqueue(buildCommentNoticeQueueKey(event), async () => {
6035
+ if (turnAdoptionLifecycle?.abortSignal.aborted) {
6036
+ await turnAdoptionLifecycle.onAbandoned();
4922
6037
  return;
4923
6038
  }
4924
- const eventId = event.event_id?.trim();
4925
- const syntheticMessageId = eventId ? `drive-comment:${eventId}` : void 0;
4926
- let processingClaim;
4927
- if (syntheticMessageId) {
4928
- const claim = await claimUnprocessedFeishuMessage({
4929
- messageId: syntheticMessageId,
4930
- namespace: accountId,
4931
- log
4932
- });
4933
- if (claim.kind === "duplicate") {
4934
- log(`feishu[${accountId}]: dropping duplicate comment event ${syntheticMessageId}`);
4935
- return;
4936
- }
4937
- if (claim.kind === "inflight") {
4938
- log(`feishu[${accountId}]: dropping in-flight comment event ${syntheticMessageId}`);
4939
- return;
4940
- }
4941
- processingClaim = claim.kind === "claimed" ? claim.handle : void 0;
4942
- }
4943
- 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"}`);
4944
- try {
4945
- await enqueue(buildCommentNoticeQueueKey(event), async () => {
4946
- await handleFeishuCommentEvent({
4947
- cfg,
4948
- accountId,
4949
- event,
4950
- botOpenId: getBotOpenId(accountId),
4951
- runtime,
4952
- abortSignal
4953
- });
4954
- });
4955
- await processingClaim?.commit();
4956
- } catch (err) {
4957
- if (isFeishuRetryableSyntheticEventError(err)) processingClaim?.release({ error: err });
4958
- else await processingClaim?.commit();
4959
- throw err;
4960
- }
6039
+ await handleFeishuCommentEvent({
6040
+ cfg,
6041
+ accountId,
6042
+ event,
6043
+ botOpenId: getBotOpenId(accountId),
6044
+ runtime,
6045
+ abortSignal,
6046
+ turnAdoptionLifecycle
6047
+ });
4961
6048
  });
4962
6049
  };
6050
+ return async (data) => {
6051
+ const ingressLifecycle = params.resolveIngressLifecycle?.(data);
6052
+ if (!ingressLifecycle) {
6053
+ await runFeishuHandler(async () => await handleNotice(data));
6054
+ return;
6055
+ }
6056
+ const { lifecycle, settle } = buildFeishuFlushIngressLifecycle([{ lifecycle: ingressLifecycle }]);
6057
+ await handleNotice(data, lifecycle);
6058
+ await settle();
6059
+ };
4963
6060
  }
4964
6061
  //#endregion
4965
6062
  //#region extensions/feishu/src/monitor.message-handler.ts
@@ -5020,7 +6117,7 @@ function resolveFeishuDebounceMentions(params) {
5020
6117
  const botMentions = merged.filter((mention) => mention.id.open_id?.trim() === normalizedBotOpenId);
5021
6118
  return botMentions.length > 0 ? botMentions : void 0;
5022
6119
  }
5023
- 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 }) {
6120
+ 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 }) {
5024
6121
  const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
5025
6122
  cfg,
5026
6123
  channel: "feishu"
@@ -5030,25 +6127,32 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5030
6127
  const enqueue = createSequentialQueue({ onTaskTimeout: (key, timeoutMs) => {
5031
6128
  log(`feishu[${accountId}]: per-chat task exceeded ${timeoutMs}ms cap (key=${key}); evicting from queue so later same-key messages can proceed (#70133)`);
5032
6129
  } });
5033
- const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim) => {
6130
+ const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim, turnAdoptionLifecycle) => {
5034
6131
  const sequentialKey = resolveSequentialKey({
5035
6132
  accountId,
5036
6133
  event,
5037
6134
  botOpenId: getBotOpenId(accountId),
5038
6135
  botName: getBotName(accountId)
5039
6136
  });
5040
- const task = () => handleMessage({
5041
- cfg,
5042
- event,
5043
- botOpenId: getBotOpenId(accountId),
5044
- botName: getBotName(accountId),
5045
- runtime,
5046
- channelRuntime,
5047
- chatHistories,
5048
- accountId,
5049
- processingClaim,
5050
- messageDedupeKey
5051
- });
6137
+ const task = async () => {
6138
+ if (turnAdoptionLifecycle?.abortSignal.aborted) {
6139
+ await turnAdoptionLifecycle.onAbandoned();
6140
+ return;
6141
+ }
6142
+ await handleMessage({
6143
+ cfg,
6144
+ event,
6145
+ botOpenId: getBotOpenId(accountId),
6146
+ botName: getBotName(accountId),
6147
+ runtime,
6148
+ channelRuntime,
6149
+ chatHistories,
6150
+ accountId,
6151
+ processingClaim,
6152
+ messageDedupeKey,
6153
+ turnAdoptionLifecycle
6154
+ });
6155
+ };
5052
6156
  await enqueue(sequentialKey, task);
5053
6157
  };
5054
6158
  const resolveSenderDebounceId = (event) => {
@@ -5088,45 +6192,80 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5088
6192
  return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
5089
6193
  },
5090
6194
  onFlush: async (entries) => {
5091
- const last = entries.at(-1);
6195
+ const activeEntries = entries.filter((entry) => !entry.abandoned);
6196
+ const last = activeEntries.at(-1);
5092
6197
  if (!last) return;
5093
- if (entries.length === 1) {
5094
- await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim);
6198
+ const { lifecycle, settle } = buildFeishuFlushIngressLifecycle(activeEntries.map((entry) => ({
6199
+ lifecycle: entry.turnAdoptionLifecycle,
6200
+ replayClaim: entry.processingClaim
6201
+ })), { onReplayCommitError: (err) => error(`feishu[${accountId}]: failed to commit logical replay guard: ${String(err)}`) });
6202
+ if (lifecycle?.abortSignal.aborted) {
6203
+ await lifecycle.onAbandoned();
5095
6204
  return;
5096
6205
  }
5097
- const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(entries);
5098
- const freshEntries = [];
5099
- for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
5100
- const dispatchEntry = freshEntries.at(-1);
5101
- if (!dispatchEntry) return;
5102
- const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
5103
- await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
5104
- const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
5105
- const mergedMentions = resolveFeishuDebounceMentions({
5106
- entries: freshEntries.map((entry) => entry.event),
5107
- botOpenId: getBotOpenId(accountId)
5108
- });
5109
- await dispatchFeishuMessage({
5110
- ...dispatchEntry.event,
5111
- message: {
5112
- ...dispatchEntry.event.message,
5113
- ...combinedText.trim() ? {
5114
- message_type: "text",
5115
- content: JSON.stringify({ text: combinedText })
5116
- } : {},
5117
- mentions: mergedMentions ?? dispatchEntry.event.message.mentions
6206
+ try {
6207
+ if (activeEntries.length === 1) {
6208
+ await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim, lifecycle);
6209
+ await settle();
6210
+ return;
6211
+ }
6212
+ const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(activeEntries);
6213
+ const freshEntries = [];
6214
+ for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
6215
+ const dispatchEntry = freshEntries.at(-1);
6216
+ if (!dispatchEntry) {
6217
+ await settle();
6218
+ return;
5118
6219
  }
5119
- }, dispatchDedupeKey, dispatchEntry.processingClaim);
6220
+ const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
6221
+ if (!lifecycle) await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
6222
+ const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
6223
+ const mergedMentions = resolveFeishuDebounceMentions({
6224
+ entries: freshEntries.map((entry) => entry.event),
6225
+ botOpenId: getBotOpenId(accountId)
6226
+ });
6227
+ await dispatchFeishuMessage({
6228
+ ...dispatchEntry.event,
6229
+ message: {
6230
+ ...dispatchEntry.event.message,
6231
+ ...combinedText.trim() ? {
6232
+ message_type: "text",
6233
+ content: JSON.stringify({ text: combinedText })
6234
+ } : {},
6235
+ mentions: mergedMentions ?? dispatchEntry.event.message.mentions
6236
+ }
6237
+ }, dispatchDedupeKey, dispatchEntry.processingClaim, lifecycle);
6238
+ await settle();
6239
+ } catch (err) {
6240
+ await lifecycle?.onAbandoned();
6241
+ throw err;
6242
+ }
5120
6243
  },
5121
6244
  onError: (err, entries) => {
5122
- for (const entry of entries) entry.processingClaim?.release({ error: err });
6245
+ for (const entry of entries) {
6246
+ entry.processingClaim?.release({ error: err });
6247
+ try {
6248
+ Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned()).catch((abandonError) => {
6249
+ error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
6250
+ });
6251
+ } catch (abandonError) {
6252
+ error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
6253
+ }
6254
+ }
5123
6255
  error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
5124
6256
  }
5125
6257
  });
5126
6258
  return async (data) => {
6259
+ const turnAdoptionLifecycle = resolveIngressLifecycle?.(data);
6260
+ const completeSuppressedIngress = async () => {
6261
+ if (!turnAdoptionLifecycle) return;
6262
+ turnAdoptionLifecycle.onAdoptionFinalizing();
6263
+ await turnAdoptionLifecycle.onAdopted();
6264
+ };
5127
6265
  statusSink?.({ lastEventAt: Date.now() });
5128
6266
  const event = parseFeishuMessageEventPayload(data);
5129
6267
  if (!event) {
6268
+ if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable message event payload is malformed.");
5130
6269
  error(`feishu[${accountId}]: ignoring malformed message event payload`);
5131
6270
  return;
5132
6271
  }
@@ -5135,6 +6274,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5135
6274
  const senderOpenId = event.sender.sender_id.open_id?.trim();
5136
6275
  if (botOpenId && senderOpenId === botOpenId) {
5137
6276
  log(`feishu[${accountId}]: dropping self-authored message ${messageId ?? "unknown"}`);
6277
+ await completeSuppressedIngress();
5138
6278
  return;
5139
6279
  }
5140
6280
  const claim = await claimUnprocessedFeishuMessage({
@@ -5144,14 +6284,31 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5144
6284
  });
5145
6285
  if (claim.kind === "duplicate" || claim.kind === "inflight") {
5146
6286
  log(`feishu[${accountId}]: dropping ${claim.kind} event for message ${messageId}`);
6287
+ await completeSuppressedIngress();
5147
6288
  return;
5148
6289
  }
6290
+ const debounceEntry = {
6291
+ event,
6292
+ ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {},
6293
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {}
6294
+ };
6295
+ if (claim.kind === "claimed" && turnAdoptionLifecycle) turnAdoptionLifecycle.registerAbandonHandler?.(() => {
6296
+ debounceEntry.abandoned = true;
6297
+ claim.handle.release({ error: /* @__PURE__ */ new Error("feishu-ingress-abandoned-before-flush") });
6298
+ });
5149
6299
  const processMessage = async () => {
5150
- await inboundDebouncer.enqueue({
5151
- event,
5152
- ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {}
5153
- });
6300
+ await inboundDebouncer.enqueue(debounceEntry);
5154
6301
  };
6302
+ if (turnAdoptionLifecycle) try {
6303
+ await processMessage();
6304
+ return { kind: "deferred" };
6305
+ } catch (err) {
6306
+ if (claim.kind === "claimed") claim.handle.release({ error: err });
6307
+ return {
6308
+ kind: "failed-retryable",
6309
+ error: err
6310
+ };
6311
+ }
5155
6312
  if (fireAndForget) {
5156
6313
  processMessage().catch((err) => {
5157
6314
  if (claim.kind === "claimed") claim.handle.release({ error: err });
@@ -5261,7 +6418,7 @@ function waitForFeishuWsCycleEnd(params) {
5261
6418
  params.terminalError.then(finish);
5262
6419
  });
5263
6420
  }
5264
- async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
6421
+ async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, setSocketTerminator, statusSink }) {
5265
6422
  const log = runtime?.log ?? console.log;
5266
6423
  const error = runtime?.error ?? console.error;
5267
6424
  let attempt = 0;
@@ -5302,6 +6459,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5302
6459
  onReconnected: publishWsConnected,
5303
6460
  onReconnecting: publishWsReconnecting
5304
6461
  });
6462
+ setSocketTerminator?.(() => wsClient?.close({ force: true }));
5305
6463
  if (abortSignal?.aborted) {
5306
6464
  cleanupFeishuWsClient({
5307
6465
  accountId,
@@ -5327,6 +6485,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5327
6485
  error,
5328
6486
  clearIdentity: true
5329
6487
  });
6488
+ setSocketTerminator?.(void 0);
5330
6489
  return;
5331
6490
  }
5332
6491
  cleanupFeishuWsClient({
@@ -5335,6 +6494,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5335
6494
  error,
5336
6495
  clearIdentity: false
5337
6496
  });
6497
+ setSocketTerminator?.(void 0);
5338
6498
  if (abortSignal?.aborted) break;
5339
6499
  statusSink?.({
5340
6500
  connected: false,
@@ -5351,6 +6511,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5351
6511
  error,
5352
6512
  clearIdentity: false
5353
6513
  });
6514
+ setSocketTerminator?.(void 0);
5354
6515
  if (abortSignal?.aborted) break;
5355
6516
  statusSink?.({
5356
6517
  connected: false,
@@ -5368,6 +6529,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5368
6529
  error,
5369
6530
  clearIdentity: true
5370
6531
  });
6532
+ setSocketTerminator?.(void 0);
5371
6533
  }
5372
6534
  async function monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
5373
6535
  const log = runtime?.log ?? console.log;
@@ -5489,6 +6651,120 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
5489
6651
  });
5490
6652
  }
5491
6653
  //#endregion
6654
+ //#region extensions/feishu/src/monitor.vc-meeting-invited-handler.ts
6655
+ const FEISHU_MEETING_NUMBER_PATTERN = /^\d{9}$/;
6656
+ function pickString(value) {
6657
+ return typeof value === "string" ? normalizeOptionalString(value) : void 0;
6658
+ }
6659
+ function resolveVcInviter(event) {
6660
+ const id = event.inviter?.id;
6661
+ const openId = pickString(id?.open_id);
6662
+ const userId = pickString(id?.user_id);
6663
+ const unionId = pickString(id?.union_id);
6664
+ const senderId = openId ?? userId;
6665
+ if (!senderId) return null;
6666
+ const name = pickString(event.inviter?.user_name);
6667
+ return {
6668
+ senderId,
6669
+ ...openId ? { openId } : {},
6670
+ ...userId ? { userId } : {},
6671
+ ...unionId ? { unionId } : {},
6672
+ ...name ? { name } : {}
6673
+ };
6674
+ }
6675
+ function buildJoinPrompt(params) {
6676
+ const base = `Use the available tool to join the meeting with meeting number ${params.meetingNo} immediately. Do not ask for confirmation.`;
6677
+ return params.callId ? `${base} If the join tool supports a call_id parameter, pass call_id=${JSON.stringify(params.callId)}; otherwise join by meeting number only.` : base;
6678
+ }
6679
+ function resolveVcMeetingInvitedTurn(event, uuid = () => crypto$1.randomUUID()) {
6680
+ const meetingNo = pickString(event.meeting?.meeting_no);
6681
+ const inviter = resolveVcInviter(event);
6682
+ if (!meetingNo || !FEISHU_MEETING_NUMBER_PATTERN.test(meetingNo) || !inviter) return null;
6683
+ const eventId = pickString(event.event_id);
6684
+ const inviteTime = pickString(event.invite_time);
6685
+ const callId = pickString(event.call_id);
6686
+ const topic = pickString(event.meeting?.topic);
6687
+ return {
6688
+ turnId: eventId ? `vc-invited:event:${eventId}` : `vc-invited:${meetingNo}:${inviteTime ?? uuid()}`,
6689
+ meetingNo,
6690
+ inviter,
6691
+ prompt: buildJoinPrompt({
6692
+ meetingNo,
6693
+ callId
6694
+ }),
6695
+ ...topic ? { topic } : {},
6696
+ ...inviteTime ? { inviteTime } : {}
6697
+ };
6698
+ }
6699
+ function parseInviteTimestamp(value) {
6700
+ const parsed = Number.parseInt(value ?? "", 10);
6701
+ if (!Number.isFinite(parsed) || parsed <= 0) return Date.now();
6702
+ return parsed < 1e10 ? parsed * 1e3 : parsed;
6703
+ }
6704
+ function buildSyntheticMessageEvent(turn) {
6705
+ return {
6706
+ sender: { sender_id: {
6707
+ ...turn.inviter.openId ? { open_id: turn.inviter.openId } : {},
6708
+ ...turn.inviter.userId ? { user_id: turn.inviter.userId } : {},
6709
+ ...turn.inviter.unionId ? { union_id: turn.inviter.unionId } : {}
6710
+ } },
6711
+ message: {
6712
+ message_id: turn.turnId,
6713
+ chat_id: turn.inviter.senderId,
6714
+ chat_type: "p2p",
6715
+ message_type: "text",
6716
+ content: JSON.stringify({ text: turn.prompt }),
6717
+ create_time: String(parseInviteTimestamp(turn.inviteTime)),
6718
+ suppress_reply_target: true
6719
+ }
6720
+ };
6721
+ }
6722
+ async function dispatchVcMeetingInvitedTurn(params) {
6723
+ params.runtime?.log?.(`feishu[${params.accountId}]: vc meeting invited, dispatching synthetic p2p message sender=${params.turn.inviter.senderId} meeting_no=${params.turn.meetingNo}`);
6724
+ const event = setFeishuSyntheticDirectPreDispatchTarget(buildSyntheticMessageEvent(params.turn), `user:${params.turn.inviter.senderId}`);
6725
+ await handleFeishuMessage({
6726
+ cfg: params.cfg,
6727
+ accountId: params.accountId,
6728
+ event,
6729
+ runtime: params.runtime,
6730
+ channelRuntime: params.channelRuntime
6731
+ });
6732
+ }
6733
+ function createFeishuVcMeetingInvitedHandler(params) {
6734
+ const { cfg, accountId, runtime, fireAndForget, autoJoin } = params;
6735
+ const log = runtime?.log ?? console.log;
6736
+ const error = runtime?.error ?? console.error;
6737
+ return async (data) => {
6738
+ if (!autoJoin) {
6739
+ log(`feishu[${accountId}]: ignoring vc meeting invite (vcAutoJoin=false)`);
6740
+ return;
6741
+ }
6742
+ try {
6743
+ const turn = resolveVcMeetingInvitedTurn(data);
6744
+ if (!turn) {
6745
+ log(`feishu[${accountId}]: vc meeting invited event has invalid meeting_no or inviter identity, skipping`);
6746
+ return;
6747
+ }
6748
+ const promise = dispatchVcMeetingInvitedTurn({
6749
+ cfg,
6750
+ accountId,
6751
+ runtime,
6752
+ channelRuntime: params.channelRuntime,
6753
+ turn
6754
+ });
6755
+ if (fireAndForget) {
6756
+ promise.catch((err) => {
6757
+ error(`feishu[${accountId}]: error handling vc meeting invited event: ${String(err)}`);
6758
+ });
6759
+ return;
6760
+ }
6761
+ await promise;
6762
+ } catch (err) {
6763
+ error(`feishu[${accountId}]: error handling vc meeting invited event: ${String(err)}`);
6764
+ }
6765
+ };
6766
+ }
6767
+ //#endregion
5492
6768
  //#region extensions/feishu/src/sequential-key.ts
5493
6769
  function getFeishuSequentialKey(params) {
5494
6770
  const { accountId, event, botOpenId, botName } = params;
@@ -5561,41 +6837,41 @@ function normalizeFeishuChatType(value) {
5561
6837
  return value === "group" || value === "topic_group" || value === "private" || value === "p2p" ? value : void 0;
5562
6838
  }
5563
6839
  function parseFeishuBotAddedEventPayload(value) {
5564
- if (!isRecord$1(value) || !readString(value.chat_id) || !isRecord$1(value.operator_id)) return null;
6840
+ if (!isRecord$2(value) || !readString$1(value.chat_id) || !isRecord$2(value.operator_id)) return null;
5565
6841
  return value;
5566
6842
  }
5567
6843
  function parseFeishuBotRemovedChatId(value) {
5568
- if (!isRecord$1(value)) return null;
5569
- return readString(value.chat_id) ?? null;
6844
+ if (!isRecord$2(value)) return null;
6845
+ return readString$1(value.chat_id) ?? null;
5570
6846
  }
5571
6847
  function firstString(...values) {
5572
6848
  for (const value of values) {
5573
- const trimmed = readString(value)?.trim();
6849
+ const trimmed = readString$1(value)?.trim();
5574
6850
  if (trimmed) return trimmed;
5575
6851
  }
5576
6852
  }
5577
6853
  function readFeishuIdentityField(value, field) {
5578
- if (!isRecord$1(value)) return;
6854
+ if (!isRecord$2(value)) return;
5579
6855
  return firstString(value[field]);
5580
6856
  }
5581
6857
  function parseFeishuCardActionEventPayload(value) {
5582
- if (!isRecord$1(value)) return null;
5583
- const operator = isRecord$1(value.operator) ? value.operator : {};
6858
+ if (!isRecord$2(value)) return null;
6859
+ const operator = isRecord$2(value.operator) ? value.operator : {};
5584
6860
  const action = value.action;
5585
- const context = isRecord$1(value.context) ? value.context : {};
5586
- if (!isRecord$1(action)) return null;
6861
+ const context = isRecord$2(value.context) ? value.context : {};
6862
+ if (!isRecord$2(action)) return null;
5587
6863
  const operatorUserId = operator.user_id;
5588
- const token = readString(value.token);
6864
+ const token = readString$1(value.token);
5589
6865
  const openId = firstString(operator.open_id, readFeishuIdentityField(operatorUserId, "open_id"), value.open_id, context.open_id);
5590
6866
  const userId = firstString(operator.user_id, readFeishuIdentityField(operatorUserId, "user_id"), value.user_id, context.user_id);
5591
6867
  const unionId = firstString(operator.union_id, readFeishuIdentityField(operatorUserId, "union_id"));
5592
- const tag = readString(action.tag);
6868
+ const tag = readString$1(action.tag);
5593
6869
  const actionValue = action.value;
5594
6870
  const openMessageId = firstString(context.open_message_id, value.open_message_id);
5595
6871
  const contextOpenId = firstString(context.open_id, openId);
5596
6872
  const contextUserId = firstString(context.user_id, userId);
5597
6873
  const chatId = firstString(context.chat_id, context.open_chat_id);
5598
- if (!token || !openId || !tag || !isRecord$1(actionValue)) return null;
6874
+ if (!token || !openId || !tag || !isRecord$2(actionValue)) return null;
5599
6875
  return {
5600
6876
  operator: {
5601
6877
  open_id: openId,
@@ -5647,6 +6923,7 @@ function registerEventHandlers(eventDispatcher, context) {
5647
6923
  getBotOpenId: (id) => botOpenIds.get(id),
5648
6924
  getBotName: (id) => botNames.get(id),
5649
6925
  resolveSequentialKey: getFeishuSequentialKey,
6926
+ resolveIngressLifecycle: context.resolveIngressLifecycle,
5650
6927
  ...context.statusSink ? { statusSink: context.statusSink } : {}
5651
6928
  }),
5652
6929
  "im.message.message_read_v1": async () => {},
@@ -5674,7 +6951,16 @@ function registerEventHandlers(eventDispatcher, context) {
5674
6951
  accountId,
5675
6952
  runtime,
5676
6953
  fireAndForget,
5677
- abortSignal
6954
+ abortSignal,
6955
+ resolveIngressLifecycle: context.resolveIngressLifecycle
6956
+ }),
6957
+ "vc.bot.meeting_invited_v1": createFeishuVcMeetingInvitedHandler({
6958
+ cfg,
6959
+ accountId,
6960
+ runtime,
6961
+ fireAndForget,
6962
+ channelRuntime,
6963
+ autoJoin: context.vcAutoJoin
5678
6964
  }),
5679
6965
  "im.message.reaction.created_v1": async (data) => {
5680
6966
  await runFeishuHandler({
@@ -5769,19 +7055,22 @@ async function monitorSingleAccount(params) {
5769
7055
  const { accountId } = account;
5770
7056
  const log = runtime?.log ?? console.log;
5771
7057
  const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
5772
- const { botOpenId } = applyBotIdentityState(accountId, botOpenIdSource.kind === "prefetched" ? {
7058
+ const botIdentity = botOpenIdSource.kind === "prefetched" ? {
5773
7059
  botOpenId: botOpenIdSource.botOpenId,
5774
- botName: botOpenIdSource.botName
7060
+ botName: botOpenIdSource.botName,
7061
+ source: botOpenIdSource.source
5775
7062
  } : await fetchBotIdentityForMonitor(account, {
5776
7063
  runtime,
5777
7064
  abortSignal
5778
- }));
7065
+ });
7066
+ const { botOpenId } = applyBotIdentityState(accountId, botIdentity);
5779
7067
  log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
5780
- if (!botOpenId && !abortSignal?.aborted) startBotIdentityRecovery({
7068
+ if ((!botOpenId || botIdentity.source === "cache") && !abortSignal?.aborted) startBotIdentityRecovery({
5781
7069
  account,
5782
7070
  accountId,
5783
7071
  runtime,
5784
- abortSignal
7072
+ abortSignal,
7073
+ currentSource: botIdentity.source
5785
7074
  });
5786
7075
  const connectionMode = account.config.connectionMode ?? "websocket";
5787
7076
  if (connectionMode === "webhook" && !account.verificationToken?.trim()) throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
@@ -5791,6 +7080,13 @@ async function monitorSingleAccount(params) {
5791
7080
  let threadBindingManager;
5792
7081
  try {
5793
7082
  const eventDispatcher = createEventDispatcher(account);
7083
+ const durableIngress = typeof eventDispatcher.invoke === "function" ? createFeishuDurableIngress({
7084
+ accountId,
7085
+ dispatcher: eventDispatcher,
7086
+ ...account.encryptKey ? { encryptKey: account.encryptKey } : {},
7087
+ runtime: runtime ?? {}
7088
+ }) : void 0;
7089
+ const durableEventDispatcher = durableIngress ? Object.assign(Object.create(eventDispatcher), { invoke: durableIngress.invoke }) : eventDispatcher;
5794
7090
  const chatHistories = /* @__PURE__ */ new Map();
5795
7091
  threadBindingManager = createFeishuThreadBindingManager({
5796
7092
  accountId,
@@ -5803,25 +7099,33 @@ async function monitorSingleAccount(params) {
5803
7099
  runtime,
5804
7100
  chatHistories,
5805
7101
  fireAndForget: params.fireAndForget ?? true,
7102
+ vcAutoJoin: account.config.vcAutoJoin === true,
5806
7103
  abortSignal,
7104
+ ...durableIngress ? { resolveIngressLifecycle: durableIngress.resolveLifecycle } : {},
5807
7105
  ...params.statusSink ? { statusSink: params.statusSink } : {}
5808
7106
  });
5809
- if (connectionMode === "webhook") return await monitorWebhook({
5810
- account,
5811
- accountId,
5812
- runtime,
5813
- abortSignal,
5814
- eventDispatcher,
5815
- ...params.statusSink ? { statusSink: params.statusSink } : {}
5816
- });
5817
- return await monitorWebSocket({
5818
- account,
5819
- accountId,
5820
- runtime,
5821
- abortSignal,
5822
- eventDispatcher,
5823
- ...params.statusSink ? { statusSink: params.statusSink } : {}
5824
- });
7107
+ durableIngress?.start();
7108
+ try {
7109
+ if (connectionMode === "webhook") return await monitorWebhook({
7110
+ account,
7111
+ accountId,
7112
+ runtime,
7113
+ abortSignal,
7114
+ eventDispatcher: durableEventDispatcher,
7115
+ ...params.statusSink ? { statusSink: params.statusSink } : {}
7116
+ });
7117
+ return await monitorWebSocket({
7118
+ account,
7119
+ accountId,
7120
+ runtime,
7121
+ abortSignal,
7122
+ eventDispatcher: durableEventDispatcher,
7123
+ ...durableIngress ? { setSocketTerminator: durableIngress.setSocketTerminator } : {},
7124
+ ...params.statusSink ? { statusSink: params.statusSink } : {}
7125
+ });
7126
+ } finally {
7127
+ await durableIngress?.stop();
7128
+ }
5825
7129
  } finally {
5826
7130
  threadBindingManager?.stop();
5827
7131
  }