@openclaw/feishu 2026.5.2 → 2026.5.3-beta.2

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 (224) hide show
  1. package/dist/accounts-Ba3-WP1z.js +423 -0
  2. package/dist/api.js +2280 -0
  3. package/dist/app-registration-B8qc1MCM.js +184 -0
  4. package/dist/audio-preflight.runtime-BPlzkO3l.js +7 -0
  5. package/dist/card-interaction-BfRLgvw_.js +96 -0
  6. package/dist/channel-CSD_Jt8I.js +1668 -0
  7. package/dist/channel-entry.js +22 -0
  8. package/dist/channel-plugin-api.js +2 -0
  9. package/dist/channel.runtime-DYsXcD36.js +700 -0
  10. package/dist/client-DBVoQL5w.js +157 -0
  11. package/dist/contract-api.js +9 -0
  12. package/dist/conversation-id-DWS3Ep2A.js +139 -0
  13. package/dist/directory.static-f3EeoRJd.js +44 -0
  14. package/dist/drive-C5eJLJr7.js +883 -0
  15. package/dist/index.js +68 -0
  16. package/dist/monitor-CT189QfR.js +60 -0
  17. package/dist/monitor.account-dJV2jO8C.js +4990 -0
  18. package/dist/monitor.state-DYM02ipp.js +100 -0
  19. package/dist/policy-D6c-wMPl.js +118 -0
  20. package/dist/probe-BNzzU_uR.js +149 -0
  21. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  22. package/dist/runtime-CG0DuRCy.js +8 -0
  23. package/dist/runtime-api.js +14 -0
  24. package/dist/secret-contract-Dm4Z_zQN.js +119 -0
  25. package/dist/secret-contract-api.js +2 -0
  26. package/dist/security-audit-DqJdocrN.js +11 -0
  27. package/dist/security-audit-shared-ByuMx9cJ.js +38 -0
  28. package/dist/security-contract-api.js +2 -0
  29. package/dist/send-DowxxbpH.js +1218 -0
  30. package/dist/session-conversation-B4nrW-vo.js +27 -0
  31. package/dist/session-key-api.js +2 -0
  32. package/dist/setup-api.js +2 -0
  33. package/dist/setup-entry.js +15 -0
  34. package/dist/subagent-hooks-C3UhPVLV.js +227 -0
  35. package/dist/subagent-hooks-api.js +23 -0
  36. package/dist/targets-JMFJRKSe.js +48 -0
  37. package/dist/thread-bindings-BmS6TLes.js +222 -0
  38. package/package.json +15 -6
  39. package/api.ts +0 -31
  40. package/channel-entry.ts +0 -20
  41. package/channel-plugin-api.ts +0 -1
  42. package/contract-api.ts +0 -16
  43. package/index.ts +0 -82
  44. package/runtime-api.ts +0 -55
  45. package/secret-contract-api.ts +0 -5
  46. package/security-contract-api.ts +0 -1
  47. package/session-key-api.ts +0 -1
  48. package/setup-api.ts +0 -3
  49. package/setup-entry.test.ts +0 -14
  50. package/setup-entry.ts +0 -13
  51. package/src/accounts.test.ts +0 -459
  52. package/src/accounts.ts +0 -326
  53. package/src/app-registration.ts +0 -331
  54. package/src/approval-auth.test.ts +0 -24
  55. package/src/approval-auth.ts +0 -25
  56. package/src/async.test.ts +0 -35
  57. package/src/async.ts +0 -104
  58. package/src/audio-preflight.runtime.ts +0 -9
  59. package/src/bitable.test.ts +0 -131
  60. package/src/bitable.ts +0 -762
  61. package/src/bot-content.ts +0 -474
  62. package/src/bot-group-name.test.ts +0 -108
  63. package/src/bot-runtime-api.ts +0 -12
  64. package/src/bot-sender-name.ts +0 -125
  65. package/src/bot.broadcast.test.ts +0 -463
  66. package/src/bot.card-action.test.ts +0 -577
  67. package/src/bot.checkBotMentioned.test.ts +0 -265
  68. package/src/bot.helpers.test.ts +0 -118
  69. package/src/bot.stripBotMention.test.ts +0 -126
  70. package/src/bot.test.ts +0 -3040
  71. package/src/bot.ts +0 -1559
  72. package/src/card-action.ts +0 -447
  73. package/src/card-interaction.test.ts +0 -129
  74. package/src/card-interaction.ts +0 -159
  75. package/src/card-test-helpers.ts +0 -47
  76. package/src/card-ux-approval.ts +0 -65
  77. package/src/card-ux-launcher.test.ts +0 -99
  78. package/src/card-ux-launcher.ts +0 -121
  79. package/src/card-ux-shared.ts +0 -33
  80. package/src/channel-runtime-api.ts +0 -16
  81. package/src/channel.runtime.ts +0 -47
  82. package/src/channel.test.ts +0 -959
  83. package/src/channel.ts +0 -1313
  84. package/src/chat-schema.ts +0 -25
  85. package/src/chat.test.ts +0 -196
  86. package/src/chat.ts +0 -188
  87. package/src/client.test.ts +0 -433
  88. package/src/client.ts +0 -290
  89. package/src/comment-dispatcher-runtime-api.ts +0 -6
  90. package/src/comment-dispatcher.test.ts +0 -169
  91. package/src/comment-dispatcher.ts +0 -107
  92. package/src/comment-handler-runtime-api.ts +0 -3
  93. package/src/comment-handler.test.ts +0 -486
  94. package/src/comment-handler.ts +0 -309
  95. package/src/comment-reaction.test.ts +0 -166
  96. package/src/comment-reaction.ts +0 -259
  97. package/src/comment-shared.test.ts +0 -182
  98. package/src/comment-shared.ts +0 -406
  99. package/src/comment-target.ts +0 -44
  100. package/src/config-schema.test.ts +0 -309
  101. package/src/config-schema.ts +0 -333
  102. package/src/conversation-id.test.ts +0 -18
  103. package/src/conversation-id.ts +0 -199
  104. package/src/dedup-runtime-api.ts +0 -1
  105. package/src/dedup.ts +0 -141
  106. package/src/directory.static.ts +0 -61
  107. package/src/directory.test.ts +0 -136
  108. package/src/directory.ts +0 -124
  109. package/src/doc-schema.ts +0 -182
  110. package/src/docx-batch-insert.test.ts +0 -91
  111. package/src/docx-batch-insert.ts +0 -223
  112. package/src/docx-color-text.ts +0 -154
  113. package/src/docx-table-ops.test.ts +0 -53
  114. package/src/docx-table-ops.ts +0 -316
  115. package/src/docx-types.ts +0 -38
  116. package/src/docx.account-selection.test.ts +0 -79
  117. package/src/docx.test.ts +0 -685
  118. package/src/docx.ts +0 -1616
  119. package/src/drive-schema.ts +0 -92
  120. package/src/drive.test.ts +0 -1219
  121. package/src/drive.ts +0 -829
  122. package/src/dynamic-agent.ts +0 -137
  123. package/src/event-types.ts +0 -45
  124. package/src/external-keys.test.ts +0 -20
  125. package/src/external-keys.ts +0 -19
  126. package/src/lifecycle.test-support.ts +0 -220
  127. package/src/media.test.ts +0 -900
  128. package/src/media.ts +0 -861
  129. package/src/mention-target.types.ts +0 -5
  130. package/src/mention.ts +0 -114
  131. package/src/message-action-contract.ts +0 -13
  132. package/src/monitor-state-runtime-api.ts +0 -7
  133. package/src/monitor-transport-runtime-api.ts +0 -7
  134. package/src/monitor.account.ts +0 -468
  135. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
  136. package/src/monitor.bot-identity.ts +0 -86
  137. package/src/monitor.bot-menu-handler.ts +0 -165
  138. package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
  139. package/src/monitor.bot-menu.test.ts +0 -178
  140. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
  141. package/src/monitor.card-action.lifecycle.test-support.ts +0 -373
  142. package/src/monitor.cleanup.test.ts +0 -376
  143. package/src/monitor.comment-notice-handler.ts +0 -105
  144. package/src/monitor.comment.test.ts +0 -937
  145. package/src/monitor.comment.ts +0 -1386
  146. package/src/monitor.lifecycle.test.ts +0 -4
  147. package/src/monitor.message-handler.ts +0 -339
  148. package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
  149. package/src/monitor.reaction.test.ts +0 -713
  150. package/src/monitor.startup.test.ts +0 -192
  151. package/src/monitor.startup.ts +0 -74
  152. package/src/monitor.state.defaults.test.ts +0 -46
  153. package/src/monitor.state.ts +0 -170
  154. package/src/monitor.synthetic-error.ts +0 -18
  155. package/src/monitor.test-mocks.ts +0 -45
  156. package/src/monitor.transport.ts +0 -424
  157. package/src/monitor.ts +0 -100
  158. package/src/monitor.webhook-e2e.test.ts +0 -272
  159. package/src/monitor.webhook-security.test.ts +0 -264
  160. package/src/monitor.webhook.test-helpers.ts +0 -116
  161. package/src/outbound-runtime-api.ts +0 -1
  162. package/src/outbound.test.ts +0 -935
  163. package/src/outbound.ts +0 -718
  164. package/src/perm-schema.ts +0 -52
  165. package/src/perm.ts +0 -170
  166. package/src/pins.ts +0 -108
  167. package/src/policy.test.ts +0 -334
  168. package/src/policy.ts +0 -236
  169. package/src/post.test.ts +0 -105
  170. package/src/post.ts +0 -275
  171. package/src/probe.test.ts +0 -275
  172. package/src/probe.ts +0 -166
  173. package/src/processing-claims.ts +0 -59
  174. package/src/qr-terminal.ts +0 -1
  175. package/src/reactions.ts +0 -123
  176. package/src/reasoning-preview.test.ts +0 -59
  177. package/src/reasoning-preview.ts +0 -20
  178. package/src/reply-dispatcher-runtime-api.ts +0 -7
  179. package/src/reply-dispatcher.test.ts +0 -1144
  180. package/src/reply-dispatcher.ts +0 -650
  181. package/src/runtime.ts +0 -9
  182. package/src/secret-contract.ts +0 -145
  183. package/src/secret-input.ts +0 -1
  184. package/src/security-audit-shared.ts +0 -69
  185. package/src/security-audit.test.ts +0 -61
  186. package/src/security-audit.ts +0 -1
  187. package/src/send-result.ts +0 -29
  188. package/src/send-target.test.ts +0 -80
  189. package/src/send-target.ts +0 -35
  190. package/src/send.reply-fallback.test.ts +0 -292
  191. package/src/send.test.ts +0 -550
  192. package/src/send.ts +0 -800
  193. package/src/sequential-key.test.ts +0 -72
  194. package/src/sequential-key.ts +0 -28
  195. package/src/sequential-queue.test.ts +0 -92
  196. package/src/sequential-queue.ts +0 -16
  197. package/src/session-conversation.ts +0 -42
  198. package/src/session-route.ts +0 -48
  199. package/src/setup-core.ts +0 -51
  200. package/src/setup-surface.test.ts +0 -174
  201. package/src/setup-surface.ts +0 -581
  202. package/src/streaming-card.test.ts +0 -190
  203. package/src/streaming-card.ts +0 -490
  204. package/src/subagent-hooks.test.ts +0 -603
  205. package/src/subagent-hooks.ts +0 -397
  206. package/src/targets.ts +0 -97
  207. package/src/test-support/lifecycle-test-support.ts +0 -453
  208. package/src/thread-bindings.test.ts +0 -143
  209. package/src/thread-bindings.ts +0 -330
  210. package/src/tool-account-routing.test.ts +0 -187
  211. package/src/tool-account.test.ts +0 -44
  212. package/src/tool-account.ts +0 -93
  213. package/src/tool-factory-test-harness.ts +0 -79
  214. package/src/tool-result.test.ts +0 -32
  215. package/src/tool-result.ts +0 -16
  216. package/src/tools-config.test.ts +0 -21
  217. package/src/tools-config.ts +0 -22
  218. package/src/types.ts +0 -104
  219. package/src/typing.test.ts +0 -144
  220. package/src/typing.ts +0 -214
  221. package/src/wiki-schema.ts +0 -55
  222. package/src/wiki.ts +0 -227
  223. package/subagent-hooks-api.ts +0 -31
  224. package/tsconfig.json +0 -16
