mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 13:04:49 +00:00
docs: 文档模块化重构(guide/api 多页),文档站迁移 GitHub Pages Actions 部署
GUIDE/API 拆为 docs/guide/ 五页与 docs/api/ 七页,侧边栏分组折叠, 旧链接 alias 映射;安装改为 pip install qqbotsdk-py,README 加徽章; 删除 docs 分支同步工作流,pages.yml 直部署(自定义域名 CNAME 随产物带上)。
This commit is contained in:
1 parent
652a142e86
commit
bcd69099b6
18 files changed
+829
-71
No files matched your search
@@ -0,0 +1,74 @@
|
||||
# 配置与订阅
|
||||
|
||||
`.env` 环境变量、`intents.toml` 事件订阅与接入方式选择。完整字段参考只在本文。
|
||||
|
||||
## .env 配置
|
||||
|
||||
SDK 启动时自动加载项目根目录的 `.env`,`APPID`/`APPSECRET` 缺失启动即报错:
|
||||
|
||||
```ini
|
||||
APPID=你的AppID
|
||||
APPSECRET=你的AppSecret
|
||||
CONNECTER=websocket # websocket 或 webhook,默认 websocket
|
||||
|
||||
BASE_URL=https://api.bot.qq.com # 可选,官方 API 地址
|
||||
TIMEOUT=5000 # 可选,HTTP 请求超时(毫秒)
|
||||
INTENTS_FILE=intents.toml # 可选,事件订阅配置文件路径
|
||||
|
||||
WEBHOOK_HOST=0.0.0.0 # 以下仅 webhook 接入时生效
|
||||
WEBHOOK_PORT=8080
|
||||
WEBHOOK_PATH=/qqbot/webhook
|
||||
```
|
||||
|
||||
| 字段 | 环境变量 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `app_id` | `APPID` | 必填 | 缺失启动即 ValueError |
|
||||
| `app_secret` | `APPSECRET` | 必填 | 同上 |
|
||||
| `connecter` | `CONNECTER` | `websocket` | 仅允许 `websocket`/`webhook` |
|
||||
| `base_url` | `BASE_URL` | `https://api.bot.qq.com` | 官方 API 根地址 |
|
||||
| `timeout` | `TIMEOUT` | `5000`(毫秒) | 转为 aiohttp `ClientTimeout(total=…)` |
|
||||
| `intents_file` | `INTENTS_FILE` | `intents.toml` | 事件订阅配置路径 |
|
||||
| `webhook_host` | `WEBHOOK_HOST` | `0.0.0.0` | webhook 监听地址 |
|
||||
| `webhook_port` | `WEBHOOK_PORT` | `8080` | webhook 监听端口 |
|
||||
| `webhook_path` | `WEBHOOK_PATH` | `/qqbot/webhook` | webhook 回调路径 |
|
||||
|
||||
`Config` 是 frozen dataclass,`Bot()` 构造时一次性读齐;handler 可标注 `config: Config` 注入。
|
||||
|
||||
## 事件订阅(intents.toml)
|
||||
|
||||
SDK 启动时读取 `intents.toml` 计算订阅掩码。订阅粒度就是下面表格里的大类(Intents 分组),把某组设为 `true` 即订阅该组下的全部事件:
|
||||
|
||||
```toml
|
||||
GROUP_AND_C2C_EVENT = true # 群/单聊消息与关注事件
|
||||
INTERACTION = true # 互动事件(卡片按钮回调)
|
||||
GUILDS = false # 频道/子频道增删改
|
||||
```
|
||||
|
||||
QQ 网关只认分组位掩码,**组内单个事件无法单独开关**——能收到哪些事件由订阅了哪些分组决定(各组包含哪些事件见[事件 payload 对照](/docs/api/events.md))。分组与特权要求:
|
||||
|
||||
| 分组 | 位掩码 | 说明 |
|
||||
|---|---|---|
|
||||
| `GUILDS` | 1 << 0 | 频道/子频道增删改 |
|
||||
| `GUILD_MEMBERS` | 1 << 1 | 成员增删改 |
|
||||
| `GUILD_MESSAGES` | 1 << 9 | 频道全量消息,**仅私域机器人** |
|
||||
| `GUILD_MESSAGE_REACTIONS` | 1 << 10 | 频道表情表态 |
|
||||
| `DIRECT_MESSAGE` | 1 << 12 | 频道私信 |
|
||||
| `GROUP_AND_C2C_EVENT` | 1 << 25 | 群/单聊消息与关注事件(群机器人核心) |
|
||||
| `INTERACTION` | 1 << 26 | 互动事件 |
|
||||
| `MESSAGE_AUDIT` | 1 << 27 | 消息审核结果 |
|
||||
| `FORUMS_EVENT` | 1 << 28 | 论坛事件,**仅私域机器人** |
|
||||
| `AUDIO_ACTION` | 1 << 29 | 音频播放/上麦下麦 |
|
||||
| `PUBLIC_GUILD_MESSAGES` | 1 << 30 | 频道公域消息(AT_MESSAGE_CREATE) |
|
||||
|
||||
分组名即 `Intent` 枚举成员名。配置只接受 `分组名 = true/false`;拼错的分组名、非布尔值,以及旧版"表内逐事件开关"的写法都会在启动时直接抛 `ValueError`(与其静默少订阅,不如启动即报错)。文件缺失则告警并按不订阅任何事件启动。
|
||||
|
||||
## 接入方式选择
|
||||
|
||||
| | WebSocket | Webhook |
|
||||
|---|---|---|
|
||||
| 网络要求 | 能访问外网即可,本地开发友好 | 需公网可达 + HTTPS,仅支持 80/443/8080/8443 端口 |
|
||||
| 配置 | 无额外配置 | 平台回调地址填 `https://<域名>:<端口><WEBHOOK_PATH>` |
|
||||
| 连接维护 | SDK 自动心跳、断线指数退避重连并 RESUME 续传;服务端约 1 小时会断开连接,SDK 会在临到期前(约 50 分钟)主动重连续传 | SDK 处理 Ed25519 验签、op=13 验证应答、60s TTL 事件去重 |
|
||||
| 适用 | 长驻进程、需要会话与重连保障 | 无状态部署、回调型服务 |
|
||||
|
||||
两种方式下 handler 的写法完全一致,切换只需改 `CONNECTER`。
|
||||
@@ -0,0 +1,74 @@
|
||||
# 事件处理
|
||||
|
||||
handler 的注册、参数注入约定与后台并发。注入约定表只在本文。
|
||||
|
||||
## 注册
|
||||
|
||||
用装饰器把 handler 注册到 `Bot`,事件名即线上 payload 的 `t` 名(如 `GROUP_AT_MESSAGE_CREATE`)。参数按类型标注注入,可任意混用:
|
||||
|
||||
```python
|
||||
from qqbotsdk import Bot
|
||||
from qqbotsdk.api import BotApi
|
||||
from qqbotsdk.events import Event
|
||||
from qqbotsdk.payloads import GroupAtMessage, Interaction
|
||||
|
||||
bot = Bot()
|
||||
|
||||
|
||||
# 标注 payload dataclass,拿到解析后的对象
|
||||
@bot.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def typed(msg: GroupAtMessage):
|
||||
print(msg.group_openid, msg.user_openid, msg.content)
|
||||
|
||||
|
||||
# payload 与组件任意组合;Event 拿完整信封(如按钮回调要 event_id)
|
||||
@bot.on("INTERACTION_CREATE")
|
||||
async def full(inter: Interaction, event: Event, api: BotApi):
|
||||
...
|
||||
```
|
||||
|
||||
handler 参数按标注解析的完整规则:
|
||||
|
||||
| 参数标注 | 得到 |
|
||||
|---|---|
|
||||
| payload dataclass(如 `GroupAtMessage`,全部类型见[事件 payload](/docs/api/events.md)) | 解析后的 dataclass 对象 |
|
||||
| `Event` | 事件封装对象,只含信封语义(`.raw`/`.data`/`.typed`/`.type`/`.event_id`);事件体字段请标注 payload dataclass 获取 |
|
||||
| 组件类型(`BotApi`/`Bot`/`Session`/`Config` 等已登记进 `bot.services` 的类型) | 按类型登记的实例 |
|
||||
|
||||
无标注或标注以上三者皆非的形参会在分发时抛 `ValueError`(记日志、按 handler 隔离)——SDK 不注入原始数据:要事件体字段就标注 payload dataclass,要完整信封就标注 `Event`。
|
||||
|
||||
## 注册与分发语义
|
||||
|
||||
- 只有**业务事件(op=0)**会分发给你的 handler;协议帧(HELLO/READY/INVALID_SESSION/op=13)由 SDK 的协议处理器就地处理,不触达 handler(`Ready` 是例外,它既是握手结果也是可监听的业务事件)。
|
||||
- 同一函数对同一事件重复注册只生效一次;handler 按注册顺序串行执行(`background=True` 的除外)。
|
||||
- 业务事件处理器的**返回值不会回流**,回复消息请在 handler 里显式调 API。
|
||||
- 单个 handler 抛异常只记录日志,不影响同事件其他 handler 和主循环。
|
||||
- 所在分组未订阅(intents.toml 中为 `false`)的事件不会到达 handler(见[配置与订阅](/docs/guide/config.md))。
|
||||
|
||||
## 后台并发 handler
|
||||
|
||||
默认同一事件的所有 handler 串行执行,某个慢 handler 会延迟后续事件。调用密集、不关心先后顺序的 handler 可加 `background=True` 丢进后台任务,事件流不再等它:
|
||||
|
||||
```python
|
||||
@bot.on("GROUP_AT_MESSAGE_CREATE", background=True)
|
||||
async def slow_work(msg: GroupAtMessage, api: BotApi):
|
||||
... # 耗时处理,不阻塞其他事件的分发
|
||||
```
|
||||
|
||||
代价是同一事件内 handler 间失去先后保证,因此仅用于业务 handler。`bot.run()` 退出时统一取消在飞的后台任务。
|
||||
|
||||
## 自定义组件注入
|
||||
|
||||
`bot.services` 是 `dict[type, Any]` 组件登记表:SDK 启动时登记内置组件(`Bot`/`EventEmitter`/`EventQueue`/`Config`/`Session`/`ClientSession`/`AccessToken`/`BotApi`),你也可以登记自定义类型:
|
||||
|
||||
```python
|
||||
bot = Bot()
|
||||
bot.services[MyService] = MyService(...) # 任意时机登记(启动前后皆可)
|
||||
|
||||
|
||||
@bot.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def with_service(msg: GroupAtMessage, svc: MyService):
|
||||
... # 形参标注 MyService 即拿到实例
|
||||
```
|
||||
|
||||
services 命中在每条事件分发时实时判定,"先注册 handler、后登记组件"的顺序可用。
|
||||
@@ -0,0 +1,92 @@
|
||||
# 收发消息
|
||||
|
||||
被动回复规则、主动消息、富媒体、Markdown 按钮与撤回。各 API 的完整参数见 [API 参考](/docs/api/v2.md)。
|
||||
|
||||
## 被动回复(最常用)
|
||||
|
||||
携带 `msg_id`(回复某条消息)或 `event_id`(响应某类事件,二选一)即为被动回复,**不需要任何消息频率白名单**。两个 id 来源不同:`msg_id` 是消息事件体里的 `d.id`;`event_id` 是**网关推送最外层**的 id(`event.event_id`,形如 `INTERACTION_CREATE:uuid`),不是事件体里的裸 UUID:
|
||||
|
||||
- 群聊:收到消息后 **5 分钟**内可回复,同一 `msg_id` 最多 **5 次**;
|
||||
- 单聊:**60 分钟**内可回复,同一 `msg_id` 最多 **4 次**;
|
||||
- 同一条消息多次回复必须递增 `msg_seq`(相同 `msg_id`+`msg_seq` 会发送失败);
|
||||
- 被动回复同样受内容审核。
|
||||
|
||||
```python
|
||||
await api.post_group_message(
|
||||
group_openid, content="你好", msg_id=msg.id, msg_seq=1,
|
||||
)
|
||||
```
|
||||
|
||||
非消息事件(入群 `GROUP_ADD_ROBOT`、按钮回调 `INTERACTION_CREATE` 等)的被动回复凭据只能取 `event.event_id`(信封顶层 id):
|
||||
|
||||
```python
|
||||
@bot.on("GROUP_ADD_ROBOT")
|
||||
async def on_added(added: GroupAddRobot, event: Event, api: BotApi):
|
||||
await api.post_group_message(
|
||||
added.group_openid, content="大家好~", event_id=event.event_id,
|
||||
)
|
||||
```
|
||||
|
||||
## 主动消息
|
||||
|
||||
不带 `msg_id`/`event_id` 即主动消息,有额外频控(群/单聊每日每对象 1000 条),且默认关闭,需在开放平台申请。用户可在资料卡关闭主动推送(对应 `C2C_MSG_REJECT`/`GROUP_MSG_REJECT` 事件,关闭后发送会失败)。
|
||||
|
||||
## 富媒体(图片/视频/语音/文件)
|
||||
|
||||
先上传拿 `file_info`,再作为 `media` 发送(此时 SDK 自动把 `msg_type` 置为 7,`content` 作媒体下方的文字):
|
||||
|
||||
```python
|
||||
upload = await api.upload_group_file(
|
||||
group_openid, file_type=1, url="https://example.com/pic.png",
|
||||
)
|
||||
await api.post_group_message(
|
||||
group_openid, content="看图", msg_id=msg.id, media=upload,
|
||||
)
|
||||
```
|
||||
|
||||
`file_type`:1 图片、2 视频、3 语音、4 文件。也可在 `upload_*_file` 传 `srv_send_msg=True` 让上传即发送。
|
||||
|
||||
## Markdown 与按钮(msg_type=2)
|
||||
|
||||
`post_group_message`/`post_c2c_message` 直接支持 `markdown=`(字符串或完整 dict,自动 `msg_type=2`,与 `content` 互斥);`button()`/`keyboard()`(`from qqbotsdk.api import button, keyboard`)构造内嵌按钮:
|
||||
|
||||
```python
|
||||
await api.post_group_message(
|
||||
group_openid,
|
||||
markdown="## 标题\n**加粗**正文",
|
||||
keyboard=keyboard(
|
||||
button("点我", data="我的按钮数据"), # 单按钮独占一行
|
||||
[button("赞", data="like"), button("踩", data="dislike")], # 列表同行并排
|
||||
),
|
||||
msg_id=msg.id,
|
||||
)
|
||||
```
|
||||
|
||||
- `button(label, data, *, type=1, permission=2, style=1)`:`type` 1 回调(点击推 `INTERACTION_CREATE`)、0 跳转链接(data 为 url)、2 指令(往输入框插入 data);`permission` 2 所有人 / 1 管理员 / 0 指定用户;`label` 官方限 10 字以内;要补嵌套字段(如 `visited_label`、`prompt`)直接修改返回的 dict。
|
||||
- 其他 body 字段可经 `**extra` 透传(如原生 `msg_type=6` 输入中状态)。
|
||||
|
||||
**处理按钮点击**:点击回调推 `INTERACTION_CREATE` 事件,`inter.button_data` 即被点按钮的 `data`:
|
||||
|
||||
```python
|
||||
@bot.on("INTERACTION_CREATE")
|
||||
async def on_button(inter: Interaction, event: Event, api: BotApi):
|
||||
# 必须应答,否则用户端按钮一直 loading(inter.id 是事件体里的互动 id)
|
||||
await api.respond_interaction(inter.id)
|
||||
await api.post_group_message(
|
||||
inter.group_openid, content=f"你点了:{inter.button_data}",
|
||||
event_id=event.event_id,
|
||||
)
|
||||
```
|
||||
|
||||
- **`event_id` 要取网关帧最外层的 id(`event.event_id`),不能用事件体里的裸 UUID `inter.id`**——`inter.id` 只用于 `respond_interaction`,这是官方文档点名的常见坑;
|
||||
- 需要在 intents.toml 开启 `INTERACTION` 分组;
|
||||
- Markdown 有平台权限要求,无权限时发送会抛 `ApiError`(业务 code 304036/40034127),按需回退纯文本。
|
||||
|
||||
## 流式消息(AI 逐段下发)
|
||||
|
||||
`post_c2c_stream_message` 把回复拆成多片依次下发(适合 LLM 逐段输出):首片不传 `stream_msg_id`,响应里的 `id` 即后续分片要携带的值,`index` 从 0 递增,最后以 `input_state=STREAM_FINISHED` 收尾。参数与常量见 [v2 API](/docs/api/v2.md#流式消息)。
|
||||
|
||||
## 撤回
|
||||
|
||||
- 群/单聊(v2):发出 **2 分钟**内可撤回(`withdraw_group_message`/`withdraw_c2c_message`);
|
||||
- 频道:仅私域机器人可撤回(`withdraw_channel_message`,`hide_tip=True` 隐藏撤回小灰条)。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 部署与常见问题
|
||||
|
||||
## 启动方式
|
||||
|
||||
普通脚本一个 `bot.run()` 阻塞运行(Ctrl+C 优雅退出):
|
||||
|
||||
```python
|
||||
bot = Bot()
|
||||
# ... @bot.on(...) 注册 handler
|
||||
bot.run()
|
||||
```
|
||||
|
||||
要嵌入自己的 asyncio 应用(如先起自定义组件再跑 SDK),用异步入口:
|
||||
|
||||
```python
|
||||
async def amain():
|
||||
async with ClientSession() as http: # 你的前置装配
|
||||
register_handlers(bot, http)
|
||||
await bot.start() # 装配 SDK 并常驻,随外层协程退出
|
||||
```
|
||||
|
||||
- **WebSocket**:进程常驻即可;SDK 自动处理鉴权(IDENTIFY,断线自动 RESUME)、心跳与重连。
|
||||
- **Webhook**:平台回调地址要求公网 HTTPS,本地调试建议用反向代理或内网穿透;开放平台会先发 op=13 验证请求,SDK 自动完成签名应答。
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **启动后频繁重连 / READY 收不到**:QQ 网关接口有频率限制(code 100017),连续重启需间隔约 1 分钟;SDK 已内置退避,等即可。
|
||||
- **`ApiError`**:HTTP ≥400 时抛出,`e.status`/`e.code`/`e.message` 可用于排查(如被动回复超时、msg_seq 重复、内容审核不通过)。
|
||||
- **日志出现 `TokenError: 获取 access_token 失败`**:appid/secret 配置错误或被平台拒绝,`code`/`message` 就是 QQ 返回的真实原因,对照官方错误码排查即可。
|
||||
- **handler 没被触发**:检查 intents.toml 中该事件所属分组是否为 `true`;群/单聊事件需开通对应能力;频道消息事件区分公私域。
|
||||
- **想收原始数据**:标注 `Event`,`event.raw` 是完整线上 payload(含 op/t/d),`event.typed` 是事件体解析结果(未收录事件类型为 dict)。
|
||||
- **日志**:SDK 用 loguru 输出中文日志,连接、重连、API 失败、handler 异常均有记录,可直接用 loguru 配置格式与落盘。
|
||||
@@ -0,0 +1,78 @@
|
||||
# 快速开始
|
||||
|
||||
从零开始接入 QQ 官方机器人:装包、配置、跑起第一个回声机器人。
|
||||
事件处理细节见[事件处理](/docs/guide/handlers.md),收发消息的规则见[收发消息](/docs/guide/messaging.md)。
|
||||
|
||||
## 接入准备
|
||||
|
||||
1. 到 [QQ 开放平台](https://q.qq.com) 注册开发者账号并创建机器人,拿到 `AppID` 与 `AppSecret`(即 `.env` 里的 `APPID`/`APPSECRET`)。
|
||||
2. 确认机器人的可用场景,这决定了你能收哪些事件、发哪些消息:
|
||||
- **群 / 单聊(v2 接口)**:需要在开放平台开通对应能力,事件与消息接口均用 openid(`group_openid` / `user_openid`),与用户的 QQ 号互不可见。
|
||||
- **频道公域机器人**:只能收 `AT_MESSAGE_CREATE`(@机器人才触发),发消息必须带 `msg_id` 被动回复。
|
||||
- **频道私域机器人**:可订阅 `GUILD_MESSAGES`(全量消息)与论坛事件,可主动发消息、撤回消息。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pip install qqbotsdk-py # 或
|
||||
uv add qqbotsdk-py
|
||||
```
|
||||
|
||||
导入名是 `qqbotsdk`(发行包名带 `-py` 后缀是因为 PyPI 上 `qqbotsdk` 与已有项目撞名)。要求 Python ≥ 3.12。
|
||||
|
||||
## 最小可跑示例
|
||||
|
||||
项目根目录放两个文件。
|
||||
|
||||
`.env`(SDK 启动时自动加载,缺失 `APPID`/`APPSECRET` 启动即报错):
|
||||
|
||||
```ini
|
||||
APPID=你的AppID
|
||||
APPSECRET=你的AppSecret
|
||||
```
|
||||
|
||||
`intents.toml`(事件订阅按分组开关,详见[配置与订阅](/docs/guide/config.md)):
|
||||
|
||||
```toml
|
||||
GROUP_AND_C2C_EVENT = true
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```python
|
||||
from qqbotsdk import Bot
|
||||
from qqbotsdk.api import BotApi
|
||||
from qqbotsdk.payloads import GroupAtMessage
|
||||
|
||||
bot = Bot()
|
||||
|
||||
|
||||
# handler 形参按标注注入:payload dataclass 与 BotApi 等组件可任意组合
|
||||
@bot.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def on_group_message(msg: GroupAtMessage, api: BotApi):
|
||||
# 被动回复带 msg_id(5 分钟内有效,多次回复递增 msg_seq);失败抛 ApiError
|
||||
await api.post_group_message(
|
||||
msg.group_openid,
|
||||
content=f"你说:{msg.content}",
|
||||
msg_id=msg.id,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot.run()
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
把机器人拉进测试群,@它说句话,就会收到回声。
|
||||
|
||||
## 下一步
|
||||
|
||||
- 两种接入方式(WebSocket / Webhook)怎么选:[配置与订阅](/docs/guide/config.md)
|
||||
- handler 参数注入、后台并发、自定义组件:[事件处理](/docs/guide/handlers.md)
|
||||
- 富媒体、Markdown 按钮、被动回复时限等消息规则:[收发消息](/docs/guide/messaging.md)
|
||||
- 完整示例(按钮卡片、入群欢迎、单聊、webhook):[examples/](https://github.com/snowy-yang/qqbotsdk/tree/main/examples)
|
||||
Reference in new issue
Block a user