From 47ef571e9c8333f2135ee92f7bc51e3fdcd3c27a Mon Sep 17 00:00:00 2001 From: Fuyan Yuan <33221728+TheRainstorm@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:05:19 +0800 Subject: [PATCH 1/2] fix(qqofficial): send proactively before passive reply window expires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QQ official passive replies are only valid for 5 minutes. Long-running tasks (e.g. agent tool loops) that exceed the window fail with "回复消息msg_id已过期". When the reply age approaches the limit, skip msg_id and send proactively (active-message API), avoiding a wasted request and the expired-msg_id error. --- .../qqofficial/qqofficial_message_event.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 806034bb94..429a304c7a 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -4,6 +4,7 @@ import logging import os import random +import time from typing import cast import aiofiles @@ -30,6 +31,12 @@ from astrbot.api.platform import AstrBotMessage, PlatformMetadata from astrbot.core.utils.media_utils import MediaResolver, file_uri_to_path, is_file_uri +# QQ official passive reply msg_id is only valid for 5 minutes +# (see botpy post_group_message docs). Long-running tasks should switch to +# proactive sending (without msg_id) before the window expires. +_QQOFFICIAL_PASSIVE_REPLY_SECONDS = 300 +_QQOFFICIAL_PROACTIVE_MARGIN_SECONDS = 60 + class APIReturnNoneError(Exception): pass @@ -317,20 +324,26 @@ async def _post_send_one( ): plain_text = plain_text + "\n" - # 根据消息链的 use_markdown_ 标记决定发送模式 + # 根据消息链的 use_markdown_ 标记决定发送模式。 + # QQ 官方被动回复 msg_id 有效期 5 分钟;长任务处理超过该窗口后直接走主动发送 + # (不带 msg_id),避免先被动发送失败再兜底,减少一次无效请求。 + force_proactive = isinstance(source, botpy.message.GroupMessage) and ( + time.time() - self.created_at + >= _QQOFFICIAL_PASSIVE_REPLY_SECONDS - _QQOFFICIAL_PROACTIVE_MARGIN_SECONDS + ) use_md = getattr(self.send_buffer, "use_markdown_", None) if use_md is False: payload: dict = { "content": plain_text, "msg_type": 0, - "msg_id": self.message_obj.message_id, } else: payload = { "markdown": MarkdownPayload(content=plain_text) if plain_text else None, "msg_type": 2, - "msg_id": self.message_obj.message_id, } + if not force_proactive: + payload["msg_id"] = self.message_obj.message_id if not isinstance(source, botpy.message.Message | botpy.message.DirectMessage): payload["msg_seq"] = random.randint(1, 10000) From 2c989463428940a7fe63b62179a3327d606e2625 Mon Sep 17 00:00:00 2001 From: Fuyan Yuan <33221728+TheRainstorm@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:33:45 +0800 Subject: [PATCH 2/2] fix(qqofficial): use monotonic clock for passive reply window check --- .../platform/sources/qqofficial/qqofficial_message_event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 429a304c7a..d37281fcf5 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -109,6 +109,8 @@ def __init__( super().__init__(message_str, message_obj, platform_meta, session_id) self.bot = bot self.send_buffer = None + # monotonic clock for the passive-reply window check, immune to wall-clock changes + self.created_at_monotonic = time.monotonic() async def send(self, message: MessageChain) -> None: self.send_buffer = message @@ -328,7 +330,7 @@ async def _post_send_one( # QQ 官方被动回复 msg_id 有效期 5 分钟;长任务处理超过该窗口后直接走主动发送 # (不带 msg_id),避免先被动发送失败再兜底,减少一次无效请求。 force_proactive = isinstance(source, botpy.message.GroupMessage) and ( - time.time() - self.created_at + time.monotonic() - self.created_at_monotonic >= _QQOFFICIAL_PASSIVE_REPLY_SECONDS - _QQOFFICIAL_PROACTIVE_MARGIN_SECONDS ) use_md = getattr(self.send_buffer, "use_markdown_", None)