diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..71ef8e0
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,57 @@
+name: 部署文档(GitHub Pages)
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "index.html"
+ - "_sidebar.md"
+ - "README.md"
+ - "docs/**"
+ - "examples/**"
+ - "CHANGELOG.md"
+ - ".github/workflows/pages.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ name: 组装纯文档并部署
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: 检出代码
+ uses: actions/checkout@v4
+
+ # 站点是零构建的 docsify 静态文件:从仓库挑出文档相关文件组装后直接部署。
+ # CNAME 必须随每次部署带上,否则 Actions 部署会把自定义域名设置抹掉。
+ - name: 组装纯文档站点
+ run: |
+ rm -rf site && mkdir site
+ cp index.html README.md _sidebar.md CHANGELOG.md site/
+ cp -r docs examples site/
+ cp .nojekyll site/ 2>/dev/null || true
+ echo "qqbotsdk.4i.hk" > site/CNAME
+ find site -type d -name __pycache__ -exec rm -rf {} +
+
+ - name: 配置 Pages
+ uses: actions/configure-pages@v5
+
+ - name: 上传站点产物
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+
+ - name: 部署
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8dc293..0a3011c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
### 新增
+- **`Bot` 门面类(新启动方式)**:`bot = Bot()` 构造即读 .env 校验配置,`@bot.on(...)` 注册 handler,`bot.run()` 同步启动;`await bot.start()` 供嵌入外部 asyncio 应用。`bot.call_api(method, path, **kwargs)` 通用 REST 入口(带鉴权/429 重试/401 自愈),`bot.api` 为类型化 API 面,`bot.services` 登记自定义组件;handler 可标注 `bot: Bot` 注入实例
+- **CI 与发布**:CI 新增构建 job(`uv build` 产出 sdist/wheel 冒烟);新增 `publish.yml`——打 `v*` 标签自动构建并经 Trusted Publishing(OIDC,无需 token)发布到 PyPI
+- **文档模块化重构**:`docs/GUIDE.md`/`docs/API.md` 拆为 `docs/guide/` 五页(start/config/handlers/messaging/ops)与 `docs/api/` 七页(core/v2/group/guild/panel/audio-forum/events),侧边栏分组折叠,旧链接经 docsify alias 映射;安装指引改为 `pip install qqbotsdk-py`,README 重写为快速开始入口并加 CI/PyPI 徽章
+- **文档站迁移 GitHub Pages(Actions 部署)**:不再同步 `docs` 孤儿分支(已删除),`pages.yml` 组装纯文档产物经 `deploy-pages` 直部署;弃用 Cloudflare Pages 托管,自定义域名 qqbotsdk.4i.hk 改由 CNAME 指向 GitHub Pages
- **旧版(频道侧)OpenAPI 对齐(对照旧版 wiki 全集核对)**:私信(`create_dms` 建会话、`post_dms_message` 发信、`withdraw_dms_message` 撤回);身份组增删改与成员授予/收回;频道成员列表/踢出(可拉黑+撤回历史消息)/身份组成员列表/子频道在线成员数;全员与批量成员禁言;子频道用户/身份组权限查改;频道消息频率设置;接口权限列表与授权链接申请;新增 `audio` 域(播放控制、上/下麦)与 `forum` 域(帖子列表/详情/发帖/删帖)组合进 `BotApi`(`AUDIO_STATUS_*`、`FORUM_FORMAT_*` 常量随模块导出)
- **事件全量对齐**:新增 `GROUP_MEMBER_ADD`/`GROUP_MEMBER_REMOVE`(共用 `GroupMemberChange`)、`GROUP_JOIN_REQUEST`(入群申请,含验证问答与自动审批命中信息,仅机器人是群管理员时推送)、`SUBSCRIBE_MESSAGE_STATUS`(订阅消息授权状态)四个 payload;`Intent` 枚举与 intents.toml 补上此前缺失的 `GROUP_MEMBER_EVENT (1<<24)` 分组(默认 false,需订阅请手动开启)
- **v2 OpenAPI 全量对齐(对照官方 2026-09 新版文档站逐页核对)**:新增 `group` 域方法集组合进 `BotApi`——群信息/成员列表/成员详情/机器人群内状态、入群申请列表与审批、群黑名单增删查、群成员批量移除、群禁言状态查询、入群自动审批策略(增删改查/执行/白名单,机器人维度全局配置)
@@ -47,6 +51,7 @@
### 移除
+- **`main()` / `run_loop()` 与 `qqbotsdk` 控制台脚本入口**(破坏性变更):启动方式统一为 `Bot` 门面——原 `ee = EventEmitter()` + `main(ee)` 改为 `bot = Bot()` + `@bot.on(...)` + `bot.run()`;原 `await run_loop(ee)` 嵌入场景改用 `await bot.start()`。pyproject 的 `[project.scripts]` 随之删除
- **`Event` 的字段名映射属性 `user_id`/`group_id`/`content`/`message_id`**(含旧版群消息 payload 顶层 `from_user_id` 字段的回退):`Event` 收敛为推送信封封装,只保留 `.raw`/`.data`/`.typed`/`.type`/`.event_id`;事件体字段一律标注 payload dataclass(如 `msg: GroupAtMessage`)获取解析结果,或经 `.data`/`.raw` 自取。破坏性变更:原 `event.group_id` 改为 typed payload 的 `msg.group_openid`,`event.message_id` 改为 `msg.id`,以此类推
- `EventEmitter.register_protocol()`、`EventEmitter.handle()` 与 `emitter` 持有协议层的机制(协议处理器改为由 connecter 持有并逐帧调用,见上文"变更")
- apscheduler 依赖(心跳迁入 connecter 后不再需要调度器)
@@ -76,4 +81,4 @@
- **文档**:使用指南、API 参考、架构说明与可运行示例(`examples/`)
- **测试**:纯逻辑单测 + 假网关 WebSocket 端到端 + Webhook 验签/去重端到端
-[0.1.0]: https://gitea.4i.hk/hhhge/qqbotsdk/releases/tag/v0.1.0
+[0.1.0]: https://github.com/snowy-yang/qqbotsdk/releases/tag/v0.1.0
diff --git a/README.md b/README.md
index 63369ce..cccbbd2 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,60 @@
# qqbotsdk
+[](https://github.com/snowy-yang/qqbotsdk/actions/workflows/ci.yml)
+[](https://pypi.org/project/qqbotsdk-py/)
+[](https://pypi.org/project/qqbotsdk-py/)
+[](https://pypistats.org/packages/qqbotsdk-py)
+[](LICENSE)
+
QQ 官方机器人 Python SDK(Python 3.12+ / asyncio / aiohttp),支持 WebSocket 与 Webhook 两种接入方式。
**📖 在线文档:**
-[使用指南](docs/GUIDE.md) · [API 参考](docs/API.md) · [架构](docs/ARCHITECTURE.md) · [示例](examples/) · [更新日志](CHANGELOG.md)
+[快速开始](docs/guide/start.md) · [使用指南](docs/guide/handlers.md) · [API 参考](docs/api/core.md) · [架构](docs/ARCHITECTURE.md) · [示例](examples/) · [更新日志](CHANGELOG.md)
+
+## 安装
+
+```bash
+pip install qqbotsdk-py # 或 uv add qqbotsdk-py
+```
+
+## 五分钟上手
+
+`.env` 配好 `APPID`/`APPSECRET`,`intents.toml` 开启 `GROUP_AND_C2C_EVENT = true`:
+
+```python
+from qqbotsdk import Bot
+from qqbotsdk.api import BotApi
+from qqbotsdk.payloads import GroupAtMessage
+
+bot = Bot()
+
+
+# handler 形参按标注注入 payload 与 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()
+```
+
+Webhook 接入:`.env` 中 `CONNECTER` 改为 `webhook`,再把回调地址配置到 QQ 开放平台(仅支持 80/443/8080/8443 端口,需公网 HTTPS),handler 写法完全一致。
+
+## 特性
+
+- `Bot` 门面:`bot.run()` 一行启动,`bot.call_api` 通用 REST,`bot.api` 类型化 API 面
+- 断线自动重连(指数退避 + 心跳看门狗),重连时自动 RESUME 续传
+- Webhook 接入自带 Ed25519 验签、op=13 验证应答与事件去重
+- 事件订阅由项目根目录的 `intents.toml` 按大类开关
+- Markdown 与内嵌按钮一等支持,`respond_interaction()` 回应按钮回调
+- 组件按类型注入 handler(自定义组件同样可注入),全 SDK 共享一个 HTTP 连接池
## 架构
@@ -24,7 +74,7 @@ flowchart TB
end
subgraph 业务层
API["BotApi(REST + ApiError)"]
- USER["用户 handler @ee.on(...)"]
+ USER["用户 handler @bot.on(...)"]
end
QQ["QQ 开放平台"]
@@ -39,65 +89,7 @@ flowchart TB
WS --> QQ
```
-组件在 `run_loop` 中显式装配,依赖关系与设计决策详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
-
-## 特性
-
-- 断线自动重连(指数退避 + 心跳看门狗),重连时自动 RESUME 续传
-- Webhook 接入自带 Ed25519 验签、op=13 验证应答与事件去重
-- 事件订阅由项目根目录的 `intents.toml` 按大类开关
-- Markdown 与内嵌按钮一等支持,`respond_interaction()` 回应按钮回调
-- 组件显式装配(无全局单例),全 SDK 共享一个 HTTP 连接池
-
-## 配置
-
-`.env`(`APPID`/`APPSECRET` 必填,缺失启动即报错):
-
-```ini
-APPID=你的AppID
-APPSECRET=你的AppSecret
-BASE_URL=https://api.bot.qq.com # 可选
-TIMEOUT=5000 # 可选,毫秒
-CONNECTER=websocket # websocket 或 webhook
-
-# 仅 webhook 接入时
-WEBHOOK_HOST=0.0.0.0
-WEBHOOK_PORT=8080
-WEBHOOK_PATH=/qqbot/webhook
-```
-
-## 使用
-
-```python
-from qqbotsdk import EventEmitter, main
-from qqbotsdk.payloads import GroupAtMessage
-from qqbotsdk.api import BotApi
-
-ee = EventEmitter()
-
-# handler 形参按标注注入 payload 与 BotApi 等组件
-@ee.on("GROUP_AT_MESSAGE_CREATE")
-async def on_group_message(msg: GroupAtMessage, api: BotApi):
- # 被动回复带 msg_id;多次回复递增 msg_seq;失败抛 ApiError
- await api.post_group_message(
- msg.group_openid,
- content=f"你说:{msg.content}",
- msg_id=msg.id,
- )
-
-if __name__ == "__main__":
- main(ee)
-```
-
-运行:
-
-```bash
-uv run python main.py
-```
-
-Webhook 接入:`.env` 中 `CONNECTER` 改为 `webhook`,再把回调地址配置到 QQ 开放平台(仅支持 80/443/8080/8443 端口,需公网 HTTPS)。
-
-完整示例见 [examples/](examples/),更多写法见[使用指南](docs/GUIDE.md)。
+组件在 `Bot.start()` 中装配,依赖关系与设计决策详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
## 开发
@@ -106,14 +98,10 @@ uv run pytest # 单元测试
uv run ruff check src tests examples && uv run pyright
```
-推送与 Pull Request 由 GitHub Actions 自动执行同样的检查([.github/workflows/ci.yml](.github/workflows/ci.yml))。
+推送与 Pull Request 由 GitHub Actions 自动执行同样的检查([.github/workflows/ci.yml](.github/workflows/ci.yml));打 `v*` 标签经 [publish.yml](.github/workflows/publish.yml) 以 Trusted Publishing 发布到 PyPI。
-文档站点基于 [docsify](https://docsify.js.org/),文档变更时自动发布([.github/workflows/deploy-docs.yml](.github/workflows/deploy-docs.yml));本地预览:
+文档站点基于 [docsify](https://docsify.js.org/),文档变更时自动部署到 GitHub Pages([.github/workflows/pages.yml](.github/workflows/pages.yml),自定义域名 qqbotsdk.4i.hk);本地预览:
```bash
npx docsify-cli serve
```
-
-## 待完善
-
-- 更多 OpenAPI 封装(按需在 `api.py` 的 `BotApi` 上加薄封装即可)
diff --git a/_sidebar.md b/_sidebar.md
index e5949c3..d0045a1 100644
--- a/_sidebar.md
+++ b/_sidebar.md
@@ -1,6 +1,18 @@
- [首页](/README.md)
-- [使用指南](/docs/GUIDE.md)
-- [API 参考](/docs/API.md)
+- 使用指南
+ - [快速开始](/docs/guide/start.md)
+ - [配置与订阅](/docs/guide/config.md)
+ - [事件处理](/docs/guide/handlers.md)
+ - [收发消息](/docs/guide/messaging.md)
+ - [部署与常见问题](/docs/guide/ops.md)
+- API 参考
+ - [核心框架](/docs/api/core.md)
+ - [群聊与单聊(v2)](/docs/api/v2.md)
+ - [群管理](/docs/api/group.md)
+ - [频道](/docs/api/guild.md)
+ - [指令面板与菜单](/docs/api/panel.md)
+ - [音频与论坛](/docs/api/audio-forum.md)
+ - [事件 payload](/docs/api/events.md)
- [架构设计](/docs/ARCHITECTURE.md)
- [示例](/examples/README.md)
- [更新日志](/CHANGELOG.md)
diff --git a/docs/api/audio-forum.md b/docs/api/audio-forum.md
new file mode 100644
index 0000000..525d757
--- /dev/null
+++ b/docs/api/audio-forum.md
@@ -0,0 +1,14 @@
+# 音频与论坛 API(私域)
+
+音频接口仅音频类机器人可用(需联系平台开通);论坛仅论坛子频道,发帖/删帖为异步任务(返回 `task_id`,结果经 FORUM_* 事件推送,见[事件 payload](/docs/api/events.md))。
+
+常量:`AUDIO_STATUS_START=0`/`PAUSE=1`/`RESUME=2`/`STOP=3`;`FORUM_FORMAT_TEXT=1`/`HTML=2`/`MARKDOWN=3`/`JSON=4`。
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `control_channel_audio(channel_id, audio_url=None, text=None, status=AUDIO_STATUS_START)` | POST /channels/{channel_id}/audio | 播放控制(START/PAUSE/RESUME/STOP);`audio_url`/`text` 仅 START 时传 |
+| `put_channel_mic(channel_id)` / `delete_channel_mic(channel_id)` | PUT / DELETE /channels/{channel_id}/mic | 机器人上麦 / 下麦 |
+| `get_channel_threads(channel_id)` | GET /channels/{channel_id}/threads | 帖子列表 `{"threads", "is_finish"}` |
+| `get_channel_thread(channel_id, thread_id)` | GET …/threads/{thread_id} | 帖子详情 |
+| `put_channel_thread(channel_id, title, content, format=FORUM_FORMAT_TEXT)` | PUT /channels/{channel_id}/threads | 发帖,`format` 为 FORUM_FORMAT_* 常量;返回 `task_id` |
+| `delete_channel_thread(channel_id, thread_id)` | DELETE …/threads/{thread_id} | 删帖,返回 `task_id` |
diff --git a/docs/api/core.md b/docs/api/core.md
new file mode 100644
index 0000000..8b3fa0e
--- /dev/null
+++ b/docs/api/core.md
@@ -0,0 +1,117 @@
+# 核心框架 API
+
+`Bot` 门面、分发层(`EventEmitter`)、事件封装(`Event`)与错误体系。
+上手流程见[快速开始](/docs/guide/start.md),分层与装配见[架构设计](/docs/ARCHITECTURE.md)。
+
+## 顶层导出
+
+```python
+from qqbotsdk import (
+ Bot, # 门面:on/run/start/call_api/api/services
+ EventEmitter, EventQueue, # 分发层
+ BaseProtocol, # 协议处理器接口(自定义接入方式时继承)
+ Config, Connecter, # 配置与连接器接口
+)
+```
+
+| 名称 | 说明 |
+|---|---|
+| `Bot(config=None)` | 门面类,见下节 |
+| `EventEmitter(queue=None)` | 分发主体,`Bot` 内部持有;`queue` 不传自动创建 |
+| `EventQueue()` | 业务事件通道(连接层 → 分发层,单向);需多处共享时手动传入(`bot.queue` 取实例) |
+| `Config` | frozen dataclass,环境变量一次性读齐(字段表见[配置与订阅](/docs/guide/config.md#env-配置)) |
+| `Connecter` | 连接适配器接口(`typing.Protocol`),按 `CONNECTER` 自动选择实现 |
+| `BaseProtocol` | 协议处理器接口(`protocol.py`),自定义接入方式时实现 `on_frame(payload)` |
+
+子包按需导入:
+
+```python
+from qqbotsdk.api import BotApi, ApiError
+from qqbotsdk.payloads import GroupAtMessage, EVENT_TYPES, parse_event
+from qqbotsdk.events import Event
+```
+
+## Bot
+
+```python
+bot = Bot() # 构造即读 .env 校验配置(fail fast)
+
+@bot.on("GROUP_AT_MESSAGE_CREATE") # 注册 handler,语义与 EventEmitter.on 一致
+async def handler(msg, api): ...
+
+bot.run() # 同步入口:阻塞运行,Ctrl+C 优雅退出
+```
+
+| 成员 | 说明 |
+|---|---|
+| `Bot(config=None)` | 不传 config 时 `Config.load()` 读环境变量;缺 `APPID`/`APPSECRET` 构造即抛 `ValueError` |
+| `bot.on(event, fn=None, *, background=False)` | 注册 handler;`event` 为事件 `t` 名或 `Opcode`,装饰器/直传皆可 |
+| `bot.run()` | 同步入口:`asyncio.run(start())`,`KeyboardInterrupt` 记日志退出 |
+| `await bot.start()` | 异步入口:装配组件并常驻运行,供嵌入外部 asyncio 应用 |
+| `bot.call_api(method, path, **kwargs)` | 通用 REST 入口:带鉴权头、429 退避重试、401 刷新 token 重试;kwargs 透传 aiohttp(`json=`/`params=` 等) |
+| `bot.api` | 类型化 API 门面(`BotApi`,97 个方法),启动(run/start)后可用,之前抛 `RuntimeError` |
+| `bot.services` | `dict[type, Any]` 组件登记表;SDK 启动登记 8 项内置组件,也可登记自定义类型(见[事件处理](/docs/guide/handlers.md#自定义组件注入)) |
+| `bot.queue` | 事件队列实例 |
+| `bot.config` | `Config` 实例 |
+
+## EventEmitter
+
+`Bot` 内部的分发主体(`bot._emitter`),独立导入用于测试或自定义装配:
+
+```python
+ee = EventEmitter()
+
+@ee.on("GROUP_AT_MESSAGE_CREATE") # 业务事件用 t 名(仅 op=0 会分发到这里)
+async def handler(...): ...
+
+@ee.on("GROUP_AT_MESSAGE_CREATE", background=True) # 后台并发执行,不阻塞事件流
+async def slow(...): ...
+```
+
+- **只分发业务事件**:`EventEmitter` 只处理 op=0(DISPATCH)事件,按 payload 的 `t` 名路由。协议帧(HELLO/READY/INVALID_SESSION/op=13 等)由各接入方式的协议处理器(`WebsocketProtocol`/`WebhookProtocol`)在连接器内就地处理,不进事件队列、也不会触达你的 handler。
+- **注册与顺序**:同一函数对同一事件重复注册只生效一次;handler 按注册顺序串行执行(`background=True` 的除外)。
+- **异常隔离**:handler 异常就地记录(loguru),不影响同事件其他 handler 与主循环。
+- **后台并发**:`background=True` 的 handler 进后台任务,emit 不等它;同一事件内失去先后保证,仅用于业务 handler。`bot.run()` 退出时统一取消在飞后台任务。
+- 业务 handler 的返回值不回流;回复消息请在 handler 里显式调 `BotApi`。
+
+handler 参数注入约定表见[事件处理](/docs/guide/handlers.md)。
+
+## Event
+
+`from qqbotsdk.events import Event`,业务事件推送信封(op/t/d/id)的轻量封装,只封装信封语义、不做字段名映射;事件体字段请标注对应 payload dataclass(见[事件 payload](/docs/api/events.md))获取解析结果:
+
+| 属性 | 说明 |
+|---|---|
+| `.raw` | 完整线上 payload(含 op/t/d) |
+| `.data` | `d` 的 dict 拷贝 |
+| `.typed` | `d` 的 dataclass 解析结果(未知事件类型为 dict) |
+| `.type` | 事件 t 名 |
+| `.event_id` | 推送信封顶层的事件 id(与 op/t/d 平级,形如 `INTERACTION_CREATE:uuid`);非消息事件(`INTERACTION_CREATE`/`GROUP_ADD_ROBOT` 等)的被动回复凭据只有这里能拿到,事件体里的裸 UUID 不行(官方点名的常见坑);消息事件的被动回复凭据是事件体的 `d.id`(payload 的 `.id` 字段,即 msg_id),不用这里 |
+
+## 错误体系
+
+`ApiError(RuntimeError)`,重试后仍失败(HTTP ≥400)时抛出:
+
+| 属性 | 说明 |
+|---|---|
+| `.status` | HTTP 状态码 |
+| `.code` | 官方业务 code(响应体中的 `code`) |
+| `.message` | 官方错误描述 |
+
+鉴权阶段(换取 access_token)的失败抛 `TokenError`(`from qqbotsdk.token import TokenError`),与 `ApiError` 同构(`RuntimeError` 子类,同样携带 `.status`/`.code`/`.message`)——appid/secret 配置错误在这里暴露 QQ 返回的真实原因。
+
+`BotApi` 的请求内核(`api/core.py`):自动附带 `Authorization: QQBot ` 鉴权头并刷新 token;429 限流自动重试(最多 3 次,优先 `Retry-After`,退避封顶 30s);401 强制刷新 token 后重试一次。`bot.call_api` 与全部类型化方法共享该内核。
+
+## 内部模块索引
+
+维护者视角的分层与装配见[架构设计](/docs/ARCHITECTURE.md):
+
+| 模块 | 职责 |
+|---|---|
+| `bot.py` | `Bot` 门面:组件装配、`run`/`start` 生命周期 |
+| `connecter.py` | `WebsocketConnecter` / `WebhookConnecter`:接入归一为业务事件流入、出站帧流出 |
+| `protocol.py` | `BaseProtocol` 协议处理器接口(`on_frame`,由连接适配器逐帧调用) |
+| `ws_protocol.py` | IDENTIFY/RESUME 鉴权、会话语义与 `s` 序列号(心跳在 connecter 内) |
+| `webhook_protocol.py` | op=13 验证应答 |
+| `queue.py` / `session.py` / `token.py` / `crypto.py` | 业务事件通道、ws 会话状态、token 缓存、Ed25519 签名 |
+| `model.py` | Opcode、payload 类型、Intent 位掩码 |
diff --git a/docs/api/events.md b/docs/api/events.md
new file mode 100644
index 0000000..5f272a1
--- /dev/null
+++ b/docs/api/events.md
@@ -0,0 +1,57 @@
+# 事件 payload
+
+`qqbotsdk.payloads` 定义事件 `d` 的 dataclass。handler 标注类型即得解析对象;未知事件类型回退原始 dict。所有字段均可为 None(线上缺省即 None),嵌套 `author` 自动解析为 `Author`。事件对照表只在本文。
+
+## 事件名 → dataclass 对照
+
+下表左列是 intents.toml 的订阅分组(订阅粒度即分组,组内事件无法单独开关,见[配置与订阅](/docs/guide/config.md#事件订阅intentstoml)),中列是同时属于该分组、会被一起推送的事件 `t` 名:
+
+| intents 分组 | 事件 t 名 | dataclass |
+|---|---|---|
+| GROUP_AND_C2C_EVENT | `C2C_MESSAGE_CREATE` | `C2CMessage` |
+| | `GROUP_AT_MESSAGE_CREATE` / `GROUP_MESSAGE_CREATE` | `GroupAtMessage` |
+| | `FRIEND_ADD` / `FRIEND_DEL` | `FriendAdd` / `FriendDel` |
+| | `GROUP_ADD_ROBOT` / `GROUP_DEL_ROBOT` | `GroupAddRobot` / `GroupDelRobot` |
+| | `C2C_MSG_REJECT` / `C2C_MSG_RECEIVE` | `C2CMsgReject` / `C2CMsgReceive` |
+| | `GROUP_MSG_REJECT` / `GROUP_MSG_RECEIVE` | `GroupMsgReject` / `GroupMsgReceive` |
+| | `SUBSCRIBE_MESSAGE_STATUS` | `SubscribeMessageStatus` |
+| GROUP_MEMBER_EVENT | `GROUP_MEMBER_ADD` / `GROUP_MEMBER_REMOVE` | `GroupMemberChange` |
+| | `GROUP_JOIN_REQUEST`(仅机器人是群管理员时推送) | `GroupJoinRequest` |
+| PUBLIC_GUILD_MESSAGES | `AT_MESSAGE_CREATE` | `AtMessage` |
+| | `PUBLIC_MESSAGE_DELETE` | `MessageDelete` |
+| GUILD_MESSAGES(私域) | `MESSAGE_DELETE` | `MessageDelete` |
+| DIRECT_MESSAGE | `DIRECT_MESSAGE_CREATE` | `DirectMessage` |
+| GUILDS | `GUILD_CREATE` / `GUILD_UPDATE` / `GUILD_DELETE` | `Guild` |
+| | `CHANNEL_CREATE` / `CHANNEL_UPDATE` / `CHANNEL_DELETE` | `Channel` |
+| GUILD_MESSAGE_REACTIONS | `MESSAGE_REACTION_ADD` / `…_REMOVE` | `MessageReaction` |
+| MESSAGE_AUDIT | `MESSAGE_AUDIT_PASS` / `…_REJECT` | `MessageAudit` |
+| INTERACTION | `INTERACTION_CREATE` | `Interaction` |
+| FORUMS_EVENT(私域) | `FORUM_THREAD_CREATE` / `…_UPDATE` / `…_DELETE` | `ForumThread` |
+| | `FORUM_POST_CREATE` / `…_DELETE` | `ForumPost` |
+| | `FORUM_REPLY_CREATE` / `…_DELETE` | `ForumReply` |
+| AUDIO_ACTION | `AUDIO_START` / `AUDIO_FINISH` / `AUDIO_ON_MIC` / `AUDIO_OFF_MIC` | `AudioAction` |
+| —(DISPATCH 协议事件) | `READY` | `Ready` |
+
+未收录的事件(如私域 `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` 去重。
+
+## 常用 dataclass 字段
+
+```python
+Author # id, user_openid, member_openid, username, bot
+GroupAtMessage # id, content, group_openid, author, timestamp,
+ # message_type, message_scene, attachments, mentions, ark_data, msg_elements
+C2CMessage # id, content, author, attachments, timestamp
+AtMessage # id, content, channel_id, guild_id, author, member, timestamp
+DirectMessage # 同 AtMessage
+GroupJoinRequest # group_openid, join_request_id, member_openid, username, risk_tips,
+ # apply_at, apply_source, invited_by, verify_info, auto_approved, …
+GroupMemberChange # timestamp, group_openid, member_openid, user_openid
+```
+
+- `GroupAtMessage.user_openid` / `C2CMessage.user_openid`:便利属性,从 `author.member_openid`/`author.user_openid` 取发言人标识。
+- 群/单聊场景拿不到用户真实 QQ 号,只有 openid;频道场景 `Author.id`/`username` 有效。
+- 其余事件 dataclass 为 2~5 个平铺字段的同构结构(openid/时间戳/信息 dict),字段名即官方文档字段。
+
+`parse_event(t, d)`:把事件 `d` 解析为对应 dataclass(`Event.typed` 的内部实现),`EVENT_TYPES` 为事件名 → 类的完整登记表。
diff --git a/docs/api/group.md b/docs/api/group.md
new file mode 100644
index 0000000..455ed20
--- /dev/null
+++ b/docs/api/group.md
@@ -0,0 +1,26 @@
+# 群管理 API
+
+`from qqbotsdk.api import BotApi`。多数接口要求机器人拥有群管理员身份;成员/黑名单/批量移除等能力官方标注"内邀接入中",未开通时服务端返回业务码 11253。
+
+入群申请的审批结果也会以事件推送(`GROUP_JOIN_REQUEST` / `GROUP_MEMBER_ADD` 等,见[事件 payload](/docs/api/events.md))。
+
+常量:`APPROVE`/`DECLINE`(审批动作)、`OP_ADD`/`OP_DEL`(黑名单/白名单操作)。
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `get_group_info(group_openid)` | GET /v2/groups/{group_openid}/info | 群名称/简介/分类/标签/成员数 |
+| `get_group_members(group_openid, cursor=None)` | GET …/members | 成员列表,每页最多 30 条,`next_cursor` 翻页(空串=末页) |
+| `get_group_member(group_openid, member_openid)` | GET …/members/{member_openid} | 成员昵称/角色(member/owner/admin)/入群时间 |
+| `get_group_bot_state(group_openid)` | GET …/bot_state | 机器人自身状态:入群时间、是否允许主动推送、群内消息接收设置、角色 |
+| `remove_group_members(group_openid, member_openids, add_to_member_blacklist=False)` | POST …/batch_remove_members | 批量移除,单次最多 20 个,可选拉黑 |
+| `get_group_join_requests(group_openid, cursor=None, limit=None)` | GET …/join_request_list | 入群申请列表(含验证问答),需群管理员;limit 默认 20 最大 50 |
+| `review_group_join_request(group_openid, member_openid, op, join_request_id=None, reject_reason=None, add_to_member_blacklist=None)` | POST …/approval_join_request/{member_openid} | `op`:`APPROVE`/`DECLINE`;拒绝时可附理由并拉黑 |
+| `get_group_mute_state(group_openid)` | GET …/restrict_chat_setting | 禁言状态:全员禁言规则(定时/周期)与被禁言成员列表 |
+| `get_group_blacklist(group_openid, cursor=None, limit=None)` | GET …/member_blacklist | 黑名单列表,limit 默认 20 最大 100 |
+| `update_group_blacklist(group_openid, op, member_openids)` | POST …/member_blacklist | `op`:`OP_ADD`/`OP_DEL`,单次最多 20 个;成员在群中时无法拉黑 |
+| `get_join_approval_strategies(cursor=None, limit=None)` | GET /v2/groups/join_approval_strategy | 自动审批策略列表(机器人维度全局配置),按创建时间倒序 |
+| `create_join_approval_strategy(*, group_openids=None, group_ids=None, is_enable=None, expire_at=None, remark=None)` | POST /v2/groups/join_approval_strategy | 关联群两种标识二选一(互斥,最多 100 个);`is_enable` "on"/"off";默认一年过期;仅机器人有群管理员身份时生效;返回 `strategy_id` |
+| `update_join_approval_strategy(strategy_id, *, is_enable=None, expire_at=None, group_action=None, remark=None)` | PATCH …/{strategy_id} | 只传变更项;`group_action` 为 `{"op": "add"/"del", "group_openids": […]}`,群标识形式须与创建时一致 |
+| `delete_join_approval_strategy(strategy_id)` | DELETE …/{strategy_id} | 删除策略 |
+| `execute_join_approval_strategy(strategy_id)` | POST …/{strategy_id}/execute | 全量扫描关联群并自动通过白名单申请;异步约 10 分钟 |
+| `update_join_approval_strategy_whitelist(strategy_id, op, whitelist_users)` | POST …/{strategy_id}/whitelist_users | QQ 号码字符串列表,单次最多 10000、上限 10 万 |
diff --git a/docs/api/guild.md b/docs/api/guild.md
new file mode 100644
index 0000000..191eb97
--- /dev/null
+++ b/docs/api/guild.md
@@ -0,0 +1,62 @@
+# 频道 API
+
+`from qqbotsdk.api import BotApi`。频道(guild)/子频道(channel)场景的旧版接口,按消息、管理、查询三组列出。标"私域"的接口仅频道私域机器人可用。
+
+频道消息的被动回复规则与群聊不同:公域机器人发消息必须带 `msg_id`。
+
+## 频道消息
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `post_channel_message(channel_id, content="", msg_id=None, msg_seq=1, image=None, **extra)` | POST /channels/{channel_id}/messages | `image` 为图片 URL;embed/ark/markdown 等经 `extra` 透传 |
+| `withdraw_channel_message(channel_id, message_id, hide_tip=False)` | DELETE /channels/{channel_id}/messages/{message_id} | **仅私域机器人**;`hide_tip` 对应官方 `hidetip` |
+| `put_channel_reaction(channel_id, message_id, emoji_type, emoji_id)` | PUT …/reactions/{type}/{emoji_id} | `emoji_type=1` 系统表情,`emoji_id` 为序号字符串 |
+| `delete_channel_reaction(channel_id, message_id, emoji_type, emoji_id)` | DELETE …/reactions/{type}/{emoji_id} | 删除自己的表态 |
+| `create_dms(recipient_id, source_guild_id)` | POST /users/@me/dms | 创建私信会话(需与用户同频道),返回私信的 `guild_id`/`channel_id` |
+| `post_dms_message(guild_id, content="", msg_id=None, msg_seq=1, image=None, **extra)` | POST /dms/{guild_id}/messages | 发私信,参数同 `post_channel_message`;主动消息每天单用户 2 条、累计 200 条,被动不限 |
+| `withdraw_dms_message(guild_id, message_id, hide_tip=False)` | DELETE /dms/{guild_id}/messages/{message_id} | 撤回自己发的私信,**仅私域** |
+| `get_channel_permissions(channel_id, user_id)` / `get_channel_role_permissions(channel_id, role_id)` | GET …/members/{user_id}/permissions · …/roles/{role_id}/permissions | 子频道权限位图(字符串) |
+| `update_channel_permissions(channel_id, user_id, *, add=None, remove=None)` / `update_channel_role_permissions(channel_id, role_id, …)` | PUT 同路径 | `add`/`remove` 为字符串权限位图,至少传一个,需管理员权限 |
+
+## 频道管理
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `create_guild_announce(guild_id, channel_id, message_id, announces_type=0, **extra)` | POST /guilds/{guild_id}/announces | `announces_type`:0 成员公告 / 1 欢迎公告;推荐频道公告经 extra(与消息公告互斥) |
+| `create_channel(guild_id, name, **fields)` | POST /guilds/{guild_id}/channels | 创建子频道,**私域**需管理员权限;`fields` 透传 type/sub_type/position/parent_id/private_type/speak_permission 等;触发 CHANNEL_CREATE |
+| `update_channel(channel_id, **fields)` | PATCH /channels/{channel_id} | 修改子频道,**私域**;只传变更项;触发 CHANNEL_UPDATE |
+| `delete_channel(channel_id)` | DELETE /channels/{channel_id} | 删除子频道,**私域**;不可恢复;触发 CHANNEL_DELETE |
+| `delete_guild_announce(guild_id, message_id)` | DELETE /guilds/{guild_id}/announces/{message_id} | 推荐频道公告传 `message_id="all"` |
+| `pin_channel_message(channel_id, message_id)` | PUT /channels/{channel_id}/pins/{message_id} | 添加精华消息 |
+| `unpin_channel_message(channel_id, message_id)` | DELETE /channels/{channel_id}/pins/{message_id} | 移出精华消息 |
+| `get_channel_pins(channel_id)` | GET /channels/{channel_id}/pins | 精华消息列表 |
+| `get_channel_schedules(channel_id, since=None)` | GET /channels/{channel_id}/schedules | 默认当天日程;`since` 毫秒时间戳 |
+| `get_channel_schedule(channel_id, schedule_id)` | GET /channels/{channel_id}/schedules/{schedule_id} | 单个日程 |
+| `create_channel_schedule(channel_id, name, start_timestamp, end_timestamp, jump_channel_id, remind_type="0", description="")` | POST /channels/{channel_id}/schedules | 时间为毫秒时间戳字符串;`remind_type` "0" 不提醒 / "1" 开始时;请求体自动包在 `schedule` 字段;单管理员每天限 10 次 |
+| `update_channel_schedule(channel_id, schedule_id, **fields)` | PATCH /channels/{channel_id}/schedules/{schedule_id} | `fields` 为 schedule 内字段透传 |
+| `delete_channel_schedule(channel_id, schedule_id)` | DELETE /channels/{channel_id}/schedules/{schedule_id} | 删除日程 |
+| `create_guild_role(guild_id, name="", color=None, hoist=None)` | POST /guilds/{guild_id}/roles | 创建身份组,返回 `{"role_id", "role"}`;`color` 为 ARGB 十进制 |
+| `update_guild_role(guild_id, role_id, name=None, color=None, hoist=None)` | PATCH /guilds/{guild_id}/roles/{role_id} | 只传变更项;系统默认身份组不可改 |
+| `delete_guild_role(guild_id, role_id)` | DELETE /guilds/{guild_id}/roles/{role_id} | 只能删除自建身份组 |
+| `add_guild_member_role(guild_id, user_id, role_id)` / `remove_guild_member_role(…)` | PUT / DELETE /guilds/{guild_id}/members/{user_id}/roles/{role_id} | 授予/收回成员身份组 |
+| `remove_guild_member(guild_id, user_id, add_blacklist=False, delete_history_msg_days=0)` | DELETE /guilds/{guild_id}/members/{user_id} | 踢人,**私域**需踢人权限;撤回消息仅支持 3/7/15/30 或 -1 全部 |
+| `mute_guild(guild_id, mute_end_timestamp=None, mute_seconds=None)` | PATCH /guilds/{guild_id}/mute | 全员禁言,两个时间字段二选一;解除都传 "0" |
+| `mute_guild_members(guild_id, user_ids, …)` | PATCH /guilds/{guild_id}/mute | 批量禁言(带 `user_ids`),单次最多 20 人 |
+| `create_api_permission_demand(guild_id, channel_id, path, method, desc)` | POST /guilds/{guild_id}/api_permission/demand | 生成接口权限授权链接发到子频道,由频道管理员授权 |
+| `mute_guild_member(guild_id, user_id, mute_seconds=0)` | PATCH /guilds/{guild_id}/members/{user_id}/mute | 禁言秒数(上限 28 天),0 解禁 |
+
+## 频道查询
+
+| 方法 | 端点 |
+|---|---|
+| `get_guild(guild_id)` | GET /guilds/{guild_id} |
+| `get_me_guilds(before=None, after=None, limit=None)` | GET /users/@me/guilds |
+| `get_guild_channels(guild_id)` | GET /channels/{guild_id}/channels |
+| `get_guild_members(guild_id)` | GET /guilds/{guild_id}/members |
+| `get_role_members(guild_id, role_id)` | GET /guilds/{guild_id}/roles/{role_id}/members |
+| `get_channel_online_nums(channel_id)` | GET /channels/{channel_id}/online_nums |
+| `get_guild_message_setting(guild_id)` | GET /guilds/{guild_id}/message/setting |
+| `get_guild_api_permissions(guild_id)` | GET /guilds/{guild_id}/api_permission |
+| `get_channel(channel_id)` | GET /channels/{channel_id} |
+| `get_guild_roles(guild_id)` | GET /guilds/{guild_id}/roles |
+| `get_guild_member(guild_id, user_id)` | GET /guilds/{guild_id}/members/{user_id} |
diff --git a/docs/api/panel.md b/docs/api/panel.md
new file mode 100644
index 0000000..a64a371
--- /dev/null
+++ b/docs/api/panel.md
@@ -0,0 +1,16 @@
+# 指令面板与菜单 API
+
+斜线指令面板即用户在聊天输入框输入 `/` 唤起的指令列表,支持 c2c(单聊)、group(群聊)、channel(文字子频道)、dm(频道私信)四种场景;c2c/group 支持 `target_type="specific"` 按指定对象生效(关联对象每次最多 20 个,详情最多查 1000 个),channel/dm 仅全局生效。一个机器人最多 20 个面板,每面板最多 20 个元素(`name` ≤14 字符、`desc` ≤30 字符)。
+
+常量:`SCOPE_C2C`/`SCOPE_GROUP`/`SCOPE_CHANNEL`/`SCOPE_DM`、`TARGET_ALL`/`TARGET_SPECIFIC`、`ITEM_COMMAND`/`ITEM_LINK`、`TARGET_OP_ADD`/`TARGET_OP_DEL`。
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `create_panel(scope, panel, *, target_type=None, user_openids=None, group_openids=None, **extra)` | POST /v2/panels | 返回 `{"panel_id": …}`;`panel` 为 `{"items": [{"type": "command"/"link", "name", "desc", "only_admin", "link"}], "remark"}`,command 类型点击后 `name` 填入输入框,link 类型点击跳转 |
+| `get_panels(scope, cursor=None, limit=None)` | GET /v2/panels | 分页列表(按设置时间倒序),返回 `records`/`next_cursor`(空串=末页)/`is_end`;`limit` 默认 20、最大 50 |
+| `get_panel(panel_id)` | GET /v2/panels/{panel_id} | 面板详情,specific 面板另带关联的 `user_openids`/`group_openids` |
+| `update_panel(panel_id, panel)` | PUT /v2/panels/{panel_id} | 整体覆盖元素与备注,不影响已关联对象;返回 `{"version": …}` |
+| `delete_panel(panel_id)` | DELETE /v2/panels/{panel_id} | 删除面板,删除后对所有对象失效 |
+| `update_panel_targets(panel_id, op, user_openids=None, group_openids=None)` | PUT /v2/panels/{panel_id}/target | `op`:`"add"`/`"del"`;`user_openids` 仅 c2c、`group_openids` 仅 group;channel/dm 与全局面板不支持 |
+| `get_menu()` | GET /v2/menu | 查询全局自定义菜单(仅 C2C、对所有用户生效),返回 `{"version", "menu"}`;未设置过时 `menu` 为空 |
+| `update_menu(menu=None)` | PUT /v2/menu | 整体覆盖菜单(≤10 项,子菜单 ≤5 且不可再嵌套);`send_message` 类型点击后把文本(如 `/help`)填入输入框,常与斜线指令配合 |
diff --git a/docs/api/v2.md b/docs/api/v2.md
new file mode 100644
index 0000000..ccae897
--- /dev/null
+++ b/docs/api/v2.md
@@ -0,0 +1,47 @@
+# 群聊与单聊 API(v2)
+
+`from qqbotsdk.api import BotApi`。经 handler 参数注入(`api: BotApi`)或 `bot.api` 使用;未封装的接口走 `bot.call_api(method, path, **kwargs)`。
+
+消息规则(被动回复时限、msg_seq、富媒体流程)见[收发消息](/docs/guide/messaging.md)。
+
+## 群聊(群 openid)
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `post_group_message(group_openid, content="", msg_id=None, msg_seq=1, media=None, event_id=None, markdown=None, keyboard=None, **extra)` | POST /v2/groups/{group_openid}/messages | `msg_id`/`event_id` 二选一即被动回复(`event_id` 支持 GROUP_ADD_ROBOT / INTERACTION_CREATE 等事件);`media` 传上传返回的 `{"file_info": …}`(自动 msg_type=7);`markdown` 为字符串或完整 dict(自动 msg_type=2,与 content 互斥);`keyboard` 为内嵌键盘 `{"content": {"rows": [...]}}` 或 `{"id": …}`;其余字段经 `extra` 透传 |
+| `upload_group_file(group_openid, file_type, url, srv_send_msg=False)` | POST /v2/groups/{group_openid}/files | 富媒体上传,返回含 `file_info`;`srv_send_msg=True` 上传即发送 |
+| `withdraw_group_message(group_openid, message_id)` | DELETE /v2/groups/{group_openid}/messages/{message_id} | 发出 2 分钟内可撤回 |
+| `mute_group_member(group_openid, mute_expire_at)` | POST /v2/groups/{group_openid}/restrict_chat_setting | `mute_expire_at` 为 RFC3339 到期时间;传早于当前的时间即解禁 |
+| `prepare_group_upload(group_openid, file_type, file_size, file_name, md5, sha1, md5_10m)` | POST /v2/groups/{group_openid}/upload_prepare | 大文件分片预上传,返回 `upload_id`/`block_size`/各分片预签名 `parts`;逐片 PUT 后调 `finish_group_upload_part`,全部完成后携 `upload_id` 调 `upload_group_file` 合并;`md5_10m` 为文件前约 10MB 的 MD5 |
+| `finish_group_upload_part(group_openid, upload_id, part_index, block_size=None, md5=None)` | POST …/upload_part_finish | 每个分片 PUT 成功后调用确认 |
+
+## 单聊(用户 openid)
+
+| 方法 | 端点 | 说明 |
+|---|---|---|
+| `post_c2c_message(openid, content="", msg_id=None, msg_seq=1, media=None, event_id=None, markdown=None, keyboard=None, **extra)` | POST /v2/users/{openid}/messages | 同 `post_group_message` |
+| `upload_c2c_file(openid, file_type, url, srv_send_msg=False)` | POST /v2/users/{openid}/files | 同 `upload_group_file` |
+| `withdraw_c2c_message(openid, message_id)` | DELETE /v2/users/{openid}/messages/{message_id} | 2 分钟内可撤回 |
+| `post_c2c_stream_message(openid, content_raw="", *, stream_msg_id=None, index=0, input_state=STREAM_GENERATING, input_mode=None, content_type=STREAM_CONTENT_MARKDOWN, msg_id=None, event_id=None, msg_seq=None, is_wakeup=None)` | POST /v2/users/{openid}/stream_messages | 流式分批发送,见下节 |
+| `prepare_c2c_upload(openid, …)` / `finish_c2c_upload_part(openid, …)` | POST /v2/users/{openid}/upload_prepare · upload_part_finish | 单聊分片上传,用法同群聊 |
+
+## 流式消息
+
+把一条回复拆成多片依次下发(LLM 逐段输出的标准玩法):
+
+- 首片不传 `stream_msg_id`,响应 `id` 即后续分片要携带的值;`index` 从 0 递增;
+- 以 `input_state=STREAM_FINISHED` 收尾;
+- `input_mode="replace"` 时 `content_raw` 须以上游已下发前缀开头;
+- `is_wakeup=True` 为召回消息,不校验被动回复有效期。
+
+## 通用方法与常量
+
+| 方法 | 说明 |
+|---|---|
+| `me()` | 机器人自身信息(GET /users/@me) |
+| `generate_url_link(callback_data=None)` | 生成机器人分享链接(POST /v2/generate_url_link),返回 `{"data": {"url": …}}`;用户经链接添加时 `callback_data`(≤32 字符)透传给开发者 |
+| `respond_interaction(interaction_id, code=0)` | 回应互动事件(PUT /interactions/{id});按钮点击收到后必须回应,否则客户端持续 loading,同一 id 仅可回应一次 |
+
+常量:`MSG_TYPE_TEXT=0`、`MSG_TYPE_MARKDOWN=2`、`MSG_TYPE_MEDIA=7`;`FILE_TYPE_IMAGE=1`、`FILE_TYPE_VIDEO=2`、`FILE_TYPE_VOICE=3`、`FILE_TYPE_FILE=4`;流式 `STREAM_INPUT_APPEND`/`STREAM_INPUT_REPLACE`、`STREAM_GENERATING=1`/`STREAM_FINISHED=10`、`STREAM_CONTENT_TEXT`/`STREAM_CONTENT_MARKDOWN`。
+
+按钮构造:`button(label, data, *, type=1, permission=2, style=1)` 生成单个按钮 dict,`keyboard(*rows)` 组装为 `keyboard=` 参数(单按钮独占一行,按钮列表同行并排)。点击回调经 `INTERACTION_CREATE` 事件处理,见[收发消息](/docs/guide/messaging.md#markdown-与按钮msg_type2)。
diff --git a/docs/guide/config.md b/docs/guide/config.md
new file mode 100644
index 0000000..034af2c
--- /dev/null
+++ b/docs/guide/config.md
@@ -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://<域名>:<端口>` |
+| 连接维护 | SDK 自动心跳、断线指数退避重连并 RESUME 续传;服务端约 1 小时会断开连接,SDK 会在临到期前(约 50 分钟)主动重连续传 | SDK 处理 Ed25519 验签、op=13 验证应答、60s TTL 事件去重 |
+| 适用 | 长驻进程、需要会话与重连保障 | 无状态部署、回调型服务 |
+
+两种方式下 handler 的写法完全一致,切换只需改 `CONNECTER`。
diff --git a/docs/guide/handlers.md b/docs/guide/handlers.md
new file mode 100644
index 0000000..5b3ce60
--- /dev/null
+++ b/docs/guide/handlers.md
@@ -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、后登记组件"的顺序可用。
diff --git a/docs/guide/messaging.md b/docs/guide/messaging.md
new file mode 100644
index 0000000..0ac751a
--- /dev/null
+++ b/docs/guide/messaging.md
@@ -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` 隐藏撤回小灰条)。
diff --git a/docs/guide/ops.md b/docs/guide/ops.md
new file mode 100644
index 0000000..8ef769a
--- /dev/null
+++ b/docs/guide/ops.md
@@ -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 配置格式与落盘。
diff --git a/docs/guide/start.md b/docs/guide/start.md
new file mode 100644
index 0000000..ce388fd
--- /dev/null
+++ b/docs/guide/start.md
@@ -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)
diff --git a/examples/README.md b/examples/README.md
index 885e28c..d11fb43 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -22,4 +22,4 @@ uv run python examples/webhook_bot/main.py
Webhook 示例启动后,把 `https://<你的域名>:<端口>/qqbot/webhook` 填到开放平台回调地址;保存时平台先发 op=13 验证请求,SDK 自动完成签名应答。
-接入方式切换与 handler 写法详见 [使用指南](docs/GUIDE.md#接入方式选择)。
+接入方式切换见[配置与订阅](../docs/guide/config.md#接入方式选择),handler 写法见[事件处理](../docs/guide/handlers.md)。
diff --git a/index.html b/index.html
index c676f85..f55f9a6 100644
--- a/index.html
+++ b/index.html
@@ -28,6 +28,8 @@
// 相对链接按当前页面所在目录解析(与 GitHub 行为一致),docs/*.md 的互链才能正确跳转
relativePath: true,
loadSidebar: true,
+ // 侧边栏分组("使用指南"/"API 参考")默认展开一级
+ sidebarDisplayLevel: 1,
subMaxLevel: 3,
auto2top: true,
themeColor: '#0f8bff',
@@ -55,6 +57,9 @@
// 缺失文件回退 index.html 且状态 200,docsify 误当作侧栏解析导致目录空白
'/docs/_sidebar.md': '/_sidebar.md',
'/examples/_sidebar.md': '/_sidebar.md',
+ // 文档模块化重构(GUIDE/API 拆为 guide/ 与 api/ 多页)后的旧链接映射
+ '/docs/GUIDE.md': '/docs/guide/start.md',
+ '/docs/API.md': '/docs/api/core.md',
},
plugins: [
// 把 ```mermaid 代码块替换为 div 后交给 mermaid 渲染(架构图/时序图)
@@ -77,6 +82,8 @@
+
+