diff --git a/src/lib/channels/feishu/inbound.ts b/src/lib/channels/feishu/inbound.ts index 590f5ae4..3d90e614 100644 --- a/src/lib/channels/feishu/inbound.ts +++ b/src/lib/channels/feishu/inbound.ts @@ -9,10 +9,20 @@ import type { FeishuConfig } from './types'; const LOG_TAG = '[feishu/inbound]'; +/** Find the bot's mention entry in the Feishu mentions array, if present. */ +function findBotMention( + mentions: any[] | undefined, + botOpenId: string, +): { key?: string } | undefined { + if (!mentions || !botOpenId) return undefined; + return mentions.find((m: any) => m?.id?.open_id === botOpenId); +} + /** Parse a raw Feishu im.message.receive_v1 event into an InboundMessage. */ export function parseInboundMessage( eventData: any, - _config: FeishuConfig, + config: FeishuConfig, + botOpenId?: string, ): InboundMessage | null { try { const event = eventData?.event ?? eventData; @@ -24,7 +34,16 @@ export function parseInboundMessage( const sender = event.sender?.sender_id?.open_id || ''; const msgType = message.message_type; - // Only handle text messages for now + const isGroupChat = chatId.startsWith('oc_'); + const botMention = isGroupChat && botOpenId + ? findBotMention(message.mentions, botOpenId) + : undefined; + + // When requireMention is enabled, drop group messages that don't @mention the bot. + if (isGroupChat && config.requireMention && !botMention) { + return null; + } + let text = ''; if (msgType === 'text') { try { @@ -34,13 +53,16 @@ export function parseInboundMessage( text = message.content || ''; } } else { - // Non-text messages — skip silently return null; } if (!text.trim()) return null; - // Build thread-session address if applicable + // Strip the @bot placeholder so the LLM sees clean input. + if (botMention?.key) { + text = text.replaceAll(botMention.key, '').trim(); + } + const rootId = message.root_id || ''; const effectiveChatId = rootId ? `${chatId}:thread:${rootId}` : chatId; diff --git a/src/lib/channels/feishu/index.ts b/src/lib/channels/feishu/index.ts index 4fec0eb4..ee139d27 100644 --- a/src/lib/channels/feishu/index.ts +++ b/src/lib/channels/feishu/index.ts @@ -11,6 +11,7 @@ import type { FeishuConfig } from './types'; import { loadFeishuConfig, validateFeishuConfig } from './config'; import { FeishuGateway } from './gateway'; import { parseInboundMessage } from './inbound'; +import { getBotInfo } from './identity'; import { sendMessage, addReaction, removeReaction } from './outbound'; import { isUserAuthorized } from './policy'; import { createCardStreamController } from './card-controller'; @@ -23,6 +24,7 @@ export class FeishuChannelPlugin implements ChannelPlugin { private config: FeishuConfig | null = null; private gateway: FeishuGateway | null = null; + private botOpenId: string = ''; private messageQueue: InboundMessage[] = []; private waitResolve: ((msg: InboundMessage | null) => void) | null = null; /** Track last received messageId per chatId for reaction acknowledgment. */ @@ -64,9 +66,11 @@ export class FeishuChannelPlugin implements ChannelPlugin { this.gateway = new FeishuGateway(this.config); - // Register message handler — pushes to internal queue + // Register message handler — pushes to internal queue. + // Reads this.botOpenId at call time, so mention checks activate + // once resolveBotIdentity() completes after gateway.start(). this.gateway.registerMessageHandler((data: unknown) => { - const msg = parseInboundMessage(data, this.config!); + const msg = parseInboundMessage(data, this.config!, this.botOpenId); if (!msg) return; this.enqueueMessage(msg); }); @@ -148,6 +152,29 @@ export class FeishuChannelPlugin implements ChannelPlugin { }); await this.gateway.start(); + + // Resolve bot identity so mention checks work. + // Handler reads this.botOpenId dynamically — once resolved, all + // subsequent messages get proper mention filtering. + await this.resolveBotIdentity(); + } + + /** Fetch bot open_id with retry so mention features degrade gracefully. */ + private async resolveBotIdentity(maxRetries = 3): Promise { + const client = this.gateway?.getRestClient(); + if (!client) return; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const info = await getBotInfo(client); + if (info?.openId) { + this.botOpenId = info.openId; + return; + } + console.warn('[feishu/plugin]', `Bot identity attempt ${attempt}/${maxRetries} failed`); + if (attempt < maxRetries) { + await new Promise((r) => setTimeout(r, 2000 * attempt)); + } + } + console.error('[feishu/plugin]', 'Could not resolve bot identity — mention features disabled'); } async stop(): Promise {