@openclaw/feishu 2026.3.1 → 2026.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +268 -11
- package/src/accounts.ts +101 -14
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +9 -1
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +945 -77
- package/src/bot.ts +492 -165
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +72 -68
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +221 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +33 -6
- package/src/config-schema.ts +18 -10
- package/src/dedup.ts +47 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/doc-schema.ts +16 -22
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +10 -16
- package/src/docx.test.ts +41 -189
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +164 -14
- package/src/media.ts +44 -10
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +284 -25
- package/src/monitor.reaction.test.ts +395 -46
- package/src/monitor.startup.test.ts +25 -8
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +88 -9
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +13 -11
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +213 -106
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +54 -36
- package/src/probe.ts +57 -37
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +216 -0
- package/src/reply-dispatcher.ts +89 -22
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +7 -3
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.test.ts +29 -0
- package/src/targets.ts +25 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +11 -4
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/bot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import {
|
|
3
3
|
buildAgentMediaPayload,
|
|
4
4
|
buildPendingHistoryContextFromMap,
|
|
@@ -6,23 +6,20 @@ import {
|
|
|
6
6
|
createScopedPairingAccess,
|
|
7
7
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
8
8
|
type HistoryEntry,
|
|
9
|
+
issuePairingChallenge,
|
|
10
|
+
normalizeAgentId,
|
|
9
11
|
recordPendingHistoryEntryIfEnabled,
|
|
10
12
|
resolveOpenProviderRuntimeGroupPolicy,
|
|
11
13
|
resolveDefaultGroupPolicy,
|
|
12
14
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
13
|
-
} from "openclaw/plugin-sdk";
|
|
15
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
14
16
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
15
17
|
import { createFeishuClient } from "./client.js";
|
|
16
18
|
import { tryRecordMessage, tryRecordMessagePersistent } from "./dedup.js";
|
|
17
19
|
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
18
20
|
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
|
19
21
|
import { downloadMessageResourceFeishu } from "./media.js";
|
|
20
|
-
import {
|
|
21
|
-
escapeRegExp,
|
|
22
|
-
extractMentionTargets,
|
|
23
|
-
extractMessageBody,
|
|
24
|
-
isMentionForwardRequest,
|
|
25
|
-
} from "./mention.js";
|
|
22
|
+
import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
|
|
26
23
|
import {
|
|
27
24
|
resolveFeishuGroupConfig,
|
|
28
25
|
resolveFeishuReplyPolicy,
|
|
@@ -44,6 +41,29 @@ type PermissionError = {
|
|
|
44
41
|
grantUrl?: string;
|
|
45
42
|
};
|
|
46
43
|
|
|
44
|
+
const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"];
|
|
45
|
+
|
|
46
|
+
// Feishu API sometimes returns incorrect scope names in permission error
|
|
47
|
+
// responses (e.g. "contact:contact.base:readonly" instead of the valid
|
|
48
|
+
// "contact:user.base:readonly"). This map corrects known mismatches.
|
|
49
|
+
const FEISHU_SCOPE_CORRECTIONS: Record<string, string> = {
|
|
50
|
+
"contact:contact.base:readonly": "contact:user.base:readonly",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function correctFeishuScopeInUrl(url: string): string {
|
|
54
|
+
let corrected = url;
|
|
55
|
+
for (const [wrong, right] of Object.entries(FEISHU_SCOPE_CORRECTIONS)) {
|
|
56
|
+
corrected = corrected.replaceAll(encodeURIComponent(wrong), encodeURIComponent(right));
|
|
57
|
+
corrected = corrected.replaceAll(wrong, right);
|
|
58
|
+
}
|
|
59
|
+
return corrected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shouldSuppressPermissionErrorNotice(permissionError: PermissionError): boolean {
|
|
63
|
+
const message = permissionError.message.toLowerCase();
|
|
64
|
+
return IGNORED_PERMISSION_SCOPE_TOKENS.some((token) => message.includes(token));
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
function extractPermissionError(err: unknown): PermissionError | null {
|
|
48
68
|
if (!err || typeof err !== "object") return null;
|
|
49
69
|
|
|
@@ -64,7 +84,7 @@ function extractPermissionError(err: unknown): PermissionError | null {
|
|
|
64
84
|
// Extract the grant URL from the error message (contains the direct link)
|
|
65
85
|
const msg = feishuErr.msg ?? "";
|
|
66
86
|
const urlMatch = msg.match(/https:\/\/[^\s,]+\/app\/[^\s,]+/);
|
|
67
|
-
const grantUrl = urlMatch?.[0];
|
|
87
|
+
const grantUrl = urlMatch?.[0] ? correctFeishuScopeInUrl(urlMatch[0]) : undefined;
|
|
68
88
|
|
|
69
89
|
return {
|
|
70
90
|
code: feishuErr.code,
|
|
@@ -140,6 +160,10 @@ async function resolveFeishuSenderName(params: {
|
|
|
140
160
|
// Check if this is a permission error
|
|
141
161
|
const permErr = extractPermissionError(err);
|
|
142
162
|
if (permErr) {
|
|
163
|
+
if (shouldSuppressPermissionErrorNotice(permErr)) {
|
|
164
|
+
log(`feishu: ignoring stale permission scope error: ${permErr.message}`);
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
143
167
|
log(`feishu: permission error resolving sender name: code=${permErr.code}`);
|
|
144
168
|
return { permissionError: permErr };
|
|
145
169
|
}
|
|
@@ -164,8 +188,9 @@ export type FeishuMessageEvent = {
|
|
|
164
188
|
message_id: string;
|
|
165
189
|
root_id?: string;
|
|
166
190
|
parent_id?: string;
|
|
191
|
+
thread_id?: string;
|
|
167
192
|
chat_id: string;
|
|
168
|
-
chat_type: "p2p" | "group";
|
|
193
|
+
chat_type: "p2p" | "group" | "private";
|
|
169
194
|
message_type: string;
|
|
170
195
|
content: string;
|
|
171
196
|
create_time?: string;
|
|
@@ -193,6 +218,94 @@ export type FeishuBotAddedEvent = {
|
|
|
193
218
|
operator_tenant_key?: string;
|
|
194
219
|
};
|
|
195
220
|
|
|
221
|
+
type GroupSessionScope = "group" | "group_sender" | "group_topic" | "group_topic_sender";
|
|
222
|
+
|
|
223
|
+
type ResolvedFeishuGroupSession = {
|
|
224
|
+
peerId: string;
|
|
225
|
+
parentPeer: { kind: "group"; id: string } | null;
|
|
226
|
+
groupSessionScope: GroupSessionScope;
|
|
227
|
+
replyInThread: boolean;
|
|
228
|
+
threadReply: boolean;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function resolveFeishuGroupSession(params: {
|
|
232
|
+
chatId: string;
|
|
233
|
+
senderOpenId: string;
|
|
234
|
+
messageId: string;
|
|
235
|
+
rootId?: string;
|
|
236
|
+
threadId?: string;
|
|
237
|
+
groupConfig?: {
|
|
238
|
+
groupSessionScope?: GroupSessionScope;
|
|
239
|
+
topicSessionMode?: "enabled" | "disabled";
|
|
240
|
+
replyInThread?: "enabled" | "disabled";
|
|
241
|
+
};
|
|
242
|
+
feishuCfg?: {
|
|
243
|
+
groupSessionScope?: GroupSessionScope;
|
|
244
|
+
topicSessionMode?: "enabled" | "disabled";
|
|
245
|
+
replyInThread?: "enabled" | "disabled";
|
|
246
|
+
};
|
|
247
|
+
}): ResolvedFeishuGroupSession {
|
|
248
|
+
const { chatId, senderOpenId, messageId, rootId, threadId, groupConfig, feishuCfg } = params;
|
|
249
|
+
|
|
250
|
+
const normalizedThreadId = threadId?.trim();
|
|
251
|
+
const normalizedRootId = rootId?.trim();
|
|
252
|
+
const threadReply = Boolean(normalizedThreadId || normalizedRootId);
|
|
253
|
+
const replyInThread =
|
|
254
|
+
(groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled" ||
|
|
255
|
+
threadReply;
|
|
256
|
+
|
|
257
|
+
const legacyTopicSessionMode =
|
|
258
|
+
groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
|
|
259
|
+
const groupSessionScope: GroupSessionScope =
|
|
260
|
+
groupConfig?.groupSessionScope ??
|
|
261
|
+
feishuCfg?.groupSessionScope ??
|
|
262
|
+
(legacyTopicSessionMode === "enabled" ? "group_topic" : "group");
|
|
263
|
+
|
|
264
|
+
// Keep topic session keys stable across the "first turn creates thread" flow:
|
|
265
|
+
// first turn may only have message_id, while the next turn carries root_id/thread_id.
|
|
266
|
+
// Prefer root_id first so both turns stay on the same peer key.
|
|
267
|
+
const topicScope =
|
|
268
|
+
groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender"
|
|
269
|
+
? (normalizedRootId ?? normalizedThreadId ?? (replyInThread ? messageId : null))
|
|
270
|
+
: null;
|
|
271
|
+
|
|
272
|
+
let peerId = chatId;
|
|
273
|
+
switch (groupSessionScope) {
|
|
274
|
+
case "group_sender":
|
|
275
|
+
peerId = `${chatId}:sender:${senderOpenId}`;
|
|
276
|
+
break;
|
|
277
|
+
case "group_topic":
|
|
278
|
+
peerId = topicScope ? `${chatId}:topic:${topicScope}` : chatId;
|
|
279
|
+
break;
|
|
280
|
+
case "group_topic_sender":
|
|
281
|
+
peerId = topicScope
|
|
282
|
+
? `${chatId}:topic:${topicScope}:sender:${senderOpenId}`
|
|
283
|
+
: `${chatId}:sender:${senderOpenId}`;
|
|
284
|
+
break;
|
|
285
|
+
case "group":
|
|
286
|
+
default:
|
|
287
|
+
peerId = chatId;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const parentPeer =
|
|
292
|
+
topicScope &&
|
|
293
|
+
(groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender")
|
|
294
|
+
? {
|
|
295
|
+
kind: "group" as const,
|
|
296
|
+
id: chatId,
|
|
297
|
+
}
|
|
298
|
+
: null;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
peerId,
|
|
302
|
+
parentPeer,
|
|
303
|
+
groupSessionScope,
|
|
304
|
+
replyInThread,
|
|
305
|
+
threadReply,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
196
309
|
function parseMessageContent(content: string, messageType: string): string {
|
|
197
310
|
if (messageType === "post") {
|
|
198
311
|
// Extract text content from rich text post
|
|
@@ -340,8 +453,12 @@ function formatSubMessageContent(content: string, contentType: string): string {
|
|
|
340
453
|
|
|
341
454
|
function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boolean {
|
|
342
455
|
if (!botOpenId) return false;
|
|
456
|
+
// Check for @all (@_all in Feishu) — treat as mentioning every bot
|
|
457
|
+
const rawContent = event.message.content ?? "";
|
|
458
|
+
if (rawContent.includes("@_all")) return true;
|
|
343
459
|
const mentions = event.message.mentions ?? [];
|
|
344
460
|
if (mentions.length > 0) {
|
|
461
|
+
// Rely on Feishu mention IDs; display names can vary by alias/context.
|
|
345
462
|
return mentions.some((m) => m.id.open_id === botOpenId);
|
|
346
463
|
}
|
|
347
464
|
// Post (rich text) messages may have empty message.mentions when they contain docs/paste
|
|
@@ -352,17 +469,41 @@ function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boole
|
|
|
352
469
|
return false;
|
|
353
470
|
}
|
|
354
471
|
|
|
355
|
-
|
|
472
|
+
function normalizeMentions(
|
|
356
473
|
text: string,
|
|
357
474
|
mentions?: FeishuMessageEvent["message"]["mentions"],
|
|
475
|
+
botStripId?: string,
|
|
358
476
|
): string {
|
|
359
477
|
if (!mentions || mentions.length === 0) return text;
|
|
478
|
+
|
|
479
|
+
const escaped = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
480
|
+
const escapeName = (value: string) => value.replace(/</g, "<").replace(/>/g, ">");
|
|
360
481
|
let result = text;
|
|
482
|
+
|
|
361
483
|
for (const mention of mentions) {
|
|
362
|
-
|
|
363
|
-
|
|
484
|
+
const mentionId = mention.id.open_id;
|
|
485
|
+
const replacement =
|
|
486
|
+
botStripId && mentionId === botStripId
|
|
487
|
+
? ""
|
|
488
|
+
: mentionId
|
|
489
|
+
? `<at user_id="${mentionId}">${escapeName(mention.name)}</at>`
|
|
490
|
+
: `@${mention.name}`;
|
|
491
|
+
|
|
492
|
+
result = result.replace(new RegExp(escaped(mention.key), "g"), () => replacement).trim();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function normalizeFeishuCommandProbeBody(text: string): string {
|
|
499
|
+
if (!text) {
|
|
500
|
+
return "";
|
|
364
501
|
}
|
|
365
|
-
return
|
|
502
|
+
return text
|
|
503
|
+
.replace(/<at\b[^>]*>[^<]*<\/at>/giu, " ")
|
|
504
|
+
.replace(/(^|\s)@[^/\s]+(?=\s|$|\/)/gu, "$1")
|
|
505
|
+
.replace(/\s+/g, " ")
|
|
506
|
+
.trim();
|
|
366
507
|
}
|
|
367
508
|
|
|
368
509
|
/**
|
|
@@ -598,6 +739,31 @@ async function resolveFeishuMediaList(params: {
|
|
|
598
739
|
return out;
|
|
599
740
|
}
|
|
600
741
|
|
|
742
|
+
// --- Broadcast support ---
|
|
743
|
+
// Resolve broadcast agent list for a given peer (group) ID.
|
|
744
|
+
// Returns null if no broadcast config exists or the peer is not in the broadcast list.
|
|
745
|
+
export function resolveBroadcastAgents(cfg: ClawdbotConfig, peerId: string): string[] | null {
|
|
746
|
+
const broadcast = (cfg as Record<string, unknown>).broadcast;
|
|
747
|
+
if (!broadcast || typeof broadcast !== "object") return null;
|
|
748
|
+
const agents = (broadcast as Record<string, unknown>)[peerId];
|
|
749
|
+
if (!Array.isArray(agents) || agents.length === 0) return null;
|
|
750
|
+
return agents as string[];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Build a session key for a broadcast target agent by replacing the agent ID prefix.
|
|
754
|
+
// Session keys follow the format: agent:<agentId>:<channel>:<peerKind>:<peerId>
|
|
755
|
+
export function buildBroadcastSessionKey(
|
|
756
|
+
baseSessionKey: string,
|
|
757
|
+
originalAgentId: string,
|
|
758
|
+
targetAgentId: string,
|
|
759
|
+
): string {
|
|
760
|
+
const prefix = `agent:${originalAgentId}:`;
|
|
761
|
+
if (baseSessionKey.startsWith(prefix)) {
|
|
762
|
+
return `agent:${targetAgentId}:${baseSessionKey.slice(prefix.length)}`;
|
|
763
|
+
}
|
|
764
|
+
return baseSessionKey;
|
|
765
|
+
}
|
|
766
|
+
|
|
601
767
|
/**
|
|
602
768
|
* Build media payload for inbound context.
|
|
603
769
|
* Similar to Discord's buildDiscordMediaPayload().
|
|
@@ -605,10 +771,17 @@ async function resolveFeishuMediaList(params: {
|
|
|
605
771
|
export function parseFeishuMessageEvent(
|
|
606
772
|
event: FeishuMessageEvent,
|
|
607
773
|
botOpenId?: string,
|
|
774
|
+
_botName?: string,
|
|
608
775
|
): FeishuMessageContext {
|
|
609
776
|
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
|
|
610
777
|
const mentionedBot = checkBotMentioned(event, botOpenId);
|
|
611
|
-
const
|
|
778
|
+
const hasAnyMention = (event.message.mentions?.length ?? 0) > 0;
|
|
779
|
+
// Strip the bot's own mention so slash commands like @Bot /help retain
|
|
780
|
+
// the leading /. This applies in both p2p *and* group contexts — the
|
|
781
|
+
// mentionedBot flag already captures whether the bot was addressed, so
|
|
782
|
+
// keeping the mention tag in content only breaks command detection (#35994).
|
|
783
|
+
// Non-bot mentions (e.g. mention-forward targets) are still normalized to <at> tags.
|
|
784
|
+
const content = normalizeMentions(rawContent, event.message.mentions, botOpenId);
|
|
612
785
|
const senderOpenId = event.sender.sender_id.open_id?.trim();
|
|
613
786
|
const senderUserId = event.sender.sender_id.user_id?.trim();
|
|
614
787
|
const senderFallbackId = senderOpenId || senderUserId || "";
|
|
@@ -622,8 +795,10 @@ export function parseFeishuMessageEvent(
|
|
|
622
795
|
senderOpenId: senderFallbackId,
|
|
623
796
|
chatType: event.message.chat_type,
|
|
624
797
|
mentionedBot,
|
|
798
|
+
hasAnyMention,
|
|
625
799
|
rootId: event.message.root_id || undefined,
|
|
626
800
|
parentId: event.message.parent_id || undefined,
|
|
801
|
+
threadId: event.message.thread_id || undefined,
|
|
627
802
|
content,
|
|
628
803
|
contentType: event.message.message_type,
|
|
629
804
|
};
|
|
@@ -633,9 +808,6 @@ export function parseFeishuMessageEvent(
|
|
|
633
808
|
const mentionTargets = extractMentionTargets(event, botOpenId);
|
|
634
809
|
if (mentionTargets.length > 0) {
|
|
635
810
|
ctx.mentionTargets = mentionTargets;
|
|
636
|
-
// Extract message body (remove all @ placeholders)
|
|
637
|
-
const allMentionKeys = (event.message.mentions ?? []).map((m) => m.key);
|
|
638
|
-
ctx.mentionMessageBody = extractMessageBody(content, allMentionKeys);
|
|
639
811
|
}
|
|
640
812
|
}
|
|
641
813
|
|
|
@@ -645,12 +817,13 @@ export function parseFeishuMessageEvent(
|
|
|
645
817
|
export function buildFeishuAgentBody(params: {
|
|
646
818
|
ctx: Pick<
|
|
647
819
|
FeishuMessageContext,
|
|
648
|
-
"content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId"
|
|
820
|
+
"content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId" | "hasAnyMention"
|
|
649
821
|
>;
|
|
650
822
|
quotedContent?: string;
|
|
651
823
|
permissionErrorForAgent?: PermissionError;
|
|
824
|
+
botOpenId?: string;
|
|
652
825
|
}): string {
|
|
653
|
-
const { ctx, quotedContent, permissionErrorForAgent } = params;
|
|
826
|
+
const { ctx, quotedContent, permissionErrorForAgent, botOpenId } = params;
|
|
654
827
|
let messageBody = ctx.content;
|
|
655
828
|
if (quotedContent) {
|
|
656
829
|
messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
|
|
@@ -660,6 +833,16 @@ export function buildFeishuAgentBody(params: {
|
|
|
660
833
|
const speaker = ctx.senderName ?? ctx.senderOpenId;
|
|
661
834
|
messageBody = `${speaker}: ${messageBody}`;
|
|
662
835
|
|
|
836
|
+
if (ctx.hasAnyMention) {
|
|
837
|
+
const botIdHint = botOpenId?.trim();
|
|
838
|
+
messageBody +=
|
|
839
|
+
`\n\n[System: The content may include mention tags in the form <at user_id="...">name</at>. ` +
|
|
840
|
+
`Treat these as real mentions of Feishu entities (users or bots).]`;
|
|
841
|
+
if (botIdHint) {
|
|
842
|
+
messageBody += `\n[System: If user_id is "${botIdHint}", that mention refers to you.]`;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
663
846
|
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
|
664
847
|
const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
|
|
665
848
|
messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
|
|
@@ -680,11 +863,12 @@ export async function handleFeishuMessage(params: {
|
|
|
680
863
|
cfg: ClawdbotConfig;
|
|
681
864
|
event: FeishuMessageEvent;
|
|
682
865
|
botOpenId?: string;
|
|
866
|
+
botName?: string;
|
|
683
867
|
runtime?: RuntimeEnv;
|
|
684
868
|
chatHistories?: Map<string, HistoryEntry[]>;
|
|
685
869
|
accountId?: string;
|
|
686
870
|
}): Promise<void> {
|
|
687
|
-
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
|
871
|
+
const { cfg, event, botOpenId, botName, runtime, chatHistories, accountId } = params;
|
|
688
872
|
|
|
689
873
|
// Resolve account with merged config
|
|
690
874
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
@@ -707,8 +891,9 @@ export async function handleFeishuMessage(params: {
|
|
|
707
891
|
return;
|
|
708
892
|
}
|
|
709
893
|
|
|
710
|
-
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
894
|
+
let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
|
|
711
895
|
const isGroup = ctx.chatType === "group";
|
|
896
|
+
const isDirect = !isGroup;
|
|
712
897
|
const senderUserId = event.sender.sender_id.user_id?.trim() || undefined;
|
|
713
898
|
|
|
714
899
|
// Handle merge_forward messages: fetch full message via API then expand sub-messages
|
|
@@ -784,10 +969,27 @@ export async function handleFeishuMessage(params: {
|
|
|
784
969
|
const groupConfig = isGroup
|
|
785
970
|
? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })
|
|
786
971
|
: undefined;
|
|
972
|
+
const groupSession = isGroup
|
|
973
|
+
? resolveFeishuGroupSession({
|
|
974
|
+
chatId: ctx.chatId,
|
|
975
|
+
senderOpenId: ctx.senderOpenId,
|
|
976
|
+
messageId: ctx.messageId,
|
|
977
|
+
rootId: ctx.rootId,
|
|
978
|
+
threadId: ctx.threadId,
|
|
979
|
+
groupConfig,
|
|
980
|
+
feishuCfg,
|
|
981
|
+
})
|
|
982
|
+
: null;
|
|
983
|
+
const groupHistoryKey = isGroup ? (groupSession?.peerId ?? ctx.chatId) : undefined;
|
|
787
984
|
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
788
985
|
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
789
986
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
987
|
+
const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
|
|
988
|
+
const broadcastAgents = rawBroadcastAgents
|
|
989
|
+
? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
|
|
990
|
+
: null;
|
|
790
991
|
|
|
992
|
+
let requireMention = false; // DMs never require mention; groups may override below
|
|
791
993
|
if (isGroup) {
|
|
792
994
|
if (groupConfig?.enabled === false) {
|
|
793
995
|
log(`feishu[${account.accountId}]: group ${ctx.chatId} is disabled`);
|
|
@@ -842,20 +1044,22 @@ export async function handleFeishuMessage(params: {
|
|
|
842
1044
|
}
|
|
843
1045
|
}
|
|
844
1046
|
|
|
845
|
-
|
|
1047
|
+
({ requireMention } = resolveFeishuReplyPolicy({
|
|
846
1048
|
isDirectMessage: false,
|
|
847
1049
|
globalConfig: feishuCfg,
|
|
848
1050
|
groupConfig,
|
|
849
|
-
});
|
|
1051
|
+
}));
|
|
850
1052
|
|
|
851
1053
|
if (requireMention && !ctx.mentionedBot) {
|
|
852
|
-
log(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1054
|
+
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
|
|
1055
|
+
// Record to pending history for non-broadcast groups only. For broadcast groups,
|
|
1056
|
+
// the mentioned handler's broadcast dispatch writes the turn directly into all
|
|
1057
|
+
// agent sessions — buffering here would cause duplicate replay when this account
|
|
1058
|
+
// later becomes active via buildPendingHistoryContextFromMap.
|
|
1059
|
+
if (!broadcastAgents && chatHistories && groupHistoryKey) {
|
|
856
1060
|
recordPendingHistoryEntryIfEnabled({
|
|
857
1061
|
historyMap: chatHistories,
|
|
858
|
-
historyKey:
|
|
1062
|
+
historyKey: groupHistoryKey,
|
|
859
1063
|
limit: historyLimit,
|
|
860
1064
|
entry: {
|
|
861
1065
|
sender: ctx.senderOpenId,
|
|
@@ -877,8 +1081,9 @@ export async function handleFeishuMessage(params: {
|
|
|
877
1081
|
channel: "feishu",
|
|
878
1082
|
accountId: account.accountId,
|
|
879
1083
|
});
|
|
1084
|
+
const commandProbeBody = isGroup ? normalizeFeishuCommandProbeBody(ctx.content) : ctx.content;
|
|
880
1085
|
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
|
|
881
|
-
|
|
1086
|
+
commandProbeBody,
|
|
882
1087
|
cfg,
|
|
883
1088
|
);
|
|
884
1089
|
const storeAllowFrom =
|
|
@@ -895,31 +1100,31 @@ export async function handleFeishuMessage(params: {
|
|
|
895
1100
|
senderName: ctx.senderName,
|
|
896
1101
|
}).allowed;
|
|
897
1102
|
|
|
898
|
-
if (
|
|
1103
|
+
if (isDirect && dmPolicy !== "open" && !dmAllowed) {
|
|
899
1104
|
if (dmPolicy === "pairing") {
|
|
900
|
-
|
|
901
|
-
|
|
1105
|
+
await issuePairingChallenge({
|
|
1106
|
+
channel: "feishu",
|
|
1107
|
+
senderId: ctx.senderOpenId,
|
|
1108
|
+
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
902
1109
|
meta: { name: ctx.senderName },
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1110
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
1111
|
+
onCreated: () => {
|
|
1112
|
+
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
|
1113
|
+
},
|
|
1114
|
+
sendPairingReply: async (text) => {
|
|
907
1115
|
await sendMessageFeishu({
|
|
908
1116
|
cfg,
|
|
909
|
-
to: `
|
|
910
|
-
text
|
|
911
|
-
channel: "feishu",
|
|
912
|
-
idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
913
|
-
code,
|
|
914
|
-
}),
|
|
1117
|
+
to: `chat:${ctx.chatId}`,
|
|
1118
|
+
text,
|
|
915
1119
|
accountId: account.accountId,
|
|
916
1120
|
});
|
|
917
|
-
}
|
|
1121
|
+
},
|
|
1122
|
+
onReplyError: (err) => {
|
|
918
1123
|
log(
|
|
919
1124
|
`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
|
|
920
1125
|
);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
923
1128
|
} else {
|
|
924
1129
|
log(
|
|
925
1130
|
`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
|
|
@@ -950,50 +1155,14 @@ export async function handleFeishuMessage(params: {
|
|
|
950
1155
|
// Using a group-scoped From causes the agent to treat different users as the same person.
|
|
951
1156
|
const feishuFrom = `feishu:${ctx.senderOpenId}`;
|
|
952
1157
|
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
|
|
1158
|
+
const peerId = isGroup ? (groupSession?.peerId ?? ctx.chatId) : ctx.senderOpenId;
|
|
1159
|
+
const parentPeer = isGroup ? (groupSession?.parentPeer ?? null) : null;
|
|
1160
|
+
const replyInThread = isGroup ? (groupSession?.replyInThread ?? false) : false;
|
|
953
1161
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
"group";
|
|
959
|
-
let topicRootForSession: string | null = null;
|
|
960
|
-
const replyInThread =
|
|
961
|
-
isGroup &&
|
|
962
|
-
(groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
|
|
963
|
-
|
|
964
|
-
if (isGroup) {
|
|
965
|
-
const legacyTopicSessionMode =
|
|
966
|
-
groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
|
|
967
|
-
groupSessionScope =
|
|
968
|
-
groupConfig?.groupSessionScope ??
|
|
969
|
-
feishuCfg?.groupSessionScope ??
|
|
970
|
-
(legacyTopicSessionMode === "enabled" ? "group_topic" : "group");
|
|
971
|
-
|
|
972
|
-
// When topic-scoped sessions are enabled and replyInThread is on, the first
|
|
973
|
-
// bot reply creates the thread rooted at the current message ID.
|
|
974
|
-
if (groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender") {
|
|
975
|
-
topicRootForSession = ctx.rootId ?? (replyInThread ? ctx.messageId : null);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
switch (groupSessionScope) {
|
|
979
|
-
case "group_sender":
|
|
980
|
-
peerId = `${ctx.chatId}:sender:${ctx.senderOpenId}`;
|
|
981
|
-
break;
|
|
982
|
-
case "group_topic":
|
|
983
|
-
peerId = topicRootForSession ? `${ctx.chatId}:topic:${topicRootForSession}` : ctx.chatId;
|
|
984
|
-
break;
|
|
985
|
-
case "group_topic_sender":
|
|
986
|
-
peerId = topicRootForSession
|
|
987
|
-
? `${ctx.chatId}:topic:${topicRootForSession}:sender:${ctx.senderOpenId}`
|
|
988
|
-
: `${ctx.chatId}:sender:${ctx.senderOpenId}`;
|
|
989
|
-
break;
|
|
990
|
-
case "group":
|
|
991
|
-
default:
|
|
992
|
-
peerId = ctx.chatId;
|
|
993
|
-
break;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
log(`feishu[${account.accountId}]: group session scope=${groupSessionScope}, peer=${peerId}`);
|
|
1162
|
+
if (isGroup && groupSession) {
|
|
1163
|
+
log(
|
|
1164
|
+
`feishu[${account.accountId}]: group session scope=${groupSession.groupSessionScope}, peer=${peerId}`,
|
|
1165
|
+
);
|
|
997
1166
|
}
|
|
998
1167
|
|
|
999
1168
|
let route = core.channel.routing.resolveAgentRoute({
|
|
@@ -1004,16 +1173,7 @@ export async function handleFeishuMessage(params: {
|
|
|
1004
1173
|
kind: isGroup ? "group" : "direct",
|
|
1005
1174
|
id: peerId,
|
|
1006
1175
|
},
|
|
1007
|
-
|
|
1008
|
-
parentPeer:
|
|
1009
|
-
isGroup &&
|
|
1010
|
-
topicRootForSession &&
|
|
1011
|
-
(groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender")
|
|
1012
|
-
? {
|
|
1013
|
-
kind: "group",
|
|
1014
|
-
id: ctx.chatId,
|
|
1015
|
-
}
|
|
1016
|
-
: null,
|
|
1176
|
+
parentPeer,
|
|
1017
1177
|
});
|
|
1018
1178
|
|
|
1019
1179
|
// Dynamic agent creation for DM users
|
|
@@ -1094,6 +1254,7 @@ export async function handleFeishuMessage(params: {
|
|
|
1094
1254
|
ctx,
|
|
1095
1255
|
quotedContent,
|
|
1096
1256
|
permissionErrorForAgent,
|
|
1257
|
+
botOpenId,
|
|
1097
1258
|
});
|
|
1098
1259
|
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
|
|
1099
1260
|
if (permissionErrorForAgent) {
|
|
@@ -1110,7 +1271,7 @@ export async function handleFeishuMessage(params: {
|
|
|
1110
1271
|
});
|
|
1111
1272
|
|
|
1112
1273
|
let combinedBody = body;
|
|
1113
|
-
const historyKey =
|
|
1274
|
+
const historyKey = groupHistoryKey;
|
|
1114
1275
|
|
|
1115
1276
|
if (isGroup && historyKey && chatHistories) {
|
|
1116
1277
|
combinedBody = buildPendingHistoryContextFromMap({
|
|
@@ -1139,81 +1300,247 @@ export async function handleFeishuMessage(params: {
|
|
|
1139
1300
|
}))
|
|
1140
1301
|
: undefined;
|
|
1141
1302
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1303
|
+
// --- Shared context builder for dispatch ---
|
|
1304
|
+
const buildCtxPayloadForAgent = (
|
|
1305
|
+
agentSessionKey: string,
|
|
1306
|
+
agentAccountId: string,
|
|
1307
|
+
wasMentioned: boolean,
|
|
1308
|
+
) =>
|
|
1309
|
+
core.channel.reply.finalizeInboundContext({
|
|
1310
|
+
Body: combinedBody,
|
|
1311
|
+
BodyForAgent: messageBody,
|
|
1312
|
+
InboundHistory: inboundHistory,
|
|
1313
|
+
ReplyToId: ctx.parentId,
|
|
1314
|
+
RootMessageId: ctx.rootId,
|
|
1315
|
+
RawBody: ctx.content,
|
|
1316
|
+
CommandBody: ctx.content,
|
|
1317
|
+
From: feishuFrom,
|
|
1318
|
+
To: feishuTo,
|
|
1319
|
+
SessionKey: agentSessionKey,
|
|
1320
|
+
AccountId: agentAccountId,
|
|
1321
|
+
ChatType: isGroup ? "group" : "direct",
|
|
1322
|
+
GroupSubject: isGroup ? ctx.chatId : undefined,
|
|
1323
|
+
SenderName: ctx.senderName ?? ctx.senderOpenId,
|
|
1324
|
+
SenderId: ctx.senderOpenId,
|
|
1325
|
+
Provider: "feishu" as const,
|
|
1326
|
+
Surface: "feishu" as const,
|
|
1327
|
+
MessageSid: ctx.messageId,
|
|
1328
|
+
ReplyToBody: quotedContent ?? undefined,
|
|
1329
|
+
Timestamp: Date.now(),
|
|
1330
|
+
WasMentioned: wasMentioned,
|
|
1331
|
+
CommandAuthorized: commandAuthorized,
|
|
1332
|
+
OriginatingChannel: "feishu" as const,
|
|
1333
|
+
OriginatingTo: feishuTo,
|
|
1334
|
+
GroupSystemPrompt: isGroup ? groupConfig?.systemPrompt?.trim() || undefined : undefined,
|
|
1335
|
+
...mediaPayload,
|
|
1336
|
+
});
|
|
1171
1337
|
|
|
1172
1338
|
// Parse message create_time (Feishu uses millisecond epoch string).
|
|
1173
1339
|
const messageCreateTimeMs = event.message.create_time
|
|
1174
1340
|
? parseInt(event.message.create_time, 10)
|
|
1175
1341
|
: undefined;
|
|
1342
|
+
// Determine reply target based on group session mode:
|
|
1343
|
+
// - Topic-mode groups (group_topic / group_topic_sender): reply to the topic
|
|
1344
|
+
// root so the bot stays in the same thread.
|
|
1345
|
+
// - Groups with explicit replyInThread config: reply to the root so the bot
|
|
1346
|
+
// stays in the thread the user expects.
|
|
1347
|
+
// - Normal groups (auto-detected threadReply from root_id): reply to the
|
|
1348
|
+
// triggering message itself. Using rootId here would silently push the
|
|
1349
|
+
// reply into a topic thread invisible in the main chat view (#32980).
|
|
1350
|
+
const isTopicSession =
|
|
1351
|
+
isGroup &&
|
|
1352
|
+
(groupSession?.groupSessionScope === "group_topic" ||
|
|
1353
|
+
groupSession?.groupSessionScope === "group_topic_sender");
|
|
1354
|
+
const configReplyInThread =
|
|
1355
|
+
isGroup &&
|
|
1356
|
+
(groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
|
|
1357
|
+
const replyTargetMessageId =
|
|
1358
|
+
isTopicSession || configReplyInThread ? (ctx.rootId ?? ctx.messageId) : ctx.messageId;
|
|
1359
|
+
const threadReply = isGroup ? (groupSession?.threadReply ?? false) : false;
|
|
1360
|
+
|
|
1361
|
+
if (broadcastAgents) {
|
|
1362
|
+
// Cross-account dedup: in multi-account setups, Feishu delivers the same
|
|
1363
|
+
// event to every bot account in the group. Only one account should handle
|
|
1364
|
+
// broadcast dispatch to avoid duplicate agent sessions and race conditions.
|
|
1365
|
+
// Uses a shared "broadcast" namespace (not per-account) so the first handler
|
|
1366
|
+
// to reach this point claims the message; subsequent accounts skip.
|
|
1367
|
+
if (!(await tryRecordMessagePersistent(ctx.messageId, "broadcast", log))) {
|
|
1368
|
+
log(
|
|
1369
|
+
`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`,
|
|
1370
|
+
);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1176
1373
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
rootId: ctx.rootId,
|
|
1186
|
-
mentionTargets: ctx.mentionTargets,
|
|
1187
|
-
accountId: account.accountId,
|
|
1188
|
-
messageCreateTimeMs,
|
|
1189
|
-
});
|
|
1374
|
+
// --- Broadcast dispatch: send message to all configured agents ---
|
|
1375
|
+
const strategy =
|
|
1376
|
+
((cfg as Record<string, unknown>).broadcast as Record<string, unknown> | undefined)
|
|
1377
|
+
?.strategy || "parallel";
|
|
1378
|
+
const activeAgentId =
|
|
1379
|
+
ctx.mentionedBot || !requireMention ? normalizeAgentId(route.agentId) : null;
|
|
1380
|
+
const agentIds = (cfg.agents?.list ?? []).map((a: { id: string }) => normalizeAgentId(a.id));
|
|
1381
|
+
const hasKnownAgents = agentIds.length > 0;
|
|
1190
1382
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
onSettled: () => {
|
|
1195
|
-
markDispatchIdle();
|
|
1196
|
-
},
|
|
1197
|
-
run: () =>
|
|
1198
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
1199
|
-
ctx: ctxPayload,
|
|
1200
|
-
cfg,
|
|
1201
|
-
dispatcher,
|
|
1202
|
-
replyOptions,
|
|
1203
|
-
}),
|
|
1204
|
-
});
|
|
1383
|
+
log(
|
|
1384
|
+
`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`,
|
|
1385
|
+
);
|
|
1205
1386
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1387
|
+
const dispatchForAgent = async (agentId: string) => {
|
|
1388
|
+
if (hasKnownAgents && !agentIds.includes(normalizeAgentId(agentId))) {
|
|
1389
|
+
log(
|
|
1390
|
+
`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`,
|
|
1391
|
+
);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
|
|
1396
|
+
const agentCtx = buildCtxPayloadForAgent(
|
|
1397
|
+
agentSessionKey,
|
|
1398
|
+
route.accountId,
|
|
1399
|
+
ctx.mentionedBot && agentId === activeAgentId,
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
if (agentId === activeAgentId) {
|
|
1403
|
+
// Active agent: real Feishu dispatcher (responds on Feishu)
|
|
1404
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
|
|
1405
|
+
cfg,
|
|
1406
|
+
agentId,
|
|
1407
|
+
runtime: runtime as RuntimeEnv,
|
|
1408
|
+
chatId: ctx.chatId,
|
|
1409
|
+
replyToMessageId: replyTargetMessageId,
|
|
1410
|
+
skipReplyToInMessages: !isGroup,
|
|
1411
|
+
replyInThread,
|
|
1412
|
+
rootId: ctx.rootId,
|
|
1413
|
+
threadReply,
|
|
1414
|
+
mentionTargets: ctx.mentionTargets,
|
|
1415
|
+
accountId: account.accountId,
|
|
1416
|
+
messageCreateTimeMs,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
log(
|
|
1420
|
+
`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`,
|
|
1421
|
+
);
|
|
1422
|
+
await core.channel.reply.withReplyDispatcher({
|
|
1423
|
+
dispatcher,
|
|
1424
|
+
onSettled: () => markDispatchIdle(),
|
|
1425
|
+
run: () =>
|
|
1426
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
1427
|
+
ctx: agentCtx,
|
|
1428
|
+
cfg,
|
|
1429
|
+
dispatcher,
|
|
1430
|
+
replyOptions,
|
|
1431
|
+
}),
|
|
1432
|
+
});
|
|
1433
|
+
} else {
|
|
1434
|
+
// Observer agent: no-op dispatcher (session entry + inference, no Feishu reply).
|
|
1435
|
+
// Strip CommandAuthorized so slash commands (e.g. /reset) don't silently
|
|
1436
|
+
// mutate observer sessions — only the active agent should execute commands.
|
|
1437
|
+
delete (agentCtx as Record<string, unknown>).CommandAuthorized;
|
|
1438
|
+
const noopDispatcher = {
|
|
1439
|
+
sendToolResult: () => false,
|
|
1440
|
+
sendBlockReply: () => false,
|
|
1441
|
+
sendFinalReply: () => false,
|
|
1442
|
+
waitForIdle: async () => {},
|
|
1443
|
+
getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
1444
|
+
markComplete: () => {},
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
log(
|
|
1448
|
+
`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`,
|
|
1449
|
+
);
|
|
1450
|
+
await core.channel.reply.withReplyDispatcher({
|
|
1451
|
+
dispatcher: noopDispatcher,
|
|
1452
|
+
run: () =>
|
|
1453
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
1454
|
+
ctx: agentCtx,
|
|
1455
|
+
cfg,
|
|
1456
|
+
dispatcher: noopDispatcher,
|
|
1457
|
+
}),
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
if (strategy === "sequential") {
|
|
1463
|
+
for (const agentId of broadcastAgents) {
|
|
1464
|
+
try {
|
|
1465
|
+
await dispatchForAgent(agentId);
|
|
1466
|
+
} catch (err) {
|
|
1467
|
+
log(
|
|
1468
|
+
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`,
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
} else {
|
|
1473
|
+
const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
|
|
1474
|
+
for (let i = 0; i < results.length; i++) {
|
|
1475
|
+
if (results[i].status === "rejected") {
|
|
1476
|
+
log(
|
|
1477
|
+
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String((results[i] as PromiseRejectedResult).reason)}`,
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (isGroup && historyKey && chatHistories) {
|
|
1484
|
+
clearHistoryEntriesIfEnabled({
|
|
1485
|
+
historyMap: chatHistories,
|
|
1486
|
+
historyKey,
|
|
1487
|
+
limit: historyLimit,
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
log(
|
|
1492
|
+
`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`,
|
|
1493
|
+
);
|
|
1494
|
+
} else {
|
|
1495
|
+
// --- Single-agent dispatch (existing behavior) ---
|
|
1496
|
+
const ctxPayload = buildCtxPayloadForAgent(
|
|
1497
|
+
route.sessionKey,
|
|
1498
|
+
route.accountId,
|
|
1499
|
+
ctx.mentionedBot,
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
|
|
1503
|
+
cfg,
|
|
1504
|
+
agentId: route.agentId,
|
|
1505
|
+
runtime: runtime as RuntimeEnv,
|
|
1506
|
+
chatId: ctx.chatId,
|
|
1507
|
+
replyToMessageId: replyTargetMessageId,
|
|
1508
|
+
skipReplyToInMessages: !isGroup,
|
|
1509
|
+
replyInThread,
|
|
1510
|
+
rootId: ctx.rootId,
|
|
1511
|
+
threadReply,
|
|
1512
|
+
mentionTargets: ctx.mentionTargets,
|
|
1513
|
+
accountId: account.accountId,
|
|
1514
|
+
messageCreateTimeMs,
|
|
1211
1515
|
});
|
|
1212
|
-
}
|
|
1213
1516
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1517
|
+
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
1518
|
+
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
|
1519
|
+
dispatcher,
|
|
1520
|
+
onSettled: () => {
|
|
1521
|
+
markDispatchIdle();
|
|
1522
|
+
},
|
|
1523
|
+
run: () =>
|
|
1524
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
1525
|
+
ctx: ctxPayload,
|
|
1526
|
+
cfg,
|
|
1527
|
+
dispatcher,
|
|
1528
|
+
replyOptions,
|
|
1529
|
+
}),
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
if (isGroup && historyKey && chatHistories) {
|
|
1533
|
+
clearHistoryEntriesIfEnabled({
|
|
1534
|
+
historyMap: chatHistories,
|
|
1535
|
+
historyKey,
|
|
1536
|
+
limit: historyLimit,
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
log(
|
|
1541
|
+
`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1217
1544
|
} catch (err) {
|
|
1218
1545
|
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
|
|
1219
1546
|
}
|