From 38e4408ebeb7e6bb079dec101e3ee969922c097a Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 27 May 2026 21:58:34 +0800 Subject: [PATCH] feat(qqofficial): split message chain by media and update sending logic --- .../qqofficial/qqofficial_message_event.py | 62 +++++++- .../qqofficial/qqofficial_platform_adapter.py | 8 ++ tests/test_qqofficial_adapter.py | 132 ++++++++++++++++++ 3 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 tests/test_qqofficial_adapter.py diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index fa10d2876..313b00e68 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -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 diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index 27880e548..c693c546e 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -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, diff --git a/tests/test_qqofficial_adapter.py b/tests/test_qqofficial_adapter.py new file mode 100644 index 000000000..d63767592 --- /dev/null +++ b/tests/test_qqofficial_adapter.py @@ -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", + ]