From ba90a5f2b6b9bbce65465042b0128b3ca9f7986d Mon Sep 17 00:00:00 2001 From: NayukiMeko Date: Wed, 3 Jun 2026 23:06:45 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(aiocqhttp):=20=E4=BF=AE=E5=A4=8D=20At?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6=E4=B8=8E=E5=90=8E=E7=BB=AD=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E7=9A=84=E9=97=B4=E8=B7=9D=E9=97=AE=E9=A2=98=20-=20?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=20At=20=E7=BB=84=E4=BB=B6=E4=B8=8E=20Plain?= =?UTF-8?q?=20=E6=96=87=E6=9C=AC=E4=B9=8B=E9=97=B4=E6=9C=89=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=A9=BA=E6=A0=BC=E5=88=86=E9=9A=94=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E7=B2=98=E8=BF=9E=20-=20=E5=A4=84=E7=90=86=20At=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=90=8E=E7=B4=A7=E8=B7=9F=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=97=B6=E7=9A=84=E7=A9=BA=E6=A0=BC=E6=8F=92?= =?UTF-8?q?=E5=85=A5=20-=20=E8=B7=B3=E8=BF=87=E7=BA=AF=E7=A9=BA=E7=99=BD?= =?UTF-8?q?=20Plain=20=E6=96=87=E6=9C=AC=EF=BC=8C=E9=87=8D=E7=BD=AE=20At?= =?UTF-8?q?=20=E6=A0=87=E5=BF=97=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/pipeline/result_decorate/stage.py | 5 ++- .../aiocqhttp/aiocqhttp_message_event.py | 41 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 5e6bb9f9c5..2d36564a82 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -414,7 +414,10 @@ async def process( At(qq=event.get_sender_id(), name=event.get_sender_name()), ) if len(result.chain) > 1 and isinstance(result.chain[1], Plain): - result.chain[1].text = "\n" + result.chain[1].text + # At 与正文之间用一个空格分隔,不强制换行 + # 各平台适配器(如 aiocqhttp)会在发送时进一步处理前导空白, + # 确保最终渲染为 "@用户 你好" 而非 "@用户\n你好" + result.chain[1].text = " " + result.chain[1].text # 引用回复 if self.reply_with_quote: diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 4b642d8ce5..3f7426fa5c 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -68,22 +68,57 @@ async def _from_segment_to_dict(segment: BaseMessageComponent) -> dict: @staticmethod async def _parse_onebot_json(message_chain: MessageChain): - """解析成 OneBot json 格式""" + """解析成 OneBot json 格式 + + 将消息链转换为 OneBot 协议的消息段数组. + 特别处理 At 组件与后续内容的间距: + - At + Plain 文本:确保一个空格分隔,避免粘连或双空格 + - At + 非 Plain(图片/文件等):插入空格文本段分隔 + - At 在链末尾:不添加多余空格 + - 纯空白 Plain(如仅含换行/空格):跳过,重置 At 标志位 + """ ret = [] + + # 标记前一个段是否为 At 组件,用于决定是否需要在当前段前插入空格 + prev_is_at = False + for segment in message_chain.chain: if isinstance(segment, At): - # At 组件后插入一个空格,避免与后续文本粘连 + # At 组件:记录到结果,并设置标志位 + # 空格由后续段决定如何插入(避免无条件插入导致末尾多余空格) d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) ret.append(d) - ret.append({"type": "text", "data": {"text": " "}}) + prev_is_at = True + elif isinstance(segment, Plain): + # 跳过纯空白文本(如单独的换行符、空格等) if not segment.text.strip(): + prev_is_at = False continue + + if prev_is_at: + # 前一个是 At:去除 Plain 的前导空白后,统一在前面加一个空格 + # .lstrip() 的作用: + # result_decorate 阶段可能已在文本前加了空格或换行, + # 直接拼接会导致 "@用户 \n你好" 这样的双空白 + # 统一用 " " 替换所有前导空白,确保 @ 与正文之间仅有一个空格 + segment.text = " " + segment.text.lstrip() + prev_is_at = False + d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) ret.append(d) + else: + # 非 At、非 Plain 的组件(Image、Record、Video、File 等) + if prev_is_at: + # At 后紧跟媒体组件,插入一个空格文本段防止粘连 + # 例如:[At] [Image] → [At] [空格] [Image] + ret.append({"type": "text", "data": {"text": " "}}) + prev_is_at = False + d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) ret.append(d) + return ret @classmethod From efc62026d6927deb904483e0303f57d95f665cba Mon Sep 17 00:00:00 2001 From: NayukiMeko Date: Wed, 3 Jun 2026 23:39:01 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(aiocqhttp):=20=E4=BF=AE=E5=A4=8D=20At?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6=E4=B8=8E=E6=AD=A3=E6=96=87=E4=B9=8B?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E7=A9=BA=E7=99=BD=E5=A4=84=E7=90=86=20-=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=A4=84=E7=90=86=20At=20=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=B8=8E=E5=90=8E=E7=BB=AD=E5=86=85=E5=AE=B9=E4=B9=8B=E9=97=B4?= =?UTF-8?q?=E7=9A=84=E7=A9=BA=E6=A0=BC=EF=BC=8C=E9=81=BF=E5=85=8D=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E6=8D=A2=E8=A1=8C=20-=20=E7=A1=AE=E4=BF=9D=E6=9C=80?= =?UTF-8?q?=E7=BB=88=E6=B8=B2=E6=9F=93=E4=B8=BA=20"@=E7=94=A8=E6=88=B7=20?= =?UTF-8?q?=E4=BD=A0=E5=A5=BD"=20=E8=80=8C=E9=9D=9E=20"@=E7=94=A8=E6=88=B7?= =?UTF-8?q?\n=E4=BD=A0=E5=A5=BD"=20-=20=E8=B7=B3=E8=BF=87=E7=BA=AF?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=E6=96=87=E6=9C=AC=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=E6=B6=88=E6=81=AF=E9=93=BE=E7=9A=84=E5=8E=9F?= =?UTF-8?q?=E5=A7=8B=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/pipeline/result_decorate/stage.py | 2 +- .../platform/sources/aiocqhttp/aiocqhttp_message_event.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 2d36564a82..b217983560 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -417,7 +417,7 @@ async def process( # At 与正文之间用一个空格分隔,不强制换行 # 各平台适配器(如 aiocqhttp)会在发送时进一步处理前导空白, # 确保最终渲染为 "@用户 你好" 而非 "@用户\n你好" - result.chain[1].text = " " + result.chain[1].text + result.chain[1] = Plain(" " + result.chain[1].text) # 引用回复 if self.reply_with_quote: diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 3f7426fa5c..b20a08fbd9 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -92,8 +92,9 @@ async def _parse_onebot_json(message_chain: MessageChain): elif isinstance(segment, Plain): # 跳过纯空白文本(如单独的换行符、空格等) + # 注意:不重置 prev_is_at,避免 [At, Plain("\n"), Plain("你好")] + # 这种场景下空白 Plain 阻断空格插入 if not segment.text.strip(): - prev_is_at = False continue if prev_is_at: @@ -103,7 +104,11 @@ async def _parse_onebot_json(message_chain: MessageChain): # 直接拼接会导致 "@用户 \n你好" 这样的双空白 # 统一用 " " 替换所有前导空白,确保 @ 与正文之间仅有一个空格 segment.text = " " + segment.text.lstrip() + # 注意:不修改 segment.text,避免污染原始 MessageChain(影响 hook 等消费者) + text = " " + segment.text.lstrip() prev_is_at = False + ret.append({"type": "text", "data": {"text": text}}) + continue d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) ret.append(d) From cfb01c0430d482b486b48b635bb45d0f0456d05c Mon Sep 17 00:00:00 2001 From: NayukiMeko Date: Thu, 4 Jun 2026 00:02:45 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(aiocqhttp):=20=E4=BF=AE=E5=A4=8D=20At?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6=E4=B8=8E=E7=A9=BA=E7=99=BD=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=20-=20=E7=A1=AE=E4=BF=9D=20At=20+?= =?UTF-8?q?=20Plain=20=E6=96=87=E6=9C=AC=E4=B9=8B=E9=97=B4=E6=9C=89?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E7=A9=BA=E6=A0=BC=E5=88=86=E9=9A=94=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E7=B2=98=E8=BF=9E=E6=88=96=E5=8F=8C=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC=20-=20=E5=9C=A8=20At=20=E7=BB=84=E4=BB=B6=E4=B8=8E?= =?UTF-8?q?=E9=9D=9E=20Plain=20=E5=86=85=E5=AE=B9=E4=B9=8B=E9=97=B4?= =?UTF-8?q?=E6=8F=92=E5=85=A5=E7=A9=BA=E6=A0=BC=E6=96=87=E6=9C=AC=E6=AE=B5?= =?UTF-8?q?=E5=88=86=E9=9A=94=20-=20=E4=BF=AE=E6=94=B9=E7=BA=AF=E7=A9=BA?= =?UTF-8?q?=E7=99=BD=20Plain=20=E7=9A=84=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E8=B7=B3=E8=BF=87=E6=97=B6=E4=B8=8D=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=20At=20=E6=A0=87=E5=BF=97=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/platform/sources/aiocqhttp/aiocqhttp_message_event.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index b20a08fbd9..b8a4b66864 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -75,7 +75,7 @@ async def _parse_onebot_json(message_chain: MessageChain): - At + Plain 文本:确保一个空格分隔,避免粘连或双空格 - At + 非 Plain(图片/文件等):插入空格文本段分隔 - At 在链末尾:不添加多余空格 - - 纯空白 Plain(如仅含换行/空格):跳过,重置 At 标志位 + - 纯空白 Plain(如仅含换行/空格):跳过,不重置 At 标志位 """ ret = [] @@ -103,7 +103,6 @@ async def _parse_onebot_json(message_chain: MessageChain): # result_decorate 阶段可能已在文本前加了空格或换行, # 直接拼接会导致 "@用户 \n你好" 这样的双空白 # 统一用 " " 替换所有前导空白,确保 @ 与正文之间仅有一个空格 - segment.text = " " + segment.text.lstrip() # 注意:不修改 segment.text,避免污染原始 MessageChain(影响 hook 等消费者) text = " " + segment.text.lstrip() prev_is_at = False