Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
32 changes: 31 additions & 1 deletion gateway/platforms/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -972,6 +973,25 @@ 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):
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)
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:
return FeishuAdapterSettings(
Expand Down Expand Up @@ -1020,6 +1040,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=FeishuAdapter._resolve_require_mention(extra),
)
Comment on lines 1040 to 1044

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require_mention is currently only read from FEISHU_REQUIRE_MENTION. Since _load_settings() already accepts extra (and other Feishu settings like app_id, connection_mode, webhook_* can be configured via config.extra / gateway.json), it would be more consistent to also honor extra.get("require_mention") when present (e.g., bool or truthy/falsey string) and fall back to the env var otherwise. As-is, platforms.feishu.extra.require_mention (including values bridged into PlatformConfig.extra) has no effect.

Copilot uses AI. Check for mistakes.

def _apply_settings(self, settings: FeishuAdapterSettings) -> None:
Expand All @@ -1042,6 +1063,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:
Expand Down Expand Up @@ -2665,9 +2687,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
Comment on lines 2696 to +2700

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing unit tests for _should_accept_group_message() in tests/gateway/test_feishu.py, but none currently cover the new require_mention=false behavior. Please add a test case asserting that when FEISHU_REQUIRE_MENTION=false (and group policy allows the sender), a group message without mentions is accepted, and that the default behavior remains unchanged when the env var is unset.

Copilot uses AI. Check for mistakes.
# @_all is Feishu's @everyone placeholder — always route to the bot.
raw_content = getattr(message, "content", "") or ""
if "@_all" in raw_content:
Expand Down
5 changes: 5 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
99 changes: 99 additions & 0 deletions tests/gateway/test_feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2474,6 +2474,105 @@ 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))
Comment on lines +2485 to +2493

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gap: the new behavior is only tested via FEISHU_REQUIRE_MENTION env var. Since _load_settings() also reads config.extra["require_mention"], add a test that sets PlatformConfig(extra={"require_mention": False}) (with FEISHU_REQUIRE_MENTION unset) to ensure the adapter honors an explicit False value from config and doesn’t regress due to falsy or fallbacks.

Copilot uses AI. Check for mistakes.

@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))

@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):
"""Tests for _resolve_sender_name_from_api."""
Expand Down
Loading