From 7e7dcbaa941da11830c3a03cd7d5beda33dcf4e8 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:18:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor!:=20Event=20=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E4=B8=BA=E6=8E=A8=E9=80=81=E4=BF=A1=E5=B0=81=E5=B0=81=E8=A3=85?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E5=AD=97=E6=AE=B5=E5=90=8D=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除 Event 的 user_id/group_id/content/message_id 四个属性:它们是 "兼容群(openid)与频道两套字段名"的映射层,其中 user_id 还带着 旧版群消息 payload 顶层 from_user_id 的回退(现行格式发言人标识在 author 里,该回退永远轮不到)。Event 只保留信封语义: .raw/.data/.typed/.type/.event_id,事件体字段一律标注 payload dataclass(如 msg: GroupAtMessage)获取解析结果。 破坏性变更:event.group_id → msg.group_openid、event.message_id → msg.id,以此类推;被动回复凭据分工不变——消息事件用事件体 d.id (msg_id),其余事件用信封顶层 event_id。 迁移:examples/group_bot 入群欢迎改标注 GroupAddRobot(已确认 MusicBot 未使用被删属性);GUIDE/API 同步删除对应属性行;测试 重写为信封语义断言 + typed 解析断言。 107 测试全绿,ruff/pyright 干净。 --- docs/API.md | 8 ++------ docs/GUIDE.md | 10 +++++----- examples/group_bot/main.py | 14 ++++++-------- src/qqbotsdk/events.py | 28 +++++++++------------------- tests/test_events.py | 14 ++++++++++---- 5 files changed, 32 insertions(+), 42 deletions(-) diff --git a/docs/API.md b/docs/API.md index 3e8d8bb..6d1f3c3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -63,7 +63,7 @@ async def slow(...): ... ## Event -`from qqbotsdk.events import Event`,一条 DISPATCH 事件的轻量封装,属性兼容群(openid)与频道两套字段名: +`from qqbotsdk.events import Event`,业务事件推送信封(op/t/d/id)的轻量封装,只封装信封语义、不做字段名映射;事件体字段请标注对应 payload dataclass(见下文)获取解析结果: | 属性 | 说明 | |---|---| @@ -71,11 +71,7 @@ async def slow(...): ... | `.data` | `d` 的 dict 拷贝 | | `.typed` | `d` 的 dataclass 解析结果(未知事件类型为 dict) | | `.type` | 事件 t 名 | -| `.user_id` | 用户标识(兼容 `user_id`/`from_user_id`) | -| `.group_id` | `group_openid` | -| `.content` | 消息文本 | -| `.message_id` | `d["id"]`,消息事件的被动回复凭据(msg_id) | -| `.event_id` | 推送信封顶层的事件 id(与 op/t/d 平级,形如 `INTERACTION_CREATE:uuid`);非消息事件(`INTERACTION_CREATE`/`GROUP_ADD_ROBOT` 等)的被动回复凭据只有这里能拿到,事件体里的裸 UUID 不行(官方点名的常见坑) | +| `.event_id` | 推送信封顶层的事件 id(与 op/t/d 平级,形如 `INTERACTION_CREATE:uuid`);非消息事件(`INTERACTION_CREATE`/`GROUP_ADD_ROBOT` 等)的被动回复凭据只有这里能拿到,事件体里的裸 UUID 不行(官方点名的常见坑);消息事件的被动回复凭据是事件体的 `d.id`(payload 的 `.id` 字段,即 msg_id),不用这里 | ## Config diff --git a/docs/GUIDE.md b/docs/GUIDE.md index b974e41..4de7276 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -100,13 +100,13 @@ from qqbotsdk.payloads import GroupAtMessage async def typed(msg: GroupAtMessage): print(msg.group_openid, msg.user_openid, msg.content) -# 方式三:参数注入,标注组件类型即拿到实例 +# 方式三:参数注入,标注组件类型即拿到实例(payload 与组件可任意组合) from qqbotsdk.api import BotApi -from qqbotsdk.events import Event +from qqbotsdk.payloads import GroupAtMessage @ee.on("GROUP_AT_MESSAGE_CREATE") -async def di(event: Event, api: BotApi): - await api.post_group_message(event.group_id, content="收到", msg_id=event.message_id) +async def di(msg: GroupAtMessage, api: BotApi): + await api.post_group_message(msg.group_openid, content="收到", msg_id=msg.id) ``` handler 参数按标注解析的完整规则: @@ -114,7 +114,7 @@ handler 参数按标注解析的完整规则: | 参数标注 | 得到 | |---|---| | payload dataclass(如 `GroupAtMessage`) | 解析后的 dataclass 对象;未收录事件回退原始 dict | -| `Event` | 事件封装对象(`.raw`/`.data`/`.typed`/`.user_id`/`.group_id`/`.content`/`.message_id`/`.event_id`) | +| `Event` | 事件封装对象,只含信封语义(`.raw`/`.data`/`.typed`/`.type`/`.event_id`);事件体字段请标注 payload dataclass 获取 | | 组件类型(`BotApi`/`Session`/`Config` 等) | `emitter.services` 中按类型登记的实例;未登记回退传原始 d | | 无标注 | 原始 d(即事件 dict) | diff --git a/examples/group_bot/main.py b/examples/group_bot/main.py index 667eca8..d98e7fd 100644 --- a/examples/group_bot/main.py +++ b/examples/group_bot/main.py @@ -13,7 +13,7 @@ from loguru import logger from qqbotsdk import EventEmitter, main from qqbotsdk.api import BotApi, button, keyboard from qqbotsdk.events import Event -from qqbotsdk.payloads import C2CMessage, GroupAtMessage, Interaction +from qqbotsdk.payloads import C2CMessage, GroupAddRobot, GroupAtMessage, Interaction ee = EventEmitter() @@ -74,17 +74,15 @@ async def on_button_click(inter: Interaction, event: Event, api: BotApi) -> None @ee.on("GROUP_ADD_ROBOT") -async def on_robot_added(event: Event, api: BotApi) -> None: - group_openid = event.group_id +async def on_robot_added(added: GroupAddRobot, event: Event, api: BotApi) -> None: # 入群是事件而非消息:用信封顶层的事件 id 作 event_id 被动回复(拿不到则跳过) - event_id = event.event_id - if not (group_openid and event_id): - logger.info(f"机器人加入群聊 {group_openid}(无事件 ID,跳过欢迎语)") + if not (added.group_openid and event.event_id): + logger.info(f"机器人加入群聊 {added.group_openid}(无事件 ID,跳过欢迎语)") return await api.post_group_message( - group_openid, + added.group_openid, content="大家好,我是本群机器人,@我即可体验~", - event_id=event_id, + event_id=event.event_id, ) diff --git a/src/qqbotsdk/events.py b/src/qqbotsdk/events.py index c898b17..5b84e60 100644 --- a/src/qqbotsdk/events.py +++ b/src/qqbotsdk/events.py @@ -1,4 +1,9 @@ -"""事件对象:对线上 payload 的轻量封装,供业务 handler 经 DI 注入使用。""" +"""事件对象:业务事件推送信封(op/t/d/id)的轻量封装,供 handler 参数注入使用。 + +事件体字段经 `.typed`(dataclass 解析,见 payloads.py)或 `.data`/`.raw` +访问;被动回复凭据分两类——消息事件用事件体里的 id(payload 的 `id` +字段),其余事件用信封顶层的 `.event_id`。 +""" from typing import Any @@ -7,7 +12,7 @@ from .payloads import parse_event class Event: - """一条业务事件(op=0);属性取值兼容群(openid)与频道两套字段名。""" + """一条业务事件(op=0)的推送信封;只封装信封语义,不做字段名映射。""" def __init__(self, payload: Payload) -> None: self._payload = payload @@ -35,28 +40,13 @@ class Event: self._typed = parse_event(self.type, self.data) return self._typed - @property - def user_id(self) -> str | None: - return self.data.get("user_id") or self.data.get("from_user_id") - - @property - def group_id(self) -> str | None: - return self.data.get("group_openid") - - @property - def content(self) -> str | None: - return self.data.get("content") - - @property - def message_id(self) -> str | None: - return self.data.get("id") - @property def event_id(self) -> str | None: """推送信封顶层的事件 id(与 op/t/d 平级)。 非消息事件(GROUP_ADD_ROBOT 等)事件体里没有 id,被动回复的 - event_id 只有这里能拿到;消息事件用 `.message_id`(即 msg_id)。 + event_id 只有这里能拿到;消息事件的被动回复凭据是事件体里的 + id(payload 的 `id` 字段,即 msg_id),别取这里的信封 id。 """ id_ = self._payload.get("id") return id_ if isinstance(id_, str) else None diff --git a/tests/test_events.py b/tests/test_events.py index 8128a2e..798efad 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,6 +1,7 @@ -"""Event 封装属性:message_id 取自事件体 d.id,event_id 取自信封顶层 id。""" +"""Event 封装:信封顶层 event_id 与事件体 d 的访问(.data/.typed)。""" from qqbotsdk.events import Event +from qqbotsdk.payloads import GroupAddRobot def make_event() -> Event: @@ -17,7 +18,6 @@ def make_event() -> Event: def test_event_id_from_envelope(): event = make_event() assert event.event_id == "evt-1" - assert event.group_id == "G1" def test_message_id_from_event_body(): @@ -29,11 +29,17 @@ def test_message_id_from_event_body(): "d": {"id": "msgid-1", "group_openid": "G1", "content": "hi"}, } ) - assert event.message_id == "msgid-1" # 消息事件回复用 msg_id + assert event.data["id"] == "msgid-1" # 消息事件回复用 msg_id(事件体 d.id) assert event.event_id == "evt-2" # 信封顶层 id 另有其值 +def test_typed_parses_event_body(): + event = make_event() + typed = event.typed + assert isinstance(typed, GroupAddRobot) + assert typed.group_openid == "G1" + + def test_event_id_defaults_to_none(): event = Event({"op": 0, "t": "READY", "d": {}}) assert event.event_id is None - assert event.message_id is None