From 41c3e88bb4847aec220c8bdd1172726179791790 Mon Sep 17 00:00:00 2001 From: feige2003 Date: Tue, 12 May 2026 17:59:25 +0800 Subject: [PATCH] fix(feishu): keep group replies in group chats The _reply_anchor_for_event() function was falling back to event.message_id for ordinary Feishu group messages (those without a thread_id). That caused the adapter to call message.reply(...) which Feishu routes to the sender's DM instead of the group chat when the inbound message was a regular group @mention rather than a Feishu topic/thread reply. Fix: return None for Feishu non-thread messages so they go through the normal group create path instead of the reply API. Fixes #23698, #23729 --- gateway/platforms/base.py | 10 ++++-- tests/gateway/test_feishu.py | 67 +++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index ec0323d4738c..49c97fe33852 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -81,8 +81,14 @@ def _reply_anchor_for_event(event) -> str | None: return getattr(event, "message_id", None) or getattr(event, "reply_to_message_id", None) if platform == "telegram" and thread_id: return None - if platform == "feishu" and thread_id and getattr(event, "reply_to_message_id", None): - return getattr(event, "reply_to_message_id", None) + if platform == "feishu": + # Feishu topic replies must use reply semantics to stay inside the + # topic, but ordinary group messages should be sent directly to the + # group chat. Replying to a non-topic group @mention can route the + # response into the sender's DM instead of the group. + if thread_id and getattr(event, "reply_to_message_id", None): + return getattr(event, "reply_to_message_id", None) + return None return getattr(event, "message_id", None) diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 63287d88cb4b..82be60269883 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -1998,9 +1998,74 @@ async def _direct(func, *args, **kwargs): ) self.assertTrue(result.success) - self.assertEqual(captured["request"].message_id, "om_trigger") + self.assertEqual(captured["request"].message_id, "om_reply") self.assertTrue(captured["request"].request_body.reply_in_thread) + @patch.dict(os.environ, {}, clear=True) + def test_send_without_topic_metadata_creates_group_message(self): + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + captured = {} + + class _MessageAPI: + def create(self, request): + captured["request"] = request + return SimpleNamespace( + success=lambda: True, + data=SimpleNamespace(message_id="om_group_send"), + ) + + def reply(self, request): # pragma: no cover - defensive assertion path + raise AssertionError("group sends should not use reply API outside Feishu topics") + + adapter._client = SimpleNamespace( + im=SimpleNamespace(v1=SimpleNamespace(message=_MessageAPI())) + ) + + async def _direct(func, *args, **kwargs): + return func(*args, **kwargs) + + with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + result = asyncio.run( + adapter.send( + chat_id="oc_group", + content="hello group", + reply_to=None, + metadata=None, + ) + ) + + self.assertTrue(result.success) + self.assertEqual(result.message_id, "om_group_send") + self.assertEqual(captured["request"].receive_id_type, "chat_id") + self.assertEqual(captured["request"].request_body.receive_id, "oc_group") + + @patch.dict(os.environ, {}, clear=True) + def test_feishu_group_events_do_not_request_reply_anchor(self): + from gateway.config import Platform + from gateway.platforms.base import _reply_anchor_for_event + from gateway.platforms.base import MessageEvent, MessageType + from gateway.session import SessionSource + + event = MessageEvent( + text="hello", + message_type=MessageType.TEXT, + source=SessionSource( + platform=Platform.FEISHU, + chat_id="oc_group", + chat_name="Group", + chat_type="group", + user_id="ou_user", + user_name="Alice", + thread_id=None, + ), + message_id="om_group_message", + ) + + self.assertIsNone(_reply_anchor_for_event(event)) + @patch.dict(os.environ, {}, clear=True) def test_send_retries_transient_failure(self): from gateway.config import PlatformConfig