mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 16:14:49 +00:00
refactor!: 移除 dishka,改为内置裸标注参数注入
- handler 形参直接标注组件类型(api: BotApi)即注入,解析为 emitter 内同步查表(payload dataclass / Event / services 登记类型 / 回退原始 dict)
This commit is contained in:
1 parent
953d44f62e
commit
cff0c142c2
16 files changed
+161
-240
No files matched your search
+7
-9
@@ -6,21 +6,19 @@
|
||||
|
||||
```python
|
||||
from qqbotsdk import (
|
||||
main, run_loop, # 入口
|
||||
main, run_loop, # 入口(组件在 run_loop 显式装配)
|
||||
EventEmitter, BaseProtocol, EventQueue, # 分发层
|
||||
Inject, make_container, # DI
|
||||
Config, Connecter, # 配置与连接器接口
|
||||
)
|
||||
```
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `main(emitter=None)` | 同步入口:装配容器并阻塞运行,Ctrl+C 优雅退出 |
|
||||
| `main(emitter=None)` | 同步入口:装配组件并阻塞运行,Ctrl+C 优雅退出 |
|
||||
| `run_loop(emitter=None)` | 异步入口:`asyncio.run(run_loop(ee))` 的内部实现,可自行 await |
|
||||
| `EventEmitter(queue)` | 分发主体,`@ee.on(...)` 注册 handler |
|
||||
| `EventQueue()` | 事件/应答队列;自建 emitter 时传入,容器会复用同一实例 |
|
||||
| `Inject` | dishka `FromDishka` 别名,handler 参数标注 `Inject[T]` 即注入 T |
|
||||
| `make_container(emitter=None)` | 构建 dishka 容器;一般不用直接调,`main` 已封装 |
|
||||
| `ee.services` | `dict[type, Any]`,按类型登记可注入组件;`run_loop` 装配 SDK 内置组件,也可登记自定义类型 |
|
||||
| `EventQueue()` | 事件/应答队列;自建 emitter 时传入,`run_loop` 复用同一实例 |
|
||||
| `Config` | frozen dataclass,环境变量一次性读齐(见下文) |
|
||||
| `Connecter` | 连接适配器 Protocol(接口),按 `CONNECTER` 自动选择实现 |
|
||||
| `BaseProtocol` | 协议层抽象基类,自定义协议处理器时继承并实现 `register(emitter)` |
|
||||
@@ -55,7 +53,7 @@ async def handler(...): ...
|
||||
|---|---|---|
|
||||
| payload dataclass(`payloads` 中的类型) | `Event.typed` 解析对象 | 回退传原始 dict |
|
||||
| `Event` | 事件封装对象 | — |
|
||||
| `Inject[T]` | DI 容器解析的 T(如 `BotApi`) | 回退传原始 dict |
|
||||
| 组件类型(`BotApi`/`Session`/`Config` 等已登记进 `ee.services` 的类型) | 登记的实例 | 回退传原始 dict |
|
||||
| 无标注 | 原始事件 dict | — |
|
||||
|
||||
## Event
|
||||
@@ -91,7 +89,7 @@ async def handler(...): ...
|
||||
|
||||
## BotApi
|
||||
|
||||
`from qqbotsdk.api import BotApi`。经 DI 注入(`api: Inject[BotApi]`)。自动附带 `Authorization: QQBot <token>` 鉴权头并刷新 token;HTTP ≥400 抛 `ApiError`。
|
||||
`from qqbotsdk.api import BotApi`。经参数注入(`api: BotApi`)。自动附带 `Authorization: QQBot <token>` 鉴权头并刷新 token;HTTP ≥400 抛 `ApiError`。
|
||||
|
||||
### 通用
|
||||
|
||||
@@ -224,4 +222,4 @@ DirectMessage # 同 AtMessage
|
||||
| `webhook_protocol.py` | op=13 验证应答 |
|
||||
| `queue.py` / `session.py` / `token.py` / `crypto.py` | 队列、ws 会话状态、token 缓存、Ed25519 签名 |
|
||||
| `model.py` | Opcode、payload 类型、Intent 位掩码 |
|
||||
| `di.py` | dishka provider 与 `ADAPTERS` 接线表 |
|
||||
| `__init__.py` | `main`/`run_loop` 入口与组件显式装配 |
|
||||
+13
-12
@@ -13,7 +13,7 @@ flowchart TB
|
||||
|
||||
subgraph 分发层["分发层 emitter.py"]
|
||||
Q[("EventQueue\nmsg_queue / reply_queue")]
|
||||
EE["EventEmitter\nop/t 路由 + DI 注入\nhandler 异常隔离"]
|
||||
EE["EventEmitter\nop/t 路由 + 参数注入\nhandler 异常隔离"]
|
||||
end
|
||||
|
||||
subgraph 协议层["协议层 BaseProtocol"]
|
||||
@@ -41,7 +41,7 @@ flowchart TB
|
||||
EE -- "on_payload" --> WSP
|
||||
WSP -- "IDENTIFY/HEARTBEAT 等\npayload 返回值" --> Q
|
||||
EE -- "Event 注入" --> USER
|
||||
USER -- "Inject[BotApi]" --> API
|
||||
USER -- "api: BotApi" --> API
|
||||
API --> QQ
|
||||
WS --> QQ
|
||||
TK --> HTTP
|
||||
@@ -49,7 +49,7 @@ flowchart TB
|
||||
WSP --> TK
|
||||
WHP --> CRY
|
||||
WH --> CRY
|
||||
CFG -. "构造注入" .-> WS & WH & API & TK & WSP & WHP
|
||||
CFG -. "构造传参" .-> WS & WH & API & TK & WSP & WHP
|
||||
```
|
||||
|
||||
## 数据流(WebSocket 接入为例)
|
||||
@@ -80,7 +80,7 @@ sequenceDiagram
|
||||
QQ->>WS: DISPATCH 事件 (op=0, t=GROUP_AT_MESSAGE_CREATE)
|
||||
WS->>Q: put_event
|
||||
Q->>EE: dispatch 泵
|
||||
EE->>H: 开 REQUEST scope,注入 Event/Inject[BotApi]
|
||||
EE->>H: 按形参标注注入 Event/BotApi
|
||||
H->>EE: 返回值不回流(op=0)
|
||||
```
|
||||
|
||||
@@ -91,7 +91,7 @@ sequenceDiagram
|
||||
| 层 | 模块 | 职责 | 不允许出现 |
|
||||
|---|---|---|---|
|
||||
| 接入层 | `connecter.py` | 传输细节:握手、重连、验签、去重 | 事件业务语义、handler 调用 |
|
||||
| 分发层 | `emitter.py` | 路由、DI 注入、异常隔离、应答回流 | 传输细节、具体协议状态机 |
|
||||
| 分发层 | `emitter.py` | 路由、参数注入、异常隔离、应答回流 | 传输细节、具体协议状态机 |
|
||||
| 协议层 | `ws_protocol.py` `webhook_protocol.py` | 鉴权/心跳/验证等协议状态机 | 传输细节(只经队列收发) |
|
||||
| 业务层 | `api.py` `payloads.py` + 用户代码 | OpenAPI 调用、payload 解析与机器人逻辑 | 协议细节 |
|
||||
|
||||
@@ -99,22 +99,23 @@ sequenceDiagram
|
||||
|
||||
## 生命周期与依赖装配
|
||||
|
||||
- 全部组件由 dishka 以 **APP scope** 构造注入(`di.py`),无任何全局单例。
|
||||
- 唯一的 `ClientSession` 以 async-generator provider 注册,`container.close()` 时统一释放。
|
||||
- 接入方式的选择收敛在 `ADAPTERS` 表:`make_container()` 按 `CONNECTER` 环境变量动态 `provider.provide(实现类, provides=抽象)`,主体代码只依赖 `BaseProtocol`/`Connecter` 抽象。
|
||||
- 用户在 `main(ee)` 之前构造的 `EventEmitter` 会被容器复用(连同它的队列),保证 handler 注册在主体实际使用的实例上。
|
||||
- 全部组件在 `run_loop`(`__init__.py`)显式构造装配,无 DI 框架,也无任何全局单例。
|
||||
- 组件按类型登记进 `emitter.services`,handler 形参标注类型即得实例;用户自定义组件同样登记即可注入。
|
||||
- 唯一的 `ClientSession` 在 `run_loop` 构造,`finally` 中统一释放(全 SDK 共享一个连接池)。
|
||||
- 接入方式的选择收敛在 `run_loop` 的 `match config.connecter` 装配分支,主体代码只依赖 `BaseProtocol`/`Connecter` 抽象。
|
||||
- 用户在 `main(ee)` 之前构造的 `EventEmitter` 会被复用(连同它的队列),保证 handler 注册在主体实际使用的实例上。
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
1. **管线而非回调直调**:事件一律经 `EventQueue` 异步流转,接入与分发两侧解耦——Webhook handler 里不需要知道 ws 的存在,反之亦然。代价是多一跳队列延迟(可忽略)。
|
||||
2. **协议层状态机独立**(`BaseProtocol.register/on_payload`):emitter 不认识"心跳""序列号"这些 ws 概念;协议类通过返回 payload dict 经 reply 队列外发。
|
||||
3. **看门狗两层互补**(见 `connecter.py` 模块注释):静默超时抓连接彻底失联,ACK 期限检查抓"有事件流但心跳已死"。阈值 `_SILENCE_FACTOR=1.1`、`_ACK_FACTOR=1.2` 是按 ACK 自然节奏校准的,**不能压到 1× 整**,否则正常抖动会周期性误杀(误杀由 RESUME 兜底,不丢消息)。
|
||||
4. **handler 异常就地隔离**:`_invoke`/`_call_direct` 捕获记日志,`dispatch` 主循环另有兜底;单条事件的单个 handler 失败不影响进程。
|
||||
5. **注入约定**:handler 参数标注 `Inject[T]`(或裸标 `Event`/`Session`/`BotApi`)→ 从 dishka scope 解析;标注 `payloads.py` 里的 dataclass 类型 → 拿到解析后的 payload 对象(无容器路径同样生效);未标注或解析失败 → 回退传原始 `d`,保持旧处理器兼容。注意 `Inject` 不能改成 PEP 695 懒别名(`get_type_hints` 不解包)。
|
||||
4. **handler 异常就地隔离**:`_invoke` 捕获记日志,`dispatch` 主循环另有兜底;单条事件的单个 handler 失败不影响进程。
|
||||
5. **注入约定**:handler 参数按形参标注解析——`payloads.py` 里的 dataclass → 解析后的 payload 对象;`Event` → 事件封装;登记进 `emitter.services` 的组件类型(如 `BotApi`)→ 实例;未标注或未登记 → 回退传原始 `d`,保持旧处理器兼容。解析是纯同步查表(`get_type_hints` + dict),无框架、无运行时魔法。
|
||||
|
||||
## 扩展点
|
||||
|
||||
- **新增接入方式**(如轮询/其他平台):实现 `Connecter.run()` 与 `BaseProtocol.register()`,在 `di.ADAPTERS` 注册一行,`Config.load` 的白名单加一个名字。
|
||||
- **新增接入方式**(如轮询/其他平台):实现 `Connecter.run()` 与 `BaseProtocol.register()`,在 `run_loop` 的 `match` 装配分支加一个 `case`,`Config.load` 的白名单加一个名字。
|
||||
- **新增业务 API**:在 `api.py` 的 `BotApi` 上加方法(或独立模块组合 `BotApi`),全部自动获得鉴权、超时与错误处理。群/单聊消息经 `_post_message` 统一处理 msg_id/msg_seq/media 语义;已有方法覆盖:单聊/群聊消息与富媒体上传、频道消息/撤回/表态、公告、精华消息、日程、禁言、频道信息查询(guild/channels/roles/member)。
|
||||
- **新增事件类型 dataclass**:在 `payloads.py` 定义 dataclass 并登记 `EVENT_TYPES`,handler 即可按类型标注拿解析结果;未收录事件回退传 dict。当前已覆盖 intents.toml 的全部事件组(消息、成员变动、审核、表态、互动、论坛、音频)。
|
||||
|
||||
|
||||
+5
-4
@@ -76,7 +76,7 @@ SDK 启动时读取 `intents.toml` 计算订阅掩码,把想收的事件改成
|
||||
用装饰器把 handler 注册到 `EventEmitter`,事件名即线上 payload 的 `t` 名(如 `GROUP_AT_MESSAGE_CREATE`)。三种写法可混用:
|
||||
|
||||
```python
|
||||
from qqbotsdk import EventEmitter, EventQueue, Inject
|
||||
from qqbotsdk import EventEmitter, EventQueue
|
||||
|
||||
ee = EventEmitter(EventQueue())
|
||||
|
||||
@@ -92,12 +92,12 @@ from qqbotsdk.payloads import GroupAtMessage
|
||||
async def typed(msg: GroupAtMessage):
|
||||
print(msg.group_openid, msg.user_openid, msg.content)
|
||||
|
||||
# 方式三:依赖注入
|
||||
# 方式三:参数注入,标注组件类型即拿到实例
|
||||
from qqbotsdk.api import BotApi
|
||||
from qqbotsdk.events import Event
|
||||
|
||||
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def di(event: Event, api: Inject[BotApi]):
|
||||
async def di(event: Event, api: BotApi):
|
||||
await api.post_group_message(event.group_id, content="收到", msg_id=event.message_id)
|
||||
```
|
||||
|
||||
@@ -107,7 +107,7 @@ handler 参数按标注解析的完整规则:
|
||||
|---|---|
|
||||
| payload dataclass(如 `GroupAtMessage`) | 解析后的 dataclass 对象;未收录事件回退原始 dict |
|
||||
| `Event` | 事件封装对象(`.raw`/`.data`/`.typed`/`.user_id`/`.group_id`/`.content`/`.message_id`) |
|
||||
| `Inject[T]` | DI 容器解析的 `T`(如 `BotApi`);解析失败回退传原始 dict |
|
||||
| 组件类型(`BotApi`/`Session`/`Config` 等) | `emitter.services` 中按类型登记的实例;未登记回退传原始 dict |
|
||||
| 无标注 | 原始事件 dict |
|
||||
|
||||
注意:
|
||||
@@ -115,6 +115,7 @@ handler 参数按标注解析的完整规则:
|
||||
- 业务事件(op=0)处理器的**返回值不会回流**,回复消息请在 handler 里显式调 API。
|
||||
- 单个 handler 抛异常只记录日志,不影响同事件其他 handler 和主循环。
|
||||
- 未订阅(intents.toml 为 `false`)的事件不会到达 handler。
|
||||
- 自定义组件也能注入:`ee.services[MyService] = MyService(...)` 登记后,handler 形参标注 `MyService` 即可拿到实例。
|
||||
|
||||
## 发消息
|
||||
|
||||
|
||||
Reference in new issue
Block a user