From af8190dd5f9571fdbbc79de8546dafccab424d48 Mon Sep 17 00:00:00 2001 From: 9lie <26185872+9lie@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:34:44 +0800 Subject: [PATCH] feat(feishu): opt-in approval mention notifications on exec prompts Add feishu.approval_mentions config (default off). When enabled, exec-approval cards prepend for each configured admin open_id, so they receive a Feishu mention notification instead of silently waiting. Mirrors discord.approval_mentions (commit e0176cbd4): - Default off, opt-in via config.yaml - Only mentions admins with valid ou_ open_ids - Mention prepended to card markdown, before the code block - No gateway/run.py changes (adapter-internal, like Discord approach) Tests: 11 total (5 existing + 6 new covering mention rendering, config propagation, and edge cases). --- plugins/platforms/feishu/adapter.py | 26 +++- tests/gateway/test_feishu_approval_buttons.py | 114 ++++++++++++++++++ website/docs/user-guide/messaging/feishu.md | 9 ++ .../current/user-guide/messaging/feishu.md | 9 ++ 4 files changed, 157 insertions(+), 1 deletion(-) diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index 96528646939dc..e3690ac8be695 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -409,6 +409,10 @@ class FeishuAdapterSettings: group_rules: Dict[str, FeishuGroupRule] = field(default_factory=dict) allow_bots: str = "none" # "none" | "mentions" | "all" require_mention: bool = True + # When True, exec-approval cards prepend @admin + # for each configured admin so they get a Feishu mention notification. + # Mirrors discord.approval_mentions (commit e0176cbd4). Default off. + approval_mentions: bool = False @dataclass @@ -1600,6 +1604,9 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings: require_mention=_to_boolean( extra.get("require_mention", os.getenv("FEISHU_REQUIRE_MENTION", "true")) ), + approval_mentions=_to_boolean( + extra.get("approval_mentions", os.getenv("FEISHU_APPROVAL_MENTIONS", "false")) + ), ) def _apply_settings(self, settings: FeishuAdapterSettings) -> None: @@ -1632,6 +1639,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._approval_mentions = settings.approval_mentions def _build_event_handler(self) -> Any: if EventDispatcherHandler is None: @@ -2011,6 +2019,22 @@ def _btn(label: str, action_name: str, btn_type: str = "default") -> dict: actions.append(_btn("✅ Always", "approve_always")) actions.append(_btn("❌ Deny", "deny", "danger")) scope_note = "\n\n**Smart DENY:** owner override applies to this one operation only." if smart_denied else "" + + # Build mention prefix for configured admins when opt-in. + # Mirrors discord.approval_mentions: default off, only mentions + # allowlist/admin entries that are valid Feishu open_ids (ou_xxx). + mention_prefix = "" + if self._approval_mentions: + admin_open_ids = sorted( + uid for uid in self._admins + if isinstance(uid, str) and uid.startswith("ou_") + ) + if admin_open_ids: + mention_parts = [ + f'' for oid in admin_open_ids + ] + mention_prefix = " ".join(mention_parts) + "\n\n" + card = { "config": {"wide_screen_mode": True}, "header": { @@ -2020,7 +2044,7 @@ def _btn(label: str, action_name: str, btn_type: str = "default") -> dict: "elements": [ { "tag": "markdown", - "content": f"```\n{cmd_preview}\n```\n**Reason:** {description}{scope_note}", + "content": f"{mention_prefix}```\n{cmd_preview}\n```\n**Reason:** {description}{scope_note}", }, { "tag": "action", diff --git a/tests/gateway/test_feishu_approval_buttons.py b/tests/gateway/test_feishu_approval_buttons.py index f5b9a26c1e127..1f7ed14e6a8d1 100644 --- a/tests/gateway/test_feishu_approval_buttons.py +++ b/tests/gateway/test_feishu_approval_buttons.py @@ -207,6 +207,120 @@ async def test_multiple_approvals_get_unique_ids(self): ids = list(adapter._approval_state.keys()) assert ids[0] != ids[1] + # ----------------------------------------------------------------------- + # approval_mentions opt-in (mirrors discord.approval_mentions, commit e0176cbd4) + # ----------------------------------------------------------------------- + + @pytest.mark.asyncio + async def test_no_mention_when_disabled(self): + """Default off: no tag in card markdown.""" + adapter = _make_adapter() + adapter._approval_mentions = False + adapter._admins = {"ou_admin1"} + + mock_response = SimpleNamespace( + success=lambda: True, + data=SimpleNamespace(message_id="msg_m1"), + ) + with patch.object( + adapter, "_feishu_send_with_retry", new_callable=AsyncMock, + return_value=mock_response, + ) as mock_send: + await adapter.send_exec_approval( + chat_id="oc_1", command="rm -rf /tmp", session_key="s1" + ) + + card = json.loads(mock_send.call_args[1]["payload"]) + content = card["elements"][0]["content"] + assert " prepended for each admin.""" + adapter = _make_adapter() + adapter._approval_mentions = True + adapter._admins = {"ou_admin1", "ou_admin2", "not_an_open_id"} + + mock_response = SimpleNamespace( + success=lambda: True, + data=SimpleNamespace(message_id="msg_m2"), + ) + with patch.object( + adapter, "_feishu_send_with_retry", new_callable=AsyncMock, + return_value=mock_response, + ) as mock_send: + await adapter.send_exec_approval( + chat_id="oc_1", command="rm -rf /tmp", session_key="s1" + ) + + card = json.loads(mock_send.call_args[1]["payload"]) + content = card["elements"][0]["content"] + # Only ou_ prefixed IDs are mentioned; "not_an_open_id" is skipped. + assert '' in content + assert '' in content + assert "not_an_open_id" not in content + # Mention prefix comes before the code block. + assert content.index(" _apply_settings + # Mirrors Discord's test_yaml_config_bridges_approval_mentions_to_env. + # ----------------------------------------------------------------------- + + def test_yaml_config_propagates_approval_mentions(self): + """YAML extra.approval_mentions=true reaches settings.""" + settings = FeishuAdapter._load_settings({"approval_mentions": True, "admins": ["ou_admin1"]}) + assert settings.approval_mentions is True + + def test_yaml_config_defaults_off(self): + """Without approval_mentions in YAML, settings.approval_mentions is False.""" + settings = FeishuAdapter._load_settings({}) + assert settings.approval_mentions is False + + def test_apply_settings_propagates_to_instance(self): + """_apply_settings writes approval_mentions onto the adapter instance.""" + adapter = _make_adapter() + assert adapter._approval_mentions is False # default off + + from plugins.platforms.feishu.adapter import FeishuAdapterSettings + settings = FeishuAdapterSettings( + app_id="", app_secret="", domain_name="feishu", + connection_mode="websocket", encrypt_key="", + verification_token="", group_policy="allowlist", + allowed_group_users=frozenset(), + bot_open_id="", bot_user_id="", bot_name="", + dedup_cache_size=32, text_batch_delay_seconds=0.5, + text_batch_split_delay_seconds=2.0, text_batch_max_messages=3, + text_batch_max_chars=1500, media_batch_delay_seconds=1.0, + webhook_host="0.0.0.0", webhook_port=9806, webhook_path="/webhook", + approval_mentions=True, + ) + adapter._apply_settings(settings) + assert adapter._approval_mentions is True + # =========================================================================== # send_update_prompt — interactive card with buttons diff --git a/website/docs/user-guide/messaging/feishu.md b/website/docs/user-guide/messaging/feishu.md index 1c5a66543e431..df9cd897291d4 100644 --- a/website/docs/user-guide/messaging/feishu.md +++ b/website/docs/user-guide/messaging/feishu.md @@ -289,6 +289,13 @@ Card action events are dispatched with `MessageType.COMMAND`, so they flow throu This is also how **command approval** works — when the agent needs to run a dangerous command, it sends an interactive card with Allow Once / Session / Always / Deny buttons. The user clicks a button, and the card action callback delivers the approval decision back to the agent. + +### Approval Mention Notifications + +When `approval_mentions` is enabled (default: off), dangerous-command approval cards prepend an `` mention for each configured admin. This ensures admins receive a Feishu mention notification when the agent is blocked waiting for approval, instead of silently waiting. + +Only admins with valid `ou_`-prefixed open_ids are mentioned; other entries are skipped. + ### Required Feishu App Configuration Interactive cards require **three** configuration steps in the Feishu Developer Console. Missing any of them causes error **200340** when users click card buttons. @@ -499,6 +506,7 @@ platforms: default_group_policy: "open" # Default for groups not in group_rules admins: # Users who can manage bot settings - "ou_admin_open_id" + approval_mentions: false # When true, approval cards @-mention admins group_rules: "oc_group_chat_id_1": policy: "allowlist" # open | allowlist | blacklist | admin_only | disabled @@ -547,6 +555,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_APPROVAL_MENTIONS` | — | `false` | When true, approval cards @-mention configured admins | | `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..e6ab3dbd4654a 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 @@ -255,6 +255,13 @@ Gateway 驱动的更新提示使用原生飞书 `Yes` / `No` 卡片,而非回 **命令审批**也通过此机制实现——当 Agent 需要执行危险命令时,会发送一张带有「允许一次 / 本次会话 / 始终允许 / 拒绝」按钮的交互式卡片。用户点击按钮后,卡片操作回调将审批决定传回 Agent。 + +### 审批提及通知 + +当 `approval_mentions` 开启时(默认关闭),危险命令审批卡片会在内容前添加 `` 提及标记,为每个配置的 admin 发送飞书 @通知。这样当 agent 阻塞等待审批时,admin 会收到通知,而不是静默等待。 + +仅提及 `ou_` 开头的有效 open_id 的 admin;其他条目会被跳过。 + ### 飞书应用所需配置 交互式卡片需要在飞书开发者控制台完成**三项**配置。缺少任何一项,用户点击卡片按钮时将出现错误 **200340**。 @@ -442,6 +449,7 @@ platforms: default_group_policy: "open" # 未在 group_rules 中列出的群的默认策略 admins: # 可管理机器人设置的用户 - "ou_admin_open_id" + approval_mentions: false # 开启后,审批卡片会 @提及 admins group_rules: "oc_group_chat_id_1": policy: "allowlist" # open | allowlist | blacklist | admin_only | disabled @@ -490,6 +498,7 @@ platforms: | `FEISHU_ALLOWED_USERS` | — | _(空)_ | 用户白名单的逗号分隔 open_id 列表 | | `FEISHU_ALLOW_BOTS` | — | `none` | 接受其他机器人消息:`none`、`mentions` 或 `all` | | `FEISHU_REQUIRE_MENTION` | — | `true` | 群消息是否必须 @提及 机器人 | +| `FEISHU_APPROVAL_MENTIONS` | — | `false` | 开启后,审批卡片会 @提及已配置的 admins | | `FEISHU_HOME_CHANNEL` | — | — | cron/通知输出的聊天 ID | | `FEISHU_ENCRYPT_KEY` | — | _(空)_ | webhook 签名验证的加密密钥 | | `FEISHU_VERIFICATION_TOKEN` | — | _(空)_ | webhook payload 认证的验证 token |