mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Fix Weixin media uploads and refresh lockfile
This commit is contained in:
parent
3a0ec1d935
commit
8dcd08d8bb
2 changed files with 129 additions and 8 deletions
|
|
@ -1685,23 +1685,34 @@ class WeixinAdapter(BasePlatformAdapter):
|
||||||
self,
|
self,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
image_path: str,
|
image_path: str,
|
||||||
caption: str = "",
|
caption: Optional[str] = None,
|
||||||
reply_to: Optional[str] = None,
|
reply_to: Optional[str] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
**kwargs,
|
||||||
) -> SendResult:
|
) -> SendResult:
|
||||||
return await self.send_document(chat_id, file_path=image_path, caption=caption, metadata=metadata)
|
del reply_to, kwargs
|
||||||
|
return await self.send_document(
|
||||||
|
chat_id=chat_id,
|
||||||
|
file_path=image_path,
|
||||||
|
caption=caption,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
async def send_document(
|
async def send_document(
|
||||||
self,
|
self,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
caption: str = "",
|
caption: Optional[str] = None,
|
||||||
|
file_name: Optional[str] = None,
|
||||||
|
reply_to: Optional[str] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
**kwargs,
|
||||||
) -> SendResult:
|
) -> SendResult:
|
||||||
|
del file_name, reply_to, metadata, kwargs
|
||||||
if not self._send_session or not self._token:
|
if not self._send_session or not self._token:
|
||||||
return SendResult(success=False, error="Not connected")
|
return SendResult(success=False, error="Not connected")
|
||||||
try:
|
try:
|
||||||
message_id = await self._send_file(chat_id, file_path, caption)
|
message_id = await self._send_file(chat_id, file_path, caption or "")
|
||||||
return SendResult(success=True, message_id=message_id)
|
return SendResult(success=True, message_id=message_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("[%s] send_document failed to=%s: %s", self.name, _safe_id(chat_id), exc)
|
logger.error("[%s] send_document failed to=%s: %s", self.name, _safe_id(chat_id), exc)
|
||||||
|
|
@ -1811,7 +1822,6 @@ class WeixinAdapter(BasePlatformAdapter):
|
||||||
ciphertext=ciphertext,
|
ciphertext=ciphertext,
|
||||||
upload_url=upload_url,
|
upload_url=upload_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
context_token = self._token_store.get(self._account_id, chat_id)
|
context_token = self._token_store.get(self._account_id, chat_id)
|
||||||
# The iLink API expects aes_key as base64(hex_string), not base64(raw_bytes).
|
# The iLink API expects aes_key as base64(hex_string), not base64(raw_bytes).
|
||||||
# Sending base64(raw_bytes) causes images to show as grey boxes on the
|
# Sending base64(raw_bytes) causes images to show as grey boxes on the
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Tests for the Weixin platform adapter."""
|
"""Tests for the Weixin platform adapter."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -8,6 +9,7 @@ from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from gateway.config import PlatformConfig
|
from gateway.config import PlatformConfig
|
||||||
from gateway.config import GatewayConfig, HomeChannel, Platform, _apply_env_overrides
|
from gateway.config import GatewayConfig, HomeChannel, Platform, _apply_env_overrides
|
||||||
|
from gateway.platforms.base import SendResult
|
||||||
from gateway.platforms import weixin
|
from gateway.platforms import weixin
|
||||||
from gateway.platforms.weixin import ContextTokenStore, WeixinAdapter
|
from gateway.platforms.weixin import ContextTokenStore, WeixinAdapter
|
||||||
from tools.send_message_tool import _parse_target_ref, _send_to_platform
|
from tools.send_message_tool import _parse_target_ref, _send_to_platform
|
||||||
|
|
@ -357,6 +359,115 @@ class TestWeixinChunkDelivery:
|
||||||
assert first_try["client_id"] == retry["client_id"]
|
assert first_try["client_id"] == retry["client_id"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestWeixinOutboundMedia:
|
||||||
|
def test_send_image_file_accepts_keyword_image_path(self):
|
||||||
|
adapter = _make_adapter()
|
||||||
|
expected = SendResult(success=True, message_id="msg-1")
|
||||||
|
adapter.send_document = AsyncMock(return_value=expected)
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
adapter.send_image_file(
|
||||||
|
chat_id="wxid_test123",
|
||||||
|
image_path="/tmp/demo.png",
|
||||||
|
caption="截图说明",
|
||||||
|
reply_to="reply-1",
|
||||||
|
metadata={"thread_id": "t-1"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
adapter.send_document.assert_awaited_once_with(
|
||||||
|
chat_id="wxid_test123",
|
||||||
|
file_path="/tmp/demo.png",
|
||||||
|
caption="截图说明",
|
||||||
|
metadata={"thread_id": "t-1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_send_document_accepts_keyword_file_path(self):
|
||||||
|
adapter = _make_adapter()
|
||||||
|
adapter._session = object()
|
||||||
|
adapter._send_session = adapter._session
|
||||||
|
adapter._token = "test-token"
|
||||||
|
adapter._send_file = AsyncMock(return_value="msg-2")
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
adapter.send_document(
|
||||||
|
chat_id="wxid_test123",
|
||||||
|
file_path="/tmp/report.pdf",
|
||||||
|
caption="报告请看",
|
||||||
|
file_name="renamed.pdf",
|
||||||
|
reply_to="reply-1",
|
||||||
|
metadata={"thread_id": "t-1"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.success is True
|
||||||
|
assert result.message_id == "msg-2"
|
||||||
|
adapter._send_file.assert_awaited_once_with("wxid_test123", "/tmp/report.pdf", "报告请看")
|
||||||
|
|
||||||
|
def test_send_file_uses_post_for_upload_full_url_and_hex_encoded_aes_key(self, tmp_path):
|
||||||
|
class _UploadResponse:
|
||||||
|
def __init__(self):
|
||||||
|
self.status = 200
|
||||||
|
self.headers = {"x-encrypted-param": "enc-param"}
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def read(self):
|
||||||
|
return b""
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
class _RecordingSession:
|
||||||
|
def __init__(self):
|
||||||
|
self.post_calls = []
|
||||||
|
|
||||||
|
def post(self, url, **kwargs):
|
||||||
|
self.post_calls.append((url, kwargs))
|
||||||
|
return _UploadResponse()
|
||||||
|
|
||||||
|
def put(self, *_args, **_kwargs):
|
||||||
|
raise AssertionError("upload_full_url branch should use POST")
|
||||||
|
|
||||||
|
image_path = tmp_path / "demo.png"
|
||||||
|
image_path.write_bytes(b"fake-png-bytes")
|
||||||
|
|
||||||
|
adapter = _make_adapter()
|
||||||
|
session = _RecordingSession()
|
||||||
|
adapter._session = session
|
||||||
|
adapter._send_session = session
|
||||||
|
adapter._token = "test-token"
|
||||||
|
adapter._base_url = "https://weixin.example.com"
|
||||||
|
adapter._cdn_base_url = "https://cdn.example.com/c2c"
|
||||||
|
adapter._token_store.get = lambda account_id, chat_id: None
|
||||||
|
|
||||||
|
aes_key = bytes(range(16))
|
||||||
|
expected_aes_key = base64.b64encode(aes_key.hex().encode("ascii")).decode("ascii")
|
||||||
|
|
||||||
|
with patch("gateway.platforms.weixin._get_upload_url", new=AsyncMock(return_value={"upload_full_url": "https://upload.example.com/media"})), \
|
||||||
|
patch("gateway.platforms.weixin._api_post", new_callable=AsyncMock) as api_post_mock, \
|
||||||
|
patch("gateway.platforms.weixin.secrets.token_hex", return_value="filekey-123"), \
|
||||||
|
patch("gateway.platforms.weixin.secrets.token_bytes", return_value=aes_key):
|
||||||
|
message_id = asyncio.run(adapter._send_file("wxid_test123", str(image_path), ""))
|
||||||
|
|
||||||
|
assert message_id.startswith("hermes-weixin-")
|
||||||
|
assert len(session.post_calls) == 1
|
||||||
|
upload_url, upload_kwargs = session.post_calls[0]
|
||||||
|
assert upload_url == "https://upload.example.com/media"
|
||||||
|
assert upload_kwargs["headers"] == {"Content-Type": "application/octet-stream"}
|
||||||
|
assert upload_kwargs["data"]
|
||||||
|
assert upload_kwargs["timeout"].total == 120
|
||||||
|
payload = api_post_mock.await_args.kwargs["payload"]
|
||||||
|
media = payload["msg"]["item_list"][0]["image_item"]["media"]
|
||||||
|
assert media["encrypt_query_param"] == "enc-param"
|
||||||
|
assert media["aes_key"] == expected_aes_key
|
||||||
|
|
||||||
|
|
||||||
class TestWeixinRemoteMediaSafety:
|
class TestWeixinRemoteMediaSafety:
|
||||||
def test_download_remote_media_blocks_unsafe_urls(self):
|
def test_download_remote_media_blocks_unsafe_urls(self):
|
||||||
adapter = _make_adapter()
|
adapter = _make_adapter()
|
||||||
|
|
@ -544,7 +655,7 @@ class TestWeixinSendImageFileParameterName:
|
||||||
|
|
||||||
assert result.success is True
|
assert result.success is True
|
||||||
send_document_mock.assert_awaited_once_with(
|
send_document_mock.assert_awaited_once_with(
|
||||||
"wxid_test123",
|
chat_id="wxid_test123",
|
||||||
file_path="/tmp/test_image.png",
|
file_path="/tmp/test_image.png",
|
||||||
caption="Test caption",
|
caption="Test caption",
|
||||||
metadata={"thread_id": "thread-123"},
|
metadata={"thread_id": "thread-123"},
|
||||||
|
|
@ -569,9 +680,9 @@ class TestWeixinSendImageFileParameterName:
|
||||||
|
|
||||||
assert result.success is True
|
assert result.success is True
|
||||||
send_document_mock.assert_awaited_once_with(
|
send_document_mock.assert_awaited_once_with(
|
||||||
"wxid_test123",
|
chat_id="wxid_test123",
|
||||||
file_path="/tmp/test_image.jpg",
|
file_path="/tmp/test_image.jpg",
|
||||||
caption="",
|
caption=None,
|
||||||
metadata=None,
|
metadata=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue