feat(qqofficial): split message chain by media and update sending logic

This commit is contained in:
Soulter
2026-05-27 21:58:34 +08:00
parent 85f9c4dff8
commit 38e4408ebe
3 changed files with 197 additions and 5 deletions

View File

@@ -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

View File

@@ -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,

View 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",
]