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()
Comment thread
Leegenux marked this conversation as resolved.

# Telegram settings → env vars (env vars take precedence)
telegram_cfg = yaml_cfg.get("telegram", {})
if isinstance(telegram_cfg, dict):
Expand Down
37 changes: 36 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 @@ -973,6 +974,30 @@ def __init__(self, config: PlatformConfig):
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)

For the env var fallback, explicit false-like values disable mentions,
while any other value—including an empty string—keeps the documented
default of 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)

Comment thread
Leegenux marked this conversation as resolved.
env_value = os.getenv("FEISHU_REQUIRE_MENTION")
if env_value is None:
return True

normalized = env_value.strip().lower()
return normalized not in ("false", "0", "no", "off")
@staticmethod
def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings:
return FeishuAdapterSettings(
app_id=str(extra.get("app_id") or os.getenv("FEISHU_APP_ID", "")).strip(),
Expand Down Expand Up @@ -1020,6 +1045,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),
)

def _apply_settings(self, settings: FeishuAdapterSettings) -> None:
Expand All @@ -1042,6 +1068,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 +2692,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:
Expand Down
5 changes: 5 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,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 TestRequireMentionBehavior(unittest.TestCase):
"""Tests for require_mention 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))

@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