From 7eb2761b54d82ec5edce3fd187ea5400ceac65ea Mon Sep 17 00:00:00 2001 From: Ming Date: Fri, 24 Apr 2026 17:53:21 +0800 Subject: [PATCH] feat(feishu): add require_mention and free_response_chats config for group messages Align Feishu platform with other messaging platforms (Telegram, Discord, WhatsApp) by introducing a configurable mention requirement for group messages. - Add 'require_mention' config (default: true) to control whether group messages must @mention the bot to trigger processing. - Add 'free_response_chats' config to whitelist specific group chats that bypass both mention and user allowlist checks. - Support both YAML config (feishu.require_mention, feishu.free_response_chats) and env vars (FEISHU_REQUIRE_MENTION, FEISHU_FREE_RESPONSE_CHATS). - Update gateway config loader to map free_response_chats from YAML to platform extra dict. - Update tests to cover new mention bypass and free-response chat scenarios. --- gateway/config.py | 2 ++ gateway/platforms/feishu.py | 26 +++++++++++++++++++- hermes_cli/config.py | 1 + tests/gateway/test_feishu.py | 46 +++++++++++++++++++++++------------- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/gateway/config.py b/gateway/config.py index 67ebf7346..8553d2025 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -574,6 +574,8 @@ def load_gateway_config() -> GatewayConfig: bridged["require_mention"] = platform_cfg["require_mention"] if "free_response_channels" in platform_cfg: bridged["free_response_channels"] = platform_cfg["free_response_channels"] + if "free_response_chats" in platform_cfg: + bridged["free_response_chats"] = platform_cfg["free_response_chats"] if "mention_patterns" in platform_cfg: bridged["mention_patterns"] = platform_cfg["mention_patterns"] if "dm_policy" in platform_cfg: diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 718f01e99..0639f18ed 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -387,6 +387,8 @@ class FeishuAdapterSettings: admins: frozenset[str] = frozenset() default_group_policy: str = "" group_rules: Dict[str, FeishuGroupRule] = field(default_factory=dict) + require_mention: bool = True + free_response_chats: frozenset[str] = frozenset() @dataclass @@ -1446,6 +1448,15 @@ class FeishuAdapter(BasePlatformAdapter): admins=admins, default_group_policy=default_group_policy, group_rules=group_rules, + require_mention=_to_boolean( + extra.get("require_mention", os.getenv("FEISHU_REQUIRE_MENTION", "true")) + ), + free_response_chats=frozenset( + str(c).strip() + for raw in [extra.get("free_response_chats") or os.getenv("FEISHU_FREE_RESPONSE_CHATS", "")] + for c in (raw.split(",") if isinstance(raw, str) else raw) + if str(c).strip() + ), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: @@ -1460,6 +1471,8 @@ class FeishuAdapter(BasePlatformAdapter): self._admins = set(settings.admins) self._default_group_policy = settings.default_group_policy or settings.group_policy self._group_rules = settings.group_rules + self._require_mention = settings.require_mention + self._free_response_chats = set(settings.free_response_chats) self._bot_open_id = settings.bot_open_id self._bot_user_id = settings.bot_user_id self._bot_name = settings.bot_name @@ -3626,9 +3639,20 @@ class FeishuAdapter(BasePlatformAdapter): return bool(sender_ids and (sender_ids & self._allowed_group_users)) def _should_accept_group_message(self, message: Any, sender_id: Any, chat_id: str = "") -> bool: - """Require an explicit @mention before group messages enter the agent.""" + """Decide whether a group message should enter the agent pipeline.""" + + # 1. Highest priority: Free-response chats bypass ALL checks (mention & user allowlist) + if chat_id and chat_id in self._free_response_chats: + return True + + # 2. Policy gate (admins, allowlist/blacklist) if not self._allow_group_message(sender_id, chat_id): return False + + # 3. If mention requirement is disabled, accept all allowed messages + if not self._require_mention: + return True + # @_all is Feishu's @everyone placeholder — always route to the bot. raw_content = getattr(message, "content", "") or "" if "@_all" in raw_content: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 58f874595..d4e7397fe 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -40,6 +40,7 @@ _EXTRA_ENV_KEYS = frozenset({ "SIGNAL_ALLOWED_USERS", "SIGNAL_GROUP_ALLOWED_USERS", "DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_SECRET", "FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_ENCRYPT_KEY", "FEISHU_VERIFICATION_TOKEN", + "FEISHU_REQUIRE_MENTION", "FEISHU_FREE_RESPONSE_CHATS", "WECOM_BOT_ID", "WECOM_SECRET", "WECOM_CALLBACK_CORP_ID", "WECOM_CALLBACK_CORP_SECRET", "WECOM_CALLBACK_AGENT_ID", "WECOM_CALLBACK_TOKEN", "WECOM_CALLBACK_ENCODING_AES_KEY", diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index f21b7dcef..a890bd32d 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -689,32 +689,42 @@ class TestAdapterBehavior(unittest.TestCase): adapter._on_reaction_event("im.message.reaction.created_v1", data) run_threadsafe.assert_called_once() - @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) - def test_group_message_requires_mentions_even_when_policy_open(self): + def test_group_message_accepted_without_mention_when_require_mention_false(self): from gateway.config import PlatformConfig from gateway.platforms.feishu import FeishuAdapter - adapter = FeishuAdapter(PlatformConfig()) - message = SimpleNamespace(mentions=[]) + adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": False})) + adapter._group_policy = "allowlist" + adapter._allowed_group_users = {"ou_any"} sender_id = SimpleNamespace(open_id="ou_any", user_id=None) - self.assertFalse(adapter._should_accept_group_message(message, sender_id, "")) - message_with_mention = SimpleNamespace(mentions=[SimpleNamespace(key="@_user_1")]) - self.assertFalse(adapter._should_accept_group_message(message_with_mention, sender_id, "")) + # No mentions at all — should be accepted when require_mention is False + message_no_mention = SimpleNamespace(mentions=[], content="{}") + self.assertTrue(adapter._should_accept_group_message(message_no_mention, sender_id, "oc_chat_1")) - @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) - def test_group_message_with_other_user_mention_is_rejected_when_bot_identity_unknown(self): + # With @_all — should also be accepted + message_all = SimpleNamespace(mentions=[], content='{"@_all":1}') + self.assertTrue(adapter._should_accept_group_message(message_all, sender_id, "oc_chat_1")) + + def test_group_message_accepted_in_free_response_chats(self): from gateway.config import PlatformConfig from gateway.platforms.feishu import FeishuAdapter - adapter = FeishuAdapter(PlatformConfig()) + adapter = FeishuAdapter(PlatformConfig(extra={ + "require_mention": True, + "free_response_chats": ["oc_free_chat_1", "oc_free_chat_2"] + })) + adapter._group_policy = "allowlist" + adapter._allowed_group_users = {"ou_any"} sender_id = SimpleNamespace(open_id="ou_any", user_id=None) - other_mention = SimpleNamespace( - name="Other User", - id=SimpleNamespace(open_id="ou_other", user_id="u_other"), - ) - self.assertFalse(adapter._should_accept_group_message(SimpleNamespace(mentions=[other_mention]), sender_id, "")) + # Chat in free_response_chats — accepted without mention + message = SimpleNamespace(mentions=[], content="{}") + self.assertTrue(adapter._should_accept_group_message(message, sender_id, "oc_free_chat_1")) + self.assertTrue(adapter._should_accept_group_message(message, sender_id, "oc_free_chat_2")) + + # Chat NOT in free_response_chats — rejected without mention + self.assertFalse(adapter._should_accept_group_message(message, sender_id, "oc_normal_chat")) @patch.dict( os.environ, @@ -1004,13 +1014,14 @@ class TestAdapterBehavior(unittest.TestCase): ) ) - @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_group_message_matches_bot_open_id_when_configured(self): from gateway.config import PlatformConfig from gateway.platforms.feishu import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._bot_open_id = "ou_bot" + adapter._group_policy = "allowlist" + adapter._allowed_group_users = {"ou_any"} sender_id = SimpleNamespace(open_id="ou_any", user_id=None) bot_mention = SimpleNamespace( @@ -1025,7 +1036,6 @@ class TestAdapterBehavior(unittest.TestCase): self.assertTrue(adapter._should_accept_group_message(SimpleNamespace(mentions=[bot_mention]), sender_id, "")) self.assertFalse(adapter._should_accept_group_message(SimpleNamespace(mentions=[other_mention]), sender_id, "")) - @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_group_message_matches_bot_name_when_only_name_available(self): """Name fallback engages when either side lacks an open_id. When BOTH the mention and the bot carry open_ids, IDs are authoritative — a @@ -1037,6 +1047,8 @@ class TestAdapterBehavior(unittest.TestCase): # Name fallback is the only available signal for any mention. adapter = FeishuAdapter(PlatformConfig()) adapter._bot_name = "Hermes Bot" + adapter._group_policy = "allowlist" + adapter._allowed_group_users = {"ou_any"} sender_id = SimpleNamespace(open_id="ou_any", user_id=None) name_only_mention = SimpleNamespace(