mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat(qqofficial): split message chain by media and update sending logic
This commit is contained in:
@@ -183,10 +183,62 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
ret_id = getattr(ret, "id", None)
|
||||
return str(ret_id) if ret_id is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _split_message_chain_by_media(message: MessageChain) -> list[MessageChain]:
|
||||
chunks: list[MessageChain] = []
|
||||
current_chain = []
|
||||
current_has_media = False
|
||||
|
||||
for component in message.chain:
|
||||
is_media = isinstance(component, Image | Record | Video | File)
|
||||
if is_media and current_has_media:
|
||||
chunks.append(
|
||||
MessageChain(
|
||||
chain=current_chain,
|
||||
use_t2i_=message.use_t2i_,
|
||||
type=message.type,
|
||||
)
|
||||
)
|
||||
current_chain = []
|
||||
current_has_media = False
|
||||
|
||||
current_chain.append(component)
|
||||
current_has_media = current_has_media or is_media
|
||||
|
||||
if current_chain or not message.chain:
|
||||
chunks.append(
|
||||
MessageChain(
|
||||
chain=current_chain,
|
||||
use_t2i_=message.use_t2i_,
|
||||
type=message.type,
|
||||
)
|
||||
)
|
||||
|
||||
return chunks
|
||||
|
||||
async def _post_send(self, stream: dict | None = None):
|
||||
if not self.send_buffer:
|
||||
return None
|
||||
|
||||
message_chains = self._split_message_chain_by_media(self.send_buffer)
|
||||
stream_for_chain = stream if len(message_chains) == 1 else None
|
||||
|
||||
ret = None
|
||||
for message_chain in message_chains:
|
||||
ret = await self._post_send_one(message_chain, stream_for_chain)
|
||||
|
||||
self.send_buffer = None
|
||||
|
||||
return ret
|
||||
|
||||
async def _post_send_one(
|
||||
self,
|
||||
message_to_send: MessageChain,
|
||||
stream: dict | None = None,
|
||||
):
|
||||
if not message_to_send:
|
||||
return None
|
||||
|
||||
source = self.message_obj.raw_message
|
||||
|
||||
if not isinstance(
|
||||
@@ -207,10 +259,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
video_file_source,
|
||||
file_source,
|
||||
file_name,
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(message_to_send)
|
||||
|
||||
# C2C 流式仅用于文本分片,富媒体时降级为普通发送,避免平台侧流式校验报错。
|
||||
if stream and (image_base64 or record_file_path):
|
||||
if stream and (
|
||||
image_base64 or record_file_path or video_file_source or file_source
|
||||
):
|
||||
logger.debug("[QQOfficial] 检测到富媒体,降级为非流式发送。")
|
||||
stream = None
|
||||
|
||||
@@ -416,9 +470,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
case _:
|
||||
pass
|
||||
|
||||
await super().send(self.send_buffer)
|
||||
|
||||
self.send_buffer = None
|
||||
await super().send(message_to_send)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -203,6 +203,14 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
) -> None:
|
||||
message_chains = QQOfficialMessageEvent._split_message_chain_by_media(
|
||||
message_chain
|
||||
)
|
||||
if len(message_chains) > 1:
|
||||
for split_message_chain in message_chains:
|
||||
await self._send_by_session_common(session, split_message_chain)
|
||||
return
|
||||
|
||||
(
|
||||
plain_text,
|
||||
image_base64,
|
||||
|
||||
132
tests/test_qqofficial_adapter.py
Normal file
132
tests/test_qqofficial_adapter.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.api.message_components import File, Plain
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.platform.message_session import MessageSession
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.platform.sources.qqofficial.qqofficial_message_event import (
|
||||
QQOfficialMessageEvent,
|
||||
)
|
||||
from astrbot.core.platform.sources.qqofficial.qqofficial_platform_adapter import (
|
||||
QQOfficialPlatformAdapter,
|
||||
)
|
||||
|
||||
|
||||
def test_qqofficial_split_message_chain_keeps_one_media_per_chunk():
|
||||
chain = MessageChain(
|
||||
chain=[
|
||||
Plain("files:"),
|
||||
File(name="test1.txt", file="/tmp/test1.txt"),
|
||||
Plain("next"),
|
||||
File(name="test2.txt", file="/tmp/test2.txt"),
|
||||
],
|
||||
use_t2i_=False,
|
||||
type="test",
|
||||
)
|
||||
|
||||
chunks = QQOfficialMessageEvent._split_message_chain_by_media(chain)
|
||||
|
||||
assert len(chunks) == 2
|
||||
assert [type(component) for component in chunks[0].chain] == [
|
||||
Plain,
|
||||
File,
|
||||
Plain,
|
||||
]
|
||||
assert [type(component) for component in chunks[1].chain] == [File]
|
||||
assert all(chunk.use_t2i_ is False for chunk in chunks)
|
||||
assert all(chunk.type == "test" for chunk in chunks)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qqofficial_send_by_session_splits_multiple_files(
|
||||
monkeypatch,
|
||||
event_queue,
|
||||
platform_settings,
|
||||
):
|
||||
monkeypatch.setenv("ASTRBOT_DISABLE_METRICS", "1")
|
||||
|
||||
adapter = QQOfficialPlatformAdapter(
|
||||
{
|
||||
"id": "qq-official",
|
||||
"appid": "appid",
|
||||
"secret": "secret",
|
||||
"enable_group_c2c": True,
|
||||
"enable_guild_direct_message": False,
|
||||
},
|
||||
platform_settings,
|
||||
event_queue,
|
||||
)
|
||||
adapter.remember_session_message_id("user-1", "incoming-message-id")
|
||||
|
||||
uploaded_files = []
|
||||
sent_payloads = []
|
||||
|
||||
async def fake_upload_group_and_c2c_media(
|
||||
_self,
|
||||
file_source,
|
||||
file_type,
|
||||
srv_send_msg=False,
|
||||
file_name=None,
|
||||
**kwargs,
|
||||
):
|
||||
uploaded_files.append(
|
||||
{
|
||||
"file_source": file_source,
|
||||
"file_type": file_type,
|
||||
"srv_send_msg": srv_send_msg,
|
||||
"file_name": file_name,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
)
|
||||
return SimpleNamespace(file_uuid=file_source, file_info=file_name, ttl=0)
|
||||
|
||||
async def fake_post_c2c_message(_self, openid, **payload):
|
||||
sent_payloads.append({"openid": openid, "payload": payload})
|
||||
return {"id": f"sent-{len(sent_payloads)}"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
QQOfficialMessageEvent,
|
||||
"upload_group_and_c2c_media",
|
||||
fake_upload_group_and_c2c_media,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
QQOfficialMessageEvent,
|
||||
"post_c2c_message",
|
||||
fake_post_c2c_message,
|
||||
)
|
||||
|
||||
await adapter._send_by_session_common(
|
||||
MessageSession(
|
||||
platform_name="qq-official",
|
||||
message_type=MessageType.FRIEND_MESSAGE,
|
||||
session_id="user-1",
|
||||
),
|
||||
MessageChain(
|
||||
chain=[
|
||||
File(name="test1.txt", file="/tmp/test1.txt"),
|
||||
File(name="test2.txt", file="/tmp/test2.txt"),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
assert [item["file_source"] for item in uploaded_files] == [
|
||||
"/tmp/test1.txt",
|
||||
"/tmp/test2.txt",
|
||||
]
|
||||
assert [item["file_name"] for item in uploaded_files] == [
|
||||
"test1.txt",
|
||||
"test2.txt",
|
||||
]
|
||||
assert [item["kwargs"] for item in uploaded_files] == [
|
||||
{"openid": "user-1"},
|
||||
{"openid": "user-1"},
|
||||
]
|
||||
assert [item["openid"] for item in sent_payloads] == ["user-1", "user-1"]
|
||||
assert [
|
||||
item["payload"]["media"].file_uuid for item in sent_payloads
|
||||
] == [
|
||||
"/tmp/test1.txt",
|
||||
"/tmp/test2.txt",
|
||||
]
|
||||
Reference in New Issue
Block a user