mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 09:05:21 +00:00
refactor!: 协议帧与业务事件分流,各接入方式配专有协议处理器
BaseProtocol 从 emitter.py 移入新模块 protocol.py,接口由 register(emitter) 改为 async on_frame(payload)(返回需回送的响应体);协议处理器不再注册进 emitter,而由 run_loop 注入对应的 connecter,由适配器逐帧调用。 破坏性变更:只有 op=0 的业务事件进事件队列交给 EventEmitter 分发, HELLO/READY/INVALID_SESSION/op=13 等协议帧在连接器内就地处理——用户 handler 不再收到协议帧(此前 HELLO/INVALID_SESSION 等会路由到 handler)。 EventEmitter.register_protocol()/handle() 与 emitter 持有协议层的机制删除, 协议类不再用 register() 注册 handler。 附带修掉一处时序耦合:INVALID_SESSION 的会话重置此前依赖分发泵在重连前 消费掉该事件(慢 handler 会推迟它,导致带着已死 session_id 去 RESUME), 现在连接器在 break 重连前同步完成重置。 同时做的事件流优化: - emit() 先取事件名、无 listener 时不构造 Event(未订阅事件省一次对象与拷贝) - Event.data 改为惰性拷贝(只用 .typed/.raw 的 handler 不再触发 dict(d)) - ws 重连收尾 await 被取消的 reply/heartbeat 任务,避免异常无人取用 - EventQueue.put_event/put_reply 去掉多余的 return await 测试 80 全绿(新增协议帧处理测试组、ws 集成断言协议帧不触达 handler), ruff/pyright 干净。文档 ARCHITECTURE/API/GUIDE/README/CHANGELOG 同步。
This commit is contained in:
1 parent
71cbfdaf5d
commit
7accbd1ead
16 files changed
+335
-238
No files matched your search
+30
-32
@@ -11,14 +11,14 @@ flowchart TB
|
||||
WH["WebhookConnecter\naiohttp server\n验签 + TTL去重"]
|
||||
end
|
||||
|
||||
subgraph 分发层["分发层 emitter.py"]
|
||||
Q[("EventQueue\nmsg_queue / reply_queue")]
|
||||
EE["EventEmitter\nop/t 路由 + 参数注入\nhandler 异常隔离\nbackground 后台并发"]
|
||||
subgraph 协议层["协议层 protocol.py"]
|
||||
WSP["WebsocketProtocol\nHELLO→IDENTIFY/RESUME\nREADY/INVALID_SESSION + s 序列号"]
|
||||
WHP["WebhookProtocol\nop=13 验证应答"]
|
||||
end
|
||||
|
||||
subgraph 协议层["协议层 BaseProtocol"]
|
||||
WSP["WebsocketProtocol\nIDENTIFY/RESUME\ns 序列号跟踪"]
|
||||
WHP["WebhookProtocol\nop=13 验证应答"]
|
||||
subgraph 分发层["分发层 emitter.py"]
|
||||
Q[("EventQueue\nmsg_queue / reply_queue")]
|
||||
EE["EventEmitter\nop=0/t 路由 + 参数注入\nhandler 异常隔离\nbackground 后台并发"]
|
||||
end
|
||||
|
||||
subgraph 业务层["业务层"]
|
||||
@@ -35,12 +35,9 @@ flowchart TB
|
||||
|
||||
QQ["QQ 开放平台"]
|
||||
|
||||
WS -- "事件 payload" --> Q
|
||||
WH -- "事件 payload" --> Q
|
||||
WS -- "业务事件 op=0" --> Q
|
||||
WH -- "业务事件 op=0" --> Q
|
||||
Q -- "dispatch 泵" --> EE
|
||||
EE -- "on_payload" --> WSP
|
||||
WSP -- "IDENTIFY/RESUME 等\npayload 返回值" --> Q
|
||||
WS -- "HEARTBEAT(心跳任务)" --> Q
|
||||
EE -- "Event 注入" --> USER
|
||||
USER -- "api: BotApi" --> API
|
||||
API --> QQ
|
||||
@@ -48,6 +45,9 @@ flowchart TB
|
||||
TK --> HTTP
|
||||
API --> TK
|
||||
WSP --> TK
|
||||
WS -. "逐帧 on_frame\n(协议帧就地处理,不入队)" .-> WSP
|
||||
WH -. "op=13 逐帧 on_frame" .-> WHP
|
||||
WSP -. "IDENTIFY/RESUME 应答" .-> WS
|
||||
WHP --> CRY
|
||||
WH --> CRY
|
||||
CFG -. "构造传参" .-> WS & WH & API & TK & WSP & WHP
|
||||
@@ -59,30 +59,28 @@ flowchart TB
|
||||
sequenceDiagram
|
||||
participant QQ as QQ 开放平台
|
||||
participant WS as WebsocketConnecter
|
||||
participant P as WebsocketProtocol
|
||||
participant Q as EventQueue
|
||||
participant EE as EventEmitter
|
||||
participant P as WebsocketProtocol
|
||||
participant H as 用户 handler
|
||||
|
||||
QQ->>WS: HELLO (op=10)
|
||||
WS->>Q: put_event
|
||||
Q->>EE: dispatch 泵
|
||||
EE->>P: on_payload(记 s 序列号)
|
||||
EE->>P: 路由到 HELLO handler
|
||||
P-->>Q: IDENTIFY/RESUME payload → reply_queue
|
||||
WS->>P: on_frame(握手判定)
|
||||
P-->>WS: IDENTIFY / RESUME payload
|
||||
WS->>QQ: send(过滤 NOT_SET 字段)
|
||||
|
||||
loop 每次心跳间隔(心跳任务随连接生灭)
|
||||
WS->>Q: HEARTBEAT payload
|
||||
WS->>QQ: send
|
||||
QQ->>WS: HEARTBEAT_ACK(刷新看门狗时钟)
|
||||
QQ->>WS: HEARTBEAT_ACK(刷新看门狗时钟,不进队列)
|
||||
end
|
||||
|
||||
QQ->>WS: DISPATCH 事件 (op=0, t=GROUP_AT_MESSAGE_CREATE)
|
||||
WS->>Q: put_event
|
||||
WS->>P: on_frame(记 s 序列号)
|
||||
WS->>Q: put_event(仅 op=0)
|
||||
Q->>EE: dispatch 泵
|
||||
EE->>H: 按形参标注注入 Event/BotApi
|
||||
H->>EE: 返回值不回流(op=0)
|
||||
H->>EE: 返回值不回流
|
||||
```
|
||||
|
||||
## 分层规则
|
||||
@@ -91,33 +89,33 @@ sequenceDiagram
|
||||
|
||||
| 层 | 模块 | 职责 | 不允许出现 |
|
||||
|---|---|---|---|
|
||||
| 接入层 | `connecter.py` | 传输细节:握手、心跳、重连、验签、去重 | 事件业务语义、协议状态机 |
|
||||
| 分发层 | `emitter.py` | 路由、参数注入、异常隔离、后台并发、应答回流 | 传输细节、具体协议状态机 |
|
||||
| 协议层 | `ws_protocol.py` `webhook_protocol.py` | 鉴权/验证等协议状态机(会话语义) | 传输细节(只经队列收发) |
|
||||
| 接入层 | `connecter.py` | 传输细节:握手、心跳、重连、验签、去重;逐帧驱动协议处理器 | 事件业务语义、协议状态机 |
|
||||
| 协议层 | `protocol.py` `ws_protocol.py` `webhook_protocol.py` | 适配器专有的协议处理(`BaseProtocol.on_frame`):鉴权/验证/会话语义 | 传输细节、业务语义 |
|
||||
| 分发层 | `emitter.py` | 只分发业务事件(op=0):路由、参数注入、异常隔离、后台并发 | 传输细节、协议帧、协议状态机 |
|
||||
| 业务层 | `api/` `payloads.py` + 用户代码 | OpenAPI 调用、payload 解析与机器人逻辑 | 协议细节 |
|
||||
|
||||
跨层共享的最小公共物:`model.py`(Opcode/Payload/Intent)、`queue.py`(事件通道)、`config.py`、`session.py`(ws 会话状态)、`payloads.py`(事件 dataclass 与解析,被 `events.py`/`emitter.py` 使用)。
|
||||
跨层共享的最小公共物:`model.py`(Opcode/Payload/Intent)、`protocol.py`(协议处理器接口,被接入层与协议层共同依赖)、`queue.py`(事件通道)、`config.py`、`session.py`(ws 会话状态)、`payloads.py`(事件 dataclass 与解析,被 `events.py`/`emitter.py` 使用)。
|
||||
|
||||
## 生命周期与依赖装配
|
||||
|
||||
- 全部组件在 `run_loop`(`__init__.py`)显式构造装配,无 DI 框架,也无任何全局单例。
|
||||
- 组件按类型登记进 `emitter.services`,handler 形参标注类型即得实例;用户自定义组件同样登记即可注入。
|
||||
- 唯一的 `ClientSession` 在 `run_loop` 构造,`finally` 中先 `emitter.close()` 收尾后台 handler、再统一释放连接池(全 SDK 共享一个连接池)。
|
||||
- 接入方式的选择收敛在 `run_loop` 的 `match config.connecter` 装配分支,主体代码只依赖 `BaseProtocol`/`Connecter` 抽象;两个 connecter 的依赖面一致(config + queue + 各自所需的窄依赖),`WebhookConnecter` 经 `handle_event` 回调取得 op=13 应答,不感知 `EventEmitter`。
|
||||
- 接入方式的选择收敛在 `run_loop` 的 `match config.connecter` 装配分支:每种接入方式构造**自己的协议处理器**并注入对应的 connecter(主体不再持有协议层);`WebhookConnecter` 经协议处理器的 `on_frame` 就地取得 op=13 应答,不感知 `EventEmitter`。两个 connecter 的依赖面一致(config + queue + 各自所需的窄依赖 + `BaseProtocol`)。
|
||||
- 用户在 `main(ee)` 之前构造的 `EventEmitter` 会被复用(连同它的队列),保证 handler 注册在主体实际使用的实例上。
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
1. **管线而非回调直调**:事件一律经 `EventQueue` 异步流转,接入与分发两侧解耦——Webhook handler 里不需要知道 ws 的存在,反之亦然。代价是多一跳队列延迟(可忽略)。
|
||||
2. **协议层只管会话语义,心跳归传输层**:`WebsocketProtocol` 负责 IDENTIFY/RESUME/序列号(说什么),`WebsocketConnecter` 负责连接与心跳(怎么保活)——心跳任务随连接生灭(HELLO 给出间隔后启动,断线即取消),断线期间不在 reply 队列堆积过期心跳,也免去了调度器的生命周期管理。协议类通过返回 payload dict 经 reply 队列外发。
|
||||
1. **业务事件走管线,协议帧走专有处理器**:只有 op=0 的业务事件经 `EventQueue` 异步流转给 `EventEmitter` 分发;HELLO/READY/INVALID_SESSION/op=13 等协议帧由 connecter 逐帧调用其专有协议处理器(`BaseProtocol.on_frame`)就地处理。这样用户 handler 不会看到协议帧,且"断线前重置会话"这类时序不再取决于分发泵的进度——`INVALID_SESSION` 的会话清空在 connecter `break` 重连前已同步完成,不会误 RESUME 一个已死会话。代价是多一跳队列延迟(可忽略)。
|
||||
2. **协议层只管会话语义,心跳归传输层**:`WebsocketProtocol` 负责 IDENTIFY/RESUME/序列号(说什么),`WebsocketConnecter` 负责连接与心跳(怎么保活)——心跳任务随连接生灭(HELLO 给出间隔后启动,断线即取消),断线期间不在 reply 队列堆积过期心跳,也免去了调度器的生命周期管理。协议处理器的应答经 `on_frame` 返回值由 connecter 写回(ws 走 reply 队列,webhook 走 HTTP 响应体)。
|
||||
3. **看门狗两层互补**(见 `connecter.py` 模块注释):静默超时抓连接彻底失联,ACK 期限检查抓"有事件流但心跳已死"。阈值 `_SILENCE_FACTOR=1.1`、`_ACK_FACTOR=1.2` 是按 ACK 自然节奏校准的,**不能压到 1× 整**,否则正常抖动会周期性误杀(误杀由 RESUME 兜底,不丢消息)。重连退避只在连接稳定存活(`_STABLE_SECONDS`)后才归一,抖动的服务端不会被 1s 间隔反复探测。
|
||||
4. **handler 异常就地隔离**:`_invoke` 捕获记日志,`dispatch` 主循环另有兜底;单条事件的单个 handler 失败不影响进程。`background=True` 的 handler 进后台任务(emit 不等它),任务由 emitter 持强引用、`close()` 统一取消。
|
||||
5. **注入约定**:handler 参数按形参标注解析——`payloads.py` 里的 dataclass → 解析后的 payload 对象;`Event` → 事件封装;登记进 `emitter.services` 的组件类型(如 `BotApi`)→ 实例;未标注或未登记 → 回退传**原始 d**(协议事件的 d 可能是标量,如 INVALID_SESSION 的 resumable 布尔;dict 化会丢真值——这里曾出过 bug)。解析是纯同步查表(`get_type_hints` + dict),无框架、无运行时魔法。
|
||||
5. **注入约定**:handler 参数按形参标注解析——`payloads.py` 里的 dataclass → 解析后的 payload 对象;`Event` → 事件封装;登记进 `emitter.services` 的组件类型(如 `BotApi`)→ 实例;未标注或未登记 → 回退传**原始 d**。解析是纯同步查表(`get_type_hints` + 签名缓存),无框架、无运行时魔法。签名与类型标注每个 handler 只解析一次并缓存,但 `services` 命中在每条事件时实时判定,保证"先注册 handler、后登记组件"的装配顺序可用。
|
||||
6. **OpenAPI 请求内核自治**:`api/core.py` 统一处理鉴权头、429 限流重试(优先 `Retry-After`,脏值容错,退避封顶)与 401 刷新 token 重试一次;v2/channel/guild 三个域方法集继承内核组合成 `BotApi`,新增域加模块即可,方法对用户始终平铺在 `BotApi` 上。
|
||||
|
||||
## 扩展点
|
||||
|
||||
- **新增接入方式**(如轮询/其他平台):实现 `Connecter.run()` 与 `BaseProtocol.register()`,在 `run_loop` 的 `match` 装配分支加一个 `case`,`Config.load` 的白名单加一个名字。
|
||||
- **新增接入方式**(如轮询/其他平台):实现 `Connecter.run()` 与 `BaseProtocol.on_frame()`,在 `run_loop` 的 `match` 装配分支加一个 `case`(构造该方式的协议处理器并注入 connecter),`Config.load` 的白名单加一个名字。
|
||||
- **新增业务 API**:在 `api/` 对应域模块(`v2`/`channel`/`guild`)加方法即自动平铺到 `BotApi`,全部获得鉴权、限流重试与错误处理;新增域则新建模块并组合进 `api/__init__.py` 的 `BotApi`。群/单聊消息经 `_post_message` 统一处理 msg_id/msg_seq/media 语义;已有方法覆盖:单聊/群聊消息与富媒体上传、频道消息/撤回/表态、公告、精华消息、日程、禁言、频道信息查询(guild/channels/roles/member)。
|
||||
- **新增事件类型 dataclass**:在 `payloads.py` 定义 dataclass 并登记 `EVENT_TYPES`,handler 即可按类型标注拿解析结果;未收录事件回退传 dict。当前已覆盖 intents.toml 的全部事件组(消息、成员变动、审核、表态、互动、论坛、音频)。
|
||||
|
||||
@@ -125,9 +123,9 @@ sequenceDiagram
|
||||
|
||||
`tests/` 覆盖:
|
||||
|
||||
- 纯逻辑:crypto 往返、intents 解析、Config 校验、`_route` 路由边界、webhook 去重、序列号单调性、handler 异常隔离与 reply 回流、payload dataclass 解析、BotApi 各业务方法的拼参(Recording 子类截获 request)。
|
||||
- 纯逻辑:crypto 往返、intents 解析、Config 校验、协议帧处理(`test_route.py`:IDENTIFY/RESUME 选路、READY 捕获、s 序列号、INVALID_SESSION 重置、业务事件不产生应答)、webhook 去重、序列号单调性、handler 异常隔离、payload dataclass 解析、BotApi 各业务方法的拼参(Recording 子类截获 request)。
|
||||
- **网络集成**(`test_ws_integration.py` / `test_webhook_integration.py`):
|
||||
- WebSocket:aiohttp 假网关(token 接口 + gateway + ws 协议行为)端到端跑通 HELLO → IDENTIFY → READY 会话捕获 → 心跳/ACK(心跳任务随连接生灭)→ 业务事件 dataclass 分发;
|
||||
- WebSocket:aiohttp 假网关(token 接口 + gateway + ws 协议行为)端到端跑通 HELLO → IDENTIFY → READY 会话捕获 → 心跳/ACK(心跳任务随连接生灭)→ 业务事件 dataclass 分发,并断言协议帧(HELLO/HEARTBEAT_ACK)不会触达 handler;
|
||||
- Webhook:真实起 `WebhookConnecter` 的 http server(TestServer),覆盖 op=13 验证应答(签名内容正确性)、合法签名放行分发、坏签名 401、同 id 去重。
|
||||
|
||||
另有 429/401 重试、token 并发锁、op=9 resumable 布尔注入、background 后台并发与 `close()` 收尾的专项单测。两套集成测试验证的都是真实连接路径,改接入层/协议层后跑 `uv run pytest` 即可回归。
|
||||
另有 429/401 重试、token 并发锁、background 后台并发与 `close()` 收尾的专项单测。两套集成测试验证的都是真实连接路径,改接入层/协议层后跑 `uv run pytest` 即可回归。
|
||||
Reference in new issue
Block a user