package/src/bot.ts DELETED
@@ -1,1559 +0,0 @@
1
- import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
2
- import {
3
- ensureConfiguredBindingRouteReady,
4
- resolveConfiguredBindingRoute,
5
- resolveRuntimeConversationBindingRoute,
6
- } from "openclaw/plugin-sdk/conversation-runtime";
7
- import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/outbound-runtime";
8
- import {
9
- buildPendingHistoryContextFromMap,
10
- clearHistoryEntriesIfEnabled,
11
- DEFAULT_GROUP_HISTORY_LIMIT,
12
- recordPendingHistoryEntryIfEnabled,
13
- type HistoryEntry,
14
- } from "openclaw/plugin-sdk/reply-history";
15
- import {
16
- resolveDefaultGroupPolicy,
17
- resolveOpenProviderRuntimeGroupPolicy,
18
- warnMissingProviderGroupPolicyFallbackOnce,
19
- } from "openclaw/plugin-sdk/runtime-group-policy";
20
- import { resolveOpenDmAllowlistAccess } from "openclaw/plugin-sdk/security-runtime";
21
- import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
22
- import { resolveFeishuRuntimeAccount } from "./accounts.js";
23
- import {
24
- checkBotMentioned,
25
- normalizeFeishuCommandProbeBody,
26
- normalizeMentions,
27
- parseMergeForwardContent,
28
- parseMessageContent,
29
- resolveFeishuGroupSession,
30
- resolveFeishuMediaList,
31
- toMessageResourceType,
32
- } from "./bot-content.js";
33
- import {
34
- buildAgentMediaPayload,
35
- evaluateSupplementalContextVisibility,
36
- filterSupplementalContextItems,
37
- normalizeAgentId,
38
- resolveChannelContextVisibilityMode,
39
- } from "./bot-runtime-api.js";
40
- import type { ClawdbotConfig, RuntimeEnv } from "./bot-runtime-api.js";
41
- import { type FeishuPermissionError, resolveFeishuSenderName } from "./bot-sender-name.js";
42
- import { getChatInfo } from "./chat.js";
43
- import { createFeishuClient } from "./client.js";
44
- import { finalizeFeishuMessageProcessing, tryRecordMessagePersistent } from "./dedup.js";
45
- import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
46
- import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
47
- import {
48
- hasExplicitFeishuGroupConfig,
49
- isFeishuGroupAllowed,
50
- resolveFeishuAllowlistMatch,
51
- resolveFeishuGroupConfig,
52
- resolveFeishuReplyPolicy,
53
- } from "./policy.js";
54
- import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
55
- import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
56
- import { getFeishuRuntime } from "./runtime.js";
57
- import { getMessageFeishu, listFeishuThreadMessages, sendMessageFeishu } from "./send.js";
58
- export type { FeishuBotAddedEvent, FeishuMessageEvent } from "./event-types.js";
59
- import type { FeishuMessageEvent } from "./event-types.js";
60
- import {
61
- isFeishuGroupChatType,
62
- type FeishuMessageContext,
63
- type FeishuMediaInfo,
64
- type FeishuMessageInfo,
65
- type ResolvedFeishuAccount,
66
- } from "./types.js";
67
- import type { DynamicAgentCreationConfig } from "./types.js";
68
-
69
- export { toMessageResourceType } from "./bot-content.js";
70
-
71
- // Cache permission errors to avoid spamming the user with repeated notifications.
72
- // Key: appId or "default", Value: timestamp of last notification
73
- const permissionErrorNotifiedAt = new Map<string, number>();
74
- const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
75
-
76
- const groupNameCache = new Map<string, { name: string; expiresAt: number }>();
77
- const GROUP_NAME_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
78
- const GROUP_NAME_CACHE_MAX_SIZE = 500; // hard cap
79
-
80
- function evictGroupNameCache(): void {
81
- const now = Date.now();
82
- for (const [key, val] of groupNameCache) {
83
- if (val.expiresAt <= now) {
84
- groupNameCache.delete(key);
85
- }
86
- }
87
-
88
- if (groupNameCache.size > GROUP_NAME_CACHE_MAX_SIZE) {
89
- const excess = groupNameCache.size - GROUP_NAME_CACHE_MAX_SIZE;
90
- let removed = 0;
91
- for (const key of groupNameCache.keys()) {
92
- if (removed >= excess) {
93
- break;
94
- }
95
- groupNameCache.delete(key);
96
- removed++;
97
- }
98
- }
99
- }
100
-
101
- function setCacheEntry(key: string, value: { name: string; expiresAt: number }): void {
102
- groupNameCache.delete(key);
103
- groupNameCache.set(key, value);
104
- }
105
-
106
- export function clearGroupNameCache(): void {
107
- groupNameCache.clear();
108
- }
109
-
110
- export async function resolveGroupName(params: {
111
- account: ResolvedFeishuAccount;
112
- chatId: string;
113
- log: (...args: unknown[]) => void;
114
- }): Promise<string | undefined> {
115
- const { account, chatId, log } = params;
116
- if (!account.configured) {
117
- return undefined;
118
- }
119
-
120
- const cacheKey = `${account.accountId}:${chatId}`;
121
-
122
- const cached = groupNameCache.get(cacheKey);
123
- if (cached && cached.expiresAt > Date.now()) {
124
- return cached.name || undefined;
125
- }
126
-
127
- try {
128
- const client = createFeishuClient(account);
129
- const chatInfo = await getChatInfo(client, chatId);
130
- const name = chatInfo?.name?.trim();
131
- if (name) {
132
- setCacheEntry(cacheKey, {
133
- name,
134
- expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS,
135
- });
136
- } else {
137
- setCacheEntry(cacheKey, {
138
- name: "",
139
- expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS,
140
- });
141
- }
142
- } catch (err) {
143
- log(`feishu[${account.accountId}]: getChatInfo failed for ${chatId}: ${String(err)}`);
144
- setCacheEntry(cacheKey, {
145
- name: "",
146
- expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS,
147
- });
148
- }
149
-
150
- const result = groupNameCache.get(cacheKey)?.name || undefined;
151
- evictGroupNameCache();
152
-
153
- return result;
154
- }
155
-
156
- async function resolveFeishuAudioPreflightTranscript(params: {
157
- cfg: ClawdbotConfig;
158
- mediaList: FeishuMediaInfo[];
159
- content: string;
160
- chatType: "direct" | "group";
161
- log: (msg: string) => void;
162
- }): Promise<string | undefined> {
163
- if (params.content.trim() !== "<media:audio>") {
164
- return undefined;
165
- }
166
- const audioMedia = params.mediaList.filter((media) => media.contentType?.startsWith("audio/"));
167
- if (audioMedia.length === 0) {
168
- return undefined;
169
- }
170
-
171
- try {
172
- const { transcribeFirstAudio } = await import("./audio-preflight.runtime.js");
173
- return await transcribeFirstAudio({
174
- ctx: {
175
- MediaPaths: audioMedia.map((media) => media.path),
176
- MediaTypes: audioMedia.map((media) => media.contentType).filter(Boolean) as string[],
177
- ChatType: params.chatType,
178
- },
179
- cfg: params.cfg,
180
- });
181
- } catch (err) {
182
- params.log(`feishu: audio preflight transcription failed: ${String(err)}`);
183
- return undefined;
184
- }
185
- }
186
-
187
- // --- Broadcast support ---
188
- // Resolve broadcast agent list for a given peer (group) ID.
189
- // Returns null if no broadcast config exists or the peer is not in the broadcast list.
190
- export function resolveBroadcastAgents(cfg: ClawdbotConfig, peerId: string): string[] | null {
191
- const broadcast = (cfg as Record<string, unknown>).broadcast;
192
- if (!broadcast || typeof broadcast !== "object") {
193
- return null;
194
- }
195
- const agents = (broadcast as Record<string, unknown>)[peerId];
196
- if (!Array.isArray(agents) || agents.length === 0) {
197
- return null;
198
- }
199
- return agents as string[];
200
- }
201
-
202
- // Build a session key for a broadcast target agent by replacing the agent ID prefix.
203
- // Session keys follow the format: agent:<agentId>:<channel>:<peerKind>:<peerId>
204
- export function buildBroadcastSessionKey(
205
- baseSessionKey: string,
206
- originalAgentId: string,
207
- targetAgentId: string,
208
- ): string {
209
- const prefix = `agent:${originalAgentId}:`;
210
- if (baseSessionKey.startsWith(prefix)) {
211
- return `agent:${targetAgentId}:${baseSessionKey.slice(prefix.length)}`;
212
- }
213
- return baseSessionKey;
214
- }
215
-
216
- /**
217
- * Build media payload for inbound context.
218
- * Similar to Discord's buildDiscordMediaPayload().
219
- */
220
- export function parseFeishuMessageEvent(
221
- event: FeishuMessageEvent,
222
- botOpenId?: string,
223
- _botName?: string,
224
- ): FeishuMessageContext {
225
- const rawContent = parseMessageContent(event.message.content, event.message.message_type);
226
- const mentionedBot = checkBotMentioned(event, botOpenId);
227
- const hasAnyMention = (event.message.mentions?.length ?? 0) > 0;
228
- // Strip the bot's own mention so slash commands like @Bot /help retain
229
- // the leading /. This applies in both p2p *and* group contexts — the
230
- // mentionedBot flag already captures whether the bot was addressed, so
231
- // keeping the mention tag in content only breaks command detection (#35994).
232
- // Non-bot mentions (e.g. mention-forward targets) are still normalized to <at> tags.
233
- const content = normalizeMentions(rawContent, event.message.mentions, botOpenId);
234
- const senderOpenId = event.sender.sender_id.open_id?.trim();
235
- const senderUserId = event.sender.sender_id.user_id?.trim();
236
- const senderFallbackId = senderOpenId || senderUserId || "";
237
-
238
- const ctx: FeishuMessageContext = {
239
- chatId: event.message.chat_id,
240
- messageId: event.message.message_id,
241
- replyTargetMessageId: event.message.reply_target_message_id?.trim() || undefined,
242
- suppressReplyTarget: event.message.suppress_reply_target === true,
243
- senderId: senderUserId || senderOpenId || "",
244
- // Keep the historical field name, but fall back to user_id when open_id is unavailable
245
- // (common in some mobile app deliveries).
246
- senderOpenId: senderFallbackId,
247
- chatType: event.message.chat_type,
248
- mentionedBot,
249
- hasAnyMention,
250
- rootId: event.message.root_id || undefined,
251
- parentId: event.message.parent_id || undefined,
252
- threadId: event.message.thread_id || undefined,
253
- content,
254
- contentType: event.message.message_type,
255
- };
256
-
257
- // Detect mention forward request: message mentions bot + at least one other user
258
- if (isMentionForwardRequest(event, botOpenId)) {
259
- const mentionTargets = extractMentionTargets(event, botOpenId);
260
- if (mentionTargets.length > 0) {
261
- ctx.mentionTargets = mentionTargets;
262
- }
263
- }
264
-
265
- return ctx;
266
- }
267
-
268
- export function buildFeishuAgentBody(params: {
269
- ctx: Pick<
270
- FeishuMessageContext,
271
- "content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId" | "hasAnyMention"
272
- >;
273
- quotedContent?: string;
274
- permissionErrorForAgent?: FeishuPermissionError;
275
- botOpenId?: string;
276
- }): string {
277
- const { ctx, quotedContent, permissionErrorForAgent, botOpenId } = params;
278
- let messageBody = ctx.content;
279
- if (quotedContent) {
280
- messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
281
- }
282
-
283
- // DMs already have per-sender sessions, but this label still improves attribution.
284
- const speaker = ctx.senderName ?? ctx.senderOpenId;
285
- messageBody = `${speaker}: ${messageBody}`;
286
-
287
- if (ctx.hasAnyMention) {
288
- const botIdHint = botOpenId?.trim();
289
- messageBody +=
290
- `\n\n[System: The content may include mention tags in the form <at user_id="...">name</at>. ` +
291
- `Treat these as real mentions of Feishu entities (users or bots).]`;
292
- if (botIdHint) {
293
- messageBody += `\n[System: If user_id is "${botIdHint}", that mention refers to you.]`;
294
- }
295
- }
296
-
297
- if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
298
- const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
299
- messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
300
- }
301
-
302
- // Keep message_id on its own line so shared message-id hint stripping can parse it reliably.
303
- messageBody = `[message_id: ${ctx.messageId}]\n${messageBody}`;
304
-
305
- if (permissionErrorForAgent) {
306
- const grantUrl = permissionErrorForAgent.grantUrl ?? "";
307
- messageBody += `\n\n[System: The bot encountered a Feishu API permission error. Please inform the user about this issue and provide the permission grant URL for the admin to authorize. Permission grant URL: ${grantUrl}]`;
308
- }
309
-
310
- return messageBody;
311
- }
312
-
313
- function isFetchedGroupContextSenderAllowed(params: {
314
- isGroup: boolean;
315
- allowFrom: Array<string | number>;
316
- senderId?: string;
317
- senderType?: string;
318
- }): boolean {
319
- if (!params.isGroup || params.allowFrom.length === 0) {
320
- return true;
321
- }
322
- if (params.senderType === "app") {
323
- return true;
324
- }
325
- const senderId = params.senderId?.trim();
326
- const senderAllowed =
327
- !!senderId &&
328
- isFeishuGroupAllowed({
329
- groupPolicy: "allowlist",
330
- allowFrom: params.allowFrom,
331
- senderId,
332
- senderName: undefined,
333
- });
334
- return senderAllowed;
335
- }
336
-
337
- function shouldIncludeFetchedGroupContextMessage(params: {
338
- isGroup: boolean;
339
- allowFrom: Array<string | number>;
340
- mode: "all" | "allowlist" | "allowlist_quote";
341
- kind: "quote" | "thread" | "history";
342
- senderId?: string;
343
- senderType?: string;
344
- }): boolean {
345
- const senderAllowed = isFetchedGroupContextSenderAllowed({
346
- isGroup: params.isGroup,
347
- allowFrom: params.allowFrom,
348
- senderId: params.senderId,
349
- senderType: params.senderType,
350
- });
351
- return evaluateSupplementalContextVisibility({
352
- mode: params.mode,
353
- kind: params.kind,
354
- senderAllowed,
355
- }).include;
356
- }
357
-
358
- function filterFetchedGroupContextMessages<
359
- T extends Pick<FeishuMessageInfo, "senderId" | "senderType">,
360
- >(
361
- messages: readonly T[],
362
- params: {
363
- isGroup: boolean;
364
- allowFrom: Array<string | number>;
365
- mode: "all" | "allowlist" | "allowlist_quote";
366
- kind: "quote" | "thread" | "history";
367
- },
368
- ): T[] {
369
- return filterSupplementalContextItems({
370
- items: messages,
371
- mode: params.mode,
372
- kind: params.kind,
373
- isSenderAllowed: (message) =>
374
- isFetchedGroupContextSenderAllowed({
375
- isGroup: params.isGroup,
376
- allowFrom: params.allowFrom,
377
- senderId: message.senderId,
378
- senderType: message.senderType,
379
- }),
380
- }).items;
381
- }
382
-
383
- export async function handleFeishuMessage(params: {
384
- cfg: ClawdbotConfig;
385
- event: FeishuMessageEvent;
386
- botOpenId?: string;
387
- botName?: string;
388
- runtime?: RuntimeEnv;
389
- chatHistories?: Map<string, HistoryEntry[]>;
390
- accountId?: string;
391
- processingClaimHeld?: boolean;
392
- }): Promise<void> {
393
- const {
394
- cfg,
395
- event,
396
- botOpenId,
397
- botName,
398
- runtime,
399
- chatHistories,
400
- accountId,
401
- processingClaimHeld = false,
402
- } = params;
403
-
404
- // Resolve account with merged config
405
- const account = resolveFeishuRuntimeAccount({ cfg, accountId });
406
- const feishuCfg = account.config;
407
-
408
- const log = runtime?.log ?? console.log;
409
- const error = runtime?.error ?? console.error;
410
-
411
- const messageId = event.message.message_id;
412
- if (
413
- !(await finalizeFeishuMessageProcessing({
414
- messageId,
415
- namespace: account.accountId,
416
- log,
417
- claimHeld: processingClaimHeld,
418
- }))
419
- ) {
420
- log(`feishu: skipping duplicate message ${messageId}`);
421
- return;
422
- }
423
-
424
- let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
425
- const isGroup = isFeishuGroupChatType(ctx.chatType);
426
- const isDirect = !isGroup;
427
- const senderUserId = normalizeOptionalString(event.sender.sender_id.user_id);
428
-
429
- // Handle merge_forward messages: fetch full message via API then expand sub-messages
430
- if (event.message.message_type === "merge_forward") {
431
- log(
432
- `feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`,
433
- );
434
- try {
435
- // Websocket event doesn't include sub-messages, need to fetch via API
436
- // The API returns all sub-messages in the items array
437
- const client = createFeishuClient(account);
438
- const response = (await client.im.message.get({
439
- path: { message_id: event.message.message_id },
440
- })) as { code?: number; data?: { items?: unknown[] } };
441
-
442
- if (response.code === 0 && response.data?.items && response.data.items.length > 0) {
443
- log(
444
- `feishu[${account.accountId}]: merge_forward API returned ${response.data.items.length} items`,
445
- );
446
- const expandedContent = parseMergeForwardContent({
447
- content: JSON.stringify(response.data.items),
448
- log,
449
- });
450
- ctx = { ...ctx, content: expandedContent };
451
- } else {
452
- log(`feishu[${account.accountId}]: merge_forward API returned no items`);
453
- ctx = { ...ctx, content: "[Merged and Forwarded Message - could not fetch]" };
454
- }
455
- } catch (err) {
456
- log(`feishu[${account.accountId}]: merge_forward fetch failed: ${String(err)}`);
457
- ctx = { ...ctx, content: "[Merged and Forwarded Message - fetch error]" };
458
- }
459
- }
460
-
461
- // Resolve sender display name (best-effort) so the agent can attribute messages correctly.
462
- // Optimization: skip if disabled to save API quota (Feishu free tier limit).
463
- let permissionErrorForAgent: FeishuPermissionError | undefined;
464
- if (feishuCfg?.resolveSenderNames ?? true) {
465
- const senderResult = await resolveFeishuSenderName({
466
- account,
467
- senderId: ctx.senderOpenId,
468
- log,
469
- });
470
- if (senderResult.name) {
471
- ctx = { ...ctx, senderName: senderResult.name };
472
- }
473
-
474
- // Track permission error to inform agent later (with cooldown to avoid repetition)
475
- if (senderResult.permissionError) {
476
- const appKey = account.appId ?? "default";
477
- const now = Date.now();
478
- const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
479
-
480
- if (now - lastNotified > PERMISSION_ERROR_COOLDOWN_MS) {
481
- permissionErrorNotifiedAt.set(appKey, now);
482
- permissionErrorForAgent = senderResult.permissionError;
483
- }
484
- }
485
- }
486
-
487
- log(
488
- `feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`,
489
- );
490
-
491
- // Log mention targets if detected
492
- if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
493
- const names = ctx.mentionTargets.map((t) => t.name).join(", ");
494
- log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
495
- }
496
-
497
- const historyLimit = Math.max(
498
- 0,
499
- feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
500
- );
501
- const groupConfig = isGroup
502
- ? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })
503
- : undefined;
504
- const effectiveGroupSenderAllowFrom = isGroup
505
- ? (groupConfig?.allowFrom?.length ?? 0) > 0
506
- ? (groupConfig?.allowFrom ?? [])
507
- : (feishuCfg?.groupSenderAllowFrom ?? [])
508
- : [];
509
- const groupSession = isGroup
510
- ? resolveFeishuGroupSession({
511
- chatId: ctx.chatId,
512
- senderOpenId: ctx.senderOpenId,
513
- messageId: ctx.messageId,
514
- rootId: ctx.rootId,
515
- threadId: ctx.threadId,
516
- chatType: ctx.chatType,
517
- groupConfig,
518
- feishuCfg,
519
- })
520
- : null;
521
- const groupHistoryKey = isGroup ? (groupSession?.peerId ?? ctx.chatId) : undefined;
522
- const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
523
- const configAllowFrom = feishuCfg?.allowFrom ?? [];
524
- const useAccessGroups = cfg.commands?.useAccessGroups !== false;
525
- const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
526
- const broadcastAgents = rawBroadcastAgents
527
- ? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
528
- : null;
529
-
530
- // Parse message create_time early so every downstream consumer (pending
531
- // history, inbound payload, etc.) uses the original authoring timestamp
532
- // instead of the delivery/processing time. Feishu uses a millisecond
533
- // epoch string; fall back to Date.now() only when the field is absent.
534
- const messageCreateTimeMs = event.message.create_time
535
- ? Number.parseInt(event.message.create_time, 10)
536
- : Date.now();
537
-
538
- let requireMention = false; // DMs never require mention; groups may override below
539
- if (isGroup) {
540
- if (groupConfig?.enabled === false) {
541
- log(`feishu[${account.accountId}]: group ${ctx.chatId} is disabled`);
542
- return;
543
- }
544
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
545
- const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
546
- providerConfigPresent: cfg.channels?.feishu !== undefined,
547
- groupPolicy: feishuCfg?.groupPolicy,
548
- defaultGroupPolicy,
549
- });
550
- warnMissingProviderGroupPolicyFallbackOnce({
551
- providerMissingFallbackApplied,
552
- providerKey: "feishu",
553
- accountId: account.accountId,
554
- log,
555
- });
556
- const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
557
- // DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
558
-
559
- // A group explicitly configured under `channels.feishu.groups.<chat_id>` is
560
- // treated as admitted in allowlist mode even when `groupAllowFrom` is empty.
561
- // Wildcard defaults still configure matching groups, but they are not an
562
- // admission signal by themselves.
563
- const groupExplicitlyConfigured = hasExplicitFeishuGroupConfig({
564
- cfg: feishuCfg,
565
- groupId: ctx.chatId,
566
- });
567
-
568
- // Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
569
- const groupAllowed =
570
- groupPolicy !== "disabled" &&
571
- (groupExplicitlyConfigured ||
572
- isFeishuGroupAllowed({
573
- groupPolicy,
574
- allowFrom: groupAllowFrom,
575
- senderId: ctx.chatId, // Check group ID, not sender ID
576
- senderName: undefined,
577
- }));
578
-
579
- if (!groupAllowed) {
580
- log(
581
- `feishu[${account.accountId}]: group ${ctx.chatId} not in groupAllowFrom (groupPolicy=${groupPolicy})`,
582
- );
583
- return;
584
- }
585
-
586
- // Sender-level allowlist: per-group allowFrom takes precedence, then global groupSenderAllowFrom
587
- if (effectiveGroupSenderAllowFrom.length > 0) {
588
- const senderAllowed = isFeishuGroupAllowed({
589
- groupPolicy: "allowlist",
590
- allowFrom: effectiveGroupSenderAllowFrom,
591
- senderId: ctx.senderOpenId,
592
- senderIds: [senderUserId],
593
- senderName: ctx.senderName,
594
- });
595
- if (!senderAllowed) {
596
- log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
597
- return;
598
- }
599
- }
600
-
601
- ({ requireMention } = resolveFeishuReplyPolicy({
602
- isDirectMessage: false,
603
- cfg,
604
- accountId: account.accountId,
605
- groupId: ctx.chatId,
606
- groupPolicy,
607
- }));
608
-
609
- if (requireMention && !ctx.mentionedBot) {
610
- log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
611
- // Record to pending history for non-broadcast groups only. For broadcast groups,
612
- // the mentioned handler's broadcast dispatch writes the turn directly into all
613
- // agent sessions — buffering here would cause duplicate replay when this account
614
- // later becomes active via buildPendingHistoryContextFromMap.
615
- if (!broadcastAgents && chatHistories && groupHistoryKey) {
616
- recordPendingHistoryEntryIfEnabled({
617
- historyMap: chatHistories,
618
- historyKey: groupHistoryKey,
619
- limit: historyLimit,
620
- entry: {
621
- sender: ctx.senderOpenId,
622
- body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
623
- timestamp: messageCreateTimeMs,
624
- messageId: ctx.messageId,
625
- },
626
- });
627
- }
628
- return;
629
- }
630
- }
631
-
632
- try {
633
- const core = getFeishuRuntime();
634
- const pairing = createChannelPairingController({
635
- core,
636
- channel: "feishu",
637
- accountId: account.accountId,
638
- });
639
- const commandProbeBody = isGroup ? normalizeFeishuCommandProbeBody(ctx.content) : ctx.content;
640
- const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
641
- commandProbeBody,
642
- cfg,
643
- );
644
- const storeAllowFrom =
645
- !isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open"
646
- ? await pairing.readAllowFromStore().catch(() => [])
647
- : [];
648
- const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
649
- const dmAllowed = resolveFeishuAllowlistMatch({
650
- allowFrom: effectiveDmAllowFrom,
651
- senderId: ctx.senderOpenId,
652
- senderIds: [senderUserId],
653
- senderName: ctx.senderName,
654
- }).allowed;
655
-
656
- const dmAccessAllowed =
657
- dmPolicy === "open"
658
- ? resolveOpenDmAllowlistAccess({
659
- effectiveAllowFrom: effectiveDmAllowFrom,
660
- isSenderAllowed: (allowFrom) =>
661
- resolveFeishuAllowlistMatch({
662
- allowFrom,
663
- senderId: ctx.senderOpenId,
664
- senderIds: [senderUserId],
665
- senderName: ctx.senderName,
666
- }).allowed,
667
- }).decision === "allow"
668
- : dmAllowed;
669
-
670
- if (isDirect && !dmAccessAllowed) {
671
- if (dmPolicy === "pairing") {
672
- await pairing.issueChallenge({
673
- senderId: ctx.senderOpenId,
674
- senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
675
- meta: { name: ctx.senderName },
676
- onCreated: () => {
677
- log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
678
- },
679
- sendPairingReply: async (text) => {
680
- await sendMessageFeishu({
681
- cfg,
682
- to: `chat:${ctx.chatId}`,
683
- text,
684
- accountId: account.accountId,
685
- });
686
- },
687
- onReplyError: (err) => {
688
- log(
689
- `feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
690
- );
691
- },
692
- });
693
- } else {
694
- log(
695
- `feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
696
- );
697
- }
698
- return;
699
- }
700
-
701
- const commandAllowFrom = isGroup
702
- ? (groupConfig?.allowFrom ?? configAllowFrom)
703
- : effectiveDmAllowFrom;
704
- const senderAllowedForCommands = resolveFeishuAllowlistMatch({
705
- allowFrom: commandAllowFrom,
706
- senderId: ctx.senderOpenId,
707
- senderIds: [senderUserId],
708
- senderName: ctx.senderName,
709
- }).allowed;
710
-
711
- // In group chats, the session is scoped to the group, but the *speaker* is the sender.
712
- // Using a group-scoped From causes the agent to treat different users as the same person.
713
- const feishuFrom = `feishu:${ctx.senderOpenId}`;
714
- const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
715
- const peerId = isGroup ? (groupSession?.peerId ?? ctx.chatId) : ctx.senderOpenId;
716
- const parentPeer = isGroup ? (groupSession?.parentPeer ?? null) : null;
717
- const replyInThread = isGroup ? (groupSession?.replyInThread ?? false) : false;
718
- const feishuAcpConversationSupported =
719
- !isGroup ||
720
- groupSession?.groupSessionScope === "group_topic" ||
721
- groupSession?.groupSessionScope === "group_topic_sender";
722
-
723
- if (isGroup && groupSession) {
724
- log(
725
- `feishu[${account.accountId}]: group session scope=${groupSession.groupSessionScope}, peer=${peerId}`,
726
- );
727
- }
728
-
729
- let route = core.channel.routing.resolveAgentRoute({
730
- cfg,
731
- channel: "feishu",
732
- accountId: account.accountId,
733
- peer: {
734
- kind: isGroup ? "group" : "direct",
735
- id: peerId,
736
- },
737
- parentPeer,
738
- });
739
-
740
- // Dynamic agent creation for DM users
741
- // When enabled, creates a unique agent instance with its own workspace for each DM user.
742
- let effectiveCfg = cfg;
743
- if (!isGroup && route.matchedBy === "default") {
744
- const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
745
- if (dynamicCfg?.enabled) {
746
- const runtime = getFeishuRuntime();
747
- const result = await maybeCreateDynamicAgent({
748
- cfg,
749
- runtime,
750
- senderOpenId: ctx.senderOpenId,
751
- dynamicCfg,
752
- log: (msg) => log(msg),
753
- });
754
- if (result.created) {
755
- effectiveCfg = result.updatedCfg;
756
- // Re-resolve route with updated config
757
- route = core.channel.routing.resolveAgentRoute({
758
- cfg: result.updatedCfg,
759
- channel: "feishu",
760
- accountId: account.accountId,
761
- peer: { kind: "direct", id: ctx.senderOpenId },
762
- });
763
- log(
764
- `feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`,
765
- );
766
- }
767
- }
768
- }
769
-
770
- const currentConversationId = peerId;
771
- const parentConversationId = isGroup ? (parentPeer?.id ?? ctx.chatId) : undefined;
772
- let configuredBinding = null;
773
- if (feishuAcpConversationSupported) {
774
- const configuredRoute = resolveConfiguredBindingRoute({
775
- cfg: effectiveCfg,
776
- route,
777
- conversation: {
778
- channel: "feishu",
779
- accountId: account.accountId,
780
- conversationId: currentConversationId,
781
- parentConversationId,
782
- },
783
- });
784
- configuredBinding = configuredRoute.bindingResolution;
785
- route = configuredRoute.route;
786
-
787
- // Bound Feishu conversations intentionally require an exact live conversation-id match.
788
- // Sender-scoped topic sessions therefore bind on `chat:topic:root:sender:user`, while
789
- // configured ACP bindings may still inherit the shared `chat:topic:root` topic session.
790
- const runtimeRoute = resolveRuntimeConversationBindingRoute({
791
- route,
792
- conversation: {
793
- channel: "feishu",
794
- accountId: account.accountId,
795
- conversationId: currentConversationId,
796
- ...(parentConversationId ? { parentConversationId } : {}),
797
- },
798
- });
799
- route = runtimeRoute.route;
800
- if (runtimeRoute.bindingRecord) {
801
- configuredBinding = null;
802
- log(
803
- runtimeRoute.boundSessionKey
804
- ? `feishu[${account.accountId}]: routed via bound conversation ${currentConversationId} -> ${runtimeRoute.boundSessionKey}`
805
- : `feishu[${account.accountId}]: plugin-bound conversation ${currentConversationId}`,
806
- );
807
- }
808
- }
809
-
810
- if (configuredBinding) {
811
- const ensured = await ensureConfiguredBindingRouteReady({
812
- cfg: effectiveCfg,
813
- bindingResolution: configuredBinding,
814
- });
815
- if (!ensured.ok) {
816
- const replyTargetMessageId =
817
- isGroup &&
818
- (groupSession?.groupSessionScope === "group_topic" ||
819
- groupSession?.groupSessionScope === "group_topic_sender")
820
- ? (ctx.rootId ?? ctx.messageId)
821
- : ctx.messageId;
822
- await sendMessageFeishu({
823
- cfg: effectiveCfg,
824
- to: `chat:${ctx.chatId}`,
825
- text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
826
- replyToMessageId: replyTargetMessageId,
827
- replyInThread: isGroup ? (groupSession?.replyInThread ?? false) : false,
828
- accountId: account.accountId,
829
- }).catch((err) => {
830
- log(`feishu[${account.accountId}]: failed to send ACP init error reply: ${String(err)}`);
831
- });
832
- return;
833
- }
834
- }
835
-
836
- const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
837
- const inboundLabel = isGroup
838
- ? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
839
- : `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
840
- const contextVisibilityMode = resolveChannelContextVisibilityMode({
841
- cfg: effectiveCfg,
842
- channel: "feishu",
843
- accountId: account.accountId,
844
- });
845
-
846
- // Do not enqueue inbound user previews as system events.
847
- // System events are prepended to future prompts and can be misread as
848
- // authoritative transcript turns.
849
- log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
850
-
851
- // Resolve media from message
852
- const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024; // 30MB default
853
- const mediaList = await resolveFeishuMediaList({
854
- cfg,
855
- messageId: ctx.messageId,
856
- messageType: event.message.message_type,
857
- content: event.message.content,
858
- maxBytes: mediaMaxBytes,
859
- log,
860
- accountId: account.accountId,
861
- });
862
- // Skip messages with no text content and no media attachments. Feishu can
863
- // deliver empty-text events (e.g. `{"text":""}`) when a user sends a blank
864
- // message or when media parsing produces an empty string. Writing a blank
865
- // user turn to the session causes downstream LLM providers (e.g. MiniMax)
866
- // to reject the request with "messages must not be empty" errors. Logging
867
- // the skip avoids silent loss without polluting the agent session.
868
- if (!ctx.content.trim() && mediaList.length === 0) {
869
- log(
870
- `feishu[${account.accountId}]: skipping empty message (no text, no media) from ${ctx.senderOpenId}`,
871
- );
872
- return;
873
- }
874
-
875
- const mediaPayload = buildAgentMediaPayload(mediaList);
876
- const audioTranscript = await resolveFeishuAudioPreflightTranscript({
877
- cfg: effectiveCfg,
878
- mediaList,
879
- content: ctx.content,
880
- chatType: isGroup ? "group" : "direct",
881
- log,
882
- });
883
- const preflightAudioIndex =
884
- audioTranscript === undefined
885
- ? -1
886
- : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
887
- const agentFacingContent = audioTranscript ?? ctx.content;
888
- const agentFacingCtx =
889
- audioTranscript === undefined
890
- ? ctx
891
- : {
892
- ...ctx,
893
- content: audioTranscript,
894
- };
895
- const effectiveCommandProbeBody =
896
- audioTranscript === undefined
897
- ? commandProbeBody
898
- : isGroup
899
- ? normalizeFeishuCommandProbeBody(audioTranscript)
900
- : audioTranscript;
901
- const shouldComputeEffectiveCommandAuthorized =
902
- audioTranscript === undefined
903
- ? shouldComputeCommandAuthorized
904
- : core.channel.commands.shouldComputeCommandAuthorized(effectiveCommandProbeBody, cfg);
905
- const commandAuthorized = shouldComputeEffectiveCommandAuthorized
906
- ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
907
- useAccessGroups,
908
- authorizers: [
909
- { configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
910
- ],
911
- })
912
- : undefined;
913
-
914
- // Fetch quoted/replied message content if parentId exists
915
- let quotedMessageInfo: Awaited<ReturnType<typeof getMessageFeishu>> = null;
916
- let quotedContent: string | undefined;
917
- if (ctx.parentId) {
918
- try {
919
- quotedMessageInfo = await getMessageFeishu({
920
- cfg,
921
- messageId: ctx.parentId,
922
- accountId: account.accountId,
923
- });
924
- if (
925
- quotedMessageInfo &&
926
- shouldIncludeFetchedGroupContextMessage({
927
- isGroup,
928
- allowFrom: effectiveGroupSenderAllowFrom,
929
- mode: contextVisibilityMode,
930
- kind: "quote",
931
- senderId: quotedMessageInfo.senderId,
932
- senderType: quotedMessageInfo.senderType,
933
- })
934
- ) {
935
- quotedContent = quotedMessageInfo.content;
936
- log(
937
- `feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
938
- );
939
- } else if (quotedMessageInfo) {
940
- log(
941
- `feishu[${account.accountId}]: skipped quoted message from sender ${quotedMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
942
- );
943
- }
944
- } catch (err) {
945
- log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
946
- }
947
- }
948
-
949
- const isTopicSessionForThread =
950
- isGroup &&
951
- (groupSession?.groupSessionScope === "group_topic" ||
952
- groupSession?.groupSessionScope === "group_topic_sender");
953
-
954
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
955
- const messageBody = buildFeishuAgentBody({
956
- ctx: agentFacingCtx,
957
- quotedContent,
958
- permissionErrorForAgent,
959
- botOpenId,
960
- });
961
- const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
962
- if (permissionErrorForAgent) {
963
- // Keep the notice in a single dispatch to avoid duplicate replies (#27372).
964
- log(`feishu[${account.accountId}]: appending permission error notice to message body`);
965
- }
966
-
967
- const body = core.channel.reply.formatAgentEnvelope({
968
- channel: "Feishu",
969
- from: envelopeFrom,
970
- timestamp: new Date(),
971
- envelope: envelopeOptions,
972
- body: messageBody,
973
- });
974
-
975
- let combinedBody = body;
976
- const historyKey = groupHistoryKey;
977
-
978
- if (isGroup && historyKey && chatHistories) {
979
- combinedBody = buildPendingHistoryContextFromMap({
980
- historyMap: chatHistories,
981
- historyKey,
982
- limit: historyLimit,
983
- currentMessage: combinedBody,
984
- formatEntry: (entry) =>
985
- core.channel.reply.formatAgentEnvelope({
986
- channel: "Feishu",
987
- // Preserve speaker identity in group history as well.
988
- from: `${ctx.chatId}:${entry.sender}`,
989
- timestamp: entry.timestamp,
990
- body: entry.body,
991
- envelope: envelopeOptions,
992
- }),
993
- });
994
- }
995
-
996
- const inboundHistory =
997
- isGroup && historyKey && historyLimit > 0 && chatHistories
998
- ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
999
- sender: entry.sender,
1000
- body: entry.body,
1001
- timestamp: entry.timestamp,
1002
- }))
1003
- : undefined;
1004
-
1005
- const threadContextBySessionKey = new Map<
1006
- string,
1007
- {
1008
- threadStarterBody?: string;
1009
- threadHistoryBody?: string;
1010
- threadLabel?: string;
1011
- }
1012
- >();
1013
- let rootMessageInfo: Awaited<ReturnType<typeof getMessageFeishu>> | undefined;
1014
- let rootMessageThreadId: string | undefined;
1015
- let rootMessageFetched = false;
1016
- const getRootMessageInfo = async () => {
1017
- if (!ctx.rootId) {
1018
- return null;
1019
- }
1020
- if (!rootMessageFetched) {
1021
- rootMessageFetched = true;
1022
- if (ctx.rootId === ctx.parentId && quotedMessageInfo) {
1023
- rootMessageInfo = quotedMessageInfo;
1024
- } else {
1025
- try {
1026
- rootMessageInfo = await getMessageFeishu({
1027
- cfg,
1028
- messageId: ctx.rootId,
1029
- accountId: account.accountId,
1030
- });
1031
- } catch (err) {
1032
- log(`feishu[${account.accountId}]: failed to fetch root message: ${String(err)}`);
1033
- rootMessageInfo = null;
1034
- }
1035
- }
1036
- rootMessageThreadId = rootMessageInfo?.threadId;
1037
- if (
1038
- rootMessageInfo &&
1039
- !shouldIncludeFetchedGroupContextMessage({
1040
- isGroup,
1041
- allowFrom: effectiveGroupSenderAllowFrom,
1042
- mode: contextVisibilityMode,
1043
- kind: "thread",
1044
- senderId: rootMessageInfo.senderId,
1045
- senderType: rootMessageInfo.senderType,
1046
- })
1047
- ) {
1048
- log(
1049
- `feishu[${account.accountId}]: skipped thread starter from sender ${rootMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
1050
- );
1051
- rootMessageInfo = null;
1052
- }
1053
- }
1054
- return rootMessageInfo ?? null;
1055
- };
1056
- let groupNamePromise: Promise<string | undefined> | undefined;
1057
- const resolveGroupNameForLabel = (): Promise<string | undefined> => {
1058
- if (!isGroup) {
1059
- return Promise.resolve(undefined);
1060
- }
1061
- groupNamePromise ??= resolveGroupName({ account, chatId: ctx.chatId, log });
1062
- return groupNamePromise;
1063
- };
1064
-
1065
- const resolveThreadContextForAgent = async (
1066
- agentId: string,
1067
- agentSessionKey: string,
1068
- groupName: string | undefined,
1069
- ) => {
1070
- const cached = threadContextBySessionKey.get(agentSessionKey);
1071
- if (cached) {
1072
- return cached;
1073
- }
1074
-
1075
- const threadContext: {
1076
- threadStarterBody?: string;
1077
- threadHistoryBody?: string;
1078
- threadLabel?: string;
1079
- } = {
1080
- threadLabel:
1081
- (ctx.rootId || ctx.threadId) && isTopicSessionForThread
1082
- ? `Feishu thread in ${groupName ?? ctx.chatId}`
1083
- : undefined,
1084
- };
1085
-
1086
- if (!(ctx.rootId || ctx.threadId) || !isTopicSessionForThread) {
1087
- threadContextBySessionKey.set(agentSessionKey, threadContext);
1088
- return threadContext;
1089
- }
1090
-
1091
- const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
1092
- const previousThreadSessionTimestamp = core.channel.session.readSessionUpdatedAt({
1093
- storePath,
1094
- sessionKey: agentSessionKey,
1095
- });
1096
- if (previousThreadSessionTimestamp) {
1097
- log(
1098
- `feishu[${account.accountId}]: skipping thread bootstrap for existing session ${agentSessionKey}`,
1099
- );
1100
- threadContextBySessionKey.set(agentSessionKey, threadContext);
1101
- return threadContext;
1102
- }
1103
-
1104
- const rootMsg = await getRootMessageInfo();
1105
- let feishuThreadId = ctx.threadId ?? rootMessageThreadId ?? rootMsg?.threadId;
1106
- if (feishuThreadId) {
1107
- log(`feishu[${account.accountId}]: resolved thread ID: ${feishuThreadId}`);
1108
- }
1109
- if (!feishuThreadId) {
1110
- log(
1111
- `feishu[${account.accountId}]: no threadId found for root message ${ctx.rootId ?? "none"}, skipping thread history`,
1112
- );
1113
- threadContextBySessionKey.set(agentSessionKey, threadContext);
1114
- return threadContext;
1115
- }
1116
-
1117
- try {
1118
- const threadMessages = await listFeishuThreadMessages({
1119
- cfg,
1120
- threadId: feishuThreadId,
1121
- currentMessageId: ctx.messageId,
1122
- rootMessageId: ctx.rootId,
1123
- limit: 20,
1124
- accountId: account.accountId,
1125
- });
1126
- const senderScoped = groupSession?.groupSessionScope === "group_topic_sender";
1127
- const senderIds = new Set(
1128
- [ctx.senderOpenId, senderUserId]
1129
- .map((id) => id?.trim())
1130
- .filter((id): id is string => id !== undefined && id.length > 0),
1131
- );
1132
- const allowlistedMessages = filterFetchedGroupContextMessages(threadMessages, {
1133
- isGroup,
1134
- allowFrom: effectiveGroupSenderAllowFrom,
1135
- mode: contextVisibilityMode,
1136
- kind: "history",
1137
- });
1138
- const relevantMessages =
1139
- (senderScoped
1140
- ? allowlistedMessages.filter(
1141
- (msg) =>
1142
- msg.senderType === "app" ||
1143
- (msg.senderId !== undefined && senderIds.has(msg.senderId.trim())),
1144
- )
1145
- : allowlistedMessages) ?? [];
1146
-
1147
- const threadStarterBody = rootMsg?.content ?? relevantMessages[0]?.content;
1148
- const includeStarterInHistory = Boolean(rootMsg?.content || ctx.rootId);
1149
- const historyMessages = includeStarterInHistory
1150
- ? relevantMessages
1151
- : relevantMessages.slice(1);
1152
- const historyParts = historyMessages.map((msg) => {
1153
- const role = msg.senderType === "app" ? "assistant" : "user";
1154
- return core.channel.reply.formatAgentEnvelope({
1155
- channel: "Feishu",
1156
- from: `${msg.senderId ?? "Unknown"} (${role})`,
1157
- timestamp: msg.createTime,
1158
- body: msg.content,
1159
- envelope: envelopeOptions,
1160
- });
1161
- });
1162
-
1163
- threadContext.threadStarterBody = threadStarterBody;
1164
- threadContext.threadHistoryBody =
1165
- historyParts.length > 0 ? historyParts.join("\n\n") : undefined;
1166
- log(
1167
- `feishu[${account.accountId}]: populated thread bootstrap with starter=${threadStarterBody ? "yes" : "no"} history=${historyMessages.length}`,
1168
- );
1169
- } catch (err) {
1170
- log(`feishu[${account.accountId}]: failed to fetch thread history: ${String(err)}`);
1171
- }
1172
-
1173
- threadContextBySessionKey.set(agentSessionKey, threadContext);
1174
- return threadContext;
1175
- };
1176
-
1177
- // --- Shared context builder for dispatch ---
1178
- const buildCtxPayloadForAgent = async (
1179
- agentId: string,
1180
- agentSessionKey: string,
1181
- agentAccountId: string,
1182
- wasMentioned: boolean,
1183
- ) => {
1184
- const groupName = await resolveGroupNameForLabel();
1185
- const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
1186
- return core.channel.reply.finalizeInboundContext({
1187
- Body: combinedBody,
1188
- BodyForAgent: messageBody,
1189
- InboundHistory: inboundHistory,
1190
- ReplyToId: ctx.parentId,
1191
- RootMessageId: ctx.rootId,
1192
- RawBody: agentFacingContent,
1193
- CommandBody: agentFacingContent,
1194
- Transcript: audioTranscript,
1195
- From: feishuFrom,
1196
- To: feishuTo,
1197
- SessionKey: agentSessionKey,
1198
- AccountId: agentAccountId,
1199
- ChatType: isGroup ? "group" : "direct",
1200
- GroupSubject: isGroup ? groupName || ctx.chatId : undefined,
1201
- ConversationLabel: isGroup && groupName && !isTopicSessionForThread ? groupName : undefined,
1202
- SenderName: ctx.senderName ?? ctx.senderOpenId,
1203
- SenderId: ctx.senderOpenId,
1204
- Provider: "feishu" as const,
1205
- Surface: "feishu" as const,
1206
- MessageSid: ctx.messageId,
1207
- ReplyToBody: quotedContent ?? undefined,
1208
- ThreadStarterBody: threadContext.threadStarterBody,
1209
- ThreadHistoryBody: threadContext.threadHistoryBody,
1210
- ThreadLabel: threadContext.threadLabel,
1211
- // Only use rootId (om_* message anchor) — threadId (omt_*) is a container
1212
- // ID and would produce invalid reply targets downstream.
1213
- MessageThreadId: ctx.rootId && isTopicSessionForThread ? ctx.rootId : undefined,
1214
- Timestamp: messageCreateTimeMs,
1215
- WasMentioned: wasMentioned,
1216
- CommandAuthorized: commandAuthorized,
1217
- OriginatingChannel: "feishu" as const,
1218
- OriginatingTo: feishuTo,
1219
- GroupSystemPrompt: isGroup ? normalizeOptionalString(groupConfig?.systemPrompt) : undefined,
1220
- ...mediaPayload,
1221
- ...(preflightAudioIndex >= 0 ? { MediaTranscribedIndexes: [preflightAudioIndex] } : {}),
1222
- });
1223
- };
1224
-
1225
- // Determine reply target based on group session mode:
1226
- // - Topic-mode groups (group_topic / group_topic_sender): reply to the topic
1227
- // root so the bot stays in the same thread.
1228
- // - Groups with explicit replyInThread config: reply to the root so the bot
1229
- // stays in the thread the user expects.
1230
- // - Normal groups (auto-detected threadReply from root_id): reply to the
1231
- // triggering message itself. Using rootId here would silently push the
1232
- // reply into a topic thread invisible in the main chat view (#32980).
1233
- const isTopicSession =
1234
- isGroup &&
1235
- (groupSession?.groupSessionScope === "group_topic" ||
1236
- groupSession?.groupSessionScope === "group_topic_sender");
1237
- const configReplyInThread =
1238
- isGroup &&
1239
- (groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
1240
- const replyTargetMessageId =
1241
- isTopicSession || configReplyInThread
1242
- ? (ctx.rootId ??
1243
- ctx.replyTargetMessageId ??
1244
- (ctx.suppressReplyTarget ? undefined : ctx.messageId))
1245
- : (ctx.replyTargetMessageId ?? (ctx.suppressReplyTarget ? undefined : ctx.messageId));
1246
- const threadReply = isGroup ? (groupSession?.threadReply ?? false) : false;
1247
-
1248
- if (broadcastAgents) {
1249
- // Cross-account dedup: in multi-account setups, Feishu delivers the same
1250
- // event to every bot account in the group. Only one account should handle
1251
- // broadcast dispatch to avoid duplicate agent sessions and race conditions.
1252
- // Uses a shared "broadcast" namespace (not per-account) so the first handler
1253
- // to reach this point claims the message; subsequent accounts skip.
1254
- if (!(await tryRecordMessagePersistent(ctx.messageId, "broadcast", log))) {
1255
- log(
1256
- `feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`,
1257
- );
1258
- return;
1259
- }
1260
-
1261
- // --- Broadcast dispatch: send message to all configured agents ---
1262
- const rawStrategy = (
1263
- (cfg as Record<string, unknown>).broadcast as Record<string, unknown> | undefined
1264
- )?.strategy;
1265
- const strategy = rawStrategy === "sequential" ? "sequential" : "parallel";
1266
- const activeAgentId =
1267
- ctx.mentionedBot || !requireMention ? normalizeAgentId(route.agentId) : null;
1268
- const agentIds = (cfg.agents?.list ?? []).map((a: { id: string }) => normalizeAgentId(a.id));
1269
- const hasKnownAgents = agentIds.length > 0;
1270
-
1271
- log(
1272
- `feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`,
1273
- );
1274
-
1275
- const dispatchForAgent = async (agentId: string) => {
1276
- if (hasKnownAgents && !agentIds.includes(normalizeAgentId(agentId))) {
1277
- log(
1278
- `feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`,
1279
- );
1280
- return;
1281
- }
1282
-
1283
- const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
1284
- const agentStorePath = core.channel.session.resolveStorePath(cfg.session?.store, {
1285
- agentId,
1286
- });
1287
- const agentRecord = {
1288
- onRecordError: (err: unknown) => {
1289
- log(
1290
- `feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`,
1291
- );
1292
- },
1293
- };
1294
- const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
1295
- storePath: agentStorePath,
1296
- sessionKey: agentSessionKey,
1297
- });
1298
- const agentCtx = await buildCtxPayloadForAgent(
1299
- agentId,
1300
- agentSessionKey,
1301
- route.accountId,
1302
- ctx.mentionedBot && agentId === activeAgentId,
1303
- );
1304
-
1305
- if (agentId === activeAgentId) {
1306
- // Active agent: real Feishu dispatcher (responds on Feishu)
1307
- const identity = resolveAgentOutboundIdentity(cfg, agentId);
1308
- const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
1309
- cfg,
1310
- agentId,
1311
- runtime: runtime as RuntimeEnv,
1312
- chatId: ctx.chatId,
1313
- allowReasoningPreview,
1314
- replyToMessageId: replyTargetMessageId,
1315
- skipReplyToInMessages: !isGroup,
1316
- replyInThread,
1317
- rootId: ctx.rootId,
1318
- threadReply,
1319
- mentionTargets: ctx.mentionTargets,
1320
- accountId: account.accountId,
1321
- identity,
1322
- messageCreateTimeMs,
1323
- });
1324
-
1325
- log(
1326
- `feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`,
1327
- );
1328
- await core.channel.turn.run({
1329
- channel: "feishu",
1330
- accountId: route.accountId,
1331
- raw: ctx,
1332
- adapter: {
1333
- ingest: () => ({
1334
- id: ctx.messageId,
1335
- timestamp: messageCreateTimeMs,
1336
- rawText: ctx.content,
1337
- textForAgent: agentCtx.BodyForAgent,
1338
- textForCommands: agentCtx.CommandBody,
1339
- raw: ctx,
1340
- }),
1341
- resolveTurn: () => ({
1342
- channel: "feishu",
1343
- accountId: route.accountId,
1344
- routeSessionKey: agentSessionKey,
1345
- storePath: agentStorePath,
1346
- ctxPayload: agentCtx,
1347
- recordInboundSession: core.channel.session.recordInboundSession,
1348
- record: agentRecord,
1349
- onPreDispatchFailure: () =>
1350
- core.channel.reply.settleReplyDispatcher({
1351
- dispatcher,
1352
- onSettled: () => markDispatchIdle(),
1353
- }),
1354
- runDispatch: () =>
1355
- core.channel.reply.withReplyDispatcher({
1356
- dispatcher,
1357
- onSettled: () => markDispatchIdle(),
1358
- run: () =>
1359
- core.channel.reply.dispatchReplyFromConfig({
1360
- ctx: agentCtx,
1361
- cfg,
1362
- dispatcher,
1363
- replyOptions,
1364
- }),
1365
- }),
1366
- }),
1367
- },
1368
- });
1369
- } else {
1370
- // Observer agent: no-op dispatcher (session entry + inference, no Feishu reply).
1371
- // Strip CommandAuthorized so slash commands (e.g. /reset) don't silently
1372
- // mutate observer sessions — only the active agent should execute commands.
1373
- delete (agentCtx as Record<string, unknown>).CommandAuthorized;
1374
- const noopDispatcher = {
1375
- sendToolResult: () => false,
1376
- sendBlockReply: () => false,
1377
- sendFinalReply: () => false,
1378
- waitForIdle: async () => {},
1379
- getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
1380
- getFailedCounts: () => ({ tool: 0, block: 0, final: 0 }),
1381
- markComplete: () => {},
1382
- };
1383
-
1384
- log(
1385
- `feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`,
1386
- );
1387
- await core.channel.turn.run({
1388
- channel: "feishu",
1389
- accountId: route.accountId,
1390
- raw: ctx,
1391
- adapter: {
1392
- ingest: () => ({
1393
- id: ctx.messageId,
1394
- timestamp: messageCreateTimeMs,
1395
- rawText: ctx.content,
1396
- textForAgent: agentCtx.BodyForAgent,
1397
- textForCommands: agentCtx.CommandBody,
1398
- raw: ctx,
1399
- }),
1400
- resolveTurn: () => ({
1401
- channel: "feishu",
1402
- accountId: route.accountId,
1403
- routeSessionKey: agentSessionKey,
1404
- storePath: agentStorePath,
1405
- ctxPayload: agentCtx,
1406
- recordInboundSession: core.channel.session.recordInboundSession,
1407
- record: agentRecord,
1408
- runDispatch: () =>
1409
- core.channel.reply.withReplyDispatcher({
1410
- dispatcher: noopDispatcher,
1411
- run: () =>
1412
- core.channel.reply.dispatchReplyFromConfig({
1413
- ctx: agentCtx,
1414
- cfg,
1415
- dispatcher: noopDispatcher,
1416
- }),
1417
- }),
1418
- }),
1419
- },
1420
- });
1421
- }
1422
- };
1423
-
1424
- if (strategy === "sequential") {
1425
- for (const agentId of broadcastAgents) {
1426
- try {
1427
- await dispatchForAgent(agentId);
1428
- } catch (err) {
1429
- log(
1430
- `feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`,
1431
- );
1432
- }
1433
- }
1434
- } else {
1435
- const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
1436
- for (let i = 0; i < results.length; i++) {
1437
- if (results[i].status === "rejected") {
1438
- log(
1439
- `feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String((results[i] as PromiseRejectedResult).reason)}`,
1440
- );
1441
- }
1442
- }
1443
- }
1444
-
1445
- if (isGroup && historyKey && chatHistories) {
1446
- clearHistoryEntriesIfEnabled({
1447
- historyMap: chatHistories,
1448
- historyKey,
1449
- limit: historyLimit,
1450
- });
1451
- }
1452
-
1453
- log(
1454
- `feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`,
1455
- );
1456
- } else {
1457
- // --- Single-agent dispatch (existing behavior) ---
1458
- const ctxPayload = await buildCtxPayloadForAgent(
1459
- route.agentId,
1460
- route.sessionKey,
1461
- route.accountId,
1462
- ctx.mentionedBot,
1463
- );
1464
-
1465
- const identity = resolveAgentOutboundIdentity(cfg, route.agentId);
1466
- const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
1467
- agentId: route.agentId,
1468
- });
1469
- const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
1470
- storePath,
1471
- sessionKey: route.sessionKey,
1472
- });
1473
- const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
1474
- cfg,
1475
- agentId: route.agentId,
1476
- runtime: runtime as RuntimeEnv,
1477
- chatId: ctx.chatId,
1478
- allowReasoningPreview,
1479
- replyToMessageId: replyTargetMessageId,
1480
- skipReplyToInMessages: !isGroup,
1481
- replyInThread,
1482
- rootId: ctx.rootId,
1483
- threadReply,
1484
- mentionTargets: ctx.mentionTargets,
1485
- accountId: account.accountId,
1486
- identity,
1487
- messageCreateTimeMs,
1488
- });
1489
-
1490
- log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
1491
- const turnResult = await core.channel.turn.run({
1492
- channel: "feishu",
1493
- accountId: route.accountId,
1494
- raw: ctx,
1495
- adapter: {
1496
- ingest: () => ({
1497
- id: ctx.messageId,
1498
- timestamp: messageCreateTimeMs,
1499
- rawText: ctx.content,
1500
- textForAgent: ctxPayload.BodyForAgent,
1501
- textForCommands: ctxPayload.CommandBody,
1502
- raw: ctx,
1503
- }),
1504
- resolveTurn: () => ({
1505
- channel: "feishu",
1506
- accountId: route.accountId,
1507
- routeSessionKey: route.sessionKey,
1508
- storePath,
1509
- ctxPayload,
1510
- recordInboundSession: core.channel.session.recordInboundSession,
1511
- record: {
1512
- onRecordError: (err) => {
1513
- log(
1514
- `feishu[${account.accountId}]: failed to record inbound session ${route.sessionKey}: ${String(err)}`,
1515
- );
1516
- },
1517
- },
1518
- history: {
1519
- isGroup,
1520
- historyKey,
1521
- historyMap: chatHistories,
1522
- limit: historyLimit,
1523
- },
1524
- onPreDispatchFailure: () =>
1525
- core.channel.reply.settleReplyDispatcher({
1526
- dispatcher,
1527
- onSettled: () => markDispatchIdle(),
1528
- }),
1529
- runDispatch: () =>
1530
- core.channel.reply.withReplyDispatcher({
1531
- dispatcher,
1532
- onSettled: () => {
1533
- markDispatchIdle();
1534
- },
1535
- run: () =>
1536
- core.channel.reply.dispatchReplyFromConfig({
1537
- ctx: ctxPayload,
1538
- cfg,
1539
- dispatcher,
1540
- replyOptions,
1541
- }),
1542
- }),
1543
- }),
1544
- },
1545
- });
1546
- if (!turnResult.dispatched) {
1547
- return;
1548
- }
1549
- const { dispatchResult } = turnResult;
1550
- const { queuedFinal, counts } = dispatchResult;
1551
-
1552
- log(
1553
- `feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
1554
- );
1555
- }
1556
- } catch (err) {
1557
- error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
1558
- }
1559
- }