refactor!: Event 收敛为推送信封封装,删除字段名兼容属性

删除 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 干净。
This commit is contained in:
snowy-yang committed 2026-09-24 20:18:29 +08:00
1 parent 32d9438d31
commit 7e7dcbaa94
5 files changed
+32 -42

No files matched your search

+2 -6
View File
@@ -63,7 +63,7 @@ async def slow(...): ...
## Event ## 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 拷贝 | | `.data` | `d` 的 dict 拷贝 |
| `.typed` | `d` 的 dataclass 解析结果(未知事件类型为 dict) | | `.typed` | `d` 的 dataclass 解析结果(未知事件类型为 dict) |
| `.type` | 事件 t 名 | | `.type` | 事件 t 名 |
| `.user_id` | 用户标识(兼容 `user_id`/`from_user_id`) | | `.event_id` | 推送信封顶层的事件 id(与 op/t/d 平级,形如 `INTERACTION_CREATE:uuid`);非消息事件(`INTERACTION_CREATE`/`GROUP_ADD_ROBOT` 等)的被动回复凭据只有这里能拿到,事件体里的裸 UUID 不行(官方点名的常见坑);消息事件的被动回复凭据是事件体的 `d.id`(payload 的 `.id` 字段,即 msg_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 不行(官方点名的常见坑) |
## Config ## Config
+5 -5
View File
@@ -100,13 +100,13 @@ from qqbotsdk.payloads import GroupAtMessage
async def typed(msg: GroupAtMessage): async def typed(msg: GroupAtMessage):
print(msg.group_openid, msg.user_openid, msg.content) print(msg.group_openid, msg.user_openid, msg.content)
# 方式三:参数注入,标注组件类型即拿到实例 # 方式三:参数注入,标注组件类型即拿到实例(payload 与组件可任意组合)
from qqbotsdk.api import BotApi from qqbotsdk.api import BotApi
from qqbotsdk.events import Event from qqbotsdk.payloads import GroupAtMessage
@ee.on("GROUP_AT_MESSAGE_CREATE") @ee.on("GROUP_AT_MESSAGE_CREATE")
async def di(event: Event, api: BotApi): async def di(msg: GroupAtMessage, api: BotApi):
await api.post_group_message(event.group_id, content="收到", msg_id=event.message_id) await api.post_group_message(msg.group_openid, content="收到", msg_id=msg.id)
``` ```
handler 参数按标注解析的完整规则: handler 参数按标注解析的完整规则:
@@ -114,7 +114,7 @@ handler 参数按标注解析的完整规则:
| 参数标注 | 得到 | | 参数标注 | 得到 |
|---|---| |---|---|
| payload dataclass(如 `GroupAtMessage`) | 解析后的 dataclass 对象;未收录事件回退原始 dict | | 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 | | 组件类型(`BotApi`/`Session`/`Config` 等) | `emitter.services` 中按类型登记的实例;未登记回退传原始 d |
| 无标注 | 原始 d(即事件 dict) | | 无标注 | 原始 d(即事件 dict) |
+6 -8
View File
@@ -13,7 +13,7 @@ from loguru import logger
from qqbotsdk import EventEmitter, main from qqbotsdk import EventEmitter, main
from qqbotsdk.api import BotApi, button, keyboard from qqbotsdk.api import BotApi, button, keyboard
from qqbotsdk.events import Event from qqbotsdk.events import Event
from qqbotsdk.payloads import C2CMessage, GroupAtMessage, Interaction from qqbotsdk.payloads import C2CMessage, GroupAddRobot, GroupAtMessage, Interaction
ee = EventEmitter() ee = EventEmitter()
@@ -74,17 +74,15 @@ async def on_button_click(inter: Interaction, event: Event, api: BotApi) -> None
@ee.on("GROUP_ADD_ROBOT") @ee.on("GROUP_ADD_ROBOT")
async def on_robot_added(event: Event, api: BotApi) -> None: async def on_robot_added(added: GroupAddRobot, event: Event, api: BotApi) -> None:
group_openid = event.group_id
# 入群是事件而非消息:用信封顶层的事件 id 作 event_id 被动回复(拿不到则跳过) # 入群是事件而非消息:用信封顶层的事件 id 作 event_id 被动回复(拿不到则跳过)
event_id = event.event_id if not (added.group_openid and event.event_id):
if not (group_openid and event_id): logger.info(f"机器人加入群聊 {added.group_openid}(无事件 ID,跳过欢迎语)")
logger.info(f"机器人加入群聊 {group_openid}(无事件 ID,跳过欢迎语)")
return return
await api.post_group_message( await api.post_group_message(
group_openid, added.group_openid,
content="大家好,我是本群机器人,@我即可体验~", content="大家好,我是本群机器人,@我即可体验~",
event_id=event_id, event_id=event.event_id,
) )
+9 -19
View File
@@ -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 from typing import Any
@@ -7,7 +12,7 @@ from .payloads import parse_event
class Event: class Event:
"""一条业务事件(op=0);属性取值兼容群(openid)与频道两套字段名。""" """一条业务事件(op=0)的推送信封;只封装信封语义,不做字段名映射。"""
def __init__(self, payload: Payload) -> None: def __init__(self, payload: Payload) -> None:
self._payload = payload self._payload = payload
@@ -35,28 +40,13 @@ class Event:
self._typed = parse_event(self.type, self.data) self._typed = parse_event(self.type, self.data)
return self._typed 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 @property
def event_id(self) -> str | None: def event_id(self) -> str | None:
"""推送信封顶层的事件 id(与 op/t/d 平级)。 """推送信封顶层的事件 id(与 op/t/d 平级)。
非消息事件(GROUP_ADD_ROBOT 等)事件体里没有 id,被动回复的 非消息事件(GROUP_ADD_ROBOT 等)事件体里没有 id,被动回复的
event_id 只有这里能拿到;消息事件用 `.message_id`(即 msg_id)。 event_id 只有这里能拿到;消息事件的被动回复凭据是事件体里的
id(payload 的 `id` 字段,即 msg_id),别取这里的信封 id。
""" """
id_ = self._payload.get("id") id_ = self._payload.get("id")
return id_ if isinstance(id_, str) else None return id_ if isinstance(id_, str) else None
+10 -4
View File
@@ -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.events import Event
from qqbotsdk.payloads import GroupAddRobot
def make_event() -> Event: def make_event() -> Event:
@@ -17,7 +18,6 @@ def make_event() -> Event:
def test_event_id_from_envelope(): def test_event_id_from_envelope():
event = make_event() event = make_event()
assert event.event_id == "evt-1" assert event.event_id == "evt-1"
assert event.group_id == "G1"
def test_message_id_from_event_body(): 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"}, "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 另有其值 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(): def test_event_id_defaults_to_none():
event = Event({"op": 0, "t": "READY", "d": {}}) event = Event({"op": 0, "t": "READY", "d": {}})
assert event.event_id is None assert event.event_id is None
assert event.message_id is None