From 8c935981bb6de7c8d0a84add1b505e90d17919d6 Mon Sep 17 00:00:00 2001 From: whatevertogo <1879483647@qq.com> Date: Thu, 5 Mar 2026 23:02:26 +0800 Subject: [PATCH] fix: align aiocqhttp poke payload with onebot v11 (#5773) Co-authored-by: whatevertogo --- astrbot/core/message/components.py | 35 +++++++++-- astrbot/core/pipeline/respond/stage.py | 2 +- .../aiocqhttp/aiocqhttp_platform_adapter.py | 2 +- tests/unit/test_aiocqhttp_poke.py | 59 +++++++++++++++++++ 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 tests/unit/test_aiocqhttp_poke.py diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index 15265c38d..6dbe78ae4 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -539,13 +539,36 @@ class Reply(BaseMessageComponent): class Poke(BaseMessageComponent): - type: str = ComponentType.Poke - id: int | None = 0 - qq: int | None = 0 + type: ComponentType = ComponentType.Poke + _type: str | int = "126" + id: int | str | None = 0 + qq: int | str | None = 0 # deprecated: legacy field, kept for compatibility - def __init__(self, type: str, **_) -> None: - type = f"Poke:{type}" - super().__init__(type=type, **_) + def __init__(self, poke_type: str | int | None = None, **_) -> None: + # Backward compatible with old signature: Poke(type="poke", ...) + legacy_type = _.pop("type", None) + if poke_type is None: + poke_type = legacy_type + if poke_type in (None, "", "poke", "Poke"): + poke_type = "126" + super().__init__(_type=str(poke_type), **_) + + def target_id(self) -> str | None: + """Return normalized target id, compatible with old `qq` field.""" + for value in (self.id, self.qq): + if value is None: + continue + text = str(value).strip() + if text and text != "0": + return text + return None + + def toDict(self): + target_id = self.target_id() + data = {"type": str(self._type or "126")} + if target_id: + data["id"] = target_id + return {"type": "poke", "data": data} class Forward(BaseMessageComponent): diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index bd307f8b7..6a884a518 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -28,7 +28,7 @@ class RespondStage(Stage): Comp.At: lambda comp: bool(comp.qq) or bool(comp.name), # @ Comp.Image: lambda comp: bool(comp.file), # 图片 Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复 - Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳 + Comp.Poke: lambda comp: comp.target_id() is not None, # 戳一戳 Comp.Node: lambda comp: bool(comp.content), # 转发节点 Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点 Comp.File: lambda comp: bool(comp.file_ or comp.url), diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 45114382f..7110199af 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -191,7 +191,7 @@ class AiocqhttpAdapter(Platform): if "sub_type" in event: if event["sub_type"] == "poke" and "target_id" in event: - abm.message.append(Poke(qq=str(event["target_id"]), type="poke")) + abm.message.append(Poke(id=str(event["target_id"]))) return abm diff --git a/tests/unit/test_aiocqhttp_poke.py b/tests/unit/test_aiocqhttp_poke.py new file mode 100644 index 000000000..edd8f2ae2 --- /dev/null +++ b/tests/unit/test_aiocqhttp_poke.py @@ -0,0 +1,59 @@ +from unittest.mock import AsyncMock + +import pytest + +import astrbot.core.message.components as Comp +from astrbot.core.message.message_event_result import MessageChain +from astrbot.core.pipeline.respond.stage import RespondStage +from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import ( + AiocqhttpMessageEvent, +) + + +def test_poke_to_dict_matches_onebot_v11_segment_format(): + poke = Comp.Poke(type="126", id=2003) + assert poke.toDict() == { + "type": "poke", + "data": {"type": "126", "id": "2003"}, + } + + +def test_poke_to_dict_keeps_legacy_qq_compatible(): + poke = Comp.Poke(type="poke", qq=2916963017) + assert poke.toDict() == { + "type": "poke", + "data": {"type": "126", "id": "2916963017"}, + } + + +@pytest.mark.asyncio +async def test_respond_stage_treats_poke_with_target_as_non_empty(): + stage = RespondStage() + chain = [Comp.Poke(type="126", id=2003)] + assert await stage._is_empty_message_chain(chain) is False + + +@pytest.mark.asyncio +async def test_aiocqhttp_parse_json_outputs_standard_poke_data(): + chain = MessageChain([Comp.Poke(type="126", id=2003)]) + data = await AiocqhttpMessageEvent._parse_onebot_json(chain) + assert data == [{"type": "poke", "data": {"type": "126", "id": "2003"}}] + + +@pytest.mark.asyncio +async def test_aiocqhttp_send_message_dispatches_onebot_v11_poke_payload(): + bot = AsyncMock() + chain = MessageChain([Comp.Poke(type="126", id=2003)]) + + await AiocqhttpMessageEvent.send_message( + bot=bot, + message_chain=chain, + event=None, + is_group=True, + session_id="123456", + ) + + bot.send_group_msg.assert_awaited_once_with( + group_id=123456, + message=[{"type": "poke", "data": {"type": "126", "id": "2003"}}], + )