@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.1

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 (114) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. package/index.ts +0 -75
@@ -0,0 +1,941 @@
1
+ import { normalizeAgentId } from "./nextclaw-sdk/account-id.js";
2
+ import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "./nextclaw-sdk/core-channel.js";
3
+ import { buildAgentMediaPayload, createScopedPairingAccess, issuePairingChallenge } from "./nextclaw-sdk/core-pairing.js";
4
+ import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "./nextclaw-sdk/history.js";
5
+ import "./nextclaw-sdk/feishu.js";
6
+ import { resolveFeishuAccount } from "./accounts.js";
7
+ import { createFeishuClient } from "./client.js";
8
+ import { raceWithTimeoutAndAbort } from "./async.js";
9
+ import { normalizeFeishuExternalKey } from "./external-keys.js";
10
+ import { getFeishuRuntime } from "./runtime.js";
11
+ import { downloadMessageResourceFeishu } from "./media.js";
12
+ import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
13
+ import { parsePostContent } from "./post.js";
14
+ import { getMessageFeishu, sendMessageFeishu } from "./send.js";
15
+ import { isFeishuGroupAllowed, resolveFeishuAllowlistMatch, resolveFeishuGroupConfig, resolveFeishuReplyPolicy } from "./policy.js";
16
+ import { finalizeFeishuMessageProcessing, tryRecordMessagePersistent } from "./dedup.js";
17
+ import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
18
+ import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
19
+ //#region src/bot.ts
20
+ const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"];
21
+ const FEISHU_SCOPE_CORRECTIONS = { "contact:contact.base:readonly": "contact:user.base:readonly" };
22
+ function correctFeishuScopeInUrl(url) {
23
+ let corrected = url;
24
+ for (const [wrong, right] of Object.entries(FEISHU_SCOPE_CORRECTIONS)) {
25
+ corrected = corrected.replaceAll(encodeURIComponent(wrong), encodeURIComponent(right));
26
+ corrected = corrected.replaceAll(wrong, right);
27
+ }
28
+ return corrected;
29
+ }
30
+ function shouldSuppressPermissionErrorNotice(permissionError) {
31
+ const message = permissionError.message.toLowerCase();
32
+ return IGNORED_PERMISSION_SCOPE_TOKENS.some((token) => message.includes(token));
33
+ }
34
+ function extractPermissionError(err) {
35
+ if (!err || typeof err !== "object") return null;
36
+ const data = err.response?.data;
37
+ if (!data || typeof data !== "object") return null;
38
+ const feishuErr = data;
39
+ if (feishuErr.code !== 99991672) return null;
40
+ const msg = feishuErr.msg ?? "";
41
+ const urlMatch = msg.match(/https:\/\/[^\s,]+\/app\/[^\s,]+/);
42
+ const grantUrl = urlMatch?.[0] ? correctFeishuScopeInUrl(urlMatch[0]) : void 0;
43
+ return {
44
+ code: feishuErr.code,
45
+ message: msg,
46
+ grantUrl
47
+ };
48
+ }
49
+ const SENDER_NAME_TTL_MS = 600 * 1e3;
50
+ const SENDER_NAME_LOOKUP_BUDGET_MS = 1500;
51
+ const senderNameCache = /* @__PURE__ */ new Map();
52
+ const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
53
+ const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
54
+ function getCachedSenderName(senderId) {
55
+ const normalizedSenderId = senderId.trim();
56
+ if (!normalizedSenderId) return;
57
+ const cached = senderNameCache.get(normalizedSenderId);
58
+ const now = Date.now();
59
+ if (!cached || cached.expireAt <= now) return;
60
+ return cached.name;
61
+ }
62
+ function resolveSenderLookupIdType(senderId) {
63
+ const trimmed = senderId.trim();
64
+ if (trimmed.startsWith("ou_")) return "open_id";
65
+ if (trimmed.startsWith("on_")) return "union_id";
66
+ return "user_id";
67
+ }
68
+ async function resolveFeishuSenderName(params) {
69
+ const { account, senderId, log } = params;
70
+ if (!account.configured) return {};
71
+ const normalizedSenderId = senderId.trim();
72
+ if (!normalizedSenderId) return {};
73
+ const cached = senderNameCache.get(normalizedSenderId);
74
+ const now = Date.now();
75
+ if (cached && cached.expireAt > now) return { name: cached.name };
76
+ try {
77
+ const client = createFeishuClient(account);
78
+ const userIdType = resolveSenderLookupIdType(normalizedSenderId);
79
+ const res = await client.contact.user.get({
80
+ path: { user_id: normalizedSenderId },
81
+ params: { user_id_type: userIdType }
82
+ });
83
+ const name = res?.data?.user?.name || res?.data?.user?.display_name || res?.data?.user?.nickname || res?.data?.user?.en_name;
84
+ if (name && typeof name === "string") {
85
+ senderNameCache.set(normalizedSenderId, {
86
+ name,
87
+ expireAt: now + SENDER_NAME_TTL_MS
88
+ });
89
+ return { name };
90
+ }
91
+ return {};
92
+ } catch (err) {
93
+ const permErr = extractPermissionError(err);
94
+ if (permErr) {
95
+ if (shouldSuppressPermissionErrorNotice(permErr)) {
96
+ log(`feishu: ignoring stale permission scope error: ${permErr.message}`);
97
+ return {};
98
+ }
99
+ log(`feishu: permission error resolving sender name: code=${permErr.code}`);
100
+ return { permissionError: permErr };
101
+ }
102
+ log(`feishu: failed to resolve sender name for ${normalizedSenderId}: ${String(err)}`);
103
+ return {};
104
+ }
105
+ }
106
+ async function resolveFeishuSenderNameWithinBudget(params) {
107
+ const cachedName = getCachedSenderName(params.senderId);
108
+ if (cachedName) return { name: cachedName };
109
+ const timeoutMs = params.timeoutMs ?? SENDER_NAME_LOOKUP_BUDGET_MS;
110
+ const lookupPromise = resolveFeishuSenderName(params);
111
+ const lookupResult = await raceWithTimeoutAndAbort(lookupPromise, { timeoutMs });
112
+ if (lookupResult.status === "resolved") return lookupResult.value;
113
+ params.log(`feishu[${params.account.accountId}]: sender-name lookup exceeded ${timeoutMs}ms; continuing without blocking reply`);
114
+ lookupPromise.catch(() => void 0);
115
+ return {};
116
+ }
117
+ function resolveFeishuGroupSession(params) {
118
+ const { chatId, senderOpenId, messageId, rootId, threadId, groupConfig, feishuCfg } = params;
119
+ const normalizedThreadId = threadId?.trim();
120
+ const normalizedRootId = rootId?.trim();
121
+ const threadReply = Boolean(normalizedThreadId || normalizedRootId);
122
+ const replyInThread = (groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled" || threadReply;
123
+ const legacyTopicSessionMode = groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
124
+ const groupSessionScope = groupConfig?.groupSessionScope ?? feishuCfg?.groupSessionScope ?? (legacyTopicSessionMode === "enabled" ? "group_topic" : "group");
125
+ const topicScope = groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender" ? normalizedRootId ?? normalizedThreadId ?? (replyInThread ? messageId : null) : null;
126
+ let peerId = chatId;
127
+ switch (groupSessionScope) {
128
+ case "group_sender":
129
+ peerId = `${chatId}:sender:${senderOpenId}`;
130
+ break;
131
+ case "group_topic":
132
+ peerId = topicScope ? `${chatId}:topic:${topicScope}` : chatId;
133
+ break;
134
+ case "group_topic_sender":
135
+ peerId = topicScope ? `${chatId}:topic:${topicScope}:sender:${senderOpenId}` : `${chatId}:sender:${senderOpenId}`;
136
+ break;
137
+ default:
138
+ peerId = chatId;
139
+ break;
140
+ }
141
+ return {
142
+ peerId,
143
+ parentPeer: topicScope && (groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender") ? {
144
+ kind: "group",
145
+ id: chatId
146
+ } : null,
147
+ groupSessionScope,
148
+ replyInThread,
149
+ threadReply
150
+ };
151
+ }
152
+ function parseMessageContent(content, messageType) {
153
+ if (messageType === "post") {
154
+ const { textContent } = parsePostContent(content);
155
+ return textContent;
156
+ }
157
+ try {
158
+ const parsed = JSON.parse(content);
159
+ if (messageType === "text") return parsed.text || "";
160
+ if (messageType === "share_chat") {
161
+ if (parsed && typeof parsed === "object") {
162
+ const share = parsed;
163
+ if (typeof share.body === "string" && share.body.trim().length > 0) return share.body.trim();
164
+ if (typeof share.summary === "string" && share.summary.trim().length > 0) return share.summary.trim();
165
+ if (typeof share.share_chat_id === "string" && share.share_chat_id.trim().length > 0) return `[Forwarded message: ${share.share_chat_id.trim()}]`;
166
+ }
167
+ return "[Forwarded message]";
168
+ }
169
+ if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
170
+ return content;
171
+ } catch {
172
+ return content;
173
+ }
174
+ }
175
+ /**
176
+ * Parse merge_forward message content and fetch sub-messages.
177
+ * Returns formatted text content of all sub-messages.
178
+ */
179
+ function parseMergeForwardContent(params) {
180
+ const { content, log } = params;
181
+ const maxMessages = 50;
182
+ log?.(`feishu: parsing merge_forward sub-messages from API response`);
183
+ let items;
184
+ try {
185
+ items = JSON.parse(content);
186
+ } catch {
187
+ log?.(`feishu: merge_forward items parse failed`);
188
+ return "[Merged and Forwarded Message - parse error]";
189
+ }
190
+ if (!Array.isArray(items) || items.length === 0) return "[Merged and Forwarded Message - no sub-messages]";
191
+ const subMessages = items.filter((item) => item.upper_message_id);
192
+ if (subMessages.length === 0) return "[Merged and Forwarded Message - no sub-messages found]";
193
+ log?.(`feishu: merge_forward contains ${subMessages.length} sub-messages`);
194
+ subMessages.sort((a, b) => {
195
+ return parseInt(a.create_time || "0", 10) - parseInt(b.create_time || "0", 10);
196
+ });
197
+ const lines = ["[Merged and Forwarded Messages]"];
198
+ const limitedMessages = subMessages.slice(0, maxMessages);
199
+ for (const item of limitedMessages) {
200
+ const formatted = formatSubMessageContent(item.body?.content || "", item.msg_type || "text");
201
+ lines.push(`- ${formatted}`);
202
+ }
203
+ if (subMessages.length > maxMessages) lines.push(`... and ${subMessages.length - maxMessages} more messages`);
204
+ return lines.join("\n");
205
+ }
206
+ /**
207
+ * Format sub-message content based on message type.
208
+ */
209
+ function formatSubMessageContent(content, contentType) {
210
+ try {
211
+ const parsed = JSON.parse(content);
212
+ switch (contentType) {
213
+ case "text": return parsed.text || content;
214
+ case "post": {
215
+ const { textContent } = parsePostContent(content);
216
+ return textContent;
217
+ }
218
+ case "image": return "[Image]";
219
+ case "file": return `[File: ${parsed.file_name || "unknown"}]`;
220
+ case "audio": return "[Audio]";
221
+ case "video": return "[Video]";
222
+ case "sticker": return "[Sticker]";
223
+ case "merge_forward": return "[Nested Merged Forward]";
224
+ default: return `[${contentType}]`;
225
+ }
226
+ } catch {
227
+ return content;
228
+ }
229
+ }
230
+ function checkBotMentioned(event, botOpenId) {
231
+ if (!botOpenId) return false;
232
+ if ((event.message.content ?? "").includes("@_all")) return true;
233
+ const mentions = event.message.mentions ?? [];
234
+ if (mentions.length > 0) return mentions.some((m) => m.id.open_id === botOpenId);
235
+ if (event.message.message_type === "post") {
236
+ const { mentionedOpenIds } = parsePostContent(event.message.content);
237
+ return mentionedOpenIds.some((id) => id === botOpenId);
238
+ }
239
+ return false;
240
+ }
241
+ function normalizeMentions(text, mentions, botStripId) {
242
+ if (!mentions || mentions.length === 0) return text;
243
+ const escaped = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
244
+ const escapeName = (value) => value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
245
+ let result = text;
246
+ for (const mention of mentions) {
247
+ const mentionId = mention.id.open_id;
248
+ const replacement = botStripId && mentionId === botStripId ? "" : mentionId ? `<at user_id="${mentionId}">${escapeName(mention.name)}</at>` : `@${mention.name}`;
249
+ result = result.replace(new RegExp(escaped(mention.key), "g"), () => replacement).trim();
250
+ }
251
+ return result;
252
+ }
253
+ function normalizeFeishuCommandProbeBody(text) {
254
+ if (!text) return "";
255
+ return text.replace(/<at\b[^>]*>[^<]*<\/at>/giu, " ").replace(/(^|\s)@[^/\s]+(?=\s|$|\/)/gu, "$1").replace(/\s+/g, " ").trim();
256
+ }
257
+ /**
258
+ * Parse media keys from message content based on message type.
259
+ */
260
+ function parseMediaKeys(content, messageType) {
261
+ try {
262
+ const parsed = JSON.parse(content);
263
+ const imageKey = normalizeFeishuExternalKey(parsed.image_key);
264
+ const fileKey = normalizeFeishuExternalKey(parsed.file_key);
265
+ switch (messageType) {
266
+ case "image": return { imageKey };
267
+ case "file": return {
268
+ fileKey,
269
+ fileName: parsed.file_name
270
+ };
271
+ case "audio": return { fileKey };
272
+ case "video":
273
+ case "media": return {
274
+ fileKey,
275
+ imageKey
276
+ };
277
+ case "sticker": return { fileKey };
278
+ default: return {};
279
+ }
280
+ } catch {
281
+ return {};
282
+ }
283
+ }
284
+ /**
285
+ * Map Feishu message type to messageResource.get resource type.
286
+ * Feishu messageResource API supports only: image | file.
287
+ */
288
+ function toMessageResourceType(messageType) {
289
+ return messageType === "image" ? "image" : "file";
290
+ }
291
+ /**
292
+ * Infer placeholder text based on message type.
293
+ */
294
+ function inferPlaceholder(messageType) {
295
+ switch (messageType) {
296
+ case "image": return "<media:image>";
297
+ case "file": return "<media:document>";
298
+ case "audio": return "<media:audio>";
299
+ case "video":
300
+ case "media": return "<media:video>";
301
+ case "sticker": return "<media:sticker>";
302
+ default: return "<media:document>";
303
+ }
304
+ }
305
+ /**
306
+ * Resolve media from a Feishu message, downloading and saving to disk.
307
+ * Similar to Discord's resolveMediaList().
308
+ */
309
+ async function resolveFeishuMediaList(params) {
310
+ const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
311
+ if (![
312
+ "image",
313
+ "file",
314
+ "audio",
315
+ "video",
316
+ "media",
317
+ "sticker",
318
+ "post"
319
+ ].includes(messageType)) return [];
320
+ const out = [];
321
+ const core = getFeishuRuntime();
322
+ if (messageType === "post") {
323
+ const { imageKeys, mediaKeys: postMediaKeys } = parsePostContent(content);
324
+ if (imageKeys.length === 0 && postMediaKeys.length === 0) return [];
325
+ if (imageKeys.length > 0) log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
326
+ if (postMediaKeys.length > 0) log?.(`feishu: post message contains ${postMediaKeys.length} embedded media file(s)`);
327
+ for (const imageKey of imageKeys) try {
328
+ const result = await downloadMessageResourceFeishu({
329
+ cfg,
330
+ messageId,
331
+ fileKey: imageKey,
332
+ type: "image",
333
+ accountId
334
+ });
335
+ let contentType = result.contentType;
336
+ if (!contentType) contentType = await core.media.detectMime({ buffer: result.buffer });
337
+ const saved = await core.channel.media.saveMediaBuffer(result.buffer, contentType, "inbound", maxBytes);
338
+ out.push({
339
+ path: saved.path,
340
+ contentType: saved.contentType,
341
+ placeholder: "<media:image>"
342
+ });
343
+ log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
344
+ } catch (err) {
345
+ log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
346
+ }
347
+ for (const media of postMediaKeys) try {
348
+ const result = await downloadMessageResourceFeishu({
349
+ cfg,
350
+ messageId,
351
+ fileKey: media.fileKey,
352
+ type: "file",
353
+ accountId
354
+ });
355
+ let contentType = result.contentType;
356
+ if (!contentType) contentType = await core.media.detectMime({ buffer: result.buffer });
357
+ const saved = await core.channel.media.saveMediaBuffer(result.buffer, contentType, "inbound", maxBytes);
358
+ out.push({
359
+ path: saved.path,
360
+ contentType: saved.contentType,
361
+ placeholder: "<media:video>"
362
+ });
363
+ log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
364
+ } catch (err) {
365
+ log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
366
+ }
367
+ return out;
368
+ }
369
+ const mediaKeys = parseMediaKeys(content, messageType);
370
+ if (!mediaKeys.imageKey && !mediaKeys.fileKey) return [];
371
+ try {
372
+ let buffer;
373
+ let contentType;
374
+ let fileName;
375
+ const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
376
+ if (!fileKey) return [];
377
+ const result = await downloadMessageResourceFeishu({
378
+ cfg,
379
+ messageId,
380
+ fileKey,
381
+ type: toMessageResourceType(messageType),
382
+ accountId
383
+ });
384
+ buffer = result.buffer;
385
+ contentType = result.contentType;
386
+ fileName = result.fileName || mediaKeys.fileName;
387
+ if (!contentType) contentType = await core.media.detectMime({ buffer });
388
+ const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, "inbound", maxBytes, fileName);
389
+ out.push({
390
+ path: saved.path,
391
+ contentType: saved.contentType,
392
+ placeholder: inferPlaceholder(messageType)
393
+ });
394
+ log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
395
+ } catch (err) {
396
+ log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
397
+ }
398
+ return out;
399
+ }
400
+ function resolveBroadcastAgents(cfg, peerId) {
401
+ const broadcast = cfg.broadcast;
402
+ if (!broadcast || typeof broadcast !== "object") return null;
403
+ const agents = broadcast[peerId];
404
+ if (!Array.isArray(agents) || agents.length === 0) return null;
405
+ return agents;
406
+ }
407
+ function buildBroadcastSessionKey(baseSessionKey, originalAgentId, targetAgentId) {
408
+ const prefix = `agent:${originalAgentId}:`;
409
+ if (baseSessionKey.startsWith(prefix)) return `agent:${targetAgentId}:${baseSessionKey.slice(prefix.length)}`;
410
+ return baseSessionKey;
411
+ }
412
+ /**
413
+ * Build media payload for inbound context.
414
+ * Similar to Discord's buildDiscordMediaPayload().
415
+ */
416
+ function parseFeishuMessageEvent(event, botOpenId, _botName) {
417
+ const rawContent = parseMessageContent(event.message.content, event.message.message_type);
418
+ const mentionedBot = checkBotMentioned(event, botOpenId);
419
+ const hasAnyMention = (event.message.mentions?.length ?? 0) > 0;
420
+ const content = normalizeMentions(rawContent, event.message.mentions, botOpenId);
421
+ const senderOpenId = event.sender.sender_id.open_id?.trim();
422
+ const senderUserId = event.sender.sender_id.user_id?.trim();
423
+ const senderFallbackId = senderOpenId || senderUserId || "";
424
+ const ctx = {
425
+ chatId: event.message.chat_id,
426
+ messageId: event.message.message_id,
427
+ senderId: senderUserId || senderOpenId || "",
428
+ senderOpenId: senderFallbackId,
429
+ chatType: event.message.chat_type,
430
+ mentionedBot,
431
+ hasAnyMention,
432
+ rootId: event.message.root_id || void 0,
433
+ parentId: event.message.parent_id || void 0,
434
+ threadId: event.message.thread_id || void 0,
435
+ content,
436
+ contentType: event.message.message_type
437
+ };
438
+ if (isMentionForwardRequest(event, botOpenId)) {
439
+ const mentionTargets = extractMentionTargets(event, botOpenId);
440
+ if (mentionTargets.length > 0) ctx.mentionTargets = mentionTargets;
441
+ }
442
+ return ctx;
443
+ }
444
+ function buildFeishuAgentBody(params) {
445
+ const { ctx, quotedContent, permissionErrorForAgent, botOpenId } = params;
446
+ let messageBody = ctx.content;
447
+ if (quotedContent) messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
448
+ messageBody = `${ctx.senderName ?? ctx.senderOpenId}: ${messageBody}`;
449
+ if (ctx.hasAnyMention) {
450
+ const botIdHint = botOpenId?.trim();
451
+ messageBody += "\n\n[System: The content may include mention tags in the form <at user_id=\"...\">name</at>. Treat these as real mentions of Feishu entities (users or bots).]";
452
+ if (botIdHint) messageBody += `\n[System: If user_id is "${botIdHint}", that mention refers to you.]`;
453
+ }
454
+ if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
455
+ const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
456
+ messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
457
+ }
458
+ messageBody = `[message_id: ${ctx.messageId}]\n${messageBody}`;
459
+ if (permissionErrorForAgent) {
460
+ const grantUrl = permissionErrorForAgent.grantUrl ?? "";
461
+ 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}]`;
462
+ }
463
+ return messageBody;
464
+ }
465
+ async function handleFeishuMessage(params) {
466
+ const { cfg, event, botOpenId, botName, runtime, chatHistories, accountId, processingClaimHeld = false } = params;
467
+ const account = resolveFeishuAccount({
468
+ cfg,
469
+ accountId
470
+ });
471
+ const feishuCfg = account.config;
472
+ const log = runtime?.log ?? console.log;
473
+ const error = runtime?.error ?? console.error;
474
+ const messageId = event.message.message_id;
475
+ if (!await finalizeFeishuMessageProcessing({
476
+ messageId,
477
+ namespace: account.accountId,
478
+ log,
479
+ claimHeld: processingClaimHeld
480
+ })) {
481
+ log(`feishu: skipping duplicate message ${messageId}`);
482
+ return;
483
+ }
484
+ let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
485
+ const isGroup = ctx.chatType === "group";
486
+ const isDirect = !isGroup;
487
+ const senderUserId = event.sender.sender_id.user_id?.trim() || void 0;
488
+ if (event.message.message_type === "merge_forward") {
489
+ log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
490
+ try {
491
+ const response = await createFeishuClient(account).im.message.get({ path: { message_id: event.message.message_id } });
492
+ if (response.code === 0 && response.data?.items && response.data.items.length > 0) {
493
+ log(`feishu[${account.accountId}]: merge_forward API returned ${response.data.items.length} items`);
494
+ const expandedContent = parseMergeForwardContent({
495
+ content: JSON.stringify(response.data.items),
496
+ log
497
+ });
498
+ ctx = {
499
+ ...ctx,
500
+ content: expandedContent
501
+ };
502
+ } else {
503
+ log(`feishu[${account.accountId}]: merge_forward API returned no items`);
504
+ ctx = {
505
+ ...ctx,
506
+ content: "[Merged and Forwarded Message - could not fetch]"
507
+ };
508
+ }
509
+ } catch (err) {
510
+ log(`feishu[${account.accountId}]: merge_forward fetch failed: ${String(err)}`);
511
+ ctx = {
512
+ ...ctx,
513
+ content: "[Merged and Forwarded Message - fetch error]"
514
+ };
515
+ }
516
+ }
517
+ let permissionErrorForAgent;
518
+ if (feishuCfg?.resolveSenderNames ?? true) {
519
+ const senderResult = await resolveFeishuSenderNameWithinBudget({
520
+ account,
521
+ senderId: ctx.senderOpenId,
522
+ log
523
+ });
524
+ if (senderResult.name) ctx = {
525
+ ...ctx,
526
+ senderName: senderResult.name
527
+ };
528
+ if (senderResult.permissionError) {
529
+ const appKey = account.appId ?? "default";
530
+ const now = Date.now();
531
+ if (now - (permissionErrorNotifiedAt.get(appKey) ?? 0) > PERMISSION_ERROR_COOLDOWN_MS) {
532
+ permissionErrorNotifiedAt.set(appKey, now);
533
+ permissionErrorForAgent = senderResult.permissionError;
534
+ }
535
+ }
536
+ }
537
+ log(`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
538
+ if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
539
+ const names = ctx.mentionTargets.map((t) => t.name).join(", ");
540
+ log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
541
+ }
542
+ const historyLimit = Math.max(0, feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 50);
543
+ const groupConfig = isGroup ? resolveFeishuGroupConfig({
544
+ cfg: feishuCfg,
545
+ groupId: ctx.chatId
546
+ }) : void 0;
547
+ const groupSession = isGroup ? resolveFeishuGroupSession({
548
+ chatId: ctx.chatId,
549
+ senderOpenId: ctx.senderOpenId,
550
+ messageId: ctx.messageId,
551
+ rootId: ctx.rootId,
552
+ threadId: ctx.threadId,
553
+ groupConfig,
554
+ feishuCfg
555
+ }) : null;
556
+ const groupHistoryKey = isGroup ? groupSession?.peerId ?? ctx.chatId : void 0;
557
+ const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
558
+ const configAllowFrom = feishuCfg?.allowFrom ?? [];
559
+ const useAccessGroups = cfg.commands?.useAccessGroups !== false;
560
+ const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
561
+ const broadcastAgents = rawBroadcastAgents ? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))] : null;
562
+ let requireMention = false;
563
+ if (isGroup) {
564
+ if (groupConfig?.enabled === false) {
565
+ log(`feishu[${account.accountId}]: group ${ctx.chatId} is disabled`);
566
+ return;
567
+ }
568
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
569
+ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
570
+ providerConfigPresent: cfg.channels?.feishu !== void 0,
571
+ groupPolicy: feishuCfg?.groupPolicy,
572
+ defaultGroupPolicy
573
+ });
574
+ warnMissingProviderGroupPolicyFallbackOnce({
575
+ providerMissingFallbackApplied,
576
+ providerKey: "feishu",
577
+ accountId: account.accountId,
578
+ log
579
+ });
580
+ if (!isFeishuGroupAllowed({
581
+ groupPolicy,
582
+ allowFrom: feishuCfg?.groupAllowFrom ?? [],
583
+ senderId: ctx.chatId,
584
+ senderName: void 0
585
+ })) {
586
+ log(`feishu[${account.accountId}]: group ${ctx.chatId} not in groupAllowFrom (groupPolicy=${groupPolicy})`);
587
+ return;
588
+ }
589
+ const perGroupSenderAllowFrom = groupConfig?.allowFrom ?? [];
590
+ const globalSenderAllowFrom = feishuCfg?.groupSenderAllowFrom ?? [];
591
+ const effectiveSenderAllowFrom = perGroupSenderAllowFrom.length > 0 ? perGroupSenderAllowFrom : globalSenderAllowFrom;
592
+ if (effectiveSenderAllowFrom.length > 0) {
593
+ if (!isFeishuGroupAllowed({
594
+ groupPolicy: "allowlist",
595
+ allowFrom: effectiveSenderAllowFrom,
596
+ senderId: ctx.senderOpenId,
597
+ senderIds: [senderUserId],
598
+ senderName: ctx.senderName
599
+ })) {
600
+ log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
601
+ return;
602
+ }
603
+ }
604
+ ({requireMention} = resolveFeishuReplyPolicy({
605
+ isDirectMessage: false,
606
+ globalConfig: feishuCfg,
607
+ groupConfig
608
+ }));
609
+ if (requireMention && !ctx.mentionedBot) {
610
+ log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
611
+ if (!broadcastAgents && chatHistories && groupHistoryKey) recordPendingHistoryEntryIfEnabled({
612
+ historyMap: chatHistories,
613
+ historyKey: groupHistoryKey,
614
+ limit: historyLimit,
615
+ entry: {
616
+ sender: ctx.senderOpenId,
617
+ body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
618
+ timestamp: Date.now(),
619
+ messageId: ctx.messageId
620
+ }
621
+ });
622
+ return;
623
+ }
624
+ }
625
+ try {
626
+ const core = getFeishuRuntime();
627
+ const pairing = createScopedPairingAccess({
628
+ core,
629
+ channel: "feishu",
630
+ accountId: account.accountId
631
+ });
632
+ const commandProbeBody = isGroup ? normalizeFeishuCommandProbeBody(ctx.content) : ctx.content;
633
+ const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(commandProbeBody, cfg);
634
+ const storeAllowFrom = !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeCommandAuthorized) ? await pairing.readAllowFromStore().catch(() => []) : [];
635
+ const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
636
+ const dmAllowed = resolveFeishuAllowlistMatch({
637
+ allowFrom: effectiveDmAllowFrom,
638
+ senderId: ctx.senderOpenId,
639
+ senderIds: [senderUserId],
640
+ senderName: ctx.senderName
641
+ }).allowed;
642
+ if (isDirect && dmPolicy !== "open" && !dmAllowed) {
643
+ if (dmPolicy === "pairing") await issuePairingChallenge({
644
+ channel: "feishu",
645
+ senderId: ctx.senderOpenId,
646
+ senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
647
+ meta: { name: ctx.senderName },
648
+ upsertPairingRequest: pairing.upsertPairingRequest,
649
+ onCreated: () => {
650
+ log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
651
+ },
652
+ sendPairingReply: async (text) => {
653
+ await sendMessageFeishu({
654
+ cfg,
655
+ to: `chat:${ctx.chatId}`,
656
+ text,
657
+ accountId: account.accountId
658
+ });
659
+ },
660
+ onReplyError: (err) => {
661
+ log(`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`);
662
+ }
663
+ });
664
+ else log(`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`);
665
+ return;
666
+ }
667
+ const commandAllowFrom = isGroup ? groupConfig?.allowFrom ?? configAllowFrom : effectiveDmAllowFrom;
668
+ const senderAllowedForCommands = resolveFeishuAllowlistMatch({
669
+ allowFrom: commandAllowFrom,
670
+ senderId: ctx.senderOpenId,
671
+ senderIds: [senderUserId],
672
+ senderName: ctx.senderName
673
+ }).allowed;
674
+ const commandAuthorized = shouldComputeCommandAuthorized ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
675
+ useAccessGroups,
676
+ authorizers: [{
677
+ configured: commandAllowFrom.length > 0,
678
+ allowed: senderAllowedForCommands
679
+ }]
680
+ }) : void 0;
681
+ const feishuFrom = `feishu:${ctx.senderOpenId}`;
682
+ const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
683
+ const peerId = isGroup ? groupSession?.peerId ?? ctx.chatId : ctx.senderOpenId;
684
+ const parentPeer = isGroup ? groupSession?.parentPeer ?? null : null;
685
+ const replyInThread = isGroup ? groupSession?.replyInThread ?? false : false;
686
+ if (isGroup && groupSession) log(`feishu[${account.accountId}]: group session scope=${groupSession.groupSessionScope}, peer=${peerId}`);
687
+ let route = core.channel.routing.resolveAgentRoute({
688
+ cfg,
689
+ channel: "feishu",
690
+ accountId: account.accountId,
691
+ peer: {
692
+ kind: isGroup ? "group" : "direct",
693
+ id: peerId
694
+ },
695
+ parentPeer
696
+ });
697
+ if (!isGroup && route.matchedBy === "default") {
698
+ const dynamicCfg = feishuCfg?.dynamicAgentCreation;
699
+ if (dynamicCfg?.enabled) {
700
+ const result = await maybeCreateDynamicAgent({
701
+ cfg,
702
+ runtime: getFeishuRuntime(),
703
+ senderOpenId: ctx.senderOpenId,
704
+ dynamicCfg,
705
+ log: (msg) => log(msg)
706
+ });
707
+ if (result.created) {
708
+ result.updatedCfg;
709
+ route = core.channel.routing.resolveAgentRoute({
710
+ cfg: result.updatedCfg,
711
+ channel: "feishu",
712
+ accountId: account.accountId,
713
+ peer: {
714
+ kind: "direct",
715
+ id: ctx.senderOpenId
716
+ }
717
+ });
718
+ log(`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`);
719
+ }
720
+ }
721
+ }
722
+ const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
723
+ const inboundLabel = isGroup ? `Feishu[${account.accountId}] message in group ${ctx.chatId}` : `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
724
+ log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
725
+ const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024;
726
+ const mediaPayload = buildAgentMediaPayload(await resolveFeishuMediaList({
727
+ cfg,
728
+ messageId: ctx.messageId,
729
+ messageType: event.message.message_type,
730
+ content: event.message.content,
731
+ maxBytes: mediaMaxBytes,
732
+ log,
733
+ accountId: account.accountId
734
+ }));
735
+ let quotedContent;
736
+ if (ctx.parentId) try {
737
+ const quotedMsg = await getMessageFeishu({
738
+ cfg,
739
+ messageId: ctx.parentId,
740
+ accountId: account.accountId
741
+ });
742
+ if (quotedMsg) {
743
+ quotedContent = quotedMsg.content;
744
+ log(`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
745
+ }
746
+ } catch (err) {
747
+ log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
748
+ }
749
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
750
+ const messageBody = buildFeishuAgentBody({
751
+ ctx,
752
+ quotedContent,
753
+ permissionErrorForAgent,
754
+ botOpenId
755
+ });
756
+ const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
757
+ if (permissionErrorForAgent) log(`feishu[${account.accountId}]: appending permission error notice to message body`);
758
+ let combinedBody = core.channel.reply.formatAgentEnvelope({
759
+ channel: "Feishu",
760
+ from: envelopeFrom,
761
+ timestamp: /* @__PURE__ */ new Date(),
762
+ envelope: envelopeOptions,
763
+ body: messageBody
764
+ });
765
+ const historyKey = groupHistoryKey;
766
+ if (isGroup && historyKey && chatHistories) combinedBody = buildPendingHistoryContextFromMap({
767
+ historyMap: chatHistories,
768
+ historyKey,
769
+ limit: historyLimit,
770
+ currentMessage: combinedBody,
771
+ formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
772
+ channel: "Feishu",
773
+ from: `${ctx.chatId}:${entry.sender}`,
774
+ timestamp: entry.timestamp,
775
+ body: entry.body,
776
+ envelope: envelopeOptions
777
+ })
778
+ });
779
+ const inboundHistory = isGroup && historyKey && historyLimit > 0 && chatHistories ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
780
+ sender: entry.sender,
781
+ body: entry.body,
782
+ timestamp: entry.timestamp
783
+ })) : void 0;
784
+ const buildCtxPayloadForAgent = (agentSessionKey, agentAccountId, wasMentioned) => core.channel.reply.finalizeInboundContext({
785
+ Body: combinedBody,
786
+ BodyForAgent: messageBody,
787
+ InboundHistory: inboundHistory,
788
+ ReplyToId: ctx.parentId,
789
+ RootMessageId: ctx.rootId,
790
+ RawBody: ctx.content,
791
+ CommandBody: ctx.content,
792
+ From: feishuFrom,
793
+ To: feishuTo,
794
+ SessionKey: agentSessionKey,
795
+ AccountId: agentAccountId,
796
+ ChatType: isGroup ? "group" : "direct",
797
+ GroupSubject: isGroup ? ctx.chatId : void 0,
798
+ SenderName: ctx.senderName ?? ctx.senderOpenId,
799
+ SenderId: ctx.senderOpenId,
800
+ Provider: "feishu",
801
+ Surface: "feishu",
802
+ MessageSid: ctx.messageId,
803
+ ReplyToBody: quotedContent ?? void 0,
804
+ Timestamp: Date.now(),
805
+ WasMentioned: wasMentioned,
806
+ CommandAuthorized: commandAuthorized,
807
+ OriginatingChannel: "feishu",
808
+ OriginatingTo: feishuTo,
809
+ GroupSystemPrompt: isGroup ? groupConfig?.systemPrompt?.trim() || void 0 : void 0,
810
+ ...mediaPayload
811
+ });
812
+ const messageCreateTimeMs = event.message.create_time ? parseInt(event.message.create_time, 10) : void 0;
813
+ const isTopicSession = isGroup && (groupSession?.groupSessionScope === "group_topic" || groupSession?.groupSessionScope === "group_topic_sender");
814
+ const configReplyInThread = isGroup && (groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
815
+ const replyTargetMessageId = isTopicSession || configReplyInThread ? ctx.rootId ?? ctx.messageId : ctx.messageId;
816
+ const threadReply = isGroup ? groupSession?.threadReply ?? false : false;
817
+ if (broadcastAgents) {
818
+ if (!await tryRecordMessagePersistent(ctx.messageId, "broadcast", log)) {
819
+ log(`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`);
820
+ return;
821
+ }
822
+ const strategy = cfg.broadcast?.strategy || "parallel";
823
+ const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId(route.agentId) : null;
824
+ const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId(a.id));
825
+ const hasKnownAgents = agentIds.length > 0;
826
+ log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
827
+ const dispatchForAgent = async (agentId) => {
828
+ if (hasKnownAgents && !agentIds.includes(normalizeAgentId(agentId))) {
829
+ log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
830
+ return;
831
+ }
832
+ const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
833
+ const agentCtx = buildCtxPayloadForAgent(agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
834
+ if (agentId === activeAgentId) {
835
+ const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
836
+ cfg,
837
+ agentId,
838
+ runtime,
839
+ chatId: ctx.chatId,
840
+ replyToMessageId: replyTargetMessageId,
841
+ skipReplyToInMessages: !isGroup,
842
+ replyInThread,
843
+ rootId: ctx.rootId,
844
+ threadReply,
845
+ mentionTargets: ctx.mentionTargets,
846
+ accountId: account.accountId,
847
+ messageCreateTimeMs
848
+ });
849
+ log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
850
+ await core.channel.reply.withReplyDispatcher({
851
+ dispatcher,
852
+ onSettled: () => markDispatchIdle(),
853
+ run: () => core.channel.reply.dispatchReplyFromConfig({
854
+ ctx: agentCtx,
855
+ cfg,
856
+ dispatcher,
857
+ replyOptions
858
+ })
859
+ });
860
+ } else {
861
+ delete agentCtx.CommandAuthorized;
862
+ const noopDispatcher = {
863
+ sendToolResult: () => false,
864
+ sendBlockReply: () => false,
865
+ sendFinalReply: () => false,
866
+ waitForIdle: async () => {},
867
+ getQueuedCounts: () => ({
868
+ tool: 0,
869
+ block: 0,
870
+ final: 0
871
+ }),
872
+ markComplete: () => {}
873
+ };
874
+ log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
875
+ await core.channel.reply.withReplyDispatcher({
876
+ dispatcher: noopDispatcher,
877
+ run: () => core.channel.reply.dispatchReplyFromConfig({
878
+ ctx: agentCtx,
879
+ cfg,
880
+ dispatcher: noopDispatcher
881
+ })
882
+ });
883
+ }
884
+ };
885
+ if (strategy === "sequential") for (const agentId of broadcastAgents) try {
886
+ await dispatchForAgent(agentId);
887
+ } catch (err) {
888
+ log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`);
889
+ }
890
+ else {
891
+ const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
892
+ for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String(results[i].reason)}`);
893
+ }
894
+ if (isGroup && historyKey && chatHistories) clearHistoryEntriesIfEnabled({
895
+ historyMap: chatHistories,
896
+ historyKey,
897
+ limit: historyLimit
898
+ });
899
+ log(`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`);
900
+ } else {
901
+ const ctxPayload = buildCtxPayloadForAgent(route.sessionKey, route.accountId, ctx.mentionedBot);
902
+ const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
903
+ cfg,
904
+ agentId: route.agentId,
905
+ runtime,
906
+ chatId: ctx.chatId,
907
+ replyToMessageId: replyTargetMessageId,
908
+ skipReplyToInMessages: !isGroup,
909
+ replyInThread,
910
+ rootId: ctx.rootId,
911
+ threadReply,
912
+ mentionTargets: ctx.mentionTargets,
913
+ accountId: account.accountId,
914
+ messageCreateTimeMs
915
+ });
916
+ log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
917
+ const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
918
+ dispatcher,
919
+ onSettled: () => {
920
+ markDispatchIdle();
921
+ },
922
+ run: () => core.channel.reply.dispatchReplyFromConfig({
923
+ ctx: ctxPayload,
924
+ cfg,
925
+ dispatcher,
926
+ replyOptions
927
+ })
928
+ });
929
+ if (isGroup && historyKey && chatHistories) clearHistoryEntriesIfEnabled({
930
+ historyMap: chatHistories,
931
+ historyKey,
932
+ limit: historyLimit
933
+ });
934
+ log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
935
+ }
936
+ } catch (err) {
937
+ error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
938
+ }
939
+ }
940
+ //#endregion
941
+ export { handleFeishuMessage, parseFeishuMessageEvent };