ErisPulse-OneBot11Adapter 3.0.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.
- erispulse_onebot11adapter-3.0.0/ErisPulse_OneBot11Adapter.egg-info/PKG-INFO +223 -0
- erispulse_onebot11adapter-3.0.0/ErisPulse_OneBot11Adapter.egg-info/SOURCES.txt +11 -0
- erispulse_onebot11adapter-3.0.0/ErisPulse_OneBot11Adapter.egg-info/dependency_links.txt +1 -0
- erispulse_onebot11adapter-3.0.0/ErisPulse_OneBot11Adapter.egg-info/entry_points.txt +3 -0
- erispulse_onebot11adapter-3.0.0/ErisPulse_OneBot11Adapter.egg-info/top_level.txt +1 -0
- erispulse_onebot11adapter-3.0.0/LICENSE +21 -0
- erispulse_onebot11adapter-3.0.0/OneBotAdapter/Converter.py +265 -0
- erispulse_onebot11adapter-3.0.0/OneBotAdapter/Core.py +276 -0
- erispulse_onebot11adapter-3.0.0/OneBotAdapter/__init__.py +1 -0
- erispulse_onebot11adapter-3.0.0/PKG-INFO +223 -0
- erispulse_onebot11adapter-3.0.0/README.md +190 -0
- erispulse_onebot11adapter-3.0.0/pyproject.toml +18 -0
- erispulse_onebot11adapter-3.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ErisPulse-OneBot11Adapter
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: ErisPulse的OneBotV11协议适配模块,异步的OneBot触发器
|
|
5
|
+
Author-email: wsu2059q <wsu2059@qq.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 WSu2059
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-OneBot11Adapter
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# OneBotAdapter 模块文档
|
|
35
|
+
|
|
36
|
+
## 简介
|
|
37
|
+
OneBotAdapter 是基于 [ErisPulse](https://github.com/ErisPulse/ErisPulse/) 架构开发的 **OneBot V11 协议适配器模块**。它提供统一的事件处理机制、连接管理功能,并支持 Server 和 Connect 两种运行模式。
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 使用示例
|
|
42
|
+
|
|
43
|
+
### 初始化与事件处理
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from ErisPulse import sdk
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
# 初始化 SDK
|
|
50
|
+
sdk.init()
|
|
51
|
+
|
|
52
|
+
# 获取适配器实例
|
|
53
|
+
onebot = sdk.adapter.QQ
|
|
54
|
+
|
|
55
|
+
# 注册事件处理器
|
|
56
|
+
@onebot.on("message")
|
|
57
|
+
async def handle_message(data):
|
|
58
|
+
print(f"收到消息: {data}")
|
|
59
|
+
await onebot.Send.To("user", data["user_id"]).Text("已收到您的消息!")
|
|
60
|
+
|
|
61
|
+
@onebot.on("notice")
|
|
62
|
+
async def handle_notice(data):
|
|
63
|
+
print(f"收到通知: {data}")
|
|
64
|
+
|
|
65
|
+
@onebot.on("request")
|
|
66
|
+
async def handle_request(data):
|
|
67
|
+
print(f"收到请求: {data}")
|
|
68
|
+
|
|
69
|
+
# 启动适配器
|
|
70
|
+
await sdk.adapter.startup()
|
|
71
|
+
|
|
72
|
+
# 保持程序运行
|
|
73
|
+
await asyncio.Event().wait()
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
import asyncio
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 消息发送示例(DSL 链式风格)
|
|
83
|
+
|
|
84
|
+
#### 文本消息
|
|
85
|
+
```python
|
|
86
|
+
await onebot.Send.To("group", 123456).Text("Hello World!")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### 图片消息
|
|
90
|
+
```python
|
|
91
|
+
await onebot.Send.To("user", 123456).Image("http://example.com/image.jpg")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### 语音消息
|
|
95
|
+
```python
|
|
96
|
+
await onebot.Send.To("user", 123456).Voice("http://example.com/audio.mp3")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### 视频消息
|
|
100
|
+
```python
|
|
101
|
+
await onebot.Send.To("group", 123456).Video("http://example.com/video.mp4")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 发送原生 CQ 消息
|
|
105
|
+
```python
|
|
106
|
+
await onebot.Send.To("user", 123456).Raw([
|
|
107
|
+
{"type": "text", "data": {"text": "你好"}},
|
|
108
|
+
{"type": "image", "data": {"file": "http://example.com/image.png"}}
|
|
109
|
+
])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 支持的消息类型及对应方法
|
|
115
|
+
|
|
116
|
+
| 方法名 | 参数说明 | 用途 |
|
|
117
|
+
|----------|----------|------|
|
|
118
|
+
| `.Text(text: str)` | 发送纯文本消息 | 基础消息类型 |
|
|
119
|
+
| `.Image(file: str)` | 发送图片消息(URL 或 Base64) | 支持 CQ 格式 |
|
|
120
|
+
| `.Voice(file: str)` | 发送语音消息 | 支持 CQ 格式 |
|
|
121
|
+
| `.Video(file: str)` | 发送视频消息 | 支持 CQ 格式 |
|
|
122
|
+
| `.Raw(message_list: List[Dict])` | 发送原始 OneBot 消息结构 | 自定义消息内容 |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 配置说明
|
|
127
|
+
|
|
128
|
+
在 `env.py` 中进行如下配置:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
sdk.env.set("OneBotAdapter", {
|
|
132
|
+
"mode": "client", # 可选 server 或 client
|
|
133
|
+
"server": {
|
|
134
|
+
"host": "127.0.0.1", # WebSocket 监听地址
|
|
135
|
+
"port": 8080, # WebSocket 监听端口
|
|
136
|
+
"path": "/", # WebSocket 路径
|
|
137
|
+
"token": "your_token" # 认证 Token(可选)
|
|
138
|
+
},
|
|
139
|
+
"client": {
|
|
140
|
+
"url": "ws://127.0.0.1:3001", # 连接的目标地址
|
|
141
|
+
"token": "your_token" # 请求头或查询参数中的 Token
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 事件类型
|
|
149
|
+
|
|
150
|
+
支持以下 OneBot 标准事件类型:
|
|
151
|
+
|
|
152
|
+
| 事件类型 | 映射名称 | 说明 |
|
|
153
|
+
|----------------|----------|------|
|
|
154
|
+
| `message` | `message` | 消息事件 |
|
|
155
|
+
| `notice` | `notice` | 通知类事件(如群成员变动) |
|
|
156
|
+
| `request` | `request` | 请求类事件(如加群请求) |
|
|
157
|
+
| `meta_event` | `meta_event` | 元事件(如心跳包) |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## API 调用方式
|
|
162
|
+
|
|
163
|
+
通过 `call_api` 方法调用 OneBot 提供的任意 API 接口:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
response = await onebot.call_api(
|
|
167
|
+
endpoint="send_msg",
|
|
168
|
+
message_type="group",
|
|
169
|
+
group_id=123456,
|
|
170
|
+
message="Hello!"
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
该方法会自动处理响应结果并返回,若超时将抛出异常。
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 运行模式说明
|
|
179
|
+
|
|
180
|
+
### Server 模式(作为服务端监听连接)
|
|
181
|
+
|
|
182
|
+
- 启动一个 WebSocket 服务器等待 OneBot 客户端连接。
|
|
183
|
+
- 适用于部署多个 bot 客户端连接至同一服务端的场景。
|
|
184
|
+
|
|
185
|
+
### Client 模式(主动连接 OneBot)
|
|
186
|
+
|
|
187
|
+
- 主动连接到 OneBot 服务(如 go-cqhttp)。
|
|
188
|
+
- 更适合单个 bot 实例直接连接的情况。
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 生命周期管理
|
|
193
|
+
|
|
194
|
+
### 启动适配器
|
|
195
|
+
```python
|
|
196
|
+
await sdk.adapter.startup()
|
|
197
|
+
```
|
|
198
|
+
此方法会根据配置启动 Server 或连接到 OneBot 客户端。
|
|
199
|
+
|
|
200
|
+
### 关闭适配器
|
|
201
|
+
```python
|
|
202
|
+
await sdk.adapter.shutdown()
|
|
203
|
+
```
|
|
204
|
+
确保资源释放,关闭 WebSocket 连接。
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 注意事项
|
|
209
|
+
|
|
210
|
+
1. 所有 `.Send.*()` 方法返回 `asyncio.Task`,建议使用 `await` 等待执行完成。
|
|
211
|
+
2. 确保在调用 `startup()` 前完成所有事件处理器注册。
|
|
212
|
+
3. 生产环境建议启用 Token 认证以保证安全性。
|
|
213
|
+
4. 若使用 Server 模式,请注意防火墙和端口开放情况。
|
|
214
|
+
5. 程序退出时请调用 `shutdown()` 释放资源。
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 参考链接
|
|
219
|
+
|
|
220
|
+
- [ErisPulse 主库](https://github.com/ErisPulse/ErisPulse/)
|
|
221
|
+
- [OneBot V11 协议文档](https://github.com/botuniverse/onebot-11)
|
|
222
|
+
- [go-cqhttp 项目地址](https://github.com/Mrs4s/go-cqhttp)
|
|
223
|
+
- [模块开发指南](https://github.com/ErisPulse/ErisPulse/tree/main/docs/DEVELOPMENT.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ErisPulse_OneBot11Adapter.egg-info/PKG-INFO
|
|
5
|
+
ErisPulse_OneBot11Adapter.egg-info/SOURCES.txt
|
|
6
|
+
ErisPulse_OneBot11Adapter.egg-info/dependency_links.txt
|
|
7
|
+
ErisPulse_OneBot11Adapter.egg-info/entry_points.txt
|
|
8
|
+
ErisPulse_OneBot11Adapter.egg-info/top_level.txt
|
|
9
|
+
OneBotAdapter/Converter.py
|
|
10
|
+
OneBotAdapter/Core.py
|
|
11
|
+
OneBotAdapter/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OneBotAdapter
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 WSu2059
|
|
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,265 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Dict, Optional, List, Any
|
|
3
|
+
|
|
4
|
+
class OneBot11Converter:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self._setup_event_mapping()
|
|
7
|
+
|
|
8
|
+
def _setup_event_mapping(self):
|
|
9
|
+
"""初始化事件类型映射"""
|
|
10
|
+
self.event_map = {
|
|
11
|
+
"message": "message",
|
|
12
|
+
"notice": "notice",
|
|
13
|
+
"request": "request",
|
|
14
|
+
"meta_event": "meta_event"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
self.notice_subtypes = {
|
|
18
|
+
"group_upload": "group_file_upload",
|
|
19
|
+
"group_admin": "group_admin_change",
|
|
20
|
+
"group_decrease": "group_member_decrease",
|
|
21
|
+
"group_increase": "group_member_increase",
|
|
22
|
+
"group_ban": "group_ban",
|
|
23
|
+
"friend_add": "friend_increase",
|
|
24
|
+
"friend_delete": "friend_decrease",
|
|
25
|
+
"group_recall": "message_recall",
|
|
26
|
+
"friend_recall": "message_recall",
|
|
27
|
+
"notify": "notify"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def convert(self, raw_event: Dict) -> Optional[Dict]:
|
|
31
|
+
"""
|
|
32
|
+
将OneBot11事件转换为OneBot12格式
|
|
33
|
+
|
|
34
|
+
:param raw_event: 原始OneBot11事件数据
|
|
35
|
+
:return: 转换后的OneBot12事件,None表示不支持的事件类型
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(raw_event, dict):
|
|
38
|
+
raise ValueError("事件数据必须是字典类型")
|
|
39
|
+
|
|
40
|
+
post_type = raw_event.get("post_type")
|
|
41
|
+
if post_type not in self.event_map:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
# 基础事件结构
|
|
45
|
+
onebot_event = {
|
|
46
|
+
"id": str(raw_event.get("time", int(time.time()))),
|
|
47
|
+
"time": raw_event.get("time", int(time.time())),
|
|
48
|
+
"platform": "onebot11",
|
|
49
|
+
"self": {
|
|
50
|
+
"platform": "onebot11",
|
|
51
|
+
"user_id": str(raw_event.get("self_id", ""))
|
|
52
|
+
},
|
|
53
|
+
"onebot11_raw": raw_event # 保留原始数据
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# 根据事件类型分发处理
|
|
57
|
+
handler = getattr(self, f"_handle_{post_type}", None)
|
|
58
|
+
if handler:
|
|
59
|
+
return handler(raw_event, onebot_event)
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def _handle_message(self, raw_event: Dict, base_event: Dict) -> Dict:
|
|
64
|
+
"""处理消息事件"""
|
|
65
|
+
message_type = raw_event["message_type"]
|
|
66
|
+
detail_type = "private" if message_type == "private" else "group"
|
|
67
|
+
|
|
68
|
+
# 解析CQ码消息
|
|
69
|
+
message_segments = self._parse_cq_code(raw_event["message"])
|
|
70
|
+
alt_message = self._generate_alt_message(message_segments)
|
|
71
|
+
|
|
72
|
+
base_event.update({
|
|
73
|
+
"type": "message",
|
|
74
|
+
"detail_type": detail_type,
|
|
75
|
+
"message_id": str(raw_event.get("message_id", "")),
|
|
76
|
+
"message": message_segments,
|
|
77
|
+
"alt_message": alt_message,
|
|
78
|
+
"user_id": str(raw_event.get("user_id", "")),
|
|
79
|
+
"onebot11_message": raw_event["message"]
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if detail_type == "group":
|
|
83
|
+
base_event["group_id"] = str(raw_event.get("group_id", ""))
|
|
84
|
+
|
|
85
|
+
# 处理子类型
|
|
86
|
+
if raw_event.get("sub_type") == "group":
|
|
87
|
+
base_event["sub_type"] = "group"
|
|
88
|
+
elif raw_event.get("sub_type") == "friend":
|
|
89
|
+
base_event["sub_type"] = "friend"
|
|
90
|
+
|
|
91
|
+
return base_event
|
|
92
|
+
|
|
93
|
+
def _parse_cq_code(self, message: Any) -> List[Dict]:
|
|
94
|
+
"""解析CQ码消息为OneBot12消息段"""
|
|
95
|
+
if isinstance(message, str):
|
|
96
|
+
return [{"type": "text", "data": {"text": message}}]
|
|
97
|
+
|
|
98
|
+
segments = []
|
|
99
|
+
for segment in message:
|
|
100
|
+
if isinstance(segment, str):
|
|
101
|
+
segments.append({"type": "text", "data": {"text": segment}})
|
|
102
|
+
elif isinstance(segment, dict):
|
|
103
|
+
cq_type = segment["type"]
|
|
104
|
+
if cq_type == "text":
|
|
105
|
+
segments.append({"type": "text", "data": {"text": segment["data"]["text"]}})
|
|
106
|
+
elif cq_type == "image":
|
|
107
|
+
segments.append({
|
|
108
|
+
"type": "image",
|
|
109
|
+
"data": {
|
|
110
|
+
"file": segment["data"].get("file"),
|
|
111
|
+
"url": segment["data"].get("url"),
|
|
112
|
+
"cache": segment["data"].get("cache", 1),
|
|
113
|
+
"sub_type": segment["data"].get("type", 0)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
elif cq_type == "record":
|
|
117
|
+
segments.append({
|
|
118
|
+
"type": "audio",
|
|
119
|
+
"data": {
|
|
120
|
+
"file": segment["data"].get("file"),
|
|
121
|
+
"url": segment["data"].get("url"),
|
|
122
|
+
"magic": segment["data"].get("magic", 0)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
elif cq_type == "at":
|
|
126
|
+
segments.append({
|
|
127
|
+
"type": "mention",
|
|
128
|
+
"data": {
|
|
129
|
+
"user_id": segment["data"].get("qq"),
|
|
130
|
+
"user_name": segment["data"].get("name", "")
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
else:
|
|
134
|
+
segments.append({
|
|
135
|
+
"type": f"onebot11_{cq_type}",
|
|
136
|
+
"data": segment["data"]
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return segments
|
|
140
|
+
|
|
141
|
+
def _generate_alt_message(self, segments: List[Dict]) -> str:
|
|
142
|
+
"""生成替代文本消息"""
|
|
143
|
+
parts = []
|
|
144
|
+
for seg in segments:
|
|
145
|
+
if seg["type"] == "text":
|
|
146
|
+
parts.append(seg["data"]["text"])
|
|
147
|
+
elif seg["type"] == "image":
|
|
148
|
+
parts.append("[图片]")
|
|
149
|
+
elif seg["type"] == "audio":
|
|
150
|
+
parts.append("[语音]")
|
|
151
|
+
elif seg["type"] == "mention":
|
|
152
|
+
parts.append(f"@{seg['data'].get('user_name', seg['data'].get('user_id', ''))}")
|
|
153
|
+
return "".join(parts)
|
|
154
|
+
|
|
155
|
+
def _handle_notice(self, raw_event: Dict, base_event: Dict) -> Dict:
|
|
156
|
+
"""处理通知事件"""
|
|
157
|
+
notice_type = raw_event["notice_type"]
|
|
158
|
+
sub_type = raw_event.get("sub_type", "")
|
|
159
|
+
|
|
160
|
+
# 映射通知子类型
|
|
161
|
+
detail_type = self.notice_subtypes.get(notice_type, notice_type)
|
|
162
|
+
|
|
163
|
+
base_event.update({
|
|
164
|
+
"type": "notice",
|
|
165
|
+
"detail_type": detail_type,
|
|
166
|
+
"onebot11_notice": raw_event
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
# 处理不同类型的通知
|
|
170
|
+
if notice_type == "group_upload":
|
|
171
|
+
base_event.update({
|
|
172
|
+
"group_id": str(raw_event.get("group_id")),
|
|
173
|
+
"user_id": str(raw_event.get("user_id")),
|
|
174
|
+
"onebot11_file": raw_event.get("file")
|
|
175
|
+
})
|
|
176
|
+
elif notice_type == "group_admin":
|
|
177
|
+
base_event.update({
|
|
178
|
+
"group_id": str(raw_event.get("group_id")),
|
|
179
|
+
"user_id": str(raw_event.get("user_id")),
|
|
180
|
+
"sub_type": "set" if sub_type == "set" else "unset"
|
|
181
|
+
})
|
|
182
|
+
elif notice_type in ["group_increase", "group_decrease"]:
|
|
183
|
+
base_event.update({
|
|
184
|
+
"group_id": str(raw_event.get("group_id")),
|
|
185
|
+
"user_id": str(raw_event.get("user_id")),
|
|
186
|
+
"operator_id": str(raw_event.get("operator_id", "")),
|
|
187
|
+
"sub_type": sub_type
|
|
188
|
+
})
|
|
189
|
+
elif notice_type == "group_ban":
|
|
190
|
+
base_event.update({
|
|
191
|
+
"group_id": str(raw_event.get("group_id")),
|
|
192
|
+
"operator_id": str(raw_event.get("operator_id")),
|
|
193
|
+
"user_id": str(raw_event.get("user_id")),
|
|
194
|
+
"duration": raw_event.get("duration", 0)
|
|
195
|
+
})
|
|
196
|
+
elif notice_type in ["friend_add", "friend_delete"]:
|
|
197
|
+
base_event.update({
|
|
198
|
+
"user_id": str(raw_event.get("user_id"))
|
|
199
|
+
})
|
|
200
|
+
elif notice_type in ["group_recall", "friend_recall"]:
|
|
201
|
+
base_event.update({
|
|
202
|
+
"message_id": str(raw_event.get("message_id")),
|
|
203
|
+
"user_id": str(raw_event.get("user_id")),
|
|
204
|
+
"group_id": str(raw_event.get("group_id", "")) if notice_type == "group_recall" else None
|
|
205
|
+
})
|
|
206
|
+
elif notice_type == "notify":
|
|
207
|
+
if sub_type == "honor":
|
|
208
|
+
base_event.update({
|
|
209
|
+
"group_id": str(raw_event.get("group_id")),
|
|
210
|
+
"user_id": str(raw_event.get("user_id")),
|
|
211
|
+
"honor_type": raw_event.get("honor_type")
|
|
212
|
+
})
|
|
213
|
+
elif sub_type == "poke":
|
|
214
|
+
base_event.update({
|
|
215
|
+
"group_id": str(raw_event.get("group_id", "")),
|
|
216
|
+
"user_id": str(raw_event.get("user_id")),
|
|
217
|
+
"target_id": str(raw_event.get("target_id"))
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return base_event
|
|
221
|
+
|
|
222
|
+
def _handle_request(self, raw_event: Dict, base_event: Dict) -> Dict:
|
|
223
|
+
"""处理请求事件"""
|
|
224
|
+
request_type = raw_event["request_type"]
|
|
225
|
+
|
|
226
|
+
base_event.update({
|
|
227
|
+
"type": "request",
|
|
228
|
+
"detail_type": f"onebot11_{request_type}",
|
|
229
|
+
"onebot11_request": raw_event
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
if request_type == "friend":
|
|
233
|
+
base_event.update({
|
|
234
|
+
"user_id": str(raw_event.get("user_id")),
|
|
235
|
+
"comment": raw_event.get("comment"),
|
|
236
|
+
"flag": raw_event.get("flag")
|
|
237
|
+
})
|
|
238
|
+
elif request_type == "group":
|
|
239
|
+
base_event.update({
|
|
240
|
+
"group_id": str(raw_event.get("group_id")),
|
|
241
|
+
"user_id": str(raw_event.get("user_id")),
|
|
242
|
+
"comment": raw_event.get("comment"),
|
|
243
|
+
"sub_type": raw_event.get("sub_type"),
|
|
244
|
+
"flag": raw_event.get("flag")
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
return base_event
|
|
248
|
+
|
|
249
|
+
def _handle_meta_event(self, raw_event: Dict, base_event: Dict) -> Dict:
|
|
250
|
+
"""处理元事件"""
|
|
251
|
+
meta_type = raw_event["meta_event_type"]
|
|
252
|
+
|
|
253
|
+
base_event.update({
|
|
254
|
+
"type": "meta_event",
|
|
255
|
+
"detail_type": f"onebot11_{meta_type}",
|
|
256
|
+
"onebot11_meta": raw_event
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if meta_type == "lifecycle":
|
|
260
|
+
base_event["sub_type"] = raw_event.get("sub_type", "")
|
|
261
|
+
elif meta_type == "heartbeat":
|
|
262
|
+
base_event["interval"] = raw_event.get("interval", 0)
|
|
263
|
+
base_event["status"] = raw_event.get("status", {})
|
|
264
|
+
|
|
265
|
+
return base_event
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import aiohttp
|
|
4
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
5
|
+
from typing import Dict, List, Optional, Any, Type
|
|
6
|
+
from ErisPulse import sdk
|
|
7
|
+
from ErisPulse.Core import adapter_server
|
|
8
|
+
|
|
9
|
+
class OneBotAdapter(sdk.BaseAdapter):
|
|
10
|
+
class Send(sdk.BaseAdapter.Send):
|
|
11
|
+
def Text(self, text: str):
|
|
12
|
+
return asyncio.create_task(
|
|
13
|
+
self._adapter.call_api(
|
|
14
|
+
endpoint="send_msg",
|
|
15
|
+
message_type="private" if self._target_type == "user" else "group",
|
|
16
|
+
user_id=self._target_id if self._target_type == "user" else None,
|
|
17
|
+
group_id=self._target_id if self._target_type == "group" else None,
|
|
18
|
+
message=text
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def Image(self, file: str):
|
|
23
|
+
return self._send("image", {"file": file})
|
|
24
|
+
|
|
25
|
+
def Voice(self, file: str):
|
|
26
|
+
return self._send("voice", {"file": file})
|
|
27
|
+
|
|
28
|
+
def Video(self, file: str):
|
|
29
|
+
return self._send("video", {"file": file})
|
|
30
|
+
|
|
31
|
+
def Raw(self, message_list):
|
|
32
|
+
"""
|
|
33
|
+
发送原生OneBot消息列表格式
|
|
34
|
+
:param message_list: List[Dict], 例如:
|
|
35
|
+
[{"type": "text", "data": {"text": "Hello"}}, {"type": "image", "data": {"file": "http://..."}}
|
|
36
|
+
"""
|
|
37
|
+
raw_message = ''.join([
|
|
38
|
+
f"[CQ:{msg['type']},{','.join([f'{k}={v}' for k, v in msg['data'].items()])}]"
|
|
39
|
+
for msg in message_list
|
|
40
|
+
])
|
|
41
|
+
return self._send_raw(raw_message)
|
|
42
|
+
|
|
43
|
+
def Recall(self, message_id: int):
|
|
44
|
+
return asyncio.create_task(
|
|
45
|
+
self._adapter.call_api(
|
|
46
|
+
endpoint="delete_msg",
|
|
47
|
+
message_id=message_id
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def Edit(self, message_id: int, new_text: str):
|
|
52
|
+
await self.Recall(message_id)
|
|
53
|
+
return self.Text(new_text)
|
|
54
|
+
|
|
55
|
+
def Batch(self, target_ids: List[str], text: str, target_type: str = "user"):
|
|
56
|
+
tasks = []
|
|
57
|
+
for target_id in target_ids:
|
|
58
|
+
task = asyncio.create_task(
|
|
59
|
+
self._adapter.call_api(
|
|
60
|
+
endpoint="send_msg",
|
|
61
|
+
message_type=target_type,
|
|
62
|
+
user_id=target_id if target_type == "user" else None,
|
|
63
|
+
group_id=target_id if target_type == "group" else None,
|
|
64
|
+
message=text
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
tasks.append(task)
|
|
68
|
+
return tasks
|
|
69
|
+
|
|
70
|
+
def _send(self, msg_type: str, data: dict):
|
|
71
|
+
message = f"[CQ:{msg_type},{','.join([f'{k}={v}' for k, v in data.items()])}]"
|
|
72
|
+
return self._send_raw(message)
|
|
73
|
+
|
|
74
|
+
def _send_raw(self, message: str):
|
|
75
|
+
return asyncio.create_task(
|
|
76
|
+
self._adapter.call_api(
|
|
77
|
+
endpoint="send_msg",
|
|
78
|
+
message_type="private" if self._target_type == "user" else "group",
|
|
79
|
+
user_id=self._target_id if self._target_type == "user" else None,
|
|
80
|
+
group_id=self._target_id if self._target_type == "group" else None,
|
|
81
|
+
message=message
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __init__(self, sdk):
|
|
86
|
+
super().__init__()
|
|
87
|
+
self.sdk = sdk
|
|
88
|
+
self.logger = sdk.logger
|
|
89
|
+
self.adapter = self.sdk.adapter
|
|
90
|
+
|
|
91
|
+
self.config = self._load_config()
|
|
92
|
+
self._api_response_futures = {}
|
|
93
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
94
|
+
self.connection: Optional[aiohttp.ClientWebSocketResponse] = None
|
|
95
|
+
self._setup_event_mapping()
|
|
96
|
+
self.logger.info("OneBot适配器初始化完成")
|
|
97
|
+
|
|
98
|
+
self.convert = self._setup_coverter()
|
|
99
|
+
|
|
100
|
+
def _setup_coverter(self):
|
|
101
|
+
from .Converter import OneBot11Converter
|
|
102
|
+
convert = OneBot11Converter()
|
|
103
|
+
return convert.convert
|
|
104
|
+
|
|
105
|
+
def _load_config(self) -> Dict:
|
|
106
|
+
config = self.sdk.env.getConfig("OneBotv11_Adapter")
|
|
107
|
+
self.logger.debug(f"读取配置: {config}")
|
|
108
|
+
if not config:
|
|
109
|
+
default_config = {
|
|
110
|
+
"mode": "server",
|
|
111
|
+
"server": {
|
|
112
|
+
"path": "/",
|
|
113
|
+
"token": ""
|
|
114
|
+
},
|
|
115
|
+
"client": {
|
|
116
|
+
"url": "ws://127.0.0.1:3001",
|
|
117
|
+
"token": ""
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try:
|
|
121
|
+
sdk.logger.warning("配置文件不存在,已创建默认配置文件")
|
|
122
|
+
self.sdk.env.setConfig("OneBotv11_Adapter", default_config)
|
|
123
|
+
return default_config
|
|
124
|
+
except Exception as e:
|
|
125
|
+
self.logger.error(f"保存默认配置失败: {str(e)}")
|
|
126
|
+
return default_config
|
|
127
|
+
return config
|
|
128
|
+
|
|
129
|
+
def _setup_event_mapping(self):
|
|
130
|
+
self.event_map = {
|
|
131
|
+
"message": "message",
|
|
132
|
+
"notice": "notice",
|
|
133
|
+
"request": "request",
|
|
134
|
+
"meta_event": "meta_event"
|
|
135
|
+
}
|
|
136
|
+
self.logger.debug("事件映射已设置")
|
|
137
|
+
|
|
138
|
+
async def call_api(self, endpoint: str, **params):
|
|
139
|
+
if not self.connection:
|
|
140
|
+
raise ConnectionError("尚未连接到OneBot")
|
|
141
|
+
|
|
142
|
+
echo = str(hash(str(params)))
|
|
143
|
+
future = asyncio.get_event_loop().create_future()
|
|
144
|
+
self._api_response_futures[echo] = future
|
|
145
|
+
|
|
146
|
+
payload = {
|
|
147
|
+
"action": endpoint,
|
|
148
|
+
"params": params,
|
|
149
|
+
"echo": echo
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await self.connection.send_str(json.dumps(payload))
|
|
153
|
+
self.logger.debug(f"调用OneBot API: {endpoint}")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
result = await asyncio.wait_for(future, timeout=30)
|
|
157
|
+
return result
|
|
158
|
+
except asyncio.TimeoutError:
|
|
159
|
+
future.cancel()
|
|
160
|
+
self.logger.error(f"API调用超时: {endpoint}")
|
|
161
|
+
raise TimeoutError(f"API调用超时: {endpoint}")
|
|
162
|
+
return None
|
|
163
|
+
finally:
|
|
164
|
+
if echo in self._api_response_futures:
|
|
165
|
+
del self._api_response_futures[echo]
|
|
166
|
+
self.logger.debug(f"已删除API响应Future: {echo}")
|
|
167
|
+
async def connect(self, retry_interval=30):
|
|
168
|
+
if self.config.get("mode") != "client":
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
self.session = aiohttp.ClientSession()
|
|
172
|
+
headers = {}
|
|
173
|
+
if token := self.config.get("client", {}).get("token"):
|
|
174
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
175
|
+
|
|
176
|
+
url = self.config["client"]["url"]
|
|
177
|
+
retry_count = 0
|
|
178
|
+
|
|
179
|
+
while True:
|
|
180
|
+
try:
|
|
181
|
+
self.connection = await self.session.ws_connect(url, headers=headers)
|
|
182
|
+
self.logger.info(f"成功连接到OneBot服务器: {url}")
|
|
183
|
+
asyncio.create_task(self._listen())
|
|
184
|
+
return
|
|
185
|
+
except Exception as e:
|
|
186
|
+
retry_count += 1
|
|
187
|
+
self.logger.error(f"第 {retry_count} 次连接失败: {str(e)}")
|
|
188
|
+
self.logger.info(f"将在 {retry_interval} 秒后重试...")
|
|
189
|
+
await asyncio.sleep(retry_interval)
|
|
190
|
+
|
|
191
|
+
async def _listen(self):
|
|
192
|
+
try:
|
|
193
|
+
async for msg in self.connection:
|
|
194
|
+
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
195
|
+
await self._handle_message(msg.data)
|
|
196
|
+
elif msg.type == aiohttp.WSMsgType.CLOSED:
|
|
197
|
+
break
|
|
198
|
+
elif msg.type == aiohttp.WSMsgType.ERROR:
|
|
199
|
+
self.logger.error(f"WebSocket错误: {self.connection.exception()}")
|
|
200
|
+
except Exception as e:
|
|
201
|
+
self.logger.error(f"WebSocket监听异常: {str(e)}")
|
|
202
|
+
|
|
203
|
+
async def _handle_message(self, raw_msg: str):
|
|
204
|
+
try:
|
|
205
|
+
data = json.loads(raw_msg)
|
|
206
|
+
if "echo" in data:
|
|
207
|
+
echo = data["echo"]
|
|
208
|
+
future = self._api_response_futures.get(echo)
|
|
209
|
+
if future and not future.done():
|
|
210
|
+
future.set_result(data.get("data"))
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
post_type = data.get("post_type")
|
|
214
|
+
event_type = self.event_map.get(post_type, "unknown")
|
|
215
|
+
await self.emit(event_type, data)
|
|
216
|
+
self.logger.debug(f"处理OneBot事件: {event_type}")
|
|
217
|
+
if hasattr(self.adapter, "emit"):
|
|
218
|
+
onebot_event = self.convert(data)
|
|
219
|
+
self.logger.debug(f"OneBot12事件数据: {json.dumps(onebot_event, ensure_ascii=False)}")
|
|
220
|
+
if onebot_event:
|
|
221
|
+
await self.adapter.emit(onebot_event)
|
|
222
|
+
|
|
223
|
+
except json.JSONDecodeError:
|
|
224
|
+
self.logger.error(f"JSON解析失败: {raw_msg}")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
self.logger.error(f"消息处理异常: {str(e)}")
|
|
227
|
+
async def _ws_handler(self, websocket: WebSocket):
|
|
228
|
+
self.connection = websocket
|
|
229
|
+
self.logger.info("新的OneBot客户端已连接")
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
while True:
|
|
233
|
+
data = await websocket.receive_text()
|
|
234
|
+
await self._handle_message(data)
|
|
235
|
+
except WebSocketDisconnect:
|
|
236
|
+
self.logger.info("OneBot客户端断开连接")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
self.logger.error(f"WebSocket处理异常: {str(e)}")
|
|
239
|
+
finally:
|
|
240
|
+
self.connection = None
|
|
241
|
+
|
|
242
|
+
async def _auth_handler(self, websocket: WebSocket):
|
|
243
|
+
if token := self.config["server"].get("token"):
|
|
244
|
+
client_token = websocket.headers.get("Authorization", "").replace("Bearer ", "")
|
|
245
|
+
if not client_token:
|
|
246
|
+
query = dict(websocket.query_params)
|
|
247
|
+
client_token = query.get("token", "")
|
|
248
|
+
|
|
249
|
+
if client_token != token:
|
|
250
|
+
self.logger.warning("客户端提供的Token无效")
|
|
251
|
+
await websocket.close(code=1008)
|
|
252
|
+
return False
|
|
253
|
+
return True
|
|
254
|
+
async def start(self):
|
|
255
|
+
mode = self.config.get("mode")
|
|
256
|
+
if mode == "server":
|
|
257
|
+
self.logger.info("正在注册Server模式WebSocket路由")
|
|
258
|
+
adapter_server.register_websocket(
|
|
259
|
+
adapter_name="onebot",
|
|
260
|
+
path="/ws",
|
|
261
|
+
handler=self._ws_handler,
|
|
262
|
+
auth_handler=self._auth_handler
|
|
263
|
+
)
|
|
264
|
+
elif mode == "client":
|
|
265
|
+
self.logger.info("正在启动Client模式")
|
|
266
|
+
await self.connect()
|
|
267
|
+
else:
|
|
268
|
+
self.logger.error("无效的模式配置")
|
|
269
|
+
raise ValueError("模式配置错误")
|
|
270
|
+
|
|
271
|
+
async def shutdown(self):
|
|
272
|
+
if self.connection and not self.connection.closed:
|
|
273
|
+
await self.connection.close()
|
|
274
|
+
if self.session:
|
|
275
|
+
await self.session.close()
|
|
276
|
+
self.logger.info("OneBot适配器已关闭")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .Core import OneBotAdapter
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ErisPulse-OneBot11Adapter
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: ErisPulse的OneBotV11协议适配模块,异步的OneBot触发器
|
|
5
|
+
Author-email: wsu2059q <wsu2059@qq.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 WSu2059
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-OneBot11Adapter
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# OneBotAdapter 模块文档
|
|
35
|
+
|
|
36
|
+
## 简介
|
|
37
|
+
OneBotAdapter 是基于 [ErisPulse](https://github.com/ErisPulse/ErisPulse/) 架构开发的 **OneBot V11 协议适配器模块**。它提供统一的事件处理机制、连接管理功能,并支持 Server 和 Connect 两种运行模式。
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 使用示例
|
|
42
|
+
|
|
43
|
+
### 初始化与事件处理
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from ErisPulse import sdk
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
# 初始化 SDK
|
|
50
|
+
sdk.init()
|
|
51
|
+
|
|
52
|
+
# 获取适配器实例
|
|
53
|
+
onebot = sdk.adapter.QQ
|
|
54
|
+
|
|
55
|
+
# 注册事件处理器
|
|
56
|
+
@onebot.on("message")
|
|
57
|
+
async def handle_message(data):
|
|
58
|
+
print(f"收到消息: {data}")
|
|
59
|
+
await onebot.Send.To("user", data["user_id"]).Text("已收到您的消息!")
|
|
60
|
+
|
|
61
|
+
@onebot.on("notice")
|
|
62
|
+
async def handle_notice(data):
|
|
63
|
+
print(f"收到通知: {data}")
|
|
64
|
+
|
|
65
|
+
@onebot.on("request")
|
|
66
|
+
async def handle_request(data):
|
|
67
|
+
print(f"收到请求: {data}")
|
|
68
|
+
|
|
69
|
+
# 启动适配器
|
|
70
|
+
await sdk.adapter.startup()
|
|
71
|
+
|
|
72
|
+
# 保持程序运行
|
|
73
|
+
await asyncio.Event().wait()
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
import asyncio
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 消息发送示例(DSL 链式风格)
|
|
83
|
+
|
|
84
|
+
#### 文本消息
|
|
85
|
+
```python
|
|
86
|
+
await onebot.Send.To("group", 123456).Text("Hello World!")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### 图片消息
|
|
90
|
+
```python
|
|
91
|
+
await onebot.Send.To("user", 123456).Image("http://example.com/image.jpg")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### 语音消息
|
|
95
|
+
```python
|
|
96
|
+
await onebot.Send.To("user", 123456).Voice("http://example.com/audio.mp3")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### 视频消息
|
|
100
|
+
```python
|
|
101
|
+
await onebot.Send.To("group", 123456).Video("http://example.com/video.mp4")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 发送原生 CQ 消息
|
|
105
|
+
```python
|
|
106
|
+
await onebot.Send.To("user", 123456).Raw([
|
|
107
|
+
{"type": "text", "data": {"text": "你好"}},
|
|
108
|
+
{"type": "image", "data": {"file": "http://example.com/image.png"}}
|
|
109
|
+
])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 支持的消息类型及对应方法
|
|
115
|
+
|
|
116
|
+
| 方法名 | 参数说明 | 用途 |
|
|
117
|
+
|----------|----------|------|
|
|
118
|
+
| `.Text(text: str)` | 发送纯文本消息 | 基础消息类型 |
|
|
119
|
+
| `.Image(file: str)` | 发送图片消息(URL 或 Base64) | 支持 CQ 格式 |
|
|
120
|
+
| `.Voice(file: str)` | 发送语音消息 | 支持 CQ 格式 |
|
|
121
|
+
| `.Video(file: str)` | 发送视频消息 | 支持 CQ 格式 |
|
|
122
|
+
| `.Raw(message_list: List[Dict])` | 发送原始 OneBot 消息结构 | 自定义消息内容 |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 配置说明
|
|
127
|
+
|
|
128
|
+
在 `env.py` 中进行如下配置:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
sdk.env.set("OneBotAdapter", {
|
|
132
|
+
"mode": "client", # 可选 server 或 client
|
|
133
|
+
"server": {
|
|
134
|
+
"host": "127.0.0.1", # WebSocket 监听地址
|
|
135
|
+
"port": 8080, # WebSocket 监听端口
|
|
136
|
+
"path": "/", # WebSocket 路径
|
|
137
|
+
"token": "your_token" # 认证 Token(可选)
|
|
138
|
+
},
|
|
139
|
+
"client": {
|
|
140
|
+
"url": "ws://127.0.0.1:3001", # 连接的目标地址
|
|
141
|
+
"token": "your_token" # 请求头或查询参数中的 Token
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 事件类型
|
|
149
|
+
|
|
150
|
+
支持以下 OneBot 标准事件类型:
|
|
151
|
+
|
|
152
|
+
| 事件类型 | 映射名称 | 说明 |
|
|
153
|
+
|----------------|----------|------|
|
|
154
|
+
| `message` | `message` | 消息事件 |
|
|
155
|
+
| `notice` | `notice` | 通知类事件(如群成员变动) |
|
|
156
|
+
| `request` | `request` | 请求类事件(如加群请求) |
|
|
157
|
+
| `meta_event` | `meta_event` | 元事件(如心跳包) |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## API 调用方式
|
|
162
|
+
|
|
163
|
+
通过 `call_api` 方法调用 OneBot 提供的任意 API 接口:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
response = await onebot.call_api(
|
|
167
|
+
endpoint="send_msg",
|
|
168
|
+
message_type="group",
|
|
169
|
+
group_id=123456,
|
|
170
|
+
message="Hello!"
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
该方法会自动处理响应结果并返回,若超时将抛出异常。
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 运行模式说明
|
|
179
|
+
|
|
180
|
+
### Server 模式(作为服务端监听连接)
|
|
181
|
+
|
|
182
|
+
- 启动一个 WebSocket 服务器等待 OneBot 客户端连接。
|
|
183
|
+
- 适用于部署多个 bot 客户端连接至同一服务端的场景。
|
|
184
|
+
|
|
185
|
+
### Client 模式(主动连接 OneBot)
|
|
186
|
+
|
|
187
|
+
- 主动连接到 OneBot 服务(如 go-cqhttp)。
|
|
188
|
+
- 更适合单个 bot 实例直接连接的情况。
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 生命周期管理
|
|
193
|
+
|
|
194
|
+
### 启动适配器
|
|
195
|
+
```python
|
|
196
|
+
await sdk.adapter.startup()
|
|
197
|
+
```
|
|
198
|
+
此方法会根据配置启动 Server 或连接到 OneBot 客户端。
|
|
199
|
+
|
|
200
|
+
### 关闭适配器
|
|
201
|
+
```python
|
|
202
|
+
await sdk.adapter.shutdown()
|
|
203
|
+
```
|
|
204
|
+
确保资源释放,关闭 WebSocket 连接。
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 注意事项
|
|
209
|
+
|
|
210
|
+
1. 所有 `.Send.*()` 方法返回 `asyncio.Task`,建议使用 `await` 等待执行完成。
|
|
211
|
+
2. 确保在调用 `startup()` 前完成所有事件处理器注册。
|
|
212
|
+
3. 生产环境建议启用 Token 认证以保证安全性。
|
|
213
|
+
4. 若使用 Server 模式,请注意防火墙和端口开放情况。
|
|
214
|
+
5. 程序退出时请调用 `shutdown()` 释放资源。
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 参考链接
|
|
219
|
+
|
|
220
|
+
- [ErisPulse 主库](https://github.com/ErisPulse/ErisPulse/)
|
|
221
|
+
- [OneBot V11 协议文档](https://github.com/botuniverse/onebot-11)
|
|
222
|
+
- [go-cqhttp 项目地址](https://github.com/Mrs4s/go-cqhttp)
|
|
223
|
+
- [模块开发指南](https://github.com/ErisPulse/ErisPulse/tree/main/docs/DEVELOPMENT.md)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# OneBotAdapter 模块文档
|
|
2
|
+
|
|
3
|
+
## 简介
|
|
4
|
+
OneBotAdapter 是基于 [ErisPulse](https://github.com/ErisPulse/ErisPulse/) 架构开发的 **OneBot V11 协议适配器模块**。它提供统一的事件处理机制、连接管理功能,并支持 Server 和 Connect 两种运行模式。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 使用示例
|
|
9
|
+
|
|
10
|
+
### 初始化与事件处理
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from ErisPulse import sdk
|
|
14
|
+
|
|
15
|
+
async def main():
|
|
16
|
+
# 初始化 SDK
|
|
17
|
+
sdk.init()
|
|
18
|
+
|
|
19
|
+
# 获取适配器实例
|
|
20
|
+
onebot = sdk.adapter.QQ
|
|
21
|
+
|
|
22
|
+
# 注册事件处理器
|
|
23
|
+
@onebot.on("message")
|
|
24
|
+
async def handle_message(data):
|
|
25
|
+
print(f"收到消息: {data}")
|
|
26
|
+
await onebot.Send.To("user", data["user_id"]).Text("已收到您的消息!")
|
|
27
|
+
|
|
28
|
+
@onebot.on("notice")
|
|
29
|
+
async def handle_notice(data):
|
|
30
|
+
print(f"收到通知: {data}")
|
|
31
|
+
|
|
32
|
+
@onebot.on("request")
|
|
33
|
+
async def handle_request(data):
|
|
34
|
+
print(f"收到请求: {data}")
|
|
35
|
+
|
|
36
|
+
# 启动适配器
|
|
37
|
+
await sdk.adapter.startup()
|
|
38
|
+
|
|
39
|
+
# 保持程序运行
|
|
40
|
+
await asyncio.Event().wait()
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
import asyncio
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 消息发送示例(DSL 链式风格)
|
|
50
|
+
|
|
51
|
+
#### 文本消息
|
|
52
|
+
```python
|
|
53
|
+
await onebot.Send.To("group", 123456).Text("Hello World!")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### 图片消息
|
|
57
|
+
```python
|
|
58
|
+
await onebot.Send.To("user", 123456).Image("http://example.com/image.jpg")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### 语音消息
|
|
62
|
+
```python
|
|
63
|
+
await onebot.Send.To("user", 123456).Voice("http://example.com/audio.mp3")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### 视频消息
|
|
67
|
+
```python
|
|
68
|
+
await onebot.Send.To("group", 123456).Video("http://example.com/video.mp4")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### 发送原生 CQ 消息
|
|
72
|
+
```python
|
|
73
|
+
await onebot.Send.To("user", 123456).Raw([
|
|
74
|
+
{"type": "text", "data": {"text": "你好"}},
|
|
75
|
+
{"type": "image", "data": {"file": "http://example.com/image.png"}}
|
|
76
|
+
])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 支持的消息类型及对应方法
|
|
82
|
+
|
|
83
|
+
| 方法名 | 参数说明 | 用途 |
|
|
84
|
+
|----------|----------|------|
|
|
85
|
+
| `.Text(text: str)` | 发送纯文本消息 | 基础消息类型 |
|
|
86
|
+
| `.Image(file: str)` | 发送图片消息(URL 或 Base64) | 支持 CQ 格式 |
|
|
87
|
+
| `.Voice(file: str)` | 发送语音消息 | 支持 CQ 格式 |
|
|
88
|
+
| `.Video(file: str)` | 发送视频消息 | 支持 CQ 格式 |
|
|
89
|
+
| `.Raw(message_list: List[Dict])` | 发送原始 OneBot 消息结构 | 自定义消息内容 |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 配置说明
|
|
94
|
+
|
|
95
|
+
在 `env.py` 中进行如下配置:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
sdk.env.set("OneBotAdapter", {
|
|
99
|
+
"mode": "client", # 可选 server 或 client
|
|
100
|
+
"server": {
|
|
101
|
+
"host": "127.0.0.1", # WebSocket 监听地址
|
|
102
|
+
"port": 8080, # WebSocket 监听端口
|
|
103
|
+
"path": "/", # WebSocket 路径
|
|
104
|
+
"token": "your_token" # 认证 Token(可选)
|
|
105
|
+
},
|
|
106
|
+
"client": {
|
|
107
|
+
"url": "ws://127.0.0.1:3001", # 连接的目标地址
|
|
108
|
+
"token": "your_token" # 请求头或查询参数中的 Token
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 事件类型
|
|
116
|
+
|
|
117
|
+
支持以下 OneBot 标准事件类型:
|
|
118
|
+
|
|
119
|
+
| 事件类型 | 映射名称 | 说明 |
|
|
120
|
+
|----------------|----------|------|
|
|
121
|
+
| `message` | `message` | 消息事件 |
|
|
122
|
+
| `notice` | `notice` | 通知类事件(如群成员变动) |
|
|
123
|
+
| `request` | `request` | 请求类事件(如加群请求) |
|
|
124
|
+
| `meta_event` | `meta_event` | 元事件(如心跳包) |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## API 调用方式
|
|
129
|
+
|
|
130
|
+
通过 `call_api` 方法调用 OneBot 提供的任意 API 接口:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
response = await onebot.call_api(
|
|
134
|
+
endpoint="send_msg",
|
|
135
|
+
message_type="group",
|
|
136
|
+
group_id=123456,
|
|
137
|
+
message="Hello!"
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
该方法会自动处理响应结果并返回,若超时将抛出异常。
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 运行模式说明
|
|
146
|
+
|
|
147
|
+
### Server 模式(作为服务端监听连接)
|
|
148
|
+
|
|
149
|
+
- 启动一个 WebSocket 服务器等待 OneBot 客户端连接。
|
|
150
|
+
- 适用于部署多个 bot 客户端连接至同一服务端的场景。
|
|
151
|
+
|
|
152
|
+
### Client 模式(主动连接 OneBot)
|
|
153
|
+
|
|
154
|
+
- 主动连接到 OneBot 服务(如 go-cqhttp)。
|
|
155
|
+
- 更适合单个 bot 实例直接连接的情况。
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 生命周期管理
|
|
160
|
+
|
|
161
|
+
### 启动适配器
|
|
162
|
+
```python
|
|
163
|
+
await sdk.adapter.startup()
|
|
164
|
+
```
|
|
165
|
+
此方法会根据配置启动 Server 或连接到 OneBot 客户端。
|
|
166
|
+
|
|
167
|
+
### 关闭适配器
|
|
168
|
+
```python
|
|
169
|
+
await sdk.adapter.shutdown()
|
|
170
|
+
```
|
|
171
|
+
确保资源释放,关闭 WebSocket 连接。
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 注意事项
|
|
176
|
+
|
|
177
|
+
1. 所有 `.Send.*()` 方法返回 `asyncio.Task`,建议使用 `await` 等待执行完成。
|
|
178
|
+
2. 确保在调用 `startup()` 前完成所有事件处理器注册。
|
|
179
|
+
3. 生产环境建议启用 Token 认证以保证安全性。
|
|
180
|
+
4. 若使用 Server 模式,请注意防火墙和端口开放情况。
|
|
181
|
+
5. 程序退出时请调用 `shutdown()` 释放资源。
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 参考链接
|
|
186
|
+
|
|
187
|
+
- [ErisPulse 主库](https://github.com/ErisPulse/ErisPulse/)
|
|
188
|
+
- [OneBot V11 协议文档](https://github.com/botuniverse/onebot-11)
|
|
189
|
+
- [go-cqhttp 项目地址](https://github.com/Mrs4s/go-cqhttp)
|
|
190
|
+
- [模块开发指南](https://github.com/ErisPulse/ErisPulse/tree/main/docs/DEVELOPMENT.md)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ErisPulse-OneBot11Adapter"
|
|
3
|
+
version = "3.0.0"
|
|
4
|
+
description = "ErisPulse的OneBotV11协议适配模块,异步的OneBot触发器"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [ { name = "wsu2059q", email = "wsu2059@qq.com" } ]
|
|
9
|
+
dependencies = [
|
|
10
|
+
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
"homepage" = "https://github.com/ErisPulse/ErisPulse-OneBot11Adapter"
|
|
15
|
+
|
|
16
|
+
[project.entry-points."erispulse.adapter"]
|
|
17
|
+
onebot11 = "OneBotAdapter:OneBotAdapter"
|
|
18
|
+
qq = "OneBotAdapter:OneBotAdapter"
|