diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index b8f74485151f..3aac3ef56597 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -65,12 +65,14 @@ def _thread_metadata_for_source(source, reply_to_message_id: str | None = None) if thread_id is None: return None metadata = {"thread_id": thread_id} + anchor = reply_to_message_id or getattr(source, "message_id", None) + if anchor is not None and _platform_name(getattr(source, "platform", None)) != "telegram": + metadata["reply_to_message_id"] = str(anchor) if _platform_name(getattr(source, "platform", None)) == "telegram" and getattr(source, "chat_type", None) == "dm": metadata["telegram_dm_topic_reply_fallback"] = True tid = str(thread_id) if tid and tid not in {"", "1"}: metadata["direct_messages_topic_id"] = tid - anchor = reply_to_message_id or getattr(source, "message_id", None) if anchor is not None: metadata["telegram_reply_to_message_id"] = str(anchor) return metadata diff --git a/gateway/run.py b/gateway/run.py index 7887ec23c3a2..9b21089b5c70 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -14714,6 +14714,9 @@ def _thread_metadata_for_target( if thread_id is None: return None metadata: Dict[str, Any] = {"thread_id": thread_id} + platform_name = str(getattr(platform, "value", platform) or "").lower() + if reply_to_message_id is not None and platform_name != "telegram": + metadata["reply_to_message_id"] = str(reply_to_message_id) if self._is_telegram_dm_topic_target( platform, chat_id, diff --git a/tests/gateway/test_run_progress_topics.py b/tests/gateway/test_run_progress_topics.py index 5b7dfb821b0c..1465da082214 100644 --- a/tests/gateway/test_run_progress_topics.py +++ b/tests/gateway/test_run_progress_topics.py @@ -461,7 +461,10 @@ async def test_run_agent_feishu_progress_replies_inside_existing_thread(monkeypa assert result["final_response"] == "done" assert adapter.sent assert adapter.sent[0]["reply_to"] == "om_triggering_user_message" - assert adapter.sent[0]["metadata"] == {"thread_id": "topic_17585"} + assert adapter.sent[0]["metadata"] == { + "thread_id": "topic_17585", + "reply_to_message_id": "om_triggering_user_message", + } assert adapter.edits assert adapter.edits[0]["message_id"] == "progress-1" diff --git a/tests/gateway/test_tts_media_routing.py b/tests/gateway/test_tts_media_routing.py index eaf9c5928089..993fb2e65d58 100644 --- a/tests/gateway/test_tts_media_routing.py +++ b/tests/gateway/test_tts_media_routing.py @@ -19,8 +19,8 @@ class _MediaRoutingAdapter(BasePlatformAdapter): - def __init__(self): - super().__init__(PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM) + def __init__(self, platform=Platform.TELEGRAM): + super().__init__(PlatformConfig(enabled=True, token="test"), platform) async def connect(self): return True @@ -35,9 +35,9 @@ async def get_chat_info(self, chat_id): return {"id": chat_id, "type": "dm"} -def _event(thread_id=None): +def _event(thread_id=None, *, platform=Platform.TELEGRAM, reply_to_message_id=None): source = SessionSource( - platform=Platform.TELEGRAM, + platform=platform, chat_id="chat-1", chat_type="dm", thread_id=thread_id, @@ -47,6 +47,7 @@ def _event(thread_id=None): message_type=MessageType.TEXT, source=source, message_id="msg-1", + reply_to_message_id=reply_to_message_id, ) @@ -121,6 +122,27 @@ async def test_base_adapter_routes_voice_tagged_telegram_ogg_media_tag_to_voice_ adapter.send_document.assert_not_awaited() +@pytest.mark.asyncio +async def test_base_adapter_preserves_feishu_thread_reply_anchor_for_image_media(tmp_path, monkeypatch): + adapter = _MediaRoutingAdapter(platform=Platform.FEISHU) + event = _event( + thread_id="omt-thread", + platform=Platform.FEISHU, + reply_to_message_id="om-parent", + ) + media_file = _allowed_media_path(tmp_path, monkeypatch, "capture.png") + adapter._message_handler = AsyncMock(return_value=f"MEDIA:{media_file}") + adapter.send_multiple_images = AsyncMock(return_value=None) + + await adapter._process_message_background(event, build_session_key(event.source)) + + adapter.send_multiple_images.assert_awaited_once() + assert adapter.send_multiple_images.await_args.kwargs["metadata"] == { + "thread_id": "omt-thread", + "reply_to_message_id": "om-parent", + } + + def _fake_runner(thread_meta): """Build a fake GatewayRunner-like object with the helper methods needed by _deliver_media_from_response.""" @@ -131,6 +153,40 @@ def _fake_runner(thread_meta): return runner +@pytest.mark.asyncio +async def test_streaming_delivery_preserves_feishu_thread_reply_anchor_for_image_media(tmp_path, monkeypatch): + event = _event( + thread_id="omt-thread", + platform=Platform.FEISHU, + reply_to_message_id="om-parent", + ) + media_file = _allowed_media_path(tmp_path, monkeypatch, "capture.png") + adapter = SimpleNamespace( + name="feishu", + extract_media=BasePlatformAdapter.extract_media, + extract_images=BasePlatformAdapter.extract_images, + extract_local_files=BasePlatformAdapter.extract_local_files, + send_multiple_images=AsyncMock(return_value=None), + send_voice=AsyncMock(return_value=SendResult(success=True, message_id="voice")), + send_document=AsyncMock(return_value=SendResult(success=True, message_id="doc")), + send_image_file=AsyncMock(return_value=SendResult(success=True, message_id="image")), + send_video=AsyncMock(return_value=SendResult(success=True, message_id="video")), + ) + + await GatewayRunner._deliver_media_from_response( + GatewayRunner.__new__(GatewayRunner), + f"MEDIA:{media_file}", + event, + adapter, + ) + + adapter.send_multiple_images.assert_awaited_once() + assert adapter.send_multiple_images.await_args.kwargs["metadata"] == { + "thread_id": "omt-thread", + "reply_to_message_id": "om-parent", + } + + @pytest.mark.asyncio async def test_streaming_delivery_routes_telegram_flac_media_tag_to_document_sender(tmp_path, monkeypatch): event = _event(thread_id="topic-1")