From 70d73696d38ce624b511c1c6ecd451188afef578 Mon Sep 17 00:00:00 2001 From: Leegenux Date: Thu, 2 Apr 2026 21:02:17 +0800 Subject: [PATCH 1/5] feat(feishu): add require_mention config option for group chats Add feishu.require_mention config option (default: true) to control whether @mention is required for bot to respond in group chats. Previously, group_policy="open" would bypass @mention check, but this was hardcoded behavior. Now users can configure this independently: - require_mention=true (default): Only respond when bot is @mentioned or @_all - require_mention=false: Respond to all allowed group messages without @mention Configuration via config.yaml: feishu: require_mention: false Or via environment variable: FEISHU_REQUIRE_MENTION=false This matches the pattern used by Discord's require_mention setting. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- gateway/config.py | 6 ++++++ gateway/platforms/feishu.py | 13 ++++++++++++- hermes_cli/config.py | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gateway/config.py b/gateway/config.py index c7eb4adf109f..c1554fda7ad4 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -550,6 +550,12 @@ def load_gateway_config() -> GatewayConfig: if "reactions" in discord_cfg and not os.getenv("DISCORD_REACTIONS"): os.environ["DISCORD_REACTIONS"] = str(discord_cfg["reactions"]).lower() + # Feishu/Lark settings → env vars (env vars take precedence) + feishu_cfg = yaml_cfg.get("feishu", {}) + if isinstance(feishu_cfg, dict): + if "require_mention" in feishu_cfg and not os.getenv("FEISHU_REQUIRE_MENTION"): + os.environ["FEISHU_REQUIRE_MENTION"] = str(feishu_cfg["require_mention"]).lower() + # Telegram settings → env vars (env vars take precedence) telegram_cfg = yaml_cfg.get("telegram", {}) if isinstance(telegram_cfg, dict): diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index d9aaae9a747f..e1769f90d80b 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -270,6 +270,7 @@ class FeishuAdapterSettings: webhook_host: str webhook_port: int webhook_path: str + require_mention: bool # Require @mention in group chats (default: true) @dataclass @@ -1020,6 +1021,7 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: str(extra.get("webhook_path") or os.getenv("FEISHU_WEBHOOK_PATH", _DEFAULT_WEBHOOK_PATH)).strip() or _DEFAULT_WEBHOOK_PATH ), + require_mention=os.getenv("FEISHU_REQUIRE_MENTION", "true").strip().lower() not in ("false", "0", "no"), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: @@ -1042,6 +1044,7 @@ def _apply_settings(self, settings: FeishuAdapterSettings) -> None: self._webhook_host = settings.webhook_host self._webhook_port = settings.webhook_port self._webhook_path = settings.webhook_path + self._require_mention = settings.require_mention def _build_event_handler(self) -> Any: if EventDispatcherHandler is None: @@ -2665,9 +2668,17 @@ def _allow_group_message(self, sender_id: Any) -> bool: return bool(sender_open_id and sender_open_id in self._allowed_group_users) def _should_accept_group_message(self, message: Any, sender_id: Any) -> bool: - """Require an explicit @mention before group messages enter the agent.""" + """Check if a group message should be routed to the agent. + + Uses require_mention setting to control whether @mention is needed: + - require_mention=true (default): Only route if bot is @mentioned or @_all + - require_mention=false: Route all allowed group messages without @mention + """ if not self._allow_group_message(sender_id): return False + # If require_mention is disabled, accept all allowed group 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 17a1226061b9..bc59054f07b0 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -472,6 +472,11 @@ def ensure_hermes_home(): "reactions": True, # Add 👀/✅/❌ reactions to messages during processing }, + # Feishu/Lark platform settings (gateway mode) + "feishu": { + "require_mention": True, # Require @mention to respond in group chats (default: true) + }, + # WhatsApp platform settings (gateway mode) "whatsapp": { # Reply prefix prepended to every outgoing WhatsApp message. From a8cceec189b5a22bef4be74b7d04f50ae2533c2a Mon Sep 17 00:00:00 2001 From: Leegenux Date: Thu, 2 Apr 2026 21:29:44 +0800 Subject: [PATCH 2/5] fix(feishu): support extra config for require_mention and add tests - Allow require_mention from extra.get() (platforms.feishu.extra) with env var fallback - Add TestRequireMentionDisabled test class covering: - require_mention=false accepts messages without @mention - require_mention=true requires @mention (default) - Default behavior when env var unset - Allowlist policy still respected when require_mention=false Addresses Copilot PR #4591 review comments. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- gateway/platforms/feishu.py | 5 ++- tests/gateway/test_feishu.py | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index e1769f90d80b..00135253ef2a 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -1021,7 +1021,10 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: str(extra.get("webhook_path") or os.getenv("FEISHU_WEBHOOK_PATH", _DEFAULT_WEBHOOK_PATH)).strip() or _DEFAULT_WEBHOOK_PATH ), - require_mention=os.getenv("FEISHU_REQUIRE_MENTION", "true").strip().lower() not in ("false", "0", "no"), + require_mention=( + str(extra.get("require_mention") or os.getenv("FEISHU_REQUIRE_MENTION", "true")).strip().lower() + not in ("false", "0", "no") + ), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 5344cda52af0..541244411916 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -2474,6 +2474,70 @@ def test_at_all_still_requires_policy_gate(self): self.assertTrue(adapter._should_accept_group_message(message, allowed_sender)) +class TestRequireMentionDisabled(unittest.TestCase): + """Tests for require_mention=false behavior in group chats.""" + + @patch.dict( + os.environ, + {"FEISHU_GROUP_POLICY": "open", "FEISHU_REQUIRE_MENTION": "false"}, + clear=True, + ) + def test_group_message_accepted_without_mention_when_require_mention_false(self): + """When require_mention=false, group messages are accepted without @mention.""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + sender_id = SimpleNamespace(open_id="ou_any", user_id=None) + self.assertTrue(adapter._should_accept_group_message(message, sender_id)) + + @patch.dict( + os.environ, + {"FEISHU_GROUP_POLICY": "open", "FEISHU_REQUIRE_MENTION": "true"}, + clear=True, + ) + def test_group_message_rejected_without_mention_when_require_mention_true(self): + """Default behavior: require_mention=true requires @mention.""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + sender_id = SimpleNamespace(open_id="ou_any", user_id=None) + self.assertFalse(adapter._should_accept_group_message(message, sender_id)) + + @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) + def test_require_mention_defaults_to_true_when_unset(self): + """When FEISHU_REQUIRE_MENTION is unset, default is true (require mention).""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + sender_id = SimpleNamespace(open_id="ou_any", user_id=None) + self.assertFalse(adapter._should_accept_group_message(message, sender_id)) + + @patch.dict( + os.environ, + {"FEISHU_GROUP_POLICY": "allowlist", "FEISHU_ALLOWED_USERS": "ou_allowed", "FEISHU_REQUIRE_MENTION": "false"}, + clear=True, + ) + def test_require_mention_false_still_respects_allowlist_policy(self): + """require_mention=false bypasses mention check but NOT allowlist policy.""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + # Allowlisted user — should pass. + allowed_sender = SimpleNamespace(open_id="ou_allowed", user_id=None) + self.assertTrue(adapter._should_accept_group_message(message, allowed_sender)) + # Non-allowlisted user — should be blocked. + blocked_sender = SimpleNamespace(open_id="ou_blocked", user_id=None) + self.assertFalse(adapter._should_accept_group_message(message, blocked_sender)) + + @unittest.skipUnless(_HAS_LARK_OAPI, "lark-oapi not installed") class TestSenderNameResolution(unittest.TestCase): """Tests for _resolve_sender_name_from_api.""" From bacc26119b2296e4e2c0b5ec9c7144b2534ba141 Mon Sep 17 00:00:00 2001 From: Leegenux Date: Thu, 2 Apr 2026 21:45:17 +0800 Subject: [PATCH 3/5] fix(feishu): handle falsy require_mention from extra config correctly Use explicit None check instead of 'or' fallback to correctly handle require_mention=False from PlatformConfig.extra. Previously the 'or' operator treated False/0/empty string as 'not provided' and fell back to env var, making it impossible to disable mention gating via config. - Add _resolve_require_mention() helper with explicit None check - Add tests for require_mention via PlatformConfig.extra (True/False) - Add test for extra precedence over env var when explicitly False Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- gateway/platforms/feishu.py | 20 ++++++++++++++++---- tests/gateway/test_feishu.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 00135253ef2a..eccb6fb48541 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -973,6 +973,21 @@ def __init__(self, config: PlatformConfig): self._pending_media_batch_tasks = self._media_batch_state.tasks self._load_seen_message_ids() + @staticmethod + def _resolve_require_mention(extra: Dict[str, Any]) -> bool: + """Resolve require_mention setting from extra config or env var. + + Uses explicit None check to handle falsy values correctly: + - If extra["require_mention"] exists, use it (even if False) + - Otherwise fallback to FEISHU_REQUIRE_MENTION env var (default: true) + """ + configured = extra.get("require_mention") + if configured is not None: + if isinstance(configured, str): + return configured.strip().lower() in ("true", "1", "yes", "on") + return bool(configured) + return os.getenv("FEISHU_REQUIRE_MENTION", "true").strip().lower() in ("true", "1", "yes", "on") + @staticmethod def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: return FeishuAdapterSettings( @@ -1021,10 +1036,7 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: str(extra.get("webhook_path") or os.getenv("FEISHU_WEBHOOK_PATH", _DEFAULT_WEBHOOK_PATH)).strip() or _DEFAULT_WEBHOOK_PATH ), - require_mention=( - str(extra.get("require_mention") or os.getenv("FEISHU_REQUIRE_MENTION", "true")).strip().lower() - not in ("false", "0", "no") - ), + require_mention=_resolve_require_mention(extra), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 541244411916..03c8df0b2e25 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -2537,6 +2537,41 @@ def test_require_mention_false_still_respects_allowlist_policy(self): blocked_sender = SimpleNamespace(open_id="ou_blocked", user_id=None) self.assertFalse(adapter._should_accept_group_message(message, blocked_sender)) + @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) + def test_require_mention_false_from_platform_config_extra(self): + """require_mention=False via PlatformConfig.extra should be honored (no falsy fallback bug).""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + # Explicit False via extra should work even without env var + adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": False})) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + sender_id = SimpleNamespace(open_id="ou_any", user_id=None) + self.assertTrue(adapter._should_accept_group_message(message, sender_id)) + + @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) + def test_require_mention_true_from_platform_config_extra(self): + """require_mention=True via PlatformConfig.extra should be honored.""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": True})) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + sender_id = SimpleNamespace(open_id="ou_any", user_id=None) + self.assertFalse(adapter._should_accept_group_message(message, sender_id)) + + @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open", "FEISHU_REQUIRE_MENTION": "true"}, clear=True) + def test_require_mention_env_var_overridden_by_explicit_false_in_extra(self): + """extra.get("require_mention") takes precedence over env var (even when False).""" + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + # Env var says true, but extra explicitly says False — should use False + adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": False})) + message = SimpleNamespace(content='{"text":"hello"}', mentions=[]) + sender_id = SimpleNamespace(open_id="ou_any", user_id=None) + self.assertTrue(adapter._should_accept_group_message(message, sender_id)) + @unittest.skipUnless(_HAS_LARK_OAPI, "lark-oapi not installed") class TestSenderNameResolution(unittest.TestCase): From dc5dac6344bbedd8c5dcf695694083ff9a0ca4a3 Mon Sep 17 00:00:00 2001 From: Leegenux <34061155+Leegenux@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:51:23 +0800 Subject: [PATCH 4/5] Update gateway/platforms/feishu.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- gateway/platforms/feishu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index eccb6fb48541..da0195be4eda 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -1036,7 +1036,7 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: str(extra.get("webhook_path") or os.getenv("FEISHU_WEBHOOK_PATH", _DEFAULT_WEBHOOK_PATH)).strip() or _DEFAULT_WEBHOOK_PATH ), - require_mention=_resolve_require_mention(extra), + require_mention=FeishuAdapter._resolve_require_mention(extra), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: From e6c31fd3fd2c371f65891dca78da9c5b7a62e543 Mon Sep 17 00:00:00 2001 From: Leegenux <34061155+Leegenux@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:52:09 +0800 Subject: [PATCH 5/5] Update gateway/platforms/feishu.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- gateway/platforms/feishu.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index da0195be4eda..a2279232d55d 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -984,9 +984,13 @@ def _resolve_require_mention(extra: Dict[str, Any]) -> bool: configured = extra.get("require_mention") if configured is not None: if isinstance(configured, str): - return configured.strip().lower() in ("true", "1", "yes", "on") + value = configured.strip().lower() + # Default-true semantics: only explicit false-like values disable mention gating. + return value not in ("false", "0", "no", "off") return bool(configured) - return os.getenv("FEISHU_REQUIRE_MENTION", "true").strip().lower() in ("true", "1", "yes", "on") + env_value = os.getenv("FEISHU_REQUIRE_MENTION", "true").strip().lower() + # Default-true semantics for env var as well. + return env_value not in ("false", "0", "no", "off") @staticmethod def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: