diff --git a/gateway/config.py b/gateway/config.py index 6f30ee7064301..9a5023614ea66 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -830,6 +830,8 @@ def load_gateway_config() -> GatewayConfig: bridged["reply_in_thread"] = platform_cfg["reply_in_thread"] if "require_mention" in platform_cfg: bridged["require_mention"] = platform_cfg["require_mention"] + if "ignore_mention_all" in platform_cfg: + bridged["ignore_mention_all"] = platform_cfg["ignore_mention_all"] if plat == Platform.TELEGRAM and "allowed_chats" in platform_cfg: bridged["allowed_chats"] = platform_cfg["allowed_chats"] if plat == Platform.TELEGRAM and "group_allowed_chats" in platform_cfg: @@ -1118,6 +1120,8 @@ def load_gateway_config() -> GatewayConfig: if isinstance(feishu_cfg, dict): if "allow_bots" in feishu_cfg and not os.getenv("FEISHU_ALLOW_BOTS"): os.environ["FEISHU_ALLOW_BOTS"] = str(feishu_cfg["allow_bots"]).lower() + if "ignore_mention_all" in feishu_cfg and not os.getenv("FEISHU_IGNORE_MENTION_ALL"): + os.environ["FEISHU_IGNORE_MENTION_ALL"] = str(feishu_cfg["ignore_mention_all"]).lower() except Exception as e: logger.warning( diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 2831476b5ba2e..b02983f5eb318 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -393,6 +393,7 @@ class FeishuAdapterSettings: group_rules: Dict[str, FeishuGroupRule] = field(default_factory=dict) allow_bots: str = "none" # "none" | "mentions" | "all" require_mention: bool = True + ignore_mention_all: bool = False @dataclass @@ -1569,6 +1570,9 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: require_mention=_to_boolean( extra.get("require_mention", os.getenv("FEISHU_REQUIRE_MENTION", "true")) ), + ignore_mention_all=_to_boolean( + os.getenv("FEISHU_IGNORE_MENTION_ALL", extra.get("ignore_mention_all", "false")) + ), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: @@ -1601,6 +1605,7 @@ def _apply_settings(self, settings: FeishuAdapterSettings) -> None: self._ws_ping_timeout = settings.ws_ping_timeout self._allow_bots = settings.allow_bots self._require_mention = settings.require_mention + self._ignore_mention_all = settings.ignore_mention_all def _build_event_handler(self) -> Any: if EventDispatcherHandler is None: @@ -4093,7 +4098,7 @@ def _allow_group_message( def _mentions_self(self, message: Any) -> bool: # @_all is Feishu's @everyone placeholder. raw_content = getattr(message, "content", "") or "" - if "@_all" in raw_content: + if "@_all" in raw_content and not self._ignore_mention_all: return True mentions = getattr(message, "mentions", None) or [] if mentions and self._message_mentions_bot(mentions): diff --git a/tests/gateway/feishu_helpers.py b/tests/gateway/feishu_helpers.py index 753a61a70a865..dcd8ebb99fa8a 100644 --- a/tests/gateway/feishu_helpers.py +++ b/tests/gateway/feishu_helpers.py @@ -33,6 +33,7 @@ def make_adapter_skeleton( bot_user_id: str = "", allow_bots: str = "none", require_mention: bool = True, + ignore_mention_all: bool = False, group_policy: str = "allowlist", ) -> Any: from gateway.platforms.feishu import FeishuAdapter @@ -49,6 +50,7 @@ def make_adapter_skeleton( adapter._allowed_group_users = frozenset() adapter._allow_bots = allow_bots adapter._require_mention = require_mention + adapter._ignore_mention_all = ignore_mention_all return adapter diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index da7673011fe87..31cba00c0cdec 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -505,6 +505,45 @@ def test_feishu_allow_bots_env_takes_precedence_over_config_yaml(self, tmp_path, assert os.environ.get("FEISHU_ALLOW_BOTS") == "none" + def test_bridges_feishu_ignore_mention_all_from_config_yaml_to_env(self, tmp_path, monkeypatch): + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + "feishu:\n ignore_mention_all: true\n", + encoding="utf-8", + ) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("FEISHU_IGNORE_MENTION_ALL", raising=False) + + config = load_gateway_config() + + assert os.environ.get("FEISHU_IGNORE_MENTION_ALL") == "true" + assert config.platforms[Platform.FEISHU].extra["ignore_mention_all"] is True + + def test_feishu_ignore_mention_all_env_overrides_config_yaml(self, tmp_path, monkeypatch): + from gateway.platforms.feishu import FeishuAdapter + + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + "feishu:\n ignore_mention_all: true\n", + encoding="utf-8", + ) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setenv("FEISHU_APP_ID", "cli_test") + monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") + monkeypatch.setenv("FEISHU_IGNORE_MENTION_ALL", "false") + + config = load_gateway_config() + settings = FeishuAdapter._load_settings(extra=config.platforms[Platform.FEISHU].extra) + + assert config.platforms[Platform.FEISHU].extra["ignore_mention_all"] is True + assert settings.ignore_mention_all is False + def test_invalid_quick_commands_in_config_yaml_are_ignored(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() diff --git a/tests/gateway/test_feishu_bot_admission.py b/tests/gateway/test_feishu_bot_admission.py index 5ccc386d83e5f..776bd03426ba3 100644 --- a/tests/gateway/test_feishu_bot_admission.py +++ b/tests/gateway/test_feishu_bot_admission.py @@ -112,6 +112,29 @@ def test_feishu_load_settings_require_mention(monkeypatch, env_value, extra, exp assert settings.require_mention is expected +@pytest.mark.parametrize( + "env_value, extra, expected", + [ + (None, {}, False), + ("false", {}, False), + ("true", {}, True), + ("false", {"ignore_mention_all": True}, False), + ], +) +def test_feishu_load_settings_ignore_mention_all(monkeypatch, env_value, extra, expected): + from gateway.platforms.feishu import FeishuAdapter + + monkeypatch.setenv("FEISHU_APP_ID", "cli_test") + monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") + if env_value is None: + monkeypatch.delenv("FEISHU_IGNORE_MENTION_ALL", raising=False) + else: + monkeypatch.setenv("FEISHU_IGNORE_MENTION_ALL", env_value) + + settings = FeishuAdapter._load_settings(extra=extra) + assert settings.ignore_mention_all is expected + + def test_feishu_load_settings_parses_per_group_require_mention(monkeypatch): from gateway.platforms.feishu import FeishuAdapter @@ -148,6 +171,36 @@ def test_sender_identity_handles_missing_sender_id(): assert _sender_identity(SimpleNamespace()) == frozenset() +def test_mentions_self_treats_all_hands_as_mention_by_default(): + adapter = make_adapter_skeleton(bot_open_id="ou_self") + message = make_message(mentions=[]) + message.content = '{"text":"@_all please look"}' + + assert adapter._mentions_self(message) is True + + +def test_mentions_self_ignores_all_hands_when_configured(): + adapter = make_adapter_skeleton(bot_open_id="ou_self", ignore_mention_all=True) + message = make_message(mentions=[]) + message.content = '{"text":"@_all please look"}' + + assert adapter._mentions_self(message) is False + + +def test_mentions_self_explicit_bot_mention_unaffected_when_ignoring_all_hands(): + adapter = make_adapter_skeleton(bot_open_id="ou_self", ignore_mention_all=True) + message = make_message(mentions=[ + SimpleNamespace( + key="@_user_1", + id=SimpleNamespace(open_id="ou_self", user_id=""), + name="Hermes", + ) + ]) + message.content = '{"text":"@_all @_user_1 please look"}' + + assert adapter._mentions_self(message) is True + + @pytest.mark.parametrize("sender_type", ["bot", "app"]) def test_is_bot_sender_treats_bot_and_app_as_bot_origin(sender_type): from gateway.platforms.feishu import _is_bot_sender diff --git a/website/docs/user-guide/messaging/feishu.md b/website/docs/user-guide/messaging/feishu.md index 802f1d44f5a16..4d631c8dcff4d 100644 --- a/website/docs/user-guide/messaging/feishu.md +++ b/website/docs/user-guide/messaging/feishu.md @@ -204,11 +204,18 @@ FEISHU_GROUP_POLICY=allowlist # default In all modes, the bot must be explicitly @mentioned (or @all) in the group before the message is processed. Direct messages always bypass this gate. Set `FEISHU_REQUIRE_MENTION=false` to let Hermes read all group traffic without requiring an @mention: - ```bash FEISHU_REQUIRE_MENTION=false ``` +To prevent workspace-wide `@all` / `@everyone` broadcasts from waking Hermes, set: + +```bash +FEISHU_IGNORE_MENTION_ALL=true +``` + +This only changes group admission: explicit bot mentions still work, and processed messages still render Feishu `@all` mentions normally. You can also configure it as `feishu.ignore_mention_all` in `config.yaml` (env wins when both are set). + For per-chat control, set `require_mention` on a `group_rules` entry — see [Per-Group Access Control](#per-group-access-control) below. ### Bot Identity @@ -490,6 +497,7 @@ Inbound messages are deduplicated using message IDs with a 24-hour TTL. The dedu | `FEISHU_ALLOWED_USERS` | — | _(empty)_ | Comma-separated open_id list for user allowlist | | `FEISHU_ALLOW_BOTS` | — | `none` | Accept messages from other bots: `none`, `mentions`, or `all` | | `FEISHU_REQUIRE_MENTION` | — | `true` | Whether group messages must @mention the bot | +| `FEISHU_IGNORE_MENTION_ALL` | — | `false` | Ignore group `@all` / `@everyone` as a bot mention trigger | | `FEISHU_HOME_CHANNEL` | — | — | Chat ID for cron/notification output | | `FEISHU_ENCRYPT_KEY` | — | _(empty)_ | Encrypt key for webhook signature verification | | `FEISHU_VERIFICATION_TOKEN` | — | _(empty)_ | Verification token for webhook payload auth | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/feishu.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/feishu.md index 8a295b128d242..b757d762ac1c6 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/feishu.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/feishu.md @@ -204,11 +204,18 @@ FEISHU_GROUP_POLICY=allowlist # 默认 在所有模式下,消息处理前机器人必须被明确 @提及(或 @all)。私信始终绕过此限制。 设置 `FEISHU_REQUIRE_MENTION=false` 可让 Hermes 读取所有群消息而无需 @提及: - ```bash FEISHU_REQUIRE_MENTION=false ``` +如需防止全工作区 `@all` / `@everyone` 广播唤醒 Hermes,请设置: + +```bash +FEISHU_IGNORE_MENTION_ALL=true +``` + +这只影响群聊准入:明确 @提及机器人仍然有效,已处理消息中的飞书 `@all` 提及仍会正常渲染。也可在 `config.yaml` 中配置为 `feishu.ignore_mention_all`(两者同时设置时,环境变量优先)。 + 如需按群控制,在 `group_rules` 条目中设置 `require_mention`——参见下方[按群访问控制](#per-group-access-control)。 ### 机器人身份 @@ -490,6 +497,7 @@ platforms: | `FEISHU_ALLOWED_USERS` | — | _(空)_ | 用户白名单的逗号分隔 open_id 列表 | | `FEISHU_ALLOW_BOTS` | — | `none` | 接受其他机器人消息:`none`、`mentions` 或 `all` | | `FEISHU_REQUIRE_MENTION` | — | `true` | 群消息是否必须 @提及 机器人 | +| `FEISHU_IGNORE_MENTION_ALL` | — | `false` | 忽略群聊 `@all` / `@everyone` 作为机器人提及触发 | | `FEISHU_HOME_CHANNEL` | — | — | cron/通知输出的聊天 ID | | `FEISHU_ENCRYPT_KEY` | — | _(空)_ | webhook 签名验证的加密密钥 | | `FEISHU_VERIFICATION_TOKEN` | — | _(空)_ | webhook payload 认证的验证 token |