qqbotsdk-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qqbotsdk_py-0.1.0/LICENSE +21 -0
- qqbotsdk_py-0.1.0/PKG-INFO +147 -0
- qqbotsdk_py-0.1.0/README.md +119 -0
- qqbotsdk_py-0.1.0/pyproject.toml +118 -0
- qqbotsdk_py-0.1.0/pyproject.toml.orig +100 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/__init__.py +83 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/__init__.py +39 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/audio.py +45 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/channel.py +175 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/core.py +112 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/forum.py +59 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/group.py +265 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/guild.py +369 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/panel.py +137 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/api/v2.py +407 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/config.py +47 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/connecter.py +345 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/crypto.py +41 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/emitter.py +173 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/events.py +55 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/model.py +103 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/payloads.py +431 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/protocol.py +24 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/queue.py +22 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/session.py +19 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/token.py +80 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/webhook_protocol.py +33 -0
- qqbotsdk_py-0.1.0/src/qqbotsdk/ws_protocol.py +108 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 snowy-yang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qqbotsdk-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: QQ 官方机器人 Python SDK(WebSocket / Webhook)
|
|
5
|
+
Keywords: qq,qqbot,bot,sdk,websocket,webhook,机器人
|
|
6
|
+
Author: snowy-yang
|
|
7
|
+
Author-email: snowy-yang <102860110+snowy-yang@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Dist: aiohttp>=3.14.3
|
|
19
|
+
Requires-Dist: cryptography>=50.0.1
|
|
20
|
+
Requires-Dist: loguru>=0.7.3
|
|
21
|
+
Requires-Dist: python-dotenv>=1.2.3
|
|
22
|
+
Requires-Dist: ujson>=6.0.0
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Homepage, https://github.com/snowy-yang/qqbotsdk
|
|
25
|
+
Project-URL: Repository, https://github.com/snowy-yang/qqbotsdk
|
|
26
|
+
Project-URL: Changelog, https://github.com/snowy-yang/qqbotsdk/blob/main/CHANGELOG.md
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# qqbotsdk
|
|
30
|
+
|
|
31
|
+
QQ 官方机器人 Python SDK(Python 3.12+ / asyncio / aiohttp),支持 WebSocket 与 Webhook 两种接入方式。
|
|
32
|
+
|
|
33
|
+
**📖 在线文档:<https://qqbotsdk.4i.hk/>**
|
|
34
|
+
|
|
35
|
+
[使用指南](docs/GUIDE.md) · [API 参考](docs/API.md) · [架构](docs/ARCHITECTURE.md) · [示例](examples/) · [更新日志](CHANGELOG.md)
|
|
36
|
+
|
|
37
|
+
## 架构
|
|
38
|
+
|
|
39
|
+
```mermaid
|
|
40
|
+
flowchart TB
|
|
41
|
+
subgraph 接入层
|
|
42
|
+
WS["WebsocketConnecter(ws 长连接 / 重连 / 心跳看门狗)"]
|
|
43
|
+
WH["WebhookConnecter(http 回调 / 验签 / 去重)"]
|
|
44
|
+
end
|
|
45
|
+
subgraph 协议层
|
|
46
|
+
WSP["WebsocketProtocol(握手 / 会话 / 序列号)"]
|
|
47
|
+
WHP["WebhookProtocol(op=13 验证应答)"]
|
|
48
|
+
end
|
|
49
|
+
subgraph 分发层
|
|
50
|
+
Q[("EventQueue<br/>业务事件 op=0")]
|
|
51
|
+
EE["EventEmitter(路由 / 参数注入 / 异常隔离)"]
|
|
52
|
+
end
|
|
53
|
+
subgraph 业务层
|
|
54
|
+
API["BotApi(REST + ApiError)"]
|
|
55
|
+
USER["用户 handler @ee.on(...)"]
|
|
56
|
+
end
|
|
57
|
+
QQ["QQ 开放平台"]
|
|
58
|
+
|
|
59
|
+
WS -. "逐帧 on_frame" .-> WSP
|
|
60
|
+
WH -. "op=13 逐帧 on_frame" .-> WHP
|
|
61
|
+
WS -->|"业务事件"| Q
|
|
62
|
+
WH -->|"业务事件"| Q
|
|
63
|
+
Q --> EE
|
|
64
|
+
EE --> USER
|
|
65
|
+
USER --> API
|
|
66
|
+
API --> QQ
|
|
67
|
+
WS --> QQ
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
组件在 `run_loop` 中显式装配,依赖关系与设计决策详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
|
|
71
|
+
|
|
72
|
+
## 特性
|
|
73
|
+
|
|
74
|
+
- 断线自动重连(指数退避 + 心跳看门狗),重连时自动 RESUME 续传
|
|
75
|
+
- Webhook 接入自带 Ed25519 验签、op=13 验证应答与事件去重
|
|
76
|
+
- 事件订阅由项目根目录的 `intents.toml` 按大类开关
|
|
77
|
+
- Markdown 与内嵌按钮一等支持,`respond_interaction()` 回应按钮回调
|
|
78
|
+
- 组件显式装配(无全局单例),全 SDK 共享一个 HTTP 连接池
|
|
79
|
+
|
|
80
|
+
## 配置
|
|
81
|
+
|
|
82
|
+
`.env`(`APPID`/`APPSECRET` 必填,缺失启动即报错):
|
|
83
|
+
|
|
84
|
+
```ini
|
|
85
|
+
APPID=你的AppID
|
|
86
|
+
APPSECRET=你的AppSecret
|
|
87
|
+
BASE_URL=https://api.bot.qq.com # 可选
|
|
88
|
+
TIMEOUT=5000 # 可选,毫秒
|
|
89
|
+
CONNECTER=websocket # websocket 或 webhook
|
|
90
|
+
|
|
91
|
+
# 仅 webhook 接入时
|
|
92
|
+
WEBHOOK_HOST=0.0.0.0
|
|
93
|
+
WEBHOOK_PORT=8080
|
|
94
|
+
WEBHOOK_PATH=/qqbot/webhook
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 使用
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from qqbotsdk import EventEmitter, main
|
|
101
|
+
from qqbotsdk.payloads import GroupAtMessage
|
|
102
|
+
from qqbotsdk.api import BotApi
|
|
103
|
+
|
|
104
|
+
ee = EventEmitter()
|
|
105
|
+
|
|
106
|
+
# handler 形参按标注注入 payload 与 BotApi 等组件
|
|
107
|
+
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
|
108
|
+
async def on_group_message(msg: GroupAtMessage, api: BotApi):
|
|
109
|
+
# 被动回复带 msg_id;多次回复递增 msg_seq;失败抛 ApiError
|
|
110
|
+
await api.post_group_message(
|
|
111
|
+
msg.group_openid,
|
|
112
|
+
content=f"你说:{msg.content}",
|
|
113
|
+
msg_id=msg.id,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main(ee)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
运行:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv run python main.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Webhook 接入:`.env` 中 `CONNECTER` 改为 `webhook`,再把回调地址配置到 QQ 开放平台(仅支持 80/443/8080/8443 端口,需公网 HTTPS)。
|
|
127
|
+
|
|
128
|
+
完整示例见 [examples/](examples/),更多写法见[使用指南](docs/GUIDE.md)。
|
|
129
|
+
|
|
130
|
+
## 开发
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run pytest # 单元测试
|
|
134
|
+
uv run ruff check src tests examples && uv run pyright
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
推送与 Pull Request 由 GitHub Actions 自动执行同样的检查([.github/workflows/ci.yml](.github/workflows/ci.yml))。
|
|
138
|
+
|
|
139
|
+
文档站点基于 [docsify](https://docsify.js.org/),文档变更时自动发布([.github/workflows/deploy-docs.yml](.github/workflows/deploy-docs.yml));本地预览:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx docsify-cli serve
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 待完善
|
|
146
|
+
|
|
147
|
+
- 更多 OpenAPI 封装(按需在 `api.py` 的 `BotApi` 上加薄封装即可)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# qqbotsdk
|
|
2
|
+
|
|
3
|
+
QQ 官方机器人 Python SDK(Python 3.12+ / asyncio / aiohttp),支持 WebSocket 与 Webhook 两种接入方式。
|
|
4
|
+
|
|
5
|
+
**📖 在线文档:<https://qqbotsdk.4i.hk/>**
|
|
6
|
+
|
|
7
|
+
[使用指南](docs/GUIDE.md) · [API 参考](docs/API.md) · [架构](docs/ARCHITECTURE.md) · [示例](examples/) · [更新日志](CHANGELOG.md)
|
|
8
|
+
|
|
9
|
+
## 架构
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart TB
|
|
13
|
+
subgraph 接入层
|
|
14
|
+
WS["WebsocketConnecter(ws 长连接 / 重连 / 心跳看门狗)"]
|
|
15
|
+
WH["WebhookConnecter(http 回调 / 验签 / 去重)"]
|
|
16
|
+
end
|
|
17
|
+
subgraph 协议层
|
|
18
|
+
WSP["WebsocketProtocol(握手 / 会话 / 序列号)"]
|
|
19
|
+
WHP["WebhookProtocol(op=13 验证应答)"]
|
|
20
|
+
end
|
|
21
|
+
subgraph 分发层
|
|
22
|
+
Q[("EventQueue<br/>业务事件 op=0")]
|
|
23
|
+
EE["EventEmitter(路由 / 参数注入 / 异常隔离)"]
|
|
24
|
+
end
|
|
25
|
+
subgraph 业务层
|
|
26
|
+
API["BotApi(REST + ApiError)"]
|
|
27
|
+
USER["用户 handler @ee.on(...)"]
|
|
28
|
+
end
|
|
29
|
+
QQ["QQ 开放平台"]
|
|
30
|
+
|
|
31
|
+
WS -. "逐帧 on_frame" .-> WSP
|
|
32
|
+
WH -. "op=13 逐帧 on_frame" .-> WHP
|
|
33
|
+
WS -->|"业务事件"| Q
|
|
34
|
+
WH -->|"业务事件"| Q
|
|
35
|
+
Q --> EE
|
|
36
|
+
EE --> USER
|
|
37
|
+
USER --> API
|
|
38
|
+
API --> QQ
|
|
39
|
+
WS --> QQ
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
组件在 `run_loop` 中显式装配,依赖关系与设计决策详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
|
|
43
|
+
|
|
44
|
+
## 特性
|
|
45
|
+
|
|
46
|
+
- 断线自动重连(指数退避 + 心跳看门狗),重连时自动 RESUME 续传
|
|
47
|
+
- Webhook 接入自带 Ed25519 验签、op=13 验证应答与事件去重
|
|
48
|
+
- 事件订阅由项目根目录的 `intents.toml` 按大类开关
|
|
49
|
+
- Markdown 与内嵌按钮一等支持,`respond_interaction()` 回应按钮回调
|
|
50
|
+
- 组件显式装配(无全局单例),全 SDK 共享一个 HTTP 连接池
|
|
51
|
+
|
|
52
|
+
## 配置
|
|
53
|
+
|
|
54
|
+
`.env`(`APPID`/`APPSECRET` 必填,缺失启动即报错):
|
|
55
|
+
|
|
56
|
+
```ini
|
|
57
|
+
APPID=你的AppID
|
|
58
|
+
APPSECRET=你的AppSecret
|
|
59
|
+
BASE_URL=https://api.bot.qq.com # 可选
|
|
60
|
+
TIMEOUT=5000 # 可选,毫秒
|
|
61
|
+
CONNECTER=websocket # websocket 或 webhook
|
|
62
|
+
|
|
63
|
+
# 仅 webhook 接入时
|
|
64
|
+
WEBHOOK_HOST=0.0.0.0
|
|
65
|
+
WEBHOOK_PORT=8080
|
|
66
|
+
WEBHOOK_PATH=/qqbot/webhook
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 使用
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from qqbotsdk import EventEmitter, main
|
|
73
|
+
from qqbotsdk.payloads import GroupAtMessage
|
|
74
|
+
from qqbotsdk.api import BotApi
|
|
75
|
+
|
|
76
|
+
ee = EventEmitter()
|
|
77
|
+
|
|
78
|
+
# handler 形参按标注注入 payload 与 BotApi 等组件
|
|
79
|
+
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
|
80
|
+
async def on_group_message(msg: GroupAtMessage, api: BotApi):
|
|
81
|
+
# 被动回复带 msg_id;多次回复递增 msg_seq;失败抛 ApiError
|
|
82
|
+
await api.post_group_message(
|
|
83
|
+
msg.group_openid,
|
|
84
|
+
content=f"你说:{msg.content}",
|
|
85
|
+
msg_id=msg.id,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main(ee)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
运行:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv run python main.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Webhook 接入:`.env` 中 `CONNECTER` 改为 `webhook`,再把回调地址配置到 QQ 开放平台(仅支持 80/443/8080/8443 端口,需公网 HTTPS)。
|
|
99
|
+
|
|
100
|
+
完整示例见 [examples/](examples/),更多写法见[使用指南](docs/GUIDE.md)。
|
|
101
|
+
|
|
102
|
+
## 开发
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
uv run pytest # 单元测试
|
|
106
|
+
uv run ruff check src tests examples && uv run pyright
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
推送与 Pull Request 由 GitHub Actions 自动执行同样的检查([.github/workflows/ci.yml](.github/workflows/ci.yml))。
|
|
110
|
+
|
|
111
|
+
文档站点基于 [docsify](https://docsify.js.org/),文档变更时自动发布([.github/workflows/deploy-docs.yml](.github/workflows/deploy-docs.yml));本地预览:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx docsify-cli serve
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 待完善
|
|
118
|
+
|
|
119
|
+
- 更多 OpenAPI 封装(按需在 `api.py` 的 `BotApi` 上加薄封装即可)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qqbotsdk-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "QQ 官方机器人 Python SDK(WebSocket / Webhook)"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = [
|
|
9
|
+
"qq",
|
|
10
|
+
"qqbot",
|
|
11
|
+
"bot",
|
|
12
|
+
"sdk",
|
|
13
|
+
"websocket",
|
|
14
|
+
"webhook",
|
|
15
|
+
"机器人",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Natural Language :: Chinese (Simplified)",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
requires-python = ">=3.12"
|
|
28
|
+
dependencies = [
|
|
29
|
+
"aiohttp>=3.14.3",
|
|
30
|
+
"cryptography>=50.0.1",
|
|
31
|
+
"loguru>=0.7.3",
|
|
32
|
+
"python-dotenv>=1.2.3",
|
|
33
|
+
"ujson>=6.0.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[[project.authors]]
|
|
37
|
+
name = "snowy-yang"
|
|
38
|
+
email = "102860110+snowy-yang@users.noreply.github.com"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/snowy-yang/qqbotsdk"
|
|
42
|
+
Repository = "https://github.com/snowy-yang/qqbotsdk"
|
|
43
|
+
Changelog = "https://github.com/snowy-yang/qqbotsdk/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
qqbotsdk = "qqbotsdk:main"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.12.7,<0.13.0"]
|
|
50
|
+
build-backend = "uv_build"
|
|
51
|
+
|
|
52
|
+
[tool.uv.build-backend]
|
|
53
|
+
module-name = "qqbotsdk"
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
target-version = "py312"
|
|
61
|
+
line-length = 88
|
|
62
|
+
indent-width = 4
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"E",
|
|
67
|
+
"F",
|
|
68
|
+
"I",
|
|
69
|
+
"N",
|
|
70
|
+
"W",
|
|
71
|
+
"UP",
|
|
72
|
+
"B",
|
|
73
|
+
"C4",
|
|
74
|
+
"SIM",
|
|
75
|
+
]
|
|
76
|
+
ignore = [
|
|
77
|
+
"E501",
|
|
78
|
+
"B008",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.pydocstyle]
|
|
82
|
+
convention = "google"
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint.isort]
|
|
85
|
+
known-first-party = ["qqbotsdk"]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint.per-file-ignores]
|
|
88
|
+
"tests/**/*.py" = ["S"]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.format]
|
|
91
|
+
quote-style = "double"
|
|
92
|
+
indent-style = "space"
|
|
93
|
+
skip-magic-trailing-comma = false
|
|
94
|
+
line-ending = "auto"
|
|
95
|
+
|
|
96
|
+
[tool.pyright]
|
|
97
|
+
typeCheckingMode = "standard"
|
|
98
|
+
pythonVersion = "3.12"
|
|
99
|
+
include = ["src"]
|
|
100
|
+
exclude = [
|
|
101
|
+
"**/__pycache__",
|
|
102
|
+
"**/.*",
|
|
103
|
+
"**/node_modules",
|
|
104
|
+
".venv",
|
|
105
|
+
]
|
|
106
|
+
reportMissingImports = true
|
|
107
|
+
reportMissingTypeStubs = false
|
|
108
|
+
reportUnknownParameterType = false
|
|
109
|
+
reportUnknownVariableType = false
|
|
110
|
+
reportUnknownMemberType = false
|
|
111
|
+
|
|
112
|
+
[dependency-groups]
|
|
113
|
+
dev = [
|
|
114
|
+
"pyright>=1.1.411",
|
|
115
|
+
"pytest>=8.3.0",
|
|
116
|
+
"pytest-asyncio>=0.24.0",
|
|
117
|
+
"ruff>=0.16.5",
|
|
118
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qqbotsdk-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "QQ 官方机器人 Python SDK(WebSocket / Webhook)"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "snowy-yang", email = "102860110+snowy-yang@users.noreply.github.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["qq", "qqbot", "bot", "sdk", "websocket", "webhook", "机器人"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Natural Language :: Chinese (Simplified)",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.12"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"aiohttp>=3.14.3",
|
|
25
|
+
"cryptography>=50.0.1",
|
|
26
|
+
"loguru>=0.7.3",
|
|
27
|
+
"python-dotenv>=1.2.3",
|
|
28
|
+
"ujson>=6.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/snowy-yang/qqbotsdk"
|
|
33
|
+
Repository = "https://github.com/snowy-yang/qqbotsdk"
|
|
34
|
+
Changelog = "https://github.com/snowy-yang/qqbotsdk/blob/main/CHANGELOG.md"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
qqbotsdk = "qqbotsdk:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.12.7,<0.13.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
42
|
+
|
|
43
|
+
[tool.uv.build-backend]
|
|
44
|
+
module-name = "qqbotsdk"
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = ["pyright>=1.1.411", "pytest>=8.3.0", "pytest-asyncio>=0.24.0", "ruff>=0.16.5"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
line-length = 88
|
|
57
|
+
indent-width = 4
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E", # pycodestyle errors
|
|
62
|
+
"F", # Pyflakes
|
|
63
|
+
"I", # isort
|
|
64
|
+
"N", # pep8-naming
|
|
65
|
+
"W", # pycodestyle warnings
|
|
66
|
+
"UP", # pyupgrade
|
|
67
|
+
"B", # flake8-bugbear
|
|
68
|
+
"C4", # flake8-comprehensions
|
|
69
|
+
"SIM", # flake8-simplify
|
|
70
|
+
]
|
|
71
|
+
ignore = [
|
|
72
|
+
"E501", # line too long
|
|
73
|
+
"B008", # do not perform function calls in argument defaults
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint.pydocstyle]
|
|
77
|
+
convention = "google"
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.isort]
|
|
80
|
+
known-first-party = ["qqbotsdk"]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"tests/**/*.py" = ["S"]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.format]
|
|
86
|
+
quote-style = "double"
|
|
87
|
+
indent-style = "space"
|
|
88
|
+
skip-magic-trailing-comma = false
|
|
89
|
+
line-ending = "auto"
|
|
90
|
+
|
|
91
|
+
[tool.pyright]
|
|
92
|
+
typeCheckingMode = "standard"
|
|
93
|
+
pythonVersion = "3.12"
|
|
94
|
+
include = ["src"]
|
|
95
|
+
exclude = ["**/__pycache__", "**/.*", "**/node_modules", ".venv"]
|
|
96
|
+
reportMissingImports = true
|
|
97
|
+
reportMissingTypeStubs = false
|
|
98
|
+
reportUnknownParameterType = false
|
|
99
|
+
reportUnknownVariableType = false
|
|
100
|
+
reportUnknownMemberType = false
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""qqbotsdk:QQ 官方机器人 SDK 入口。
|
|
2
|
+
|
|
3
|
+
用法:构造 `EventEmitter()` 注册 handler,交给 `main(ee)`
|
|
4
|
+
启动;连接方式由 .env 的 CONNECTER 决定(websocket / webhook)。
|
|
5
|
+
架构详情见 docs/ARCHITECTURE.md。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
from aiohttp import ClientSession
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from .api import BotApi
|
|
15
|
+
from .config import Config
|
|
16
|
+
from .connecter import Connecter as Connecter
|
|
17
|
+
from .connecter import WebhookConnecter, WebsocketConnecter
|
|
18
|
+
from .emitter import EventEmitter as EventEmitter
|
|
19
|
+
from .protocol import BaseProtocol as BaseProtocol
|
|
20
|
+
from .queue import EventQueue as EventQueue
|
|
21
|
+
from .session import Session
|
|
22
|
+
from .token import AccessToken
|
|
23
|
+
from .webhook_protocol import WebhookProtocol
|
|
24
|
+
from .ws_protocol import WebsocketProtocol
|
|
25
|
+
|
|
26
|
+
load_dotenv()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def run_loop(emitter: EventEmitter | None = None) -> None:
|
|
30
|
+
"""启动 SDK。可传入外部构造的 EventEmitter 以注册 handler;不传时自建。
|
|
31
|
+
|
|
32
|
+
全部组件在此显式装配,并按类型登记到 emitter.services 供 handler
|
|
33
|
+
参数注入;共享的 HTTP 连接池随 run_loop 结束释放。
|
|
34
|
+
"""
|
|
35
|
+
if emitter is None:
|
|
36
|
+
emitter = EventEmitter()
|
|
37
|
+
|
|
38
|
+
config = Config.load()
|
|
39
|
+
http = ClientSession(timeout=config.timeout)
|
|
40
|
+
session = Session()
|
|
41
|
+
token = AccessToken(config, http)
|
|
42
|
+
api = BotApi(config, http, token)
|
|
43
|
+
emitter.services.update(
|
|
44
|
+
{
|
|
45
|
+
EventEmitter: emitter,
|
|
46
|
+
EventQueue: emitter.queue,
|
|
47
|
+
Config: config,
|
|
48
|
+
Session: session,
|
|
49
|
+
ClientSession: http,
|
|
50
|
+
AccessToken: token,
|
|
51
|
+
BotApi: api,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 各接入方式配自己的协议处理器:协议帧由适配器就近处理,只有业务事件
|
|
56
|
+
# (op=0)入队交给 emitter 分发(见 protocol.py / connecter.py)
|
|
57
|
+
match config.connecter:
|
|
58
|
+
case "websocket":
|
|
59
|
+
protocol = WebsocketProtocol(config, session, token)
|
|
60
|
+
connecter = WebsocketConnecter(
|
|
61
|
+
config, http, token, session, emitter.queue, protocol
|
|
62
|
+
)
|
|
63
|
+
case "webhook":
|
|
64
|
+
protocol = WebhookProtocol(config)
|
|
65
|
+
connecter = WebhookConnecter(config, emitter.queue, protocol)
|
|
66
|
+
case unknown:
|
|
67
|
+
raise ValueError(f"未知的 CONNECTER: {unknown}")
|
|
68
|
+
|
|
69
|
+
logger.info(f"qqbotsdk 启动,连接方式: {config.connecter}")
|
|
70
|
+
try:
|
|
71
|
+
await asyncio.gather(emitter.dispatch(), connecter.run())
|
|
72
|
+
finally:
|
|
73
|
+
# 在飞的后台 handler 先于连接池收尾,避免用到已关闭的 ClientSession
|
|
74
|
+
await emitter.close()
|
|
75
|
+
# 全 SDK 共享一个连接池,统一在此释放
|
|
76
|
+
await http.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(emitter: EventEmitter | None = None) -> None:
|
|
80
|
+
try:
|
|
81
|
+
asyncio.run(run_loop(emitter))
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
logger.info("qqbotsdk 已退出")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""OpenAPI 客户端:BotApi 按域组合——v2 群/单聊、频道消息、频道管理、
|
|
2
|
+
指令面板(斜线指令)与全局菜单、群管理、音频、论坛。
|
|
3
|
+
|
|
4
|
+
各域模块是方法集(继承 BaseApi 共享请求内核:鉴权头注入、429 自动
|
|
5
|
+
重试、401 刷新 token 重试、统一 ApiError),方法名与拆分前完全一致。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .audio import AudioApi
|
|
9
|
+
from .channel import ChannelApi
|
|
10
|
+
from .core import ApiError
|
|
11
|
+
from .core import BaseApi as BaseApi
|
|
12
|
+
from .forum import ForumApi
|
|
13
|
+
from .group import GroupApi
|
|
14
|
+
from .guild import GuildApi
|
|
15
|
+
from .panel import PanelApi
|
|
16
|
+
from .v2 import V2Api, button, keyboard
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BotApi(V2Api, ChannelApi, GuildApi, PanelApi, GroupApi, AudioApi, ForumApi):
|
|
20
|
+
"""通用 REST 封装;具体业务接口见各域方法集。
|
|
21
|
+
|
|
22
|
+
BaseApi(请求内核)经域方法集继承自然落在 MRO 末尾,
|
|
23
|
+
构造签名 (config, http, token) 由它提供。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ApiError",
|
|
29
|
+
"AudioApi",
|
|
30
|
+
"BotApi",
|
|
31
|
+
"ChannelApi",
|
|
32
|
+
"ForumApi",
|
|
33
|
+
"GroupApi",
|
|
34
|
+
"GuildApi",
|
|
35
|
+
"PanelApi",
|
|
36
|
+
"V2Api",
|
|
37
|
+
"button",
|
|
38
|
+
"keyboard",
|
|
39
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""音频方法集(仅音频类机器人):子频道播放控制与上/下麦。
|
|
2
|
+
|
|
3
|
+
需联系平台开通权限后调用;控制与上/下麦均触发 AUDIO_ACTION 事件。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .core import BaseApi
|
|
9
|
+
|
|
10
|
+
# 播放状态(AudioControl.status)
|
|
11
|
+
AUDIO_STATUS_START = 0
|
|
12
|
+
AUDIO_STATUS_PAUSE = 1
|
|
13
|
+
AUDIO_STATUS_RESUME = 2
|
|
14
|
+
AUDIO_STATUS_STOP = 3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AudioApi(BaseApi):
|
|
18
|
+
"""音频方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
|
19
|
+
|
|
20
|
+
async def control_channel_audio(
|
|
21
|
+
self,
|
|
22
|
+
channel_id: str,
|
|
23
|
+
audio_url: str | None = None,
|
|
24
|
+
text: str | None = None,
|
|
25
|
+
status: int = AUDIO_STATUS_START,
|
|
26
|
+
) -> dict:
|
|
27
|
+
"""控制子频道音频播放(POST /channels/{channel_id}/audio)。
|
|
28
|
+
|
|
29
|
+
status 为 START/PAUSE/RESUME/STOP(AUDIO_STATUS_* 常量);
|
|
30
|
+
audio_url 与状态文本 text 仅 START 时传。
|
|
31
|
+
"""
|
|
32
|
+
body: dict[str, Any] = {"status": status}
|
|
33
|
+
if audio_url is not None:
|
|
34
|
+
body["audio_url"] = audio_url
|
|
35
|
+
if text is not None:
|
|
36
|
+
body["text"] = text
|
|
37
|
+
return await self.post(f"/channels/{channel_id}/audio", json=body)
|
|
38
|
+
|
|
39
|
+
async def put_channel_mic(self, channel_id: str) -> dict:
|
|
40
|
+
"""机器人上麦(PUT /channels/{channel_id}/mic)。"""
|
|
41
|
+
return await self.put(f"/channels/{channel_id}/mic")
|
|
42
|
+
|
|
43
|
+
async def delete_channel_mic(self, channel_id: str) -> dict:
|
|
44
|
+
"""机器人下麦(DELETE /channels/{channel_id}/mic)。"""
|
|
45
|
+
return await self.delete(f"/channels/{channel_id}/mic")
|