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
9 changes: 9 additions & 0 deletions gateway/platforms/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ class FeishuAdapterSettings:
admins: frozenset[str] = frozenset()
default_group_policy: str = ""
group_rules: Dict[str, FeishuGroupRule] = field(default_factory=dict)
require_mention: bool = True


@dataclass
Expand Down Expand Up @@ -1165,6 +1166,9 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings:
# Default group policy (for groups not in group_rules)
default_group_policy = str(extra.get("default_group_policy", "")).strip().lower()

# Whether @mention is required in group chats (default: True, backward-compatible)
require_mention = str(extra.get("require_mention", "true")).strip().lower() != "false"

return FeishuAdapterSettings(
app_id=str(extra.get("app_id") or os.getenv("FEISHU_APP_ID", "")).strip(),
app_secret=str(extra.get("app_secret") or os.getenv("FEISHU_APP_SECRET", "")).strip(),
Expand Down Expand Up @@ -1221,6 +1225,7 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings:
admins=admins,
default_group_policy=default_group_policy,
group_rules=group_rules,
require_mention=require_mention,
)

def _apply_settings(self, settings: FeishuAdapterSettings) -> None:
Expand Down Expand Up @@ -1251,6 +1256,7 @@ def _apply_settings(self, settings: FeishuAdapterSettings) -> None:
self._ws_reconnect_interval = settings.ws_reconnect_interval
self._ws_ping_interval = settings.ws_ping_interval
self._ws_ping_timeout = settings.ws_ping_timeout
self._require_mention = settings.require_mention

def _build_event_handler(self) -> Any:
if EventDispatcherHandler is None:
Expand Down Expand Up @@ -3277,6 +3283,9 @@ def _allow_group_message(self, sender_id: Any, chat_id: str = "") -> bool:

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."""
# If require_mention is False, skip mention check but still enforce group policy.
if not self._require_mention:
return self._allow_group_message(sender_id, chat_id)
if not self._allow_group_message(sender_id, chat_id):
return False
# @_all is Feishu's @everyone placeholder — always route to the bot.
Expand Down
53 changes: 53 additions & 0 deletions tests/gateway/test_feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,59 @@ def test_at_all_still_requires_policy_gate(self):
self.assertTrue(adapter._should_accept_group_message(message, allowed_sender, ""))


class TestRequireMentionConfig(unittest.TestCase):
"""Tests for the require_mention config option in Feishu group chats."""

@patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True)
def test_require_mention_false_accepts_message_without_mention(self):
"""When require_mention=false, group messages without @mention are accepted (if policy allows)."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": False}))
message = SimpleNamespace(content='{"text":"hello"}', mentions=[])
sender_id = SimpleNamespace(open_id="ou_anyone", user_id=None)
self.assertTrue(adapter._should_accept_group_message(message, sender_id, ""))

@patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "allowlist", "FEISHU_ALLOWED_USERS": "ou_allowed"}, clear=True)
def test_require_mention_false_still_requires_group_policy(self):
"""require_mention=false bypasses mention check but NOT group allowlist policy."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": False}))
message = SimpleNamespace(content='{"text":"hello"}', mentions=[])
# Non-allowlisted user with allowlist policy — should be blocked.
sender_id = SimpleNamespace(open_id="ou_stranger", 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_true_default_requires_mention(self):
"""Default require_mention=true still requires @mention (backward compatible)."""
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_anyone", 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"}, clear=True)
def test_require_mention_false_with_allowlist_blocks_non_allowed_users(self):
"""require_mention=false does not bypass allowlist policy."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

adapter = FeishuAdapter(PlatformConfig(extra={"require_mention": False}))
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_stranger", 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."""
Expand Down