mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 07:00:48 +08:00
支持微信/WhatsApp 回送附件直发并补齐链路测试。
保持入站到 agent 流程不变,在回送阶段支持 media_path/media_url 与 base64 附件直发,同时让通道回包沿用会话中助手附件记录以对齐 chat 历史逻辑。 Made-with: Cursor
This commit is contained in:
parent
25180921e7
commit
1d46356d9e
8 changed files with 255 additions and 7 deletions
|
|
@ -1,6 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from oclaw.runtime.application.gateway.inbound_service import _should_suppress_channel_reply
|
||||
from dataclasses import dataclass
|
||||
|
||||
from oclaw.runtime.application.gateway.inbound_service import (
|
||||
_collect_reply_attachments_from_history,
|
||||
_parse_message_attachments,
|
||||
_should_suppress_channel_reply,
|
||||
)
|
||||
|
||||
|
||||
def test_should_suppress_weixin_openai_missing_api_key_message() -> None:
|
||||
|
|
@ -13,3 +19,35 @@ def test_should_not_suppress_non_weixin_channel() -> None:
|
|||
text = '⚠️ Missing API key for provider "openai". Configure the gateway auth for that provider, then try again.'
|
||||
assert _should_suppress_channel_reply(channel="admin_chat", text=text) is False
|
||||
|
||||
|
||||
def test_parse_message_attachments_accepts_json_string() -> None:
|
||||
out = _parse_message_attachments('[{"type":"image_ref","attachment_id":"a1"}]')
|
||||
assert len(out) == 1
|
||||
assert out[0].get("attachment_id") == "a1"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Row:
|
||||
role: str
|
||||
content: str
|
||||
attachments: object
|
||||
|
||||
|
||||
class _FakeStore:
|
||||
def __init__(self, rows: list[_Row]) -> None:
|
||||
self._rows = rows
|
||||
|
||||
def get_messages(self, *, session_id: str, limit: int = 120) -> list[_Row]:
|
||||
_ = (session_id, limit)
|
||||
return list(self._rows)
|
||||
|
||||
|
||||
def test_collect_reply_attachments_prefers_matching_assistant_text() -> None:
|
||||
rows = [
|
||||
_Row(role="assistant", content="old", attachments='[{"attachment_id":"old"}]'),
|
||||
_Row(role="assistant", content="target", attachments='[{"attachment_id":"new"}]'),
|
||||
]
|
||||
out = _collect_reply_attachments_from_history(store=_FakeStore(rows), session_id="s1", reply_text="target")
|
||||
assert len(out) == 1
|
||||
assert out[0].get("attachment_id") == "new"
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,14 @@ class WeixinIlinkApiTests(unittest.TestCase):
|
|||
{
|
||||
"chat_id": str(payload.get("chat_id") or ""),
|
||||
"text": f"native:{text}",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "image_ref",
|
||||
"name": "demo.png",
|
||||
"mime_type": "image/png",
|
||||
"data_base64": "aGVsbG8=",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -118,6 +126,9 @@ class WeixinIlinkApiTests(unittest.TestCase):
|
|||
replies = data.get("replies") if isinstance(data.get("replies"), list) else []
|
||||
self.assertEqual(len(replies), 1, data)
|
||||
self.assertEqual(str((replies[0] or {}).get("text") or ""), "native:hello native")
|
||||
atts = (replies[0] or {}).get("attachments") if isinstance((replies[0] or {}).get("attachments"), list) else []
|
||||
self.assertEqual(len(atts), 1, data)
|
||||
self.assertEqual(str((atts[0] or {}).get("name") or ""), "demo.png")
|
||||
finally:
|
||||
weixin_ilink_api._process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
|
||||
|
||||
|
|
@ -134,6 +145,14 @@ class WeixinIlinkApiTests(unittest.TestCase):
|
|||
{
|
||||
"chat_id": str(payload.get("chat_id") or ""),
|
||||
"text": f"wa:{text}",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "binary_ref",
|
||||
"name": "demo.txt",
|
||||
"mime_type": "text/plain",
|
||||
"data_base64": "d2E=",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -158,6 +177,9 @@ class WeixinIlinkApiTests(unittest.TestCase):
|
|||
replies = data.get("replies") if isinstance(data.get("replies"), list) else []
|
||||
self.assertEqual(len(replies), 1, data)
|
||||
self.assertEqual(str((replies[0] or {}).get("text") or ""), "wa:hello whatsapp")
|
||||
atts = (replies[0] or {}).get("attachments") if isinstance((replies[0] or {}).get("attachments"), list) else []
|
||||
self.assertEqual(len(atts), 1, data)
|
||||
self.assertEqual(str((atts[0] or {}).get("name") or ""), "demo.txt")
|
||||
finally:
|
||||
weixin_ilink_api._process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
|
||||
|
||||
|
|
|
|||
|
|
@ -49,3 +49,11 @@ def test_official_runner_logs_active_bridge_path() -> None:
|
|||
text = _read("runtime/operations/weixin_bridge/official_runner.ts")
|
||||
assert "official runner started account=" in text
|
||||
assert "native reply failed; no fallback enabled" in text
|
||||
|
||||
|
||||
def test_official_runner_supports_reply_attachments_base64() -> None:
|
||||
text = _read("runtime/operations/weixin_bridge/official_runner.ts")
|
||||
assert "_decodeReplyBase64Attachment" in text
|
||||
assert "data_base64" in text
|
||||
assert "media_base64" in text
|
||||
assert "reply.attachments" in text
|
||||
|
|
|
|||
|
|
@ -15,7 +15,23 @@ def test_inbound_whatsapp_accepts_basic_payload_and_returns_replies() -> None:
|
|||
assert str(payload.get("account_id") or "") == "wa-default"
|
||||
assert str(payload.get("user_id") or "") == "111@s.whatsapp.net"
|
||||
assert str(payload.get("chat_id") or "") == "111@s.whatsapp.net"
|
||||
return {"ok": True, "replies": [{"chat_id": payload.get("chat_id"), "text": "ok"}]}
|
||||
return {
|
||||
"ok": True,
|
||||
"replies": [
|
||||
{
|
||||
"chat_id": payload.get("chat_id"),
|
||||
"text": "ok",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "binary_ref",
|
||||
"name": "hello.txt",
|
||||
"mime_type": "text/plain",
|
||||
"data_base64": "aGVsbG8=",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
try:
|
||||
fastapi_app.process_inbound_payload_usecase = _fake_usecase # type: ignore[assignment]
|
||||
|
|
@ -35,6 +51,9 @@ def test_inbound_whatsapp_accepts_basic_payload_and_returns_replies() -> None:
|
|||
replies = data.get("replies") if isinstance(data.get("replies"), list) else []
|
||||
assert replies and isinstance(replies[0], dict)
|
||||
assert str(replies[0].get("text") or "") == "ok"
|
||||
atts = replies[0].get("attachments") if isinstance(replies[0].get("attachments"), list) else []
|
||||
assert len(atts) == 1
|
||||
assert str(atts[0].get("name") or "") == "hello.txt"
|
||||
finally:
|
||||
fastapi_app.process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
|
||||
|
||||
|
|
|
|||
|
|
@ -37,3 +37,12 @@ def test_start_all_gracefully_skips_missing_channel_sidecars() -> None:
|
|||
text = _read("runtime/operations/scripts/start_all.ps1")
|
||||
assert 'Warn "weixin sidecar skipped:' in text
|
||||
assert 'Warn "whatsapp sidecar skipped:' in text
|
||||
|
||||
|
||||
def test_whatsapp_runner_supports_reply_attachments_base64() -> None:
|
||||
text = _read("runtime/operations/whatsapp_bridge/baileys_runner.ts")
|
||||
assert "sendReplyWithAttachments" in text
|
||||
assert "data_base64" in text
|
||||
assert "media_base64" in text
|
||||
assert "media_path" in text
|
||||
assert "media_url" in text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue