From 711903bc5fd00be72009c0b04ab1e42d46239311 Mon Sep 17 00:00:00 2001 From: Zek Date: Mon, 9 Mar 2026 17:54:02 +0800 Subject: [PATCH] feat(feishu): add global group mention policy - Add group_policy config: 'open' (default) or 'mention' - 'open': Respond to all group messages (backward compatible) - 'mention': Only respond when @mentioned in any group - Auto-detect bot mentions by pattern matching: * If open_id configured: match against mentions * Otherwise: detect bot by empty user_id + ou_ open_id pattern - Support @_all mentions - Private chats unaffected (always respond) - Clean implementation with minimal logging docs: update Feishu README with group policy documentation --- README.md | 15 +++++++- nanobot/channels/feishu.py | 78 ++++++++++++++++++++++++++++++++++++++ nanobot/config/schema.py | 2 + 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f169bd7..29221a7 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,8 @@ Uses **WebSocket** long connection — no public IP required. "appSecret": "xxx", "encryptKey": "", "verificationToken": "", - "allowFrom": ["ou_YOUR_OPEN_ID"] + "allowFrom": ["ou_YOUR_OPEN_ID"], + "groupPolicy": "open" } } } @@ -491,6 +492,18 @@ Uses **WebSocket** long connection — no public IP required. > `encryptKey` and `verificationToken` are optional for Long Connection mode. > `allowFrom`: Add your open_id (find it in nanobot logs when you message the bot). Use `["*"]` to allow all users. +**Group Chat Policy** (optional): + +| Option | Values | Default | Description | +|--------|--------|---------|-------------| +| `groupPolicy` | `"open"` | `"open"` | Respond to all group messages (backward compatible) | +| | `"mention"` | | Only respond when @mentioned | + +> [!NOTE] +> - `"open"`: Respond to all messages in all groups +> - `"mention"`: Only respond when @mentioned in any group +> - Private chats are unaffected (always respond) + **3. Run** ```bash diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index a637025..78bf2df 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -352,6 +352,74 @@ class FeishuChannel(BaseChannel): self._running = False logger.info("Feishu bot stopped") + def _get_bot_open_id_sync(self) -> str | None: + """Get bot's own open_id for mention detection. + + 飞书 SDK 没有直接的 bot info API,从配置或缓存获取。 + """ + # 尝试从配置获取 open_id(用户可以在配置中指定) + if hasattr(self.config, 'open_id') and self.config.open_id: + return self.config.open_id + + return None + + def _is_bot_mentioned(self, message: Any, bot_open_id: str | None) -> bool: + """Check if bot is mentioned in the message. + + 飞书 mentions 数组包含被@的对象。匹配策略: + 1. 如果配置了 bot_open_id,则匹配 open_id + 2. 否则,检查 mentions 中是否有空的 user_id(bot 的特征) + + Handles: + - Direct mentions in message.mentions + - @all mentions + """ + # Check @all + raw_content = message.content or "" + if "@_all" in raw_content: + logger.debug("Feishu: @_all mention detected") + return True + + # Check mentions array + mentions = message.mentions if hasattr(message, 'mentions') and message.mentions else [] + if mentions: + if bot_open_id: + # 策略 1: 匹配配置的 open_id + for mention in mentions: + if mention.id: + open_id = getattr(mention.id, 'open_id', None) + if open_id == bot_open_id: + logger.debug("Feishu: bot mention matched") + return True + else: + # 策略 2: 检查 bot 特征 - user_id 为空且 open_id 存在 + for mention in mentions: + if mention.id: + user_id = getattr(mention.id, 'user_id', None) + open_id = getattr(mention.id, 'open_id', None) + # Bot 的特征:user_id 为空字符串,open_id 存在 + if user_id == '' and open_id and open_id.startswith('ou_'): + logger.debug("Feishu: bot mention matched") + return True + + return False + + def _should_respond_in_group( + self, + chat_id: str, + mentioned: bool + ) -> tuple[bool, str]: + """Determine if bot should respond in a group chat. + + Returns: + (should_respond, reason) + """ + # Check mention requirement + if self.config.group_policy == "mention" and not mentioned: + return False, "not mentioned in group" + + return True, "" + def _add_reaction_sync(self, message_id: str, emoji_type: str) -> None: """Sync helper for adding reaction (runs in thread pool).""" from lark_oapi.api.im.v1 import CreateMessageReactionRequest, CreateMessageReactionRequestBody, Emoji @@ -892,6 +960,16 @@ class FeishuChannel(BaseChannel): chat_type = message.chat_type msg_type = message.message_type + # Check group policy and mention requirement + if chat_type == "group": + bot_open_id = self._get_bot_open_id_sync() + mentioned = self._is_bot_mentioned(message, bot_open_id) + should_respond, reason = self._should_respond_in_group(chat_id, mentioned) + + if not should_respond: + logger.debug("Feishu: ignoring group message - {}", reason) + return + # Add reaction await self._add_reaction(message_id, self.config.react_emoji) diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 803cb61..6b2eb35 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -47,6 +47,8 @@ class FeishuConfig(Base): react_emoji: str = ( "THUMBSUP" # Emoji type for message reactions (e.g. THUMBSUP, OK, DONE, SMILE) ) + # Group chat settings + group_policy: Literal["open", "mention"] = "open" # Group response policy (default: open for backward compatibility) class DingTalkConfig(Base):