mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 11:04:48 +00:00
refactor!: handler 参数只按模型/Event/组件注入,不再回退传原始 d
解析不到的形参(无标注或标注三者皆非)在分发时抛 ValueError,经异常 隔离记日志;原始数据经 Event 的 .raw/.typed 自取。同步四处文档与测试。
This commit is contained in:
1 parent
1d9f509b25
commit
0f3ed09233
7 files changed
+46
-60
No files matched your search
+8
-7
@@ -54,12 +54,13 @@ async def slow(...): ...
|
||||
|
||||
按参数类型标注解析,可任意混用:
|
||||
|
||||
| 标注 | 得到 | 未命中时 |
|
||||
|---|---|---|
|
||||
| payload dataclass(`payloads` 中的类型) | `Event.typed` 解析对象 | 回退传原始 d |
|
||||
| `Event` | 事件封装对象 | — |
|
||||
| 组件类型(`BotApi`/`Session`/`Config` 等已登记进 `ee.services` 的类型) | 登记的实例 | 回退传原始 d |
|
||||
| 无标注 | 原始 d(业务事件即事件 dict;协议事件可能是标量,如 `INVALID_SESSION` 的 `resumable: bool`) | — |
|
||||
| 标注 | 得到 |
|
||||
|---|---|
|
||||
| payload dataclass(`payloads` 中的类型) | `Event.typed` 解析对象 |
|
||||
| `Event` | 事件封装对象 |
|
||||
| 组件类型(`BotApi`/`Session`/`Config` 等已登记进 `ee.services` 的类型) | 登记的实例 |
|
||||
|
||||
无标注或标注以上三者皆非的形参在分发时抛 `ValueError`(记日志、按 handler 隔离)——SDK 不注入原始 d,要事件体字段就标注 payload dataclass,要完整信封就标注 `Event`。
|
||||
|
||||
## Event
|
||||
|
||||
@@ -284,7 +285,7 @@ async def slow(...): ...
|
||||
| AUDIO_ACTION | `AUDIO_START` / `AUDIO_FINISH` / `AUDIO_ON_MIC` / `AUDIO_OFF_MIC` | `AudioAction` |
|
||||
| —(DISPATCH 协议事件) | `READY` | `Ready` |
|
||||
|
||||
未收录的事件(如私域 `MESSAGE_CREATE`、`DIRECT_MESSAGE_DELETE`、`GUILD_MEMBER_*`)handler 收到原始 dict;新增事件在 `payloads.py` 加 dataclass 并登记 `EVENT_TYPES` 即可。
|
||||
未收录的事件(如私域 `MESSAGE_CREATE`、`DIRECT_MESSAGE_DELETE`、`GUILD_MEMBER_*`)没有对应 dataclass 可标注,请标注 `Event` 经 `.typed`(未知事件类型为 dict)或 `.raw` 访问;新增事件在 `payloads.py` 加 dataclass 并登记 `EVENT_TYPES` 即可。
|
||||
|
||||
`GROUP_MESSAGE_CREATE` 是群消息**全量模式**:需在开放平台开通"接收所有消息"权限,群里每条消息(不限 @机器人)都推送此事件,与 `GROUP_AT_MESSAGE_CREATE` 共用 `GroupAtMessage`。官方提示同一 msg_id 可能重复推送,业务侧需按 `id` 去重。
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ sequenceDiagram
|
||||
2. **协议层只管会话语义,心跳归传输层**:`WebsocketProtocol` 负责 IDENTIFY/RESUME/序列号(说什么),`WebsocketConnecter` 负责连接与心跳(怎么保活)——心跳任务随连接生灭(HELLO 给出间隔后启动,断线即取消),断线期间不在出站缓冲堆积过期心跳,也免去了调度器的生命周期管理。协议处理器的应答经 `on_frame` 返回值交给 connecter:ws 与心跳一起进 connecter 私有的出站缓冲、由单任务发送(维持 ws 单写者不变量),webhook 则直接写进 HTTP 响应体。
|
||||
3. **看门狗两层互补 + 连接寿命主动重连**(见 `connecter.py` 模块注释):静默超时抓连接彻底失联,ACK 期限检查抓"有事件流但心跳已死"。阈值 `_SILENCE_FACTOR=1.1`、`_ACK_FACTOR=1.2` 是按 ACK 自然节奏校准的,**不能压到 1× 整**,否则正常抖动会周期性误杀(误杀由 RESUME 兜底,不丢消息)。重连退避只在连接稳定存活(`_STABLE_SECONDS`)后才归一,抖动的服务端不会被 1s 间隔反复探测。此外服务端约 1 小时会**无预警**断开连接(实测),故连接存活到 `_SERVER_LIFETIME - _RECONNECT_MARGIN`(默认 50 分钟)时 SDK 主动收尾重连——收帧超时取"静默阈值"与"寿命余量"两个截止中的先到者,静默仍交给看门狗,寿命到点则正常结束本轮连接、由 `run()` 判定为健康断开后立刻 RESUME 续传。
|
||||
4. **handler 异常就地隔离**:`_invoke` 捕获记日志,`dispatch` 主循环另有兜底;单条事件的单个 handler 失败不影响进程。`background=True` 的 handler 进后台任务(emit 不等它),任务由 emitter 持强引用、`close()` 统一取消。
|
||||
5. **注入约定**:handler 参数按形参标注解析——`payloads.py` 里的 dataclass → 解析后的 payload 对象;`Event` → 事件封装;登记进 `emitter.services` 的组件类型(如 `BotApi`)→ 实例;未标注或未登记 → 回退传**原始 d**。解析是纯同步查表,无框架、无运行时魔法:每个 handler 的 `get_type_hints` + 形参名只解析一次并缓存,但 `services` 命中在每条事件时实时判定,保证"先注册 handler、后登记组件"的装配顺序可用。
|
||||
5. **注入约定**:handler 参数按形参标注解析——`payloads.py` 里的 dataclass → 解析后的 payload 对象;`Event` → 事件封装;登记进 `emitter.services` 的组件类型(如 `BotApi`)→ 实例;无标注或标注三者皆非 → 抛 `ValueError`(原始数据不注入,需要时标 `Event` 自取 `.raw`/`.typed`)。解析是纯同步查表,无框架、无运行时魔法:每个 handler 的 `get_type_hints` + 形参名只解析一次并缓存,但 `services` 命中在每条事件时实时判定,保证"先注册 handler、后登记组件"的装配顺序可用(`run_loop` 在分发开始前登记全部内置组件,正常流程不会误抛)。
|
||||
6. **事件注册不依赖第三方库**:`EventEmitter` 内部就是一个 `事件名 → handler 列表` 的字典,`on()` 追加、`emit()` 顺序遍历。不需要 pyee 那套 `once`/`new_listener`/错误事件机制(SDK 用不到),少一个依赖、语义也一眼可读。注册语义:同一函数对同一事件重复注册只生效一次(沿用原先 pyee 的行为),分发按注册顺序串行执行(`background=True` 的除外)。
|
||||
7. **解析计划与派生密钥都预计算**:`payloads._parse_plan` 把 dataclass 的"字段名 + 是否需解析成 Author"按类缓存一次(不再每次事件重跑 `dataclasses.fields` 与联合类型解包),解析时未收录字段一律忽略——线上 payload 会带新增/未声明字段,透传会让构造报错。`crypto` 按 AppSecret 缓存派生出的 Ed25519 私钥(派生约 40µs,而每个 webhook 请求都要验签)。都是"值为输入的纯函数、结果与输入一一对应",缓存安全且收益明确;不追求极致速度,只去掉明显的重复计算。
|
||||
8. **OpenAPI 请求内核自治**:`api/core.py` 统一处理鉴权头、429 限流重试(优先 `Retry-After`,脏值容错,退避封顶)与 401 刷新 token 重试一次;v2/channel/guild 三个域方法集继承内核组合成 `BotApi`,新增域加模块即可,方法对用户始终平铺在 `BotApi` 上。
|
||||
|
||||
+8
-12
@@ -82,26 +82,21 @@ QQ 网关只认分组位掩码,**组内单个事件无法单独开关**——
|
||||
|
||||
## 编写 handler
|
||||
|
||||
用装饰器把 handler 注册到 `EventEmitter`,事件名即线上 payload 的 `t` 名(如 `GROUP_AT_MESSAGE_CREATE`)。三种写法可混用:
|
||||
用装饰器把 handler 注册到 `EventEmitter`,事件名即线上 payload 的 `t` 名(如 `GROUP_AT_MESSAGE_CREATE`)。参数按类型标注注入,可混用:
|
||||
|
||||
```python
|
||||
from qqbotsdk import EventEmitter
|
||||
|
||||
ee = EventEmitter()
|
||||
|
||||
# 方式一:裸参数,d 为原始事件 dict
|
||||
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def raw(d):
|
||||
print(d)
|
||||
|
||||
# 方式二:标注 payload dataclass,拿到解析后的对象
|
||||
# 方式一:标注 payload dataclass,拿到解析后的对象
|
||||
from qqbotsdk.payloads import GroupAtMessage
|
||||
|
||||
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def typed(msg: GroupAtMessage):
|
||||
print(msg.group_openid, msg.user_openid, msg.content)
|
||||
|
||||
# 方式三:参数注入,标注组件类型即拿到实例(payload 与组件可任意组合)
|
||||
# 方式二:参数注入,标注组件类型即拿到实例(payload 与组件可任意组合)
|
||||
from qqbotsdk.api import BotApi
|
||||
from qqbotsdk.payloads import GroupAtMessage
|
||||
|
||||
@@ -114,10 +109,11 @@ handler 参数按标注解析的完整规则:
|
||||
|
||||
| 参数标注 | 得到 |
|
||||
|---|---|
|
||||
| payload dataclass(如 `GroupAtMessage`) | 解析后的 dataclass 对象;未收录事件回退原始 dict |
|
||||
| payload dataclass(如 `GroupAtMessage`) | 解析后的 dataclass 对象 |
|
||||
| `Event` | 事件封装对象,只含信封语义(`.raw`/`.data`/`.typed`/`.type`/`.event_id`);事件体字段请标注 payload dataclass 获取 |
|
||||
| 组件类型(`BotApi`/`Session`/`Config` 等) | `emitter.services` 中按类型登记的实例;未登记回退传原始 d |
|
||||
| 无标注 | 原始 d(即事件 dict) |
|
||||
| 组件类型(`BotApi`/`Session`/`Config` 等) | `emitter.services` 中按类型登记的实例 |
|
||||
|
||||
无标注或标注以上三者皆非的形参会在分发时抛 `ValueError`(记日志、按 handler 隔离)——SDK 不注入原始数据:要事件体字段就标注 payload dataclass,要完整信封就标注 `Event`。
|
||||
|
||||
注意:
|
||||
|
||||
@@ -239,5 +235,5 @@ uv run python main.py
|
||||
- **`ApiError`**:HTTP ≥400 时抛出,`e.status`/`e.code`/`e.message` 可用于排查(如被动回复超时、msg_seq 重复、内容审核不通过)。
|
||||
- **日志出现 `TokenError: 获取 access_token 失败`**:appid/secret 配置错误或被平台拒绝,`code`/`message` 就是 QQ 返回的真实原因,对照官方错误码排查即可。
|
||||
- **handler 没被触发**:检查 intents.toml 中该事件所属分组是否为 `true`;群/单聊事件需开通对应能力;频道消息事件区分公私域。
|
||||
- **想收原始数据**:handler 裸参数或标 `Event`,`event.raw` 是完整线上 payload(含 op/t/d)。
|
||||
- **想收原始数据**:标注 `Event`,`event.raw` 是完整线上 payload(含 op/t/d),`event.typed` 是事件体解析结果(未收录事件类型为 dict)。
|
||||
- **日志**:SDK 用 loguru 输出中文日志,连接、重连、API 失败、handler 异常均有记录,可直接用 loguru 配置格式与落盘。
|
||||
Reference in new issue
Block a user