diff --git a/gateway/platforms/dingtalk.py b/gateway/platforms/dingtalk.py index 59913b8b17c..08ab1962f87 100644 --- a/gateway/platforms/dingtalk.py +++ b/gateway/platforms/dingtalk.py @@ -886,6 +886,65 @@ class DingTalkAdapter(BasePlatformAdapter): """DingTalk does not support typing indicators.""" pass + async def send_image( + self, + chat_id: str, + image_url: str, + caption: Optional[str] = None, + reply_to: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> SendResult: + """Send an image via DingTalk markdown. + + DingTalk's session webhook only supports text/markdown payloads, not + native image/file attachments. For remote image URLs, render the image + inline with markdown so the user still sees the image. Local files need + OpenAPI media upload and are handled separately. + """ + image_block = f"![image]({image_url})" + content = f"{caption}\n\n{image_block}" if caption else image_block + return await self.send( + chat_id=chat_id, + content=content, + reply_to=reply_to, + metadata=metadata, + ) + + async def send_image_file( + self, + chat_id: str, + image_path: str, + caption: Optional[str] = None, + reply_to: Optional[str] = None, + **kwargs, + ) -> SendResult: + """DingTalk webhook replies cannot send local image files directly.""" + return SendResult( + success=False, + error=( + "DingTalk session webhook replies do not support local image uploads. " + "Only markdown/text replies are supported without OpenAPI media upload." + ), + ) + + async def send_document( + self, + chat_id: str, + file_path: str, + caption: Optional[str] = None, + file_name: Optional[str] = None, + reply_to: Optional[str] = None, + **kwargs, + ) -> SendResult: + """DingTalk webhook replies cannot send local file attachments directly.""" + return SendResult( + success=False, + error=( + "DingTalk session webhook replies do not support local file attachments. " + "Only markdown/text replies are supported without OpenAPI message send." + ), + ) + async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: """Return basic info about a DingTalk conversation.""" return { diff --git a/tests/gateway/test_dingtalk.py b/tests/gateway/test_dingtalk.py index 6795f81ca94..4f54de4e4a0 100644 --- a/tests/gateway/test_dingtalk.py +++ b/tests/gateway/test_dingtalk.py @@ -223,6 +223,51 @@ class TestSend: assert result.success is False assert "400" in result.error + @pytest.mark.asyncio + async def test_send_image_renders_markdown_image(self): + from gateway.platforms.dingtalk import DingTalkAdapter + adapter = DingTalkAdapter(PlatformConfig(enabled=True)) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + adapter._http_client = mock_client + + result = await adapter.send_image( + "chat-123", + "https://example.com/demo.png", + caption="Screenshot", + metadata={"session_webhook": "https://dingtalk.example/webhook"}, + ) + + assert result.success is True + payload = mock_client.post.call_args.kwargs["json"] + assert payload["msgtype"] == "markdown" + assert payload["markdown"]["text"] == "Screenshot\n\n![image](https://example.com/demo.png)" + + @pytest.mark.asyncio + async def test_send_image_file_returns_explicit_unsupported_error(self): + from gateway.platforms.dingtalk import DingTalkAdapter + adapter = DingTalkAdapter(PlatformConfig(enabled=True)) + + result = await adapter.send_image_file("chat-123", "/tmp/demo.png") + + assert result.success is False + assert "do not support local image uploads" in result.error + + @pytest.mark.asyncio + async def test_send_document_returns_explicit_unsupported_error(self): + from gateway.platforms.dingtalk import DingTalkAdapter + adapter = DingTalkAdapter(PlatformConfig(enabled=True)) + + result = await adapter.send_document("chat-123", "/tmp/demo.pdf") + + assert result.success is False + assert "do not support local file attachments" in result.error + # --------------------------------------------------------------------------- # Connect / disconnect