From d6b959af606f9d000088e423898aa738ae85cd4b Mon Sep 17 00:00:00 2001 From: YAN Date: Wed, 25 Mar 2026 20:00:08 +0800 Subject: [PATCH] fix: enforce requireMention setting for Feishu group chats The `bridge_feishu_require_mention` setting existed in the config UI and was loaded from the database, but was never actually checked in the message handling pipeline. Group chat messages were always processed regardless of whether the bot was @mentioned. Changes: - Fetch bot identity (open_id) on plugin startup via getBotInfo() - Check `message.mentions` against botOpenId when requireMention is enabled and the message comes from a group chat (chatId starts with 'oc_') - Strip the @bot mention placeholder from message text so the LLM receives clean input - Return null (skip message) when requireMention is true and the bot is not mentioned --- src/lib/channels/feishu/inbound.ts | 30 +++++++++++++++++++++++++---- src/lib/channels/feishu/index.ts | 31 ++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) 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 {