ErisPulse-OneBot12Adapter 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,616 @@
1
+ # OneBot12Adapter/Core.py
2
+ import asyncio
3
+ import json
4
+ import aiohttp
5
+ from fastapi import WebSocket, WebSocketDisconnect
6
+ from typing import Dict, List, Optional, Union
7
+ from dataclasses import dataclass
8
+ from ErisPulse import sdk
9
+ from ErisPulse.Core import router
10
+
11
+ @dataclass
12
+ class OneBot12AccountConfig:
13
+ """OneBot12账户配置"""
14
+ mode: str # "server" or "client"
15
+ server_path: Optional[str] = "/onebot12"
16
+ server_token: Optional[str] = ""
17
+ client_url: Optional[str] = "ws://127.0.0.1:3001"
18
+ client_token: Optional[str] = ""
19
+ enabled: bool = True
20
+ name: str = ""
21
+ # OneBot12特有配置
22
+ platform: Optional[str] = "onebot12" # 平台标识
23
+ implementation: Optional[str] = "" # 实现标识
24
+
25
+ class OneBot12Adapter(sdk.BaseAdapter):
26
+ """OneBot V12 协议适配器"""
27
+
28
+ class Send(sdk.BaseAdapter.Send):
29
+ """消息发送类 - OneBot12标准"""
30
+
31
+ def Text(self, text: str):
32
+ return asyncio.create_task(
33
+ self._adapter.call_api(
34
+ endpoint="send_message",
35
+ account_id=self._account_id,
36
+ detail_type=self._get_detail_type(),
37
+ user_id=self._target_id if self._target_type == "user" else None,
38
+ group_id=self._target_id if self._target_type == "group" else None,
39
+ content={"type": "text", "data": {"text": text}}
40
+ )
41
+ )
42
+
43
+ def Image(self, file: Union[str, bytes], filename: str = "image.png"):
44
+ data = {}
45
+ if isinstance(file, bytes):
46
+ # 处理二进制图片数据
47
+ import base64
48
+ data["file_base64"] = base64.b64encode(file).decode('utf-8')
49
+ data["file_name"] = filename
50
+ else:
51
+ data["file_id"] = file
52
+
53
+ return asyncio.create_task(
54
+ self._adapter.call_api(
55
+ endpoint="send_message",
56
+ account_id=self._account_id,
57
+ detail_type=self._get_detail_type(),
58
+ user_id=self._target_id if self._target_type == "user" else None,
59
+ group_id=self._target_id if self._target_type == "group" else None,
60
+ content={"type": "image", "data": data}
61
+ )
62
+ )
63
+
64
+ def Audio(self, file: Union[str, bytes], filename: str = "audio.ogg"):
65
+ data = {}
66
+ if isinstance(file, bytes):
67
+ import base64
68
+ data["file_base64"] = base64.b64encode(file).decode('utf-8')
69
+ data["file_name"] = filename
70
+ else:
71
+ data["file_id"] = file
72
+
73
+ return asyncio.create_task(
74
+ self._adapter.call_api(
75
+ endpoint="send_message",
76
+ account_id=self._account_id,
77
+ detail_type=self._get_detail_type(),
78
+ user_id=self._target_id if self._target_type == "user" else None,
79
+ group_id=self._target_id if self._target_type == "group" else None,
80
+ content={"type": "audio", "data": data}
81
+ )
82
+ )
83
+
84
+ def Video(self, file: Union[str, bytes], filename: str = "video.mp4"):
85
+ data = {}
86
+ if isinstance(file, bytes):
87
+ import base64
88
+ data["file_base64"] = base64.b64encode(file).decode('utf-8')
89
+ data["file_name"] = filename
90
+ else:
91
+ data["file_id"] = file
92
+
93
+ return asyncio.create_task(
94
+ self._adapter.call_api(
95
+ endpoint="send_message",
96
+ account_id=self._account_id,
97
+ detail_type=self._get_detail_type(),
98
+ user_id=self._target_id if self._target_type == "user" else None,
99
+ group_id=self._target_id if self._target_type == "group" else None,
100
+ content={"type": "video", "data": data}
101
+ )
102
+ )
103
+
104
+ def Mention(self, user_id: Union[str, int], user_name: str = None):
105
+ # OneBot12的mention应该作为消息段处理
106
+ return self._send_complex_message([
107
+ {"type": "mention", "data": {"user_id": str(user_id), "user_name": user_name or ""}}
108
+ ])
109
+
110
+ def Reply(self, message_id: Union[str, int], content: str = None):
111
+ # OneBot12的回复消息
112
+ message_segments = [
113
+ {"type": "reply", "data": {"message_id": str(message_id)}}
114
+ ]
115
+ if content:
116
+ message_segments.append({"type": "text", "data": {"text": content}})
117
+
118
+ return self._send_complex_message(message_segments)
119
+
120
+ def Location(self, latitude: float, longitude: float, title: str = "", content: str = ""):
121
+ data = {
122
+ "latitude": latitude,
123
+ "longitude": longitude
124
+ }
125
+ if title:
126
+ data["title"] = title
127
+ if content:
128
+ data["content"] = content
129
+
130
+ return asyncio.create_task(
131
+ self._adapter.call_api(
132
+ endpoint="send_message",
133
+ account_id=self._account_id,
134
+ detail_type=self._get_detail_type(),
135
+ user_id=self._target_id if self._target_type == "user" else None,
136
+ group_id=self._target_id if self._target_type == "group" else None,
137
+ content={"type": "location", "data": data}
138
+ )
139
+ )
140
+
141
+ def Sticker(self, file_id: str):
142
+ """发送表情包/贴纸"""
143
+ return asyncio.create_task(
144
+ self._adapter.call_api(
145
+ endpoint="send_message",
146
+ account_id=self._account_id,
147
+ detail_type=self._get_detail_type(),
148
+ user_id=self._target_id if self._target_type == "user" else None,
149
+ group_id=self._target_id if self._target_type == "group" else None,
150
+ content={"type": "sticker", "data": {"file_id": file_id}}
151
+ )
152
+ )
153
+
154
+ def Raw(self, message_segments: List[Dict]):
155
+ """发送原始OneBot12消息段列表"""
156
+ return self._send_complex_message(message_segments)
157
+
158
+ def Recall(self, message_id: Union[str, int]):
159
+ """撤回消息"""
160
+ return asyncio.create_task(
161
+ self._adapter.call_api(
162
+ endpoint="delete_message",
163
+ account_id=self._account_id,
164
+ message_id=str(message_id)
165
+ )
166
+ )
167
+
168
+ def Edit(self, message_id: Union[str, int], content: Union[str, List[Dict]]):
169
+ """编辑消息"""
170
+ if isinstance(content, str):
171
+ content = [{"type": "text", "data": {"text": content}}]
172
+
173
+ return asyncio.create_task(
174
+ self._adapter.call_api(
175
+ endpoint="edit_message",
176
+ account_id=self._account_id,
177
+ message_id=str(message_id),
178
+ content=content
179
+ )
180
+ )
181
+
182
+ def Batch(self, target_ids: List[str], message: Union[str, List[Dict]], target_type: str = "user"):
183
+ """批量发送消息"""
184
+ tasks = []
185
+ for target_id in target_ids:
186
+ if isinstance(message, str):
187
+ task = self._adapter.call_api(
188
+ endpoint="send_message",
189
+ account_id=self._account_id,
190
+ detail_type=target_type,
191
+ user_id=target_id if target_type == "user" else None,
192
+ group_id=target_id if target_type == "group" else None,
193
+ content=[{"type": "text", "data": {"text": message}}]
194
+ )
195
+ else:
196
+ task = self._adapter.call_api(
197
+ endpoint="send_message",
198
+ account_id=self._account_id,
199
+ detail_type=target_type,
200
+ user_id=target_id if target_type == "user" else None,
201
+ group_id=target_id if target_type == "group" else None,
202
+ content=message
203
+ )
204
+ tasks.append(task)
205
+ return tasks
206
+
207
+ def _get_detail_type(self):
208
+ """获取消息详细类型"""
209
+ return "private" if self._target_type == "user" else "group"
210
+
211
+ def _send_complex_message(self, message_segments: List[Dict]):
212
+ """发送复合消息"""
213
+ return asyncio.create_task(
214
+ self._adapter.call_api(
215
+ endpoint="send_message",
216
+ account_id=self._account_id,
217
+ detail_type=self._get_detail_type(),
218
+ user_id=self._target_id if self._target_type == "user" else None,
219
+ group_id=self._target_id if self._target_type == "group" else None,
220
+ content=message_segments
221
+ )
222
+ )
223
+
224
+ def __init__(self, sdk):
225
+ super().__init__()
226
+ self.sdk = sdk
227
+ self.logger = sdk.logger
228
+ self.adapter = self.sdk.adapter
229
+
230
+ # 加载配置
231
+ self.accounts: Dict[str, OneBot12AccountConfig] = self._load_account_configs()
232
+
233
+ # 连接池 - 每个账户一个连接
234
+ self._api_response_futures: Dict[str, Dict[str, asyncio.Future]] = {}
235
+ self.sessions: Dict[str, aiohttp.ClientSession] = {}
236
+ self.connections: Dict[str, aiohttp.ClientWebSocketResponse] = {}
237
+
238
+ # 轮询任务
239
+ self.reconnect_tasks: Dict[str, asyncio.Task] = {}
240
+
241
+ # 初始化状态
242
+ self._is_running = False
243
+
244
+ # 默认配置值
245
+ self.default_retry_interval = 30
246
+ self.default_timeout = 30
247
+ self.default_max_retries = 3
248
+
249
+ self.logger.info(f"OneBot12适配器初始化完成,共加载 {len(self.accounts)} 个账户")
250
+
251
+ def _load_account_configs(self) -> Dict[str, OneBot12AccountConfig]:
252
+ """加载账户配置"""
253
+ accounts = {}
254
+
255
+ # 检查新格式的账户配置
256
+ account_configs = self.sdk.config.getConfig("OneBotv12_Adapter.accounts", {})
257
+
258
+ if not account_configs:
259
+ # 创建默认账户配置
260
+ self.logger.info("未找到配置文件,创建默认账户配置")
261
+ default_config = {
262
+ "default": {
263
+ "mode": "server",
264
+ "server_path": "/onebot12",
265
+ "server_token": "",
266
+ "client_url": "ws://127.0.0.1:3001",
267
+ "client_token": "",
268
+ "enabled": True,
269
+ "platform": "onebot12",
270
+ "implementation": ""
271
+ }
272
+ }
273
+
274
+ try:
275
+ self.sdk.config.setConfig("OneBotv12_Adapter.accounts", default_config)
276
+ account_configs = default_config
277
+ except Exception as e:
278
+ self.logger.error(f"保存默认账户配置失败: {str(e)}")
279
+ account_configs = default_config
280
+
281
+ # 创建账户配置对象
282
+ for account_name, config in account_configs.items():
283
+ merged_config = {
284
+ "mode": config.get("mode", "server"),
285
+ "server_path": config.get("server_path", "/onebot12"),
286
+ "server_token": config.get("server_token", ""),
287
+ "client_url": config.get("client_url", "ws://127.0.0.1:3001"),
288
+ "client_token": config.get("client_token", ""),
289
+ "enabled": config.get("enabled", True),
290
+ "name": account_name,
291
+ "platform": config.get("platform", "onebot12"),
292
+ "implementation": config.get("implementation", "")
293
+ }
294
+
295
+ accounts[account_name] = OneBot12AccountConfig(**merged_config)
296
+
297
+ return accounts
298
+
299
+ async def call_api(self, endpoint: str, account_id: str = None, **params):
300
+ """调用OneBot12 API"""
301
+ # 确定使用的账户ID
302
+ if account_id is None:
303
+ if not self.accounts:
304
+ raise ValueError("没有配置任何OneBot12账户")
305
+ account_id = next(iter(self.accounts.keys()))
306
+
307
+ if account_id not in self.accounts:
308
+ raise ValueError(f"账户 {account_id} 不存在")
309
+
310
+ account = self.accounts[account_id]
311
+ if not account.enabled:
312
+ raise ValueError(f"账户 {account_id} 已禁用")
313
+
314
+ connection = self.connections.get(account_id)
315
+ if not connection:
316
+ raise ConnectionError(f"账户 {account_id} 尚未连接到OneBot12")
317
+
318
+ if connection.closed:
319
+ raise ConnectionError(f"账户 {account_id} 的WebSocket连接已关闭")
320
+
321
+ # 确保该账户的响应Future字典存在
322
+ if account_id not in self._api_response_futures:
323
+ self._api_response_futures[account_id] = {}
324
+
325
+ echo = str(hash(str(params + (account_id, endpoint))))
326
+ future = asyncio.get_event_loop().create_future()
327
+ self._api_response_futures[account_id][echo] = future
328
+ self.logger.debug(f"账户 {account_id} 创建API调用Future: {echo}")
329
+
330
+ # OneBot12标准API请求格式
331
+ payload = {
332
+ "action": endpoint,
333
+ "params": params,
334
+ "echo": echo
335
+ }
336
+
337
+ self.logger.debug(f"账户 {account_id} 准备发送OneBot12 API请求: {payload}")
338
+
339
+ try:
340
+ await connection.send_str(json.dumps(payload))
341
+ self.logger.debug(f"账户 {account_id} 调用OneBot12 API: {endpoint}")
342
+ except Exception as e:
343
+ self.logger.error(f"账户 {account_id} 发送API请求失败: {str(e)}")
344
+ if echo in self._api_response_futures[account_id]:
345
+ del self._api_response_futures[account_id][echo]
346
+ raise
347
+
348
+ try:
349
+ self.logger.debug(f"账户 {account_id} 开始等待Future: {echo}")
350
+ raw_response = await asyncio.wait_for(future, timeout=self.default_timeout)
351
+ self.logger.debug(f"账户 {account_id} OneBot12 API响应: {raw_response}")
352
+
353
+ # OneBot12标准响应处理
354
+ standardized_response = {
355
+ "status": raw_response.get("status", "ok"),
356
+ "retcode": raw_response.get("retcode", 0),
357
+ "data": raw_response.get("data"),
358
+ "message": raw_response.get("message", ""),
359
+ "self": {"user_id": account_id}
360
+ }
361
+
362
+ if "message_id" in raw_response:
363
+ standardized_response["message_id"] = raw_response["message_id"]
364
+
365
+ if "echo" in params:
366
+ standardized_response["echo"] = params["echo"]
367
+
368
+ return standardized_response
369
+
370
+ except asyncio.TimeoutError:
371
+ self.logger.error(f"账户 {account_id} API调用超时: {endpoint}")
372
+ if not future.done():
373
+ future.cancel()
374
+
375
+ timeout_response = {
376
+ "status": "failed",
377
+ "retcode": 33001,
378
+ "data": None,
379
+ "message": f"账户 {account_id} API调用超时: {endpoint}",
380
+ "self": {"user_id": account_id}
381
+ }
382
+
383
+ if "echo" in params:
384
+ timeout_response["echo"] = params["echo"]
385
+
386
+ return timeout_response
387
+
388
+ finally:
389
+ async def delayed_cleanup():
390
+ await asyncio.sleep(0.1)
391
+ if account_id in self._api_response_futures and echo in self._api_response_futures[account_id]:
392
+ del self._api_response_futures[account_id][echo]
393
+ self.logger.debug(f"账户 {account_id} 已删除API响应Future: {echo}")
394
+
395
+ asyncio.create_task(delayed_cleanup())
396
+
397
+ async def connect(self, account_id: str, retry_interval=None):
398
+ """连接指定账户的OneBot12服务"""
399
+ if account_id not in self.accounts:
400
+ raise ValueError(f"账户 {account_id} 不存在")
401
+
402
+ account = self.accounts[account_id]
403
+ if account.mode != "client":
404
+ return
405
+
406
+ # 创建该账户的session
407
+ if account_id not in self.sessions:
408
+ self.sessions[account_id] = aiohttp.ClientSession()
409
+
410
+ headers = {}
411
+ if account.client_token:
412
+ headers["Authorization"] = f"Bearer {account.client_token}"
413
+
414
+ url = account.client_url
415
+ retry_count = 0
416
+ retry_interval = retry_interval or self.default_retry_interval
417
+
418
+ while self._is_running:
419
+ try:
420
+ self.connections[account_id] = await self.sessions[account_id].ws_connect(url, headers=headers)
421
+ self.logger.info(f"账户 {account_id} 成功连接到OneBot12服务器: {url}")
422
+ asyncio.create_task(self._listen(account_id))
423
+ return
424
+ except Exception as e:
425
+ retry_count += 1
426
+ self.logger.error(f"账户 {account_id} 第 {retry_count} 次连接失败: {str(e)}")
427
+ self.logger.info(f"账户 {account_id} 将在 {retry_interval} 秒后重试...")
428
+ await asyncio.sleep(retry_interval)
429
+
430
+ async def _listen(self, account_id: str):
431
+ """监听指定账户的WebSocket消息"""
432
+ connection = self.connections.get(account_id)
433
+ if not connection:
434
+ return
435
+
436
+ try:
437
+ self.logger.debug(f"账户 {account_id} 开始监听OneBot12 WebSocket消息")
438
+ async for msg in connection:
439
+ if msg.type == aiohttp.WSMsgType.TEXT:
440
+ self.logger.debug(f"账户 {account_id} 收到WebSocket消息: {msg.data[:100]}...")
441
+ asyncio.create_task(self._handle_message(msg.data, account_id))
442
+ elif msg.type == aiohttp.WSMsgType.CLOSED:
443
+ self.logger.info(f"账户 {account_id} WebSocket连接已关闭")
444
+ break
445
+ elif msg.type == aiohttp.WSMsgType.ERROR:
446
+ self.logger.error(f"账户 {account_id} WebSocket错误: {connection.exception()}")
447
+ except Exception as e:
448
+ self.logger.error(f"账户 {account_id} WebSocket监听异常: {str(e)}")
449
+ finally:
450
+ self.logger.debug(f"账户 {account_id} 退出WebSocket监听")
451
+ if account_id in self.connections:
452
+ del self.connections[account_id]
453
+
454
+ if self._is_running and self.accounts[account_id].enabled and self.accounts[account_id].mode == "client":
455
+ self.logger.info(f"账户 {account_id} 开始重连...")
456
+ self.reconnect_tasks[account_id] = asyncio.create_task(self.connect(account_id))
457
+
458
+ async def _handle_api_response(self, data: Dict, account_id: str):
459
+ """处理API响应"""
460
+ echo = data.get("echo")
461
+ self.logger.debug(f"账户 {account_id} 收到OneBot12 API响应, echo: {echo}")
462
+
463
+ if account_id not in self._api_response_futures:
464
+ self.logger.warning(f"账户 {account_id} 不存在响应Future字典")
465
+ return
466
+
467
+ future = self._api_response_futures[account_id].get(echo)
468
+
469
+ if future:
470
+ if not future.done():
471
+ future.set_result(data)
472
+ else:
473
+ self.logger.warning(f"Future已经完成,无法设置结果: {echo}")
474
+ else:
475
+ self.logger.warning(f"账户 {account_id} 未找到对应的Future: {echo}")
476
+
477
+ async def _handle_message(self, raw_msg: str, account_id: str):
478
+ """处理WebSocket消息"""
479
+ try:
480
+ data = json.loads(raw_msg)
481
+
482
+ # API响应优先处理
483
+ if "echo" in data:
484
+ self.logger.debug(f"账户 {account_id} 识别为OneBot12 API响应消息: {data.get('echo')}")
485
+ await self._handle_api_response(data, account_id)
486
+ return
487
+
488
+ self.logger.debug(f"账户 {account_id} 处理OneBot12事件: {data.get('type')}")
489
+
490
+ # OneBot12事件直接提交,无需转换
491
+ if hasattr(self.adapter, "emit") and data:
492
+ # 确保事件包含账户信息
493
+ if "self" not in data:
494
+ data["self"] = {}
495
+ if not data.get("self", {}).get("user_id"):
496
+ data["self"]["user_id"] = account_id
497
+
498
+ self.logger.debug(f"账户 {account_id} 提交OneBot12事件: {json.dumps(data, ensure_ascii=False)}")
499
+ await self.adapter.emit(data)
500
+
501
+ except json.JSONDecodeError:
502
+ self.logger.error(f"账户 {account_id} JSON解析失败: {raw_msg}")
503
+ except Exception as e:
504
+ self.logger.error(f"账户 {account_id} 消息处理异常: {str(e)}")
505
+
506
+ async def _ws_handler(self, websocket: WebSocket, account_id: str = "default"):
507
+ """WebSocket处理器"""
508
+ self.connections[account_id] = websocket
509
+ self.logger.info(f"账户 {account_id} 的OneBot12客户端已连接")
510
+
511
+ try:
512
+ while True:
513
+ data = await websocket.receive_text()
514
+ asyncio.create_task(self._handle_message(data, account_id))
515
+ except WebSocketDisconnect:
516
+ self.logger.info(f"账户 {account_id} 的OneBot12客户端断开连接")
517
+ except Exception as e:
518
+ self.logger.error(f"账户 {account_id} WebSocket处理异常: {str(e)}")
519
+ finally:
520
+ if account_id in self.connections:
521
+ del self.connections[account_id]
522
+
523
+ async def _auth_handler(self, websocket: WebSocket, account_id: str = "default"):
524
+ """认证处理器"""
525
+ if account_id not in self.accounts:
526
+ self.logger.warning(f"账户 {account_id} 不存在")
527
+ await websocket.close(code=1008)
528
+ return False
529
+
530
+ account = self.accounts[account_id]
531
+ if account.server_token:
532
+ client_token = websocket.headers.get("Authorization", "").replace("Bearer ", "")
533
+ if not client_token:
534
+ query = dict(websocket.query_params)
535
+ client_token = query.get("token", "")
536
+
537
+ if client_token != account.server_token:
538
+ self.logger.warning(f"账户 {account_id} 客户端提供的Token无效")
539
+ await websocket.close(code=1008)
540
+ return False
541
+ return True
542
+
543
+ async def register_websocket(self):
544
+ """注册WebSocket路由"""
545
+ # 注册所有server模式的账户
546
+ for account_id, account in self.accounts.items():
547
+ if account.mode == "server" and account.enabled:
548
+ path = account.server_path
549
+
550
+ def make_ws_handler(account_id):
551
+ async def ws_handler(websocket):
552
+ await self._ws_handler(websocket, account_id)
553
+ return ws_handler
554
+
555
+ def make_auth_handler(account_id):
556
+ async def auth_handler(websocket):
557
+ return await self._auth_handler(websocket, account_id)
558
+ return auth_handler
559
+
560
+ router.register_websocket(
561
+ f"onebot12_{account_id}",
562
+ path,
563
+ make_ws_handler(account_id),
564
+ auth_handler=make_auth_handler(account_id)
565
+ )
566
+ self.logger.info(f"已注册账户 {account_id} 的Server模式WebSocket路由: {path}")
567
+
568
+ async def start(self):
569
+ """启动适配器"""
570
+ self._is_running = True
571
+
572
+ server_accounts = [aid for aid, acc in self.accounts.items() if acc.mode == "server" and acc.enabled]
573
+ client_accounts = [aid for aid, acc in self.accounts.items() if acc.mode == "client" and acc.enabled]
574
+
575
+ if server_accounts:
576
+ self.logger.info(f"正在注册 {len(server_accounts)} 个Server模式账户的WebSocket路由")
577
+ await self.register_websocket()
578
+
579
+ if client_accounts:
580
+ self.logger.info(f"正在启动 {len(client_accounts)} 个Client模式账户")
581
+ for account_id in client_accounts:
582
+ self.reconnect_tasks[account_id] = asyncio.create_task(self.connect(account_id))
583
+
584
+ if not server_accounts and not client_accounts:
585
+ self.logger.warning("没有启用任何账户")
586
+
587
+ self.logger.info("OneBot12适配器启动完成")
588
+
589
+ async def shutdown(self):
590
+ """关闭适配器"""
591
+ self._is_running = False
592
+
593
+ # 取消所有重连任务
594
+ for task in self.reconnect_tasks.values():
595
+ if not task.done():
596
+ task.cancel()
597
+ self.reconnect_tasks.clear()
598
+
599
+ # 关闭所有连接
600
+ for account_id, connection in self.connections.items():
601
+ try:
602
+ if hasattr(connection, 'closed') and not connection.closed:
603
+ await connection.close()
604
+ except Exception as e:
605
+ self.logger.error(f"关闭账户 {account_id} 连接失败: {str(e)}")
606
+ self.connections.clear()
607
+
608
+ # 关闭所有session
609
+ for account_id, session in self.sessions.items():
610
+ try:
611
+ await session.close()
612
+ except Exception as e:
613
+ self.logger.error(f"关闭账户 {account_id} session失败: {str(e)}")
614
+ self.sessions.clear()
615
+
616
+ self.logger.info("OneBot12适配器已关闭")
@@ -0,0 +1,3 @@
1
+ from .Core import OneBot12Adapter
2
+
3
+ __all__ = ["OneBot12Adapter"]
@@ -0,0 +1,453 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-OneBot12Adapter
3
+ Version: 1.0.0
4
+ Summary: ErisPulse的OneBotV12协议适配模块,基线协议适配器
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-OneBot12Adapter
29
+ Requires-Python: >=3.9
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Dynamic: license-file
33
+
34
+ # OneBot12Adapter 模块文档
35
+
36
+ ## 简介
37
+
38
+ OneBot12Adapter 是基于 [ErisPulse](https://github.com/ErisPulse/ErisPulse/) 架构开发的 **OneBot V12 协议适配器模块**。作为ErisPulse框架的基线协议适配器,它提供完全符合OneBot12标准的事件处理机制、连接管理功能,并支持Server和Client两种运行模式。
39
+
40
+ ---
41
+
42
+ ## 特性
43
+
44
+ - **多账户支持** - 支持同时配置和运行多个OneBot12账户
45
+ - **双运行模式** - 支持Server(服务端)和Client(客户端)模式
46
+ - **异步处理** - 全异步设计,支持高并发操作
47
+ - **自动重连** - 网络异常时自动重连机制
48
+ - **标准化响应** - 统一的API响应格式
49
+
50
+ ---
51
+
52
+ ## 安装与配置
53
+
54
+ ### 基本安装
55
+
56
+ 将适配器模块放置在ErisPulse的适配器目录中,并在配置中启用:
57
+
58
+ ```toml
59
+ [adapters]
60
+ onebot12 = "OneBot12Adapter"
61
+ ```
62
+
63
+ ### 多账户配置
64
+
65
+ OneBot12适配器采用多账户配置结构:
66
+
67
+ ```toml
68
+ # 主账户配置
69
+ [OneBotv12_Adapter.accounts.main]
70
+ mode = "server"
71
+ server_path = "/onebot12"
72
+ server_token = "your_main_token"
73
+ enabled = true
74
+ platform = "onebot12"
75
+ implementation = "go-cqhttp"
76
+
77
+ # 备用账户配置
78
+ [OneBotv12_Adapter.accounts.backup]
79
+ mode = "client"
80
+ client_url = "ws://127.0.0.1:3002"
81
+ client_token = "your_backup_token"
82
+ enabled = true
83
+ platform = "onebot12"
84
+ implementation = "shinonome"
85
+
86
+ # 测试账户配置
87
+ [OneBotv12_Adapter.accounts.test]
88
+ mode = "client"
89
+ client_url = "ws://127.0.0.1:3003"
90
+ enabled = false
91
+ ```
92
+
93
+ ### 默认账户配置
94
+
95
+ 如果没有配置文件,适配器会自动创建默认配置:
96
+
97
+ ```toml
98
+ [OneBotv12_Adapter.accounts.default]
99
+ mode = "server"
100
+ server_path = "/onebot12"
101
+ server_token = ""
102
+ enabled = true
103
+ platform = "onebot12"
104
+ ```
105
+
106
+ ### 配置项说明
107
+
108
+ 每个账户独立配置以下选项:
109
+
110
+ - `mode`: 运行模式,可选 "server"(服务端)或 "client"(客户端)
111
+ - `server_path`: Server模式下的WebSocket路径
112
+ - `server_token`: Server模式下的认证Token(可选)
113
+ - `client_url`: Client模式下要连接的WebSocket地址
114
+ - `client_token`: Client模式下的认证Token(可选)
115
+ - `enabled`: 是否启用该账户(true/false)
116
+ - `platform`: 平台标识,默认为 "onebot12"
117
+ - `implementation`: 实现标识,如 "go-cqhttp"(可选)
118
+
119
+ ---
120
+
121
+ ## 使用方法
122
+
123
+ ### 基础用法
124
+
125
+ ```python
126
+ from ErisPulse.Core import adapter
127
+
128
+ # 获取OneBot12适配器实例
129
+ onebot12 = adapter.get("onebot12")
130
+
131
+ # 发送文本消息
132
+ await onebot12.Send.To("group", 123456).Text("Hello World!")
133
+
134
+ # 发送图片消息
135
+ await onebot12.Send.To("user", 789012).Image("http://example.com/image.jpg")
136
+
137
+ # 发送@消息
138
+ await onebot12.Send.To("group", 123456).Mention(789012, "用户名")
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 消息发送示例(DSL 链式风格)
144
+
145
+ ### 基础消息类型
146
+
147
+ #### 文本消息
148
+ ```python
149
+ await onebot12.Send.To("user", 123456).Text("Hello World!")
150
+ ```
151
+
152
+ #### 图片消息
153
+ ```python
154
+ # 支持URL或二进制数据
155
+ await onebot12.Send.To("user", 123456).Image("http://example.com/image.jpg")
156
+
157
+ # 支持二进制数据
158
+ with open("local_image.png", "rb") as f:
159
+ image_data = f.read()
160
+ await onebot12.Send.To("user", 123456).Image(image_data)
161
+ ```
162
+
163
+ #### 音频消息
164
+ ```python
165
+ await onebot12.Send.To("user", 123456).Audio("http://example.com/audio.ogg")
166
+ ```
167
+
168
+ #### 视频消息
169
+ ```python
170
+ await onebot12.Send.To("user", 123456).Video("http://example.com/video.mp4")
171
+ ```
172
+
173
+ ### 交互消息
174
+
175
+ #### @消息
176
+ ```python
177
+ await onebot12.Send.To("group", 123456).Mention(789012, "用户名")
178
+ ```
179
+
180
+ #### 回复消息
181
+ ```python
182
+ # 仅回复,不附加内容
183
+ await onebot12.Send.To("group", 123456).Reply("msg_id_123")
184
+
185
+ # 回复并附加内容
186
+ await onebot12.Send.To("group", 123456).Reply("msg_id_123", "这是回复内容")
187
+ ```
188
+
189
+ #### 表情包/贴纸
190
+ ```python
191
+ await onebot12.Send.To("user", 123456).Sticker("sticker_id_456")
192
+ ```
193
+
194
+ #### 位置消息
195
+ ```python
196
+ await onebot12.Send.To("group", 123456).Location(
197
+ latitude=39.9042,
198
+ longitude=116.4074,
199
+ title="北京市",
200
+ content="中华人民共和国首都"
201
+ )
202
+ ```
203
+
204
+ ### 复合消息
205
+
206
+ #### 发送原始消息段
207
+ ```python
208
+ message_segments = [
209
+ {"type": "text", "data": {"text": "你好"}},
210
+ {"type": "mention", "data": {"user_id": "123456", "user_name": "用户名"}},
211
+ {"type": "image", "data": {"file_id": "image_id"}}
212
+ ]
213
+ await onebot12.Send.To("group", 123456).Raw(message_segments)
214
+ ```
215
+
216
+ #### 批量发送
217
+ ```python
218
+ targets = ["user1", "user2", "user3"]
219
+ await onebot12.Send.To("user", targets).Batch(["消息1", "消息2", "消息3"])
220
+ ```
221
+
222
+ ### 消息管理
223
+
224
+ #### 撤回消息
225
+ ```python
226
+ await onebot12.Send.To("group", 123456).Recall("message_id_123")
227
+ ```
228
+
229
+ #### 编辑消息
230
+ ```python
231
+ # 编辑为文本
232
+ await onebot12.Send.To("group", 123456).Edit("message_id_123", "修改后的内容")
233
+
234
+ # 编辑为复合消息
235
+ new_content = [
236
+ {"type": "text", "data": {"text": "修改后的内容"}},
237
+ {"type": "image", "data": {"file_id": "new_image_id"}}
238
+ ]
239
+ await onebot12.Send.To("group", 123456).Edit("message_id_123", new_content)
240
+ ```
241
+
242
+ ---
243
+
244
+ ## API 调用方式
245
+
246
+ ### 多账户消息发送
247
+
248
+ ```python
249
+ # 使用指定账户发送消息
250
+ await onebot12.Send.To("group", 123456).Account("main").Text("来自主账户的消息")
251
+ await onebot12.Send.To("group", 123456).Account("backup").Text("来自备用账户的消息")
252
+
253
+ # 使用默认账户发送(第一个启用的账户)
254
+ await onebot12.Send.To("group", 123456).Text("来自默认账户的消息")
255
+ ```
256
+
257
+ ### 直接API调用
258
+
259
+ ```python
260
+ # 发送消息
261
+ response = await onebot12.call_api(
262
+ "send_message",
263
+ account_id="main",
264
+ detail_type="group",
265
+ group_id=123456,
266
+ content=[{"type": "text", "data": {"text": "Hello"}}]
267
+ )
268
+
269
+ # 获取自身信息
270
+ self_info = await onebot12.call_api("get_self_info", account_id="main")
271
+
272
+ # 获取用户信息
273
+ user_info = await onebot12.call_api(
274
+ "get_user_info",
275
+ account_id="main",
276
+ user_id="user123"
277
+ )
278
+
279
+ # 获取群组信息
280
+ group_info = await onebot12.call_api(
281
+ "get_group_info",
282
+ account_id="main",
283
+ group_id="group456"
284
+ )
285
+ ```
286
+
287
+ ---
288
+
289
+ ## 事件处理
290
+
291
+ OneBot12适配器支持标准的事件监听方式:
292
+
293
+ ```python
294
+ # 监听消息事件
295
+ @sdk.adapter.OneBot12.on("message")
296
+ async def handle_message(event):
297
+ if event["detail_type"] == "private":
298
+ print(f"收到私聊消息: {event['alt_message']}")
299
+ elif event["detail_type"] == "group":
300
+ print(f"收到群聊消息: {event['alt_message']}")
301
+
302
+ # 监听通知事件
303
+ @sdk.adapter.OneBot12.on("notice")
304
+ async def handle_notice(event):
305
+ if event["detail_type"] == "group_member_increase":
306
+ print(f"群成员增加: {event['user_id']} 加入了 {event['group_id']}")
307
+ elif event["detail_type"] == "group_member_decrease":
308
+ print(f"群成员减少: {event['user_id']} 离开了 {event['group_id']}")
309
+
310
+ # 监听请求事件
311
+ @sdk.adapter.OneBot12.on("request")
312
+ async def handle_request(event):
313
+ if event["detail_type"] == "friend":
314
+ print(f"收到好友请求: {event['user_id']}")
315
+ elif event["detail_type"] == "group":
316
+ print(f"收到群邀请: {event['group_id']}")
317
+
318
+ # 监听元事件
319
+ @sdk.adapter.OneBot12.on("meta_event")
320
+ async def handle_meta_event(event):
321
+ if event["detail_type"] == "lifecycle":
322
+ print(f"生命周期事件: {event['sub_type']}")
323
+ elif event["detail_type"] == "heartbeat":
324
+ print(f"心跳事件: 间隔 {event['interval']}ms")
325
+ ```
326
+
327
+ ### 事件数据结构
328
+
329
+ OneBot12适配器直接处理标准格式的OneBot12事件,无需转换:
330
+
331
+ ```python
332
+ # 私聊消息事件示例
333
+ {
334
+ "id": "event-uuid",
335
+ "type": "message",
336
+ "detail_type": "private",
337
+ "self": {"user_id": "bot-id"},
338
+ "user_id": "user-id",
339
+ "message": [{"type": "text", "data": {"text": "Hello"}}],
340
+ "alt_message": "Hello",
341
+ "time": 1234567890
342
+ }
343
+
344
+ # 群聊消息事件示例
345
+ {
346
+ "id": "event-uuid",
347
+ "type": "message",
348
+ "detail_type": "group",
349
+ "self": {"user_id": "bot-id"},
350
+ "user_id": "user-id",
351
+ "group_id": "group-id",
352
+ "message": [{"type": "text", "data": {"text": "Hello group"}}],
353
+ "alt_message": "Hello group",
354
+ "time": 1234567890
355
+ }
356
+ ```
357
+
358
+ ---
359
+
360
+ ## 运行模式说明
361
+
362
+ ### 多账户运行模式
363
+
364
+ OneBot12适配器支持同时运行多个账户,每个账户可以独立配置为Server或Client模式:
365
+
366
+ ```python
367
+ # 查看所有账户
368
+ accounts = onebot12.accounts
369
+ print(f"已配置账户: {list(accounts.keys())}")
370
+
371
+ # 检查特定账户状态
372
+ if "test" in accounts:
373
+ test_account = accounts["test"]
374
+ print(f"测试账户模式: {test_account.mode}, 启用状态: {test_account.enabled}")
375
+ ```
376
+
377
+ ### Server 模式(作为服务端监听连接)
378
+
379
+ - 启动一个 WebSocket 服务器等待 OneBot12 客户端连接
380
+ - 适用于部署多个 bot 客户端连接至同一服务端的场景
381
+ - 每个Server账户会注册独立的WebSocket路由路径
382
+
383
+ ### Client 模式(主动连接 OneBot12)
384
+
385
+ - 主动连接到 OneBot12 服务端
386
+ - 更适合单个 bot 实例直接连接的情况
387
+ - 支持自动重连机制
388
+
389
+ ---
390
+
391
+ ## 支持的消息类型及对应方法
392
+
393
+ | 方法名 | 参数说明 | 用途 |
394
+ |--------|----------|------|
395
+ | `.Text(text: str)` | 发送纯文本消息 | 基础消息类型 |
396
+ | `.Image(file: str/bytes, filename: str)` | 发送图片消息 | 支持URL、Base64或bytes |
397
+ | `.Audio(file: str/bytes, filename: str)` | 发送音频消息 | 支持标准音频格式 |
398
+ | `.Video(file: str/bytes, filename: str)` | 发送视频消息 | 支持标准视频格式 |
399
+ | `.Mention(user_id: str/int, user_name: str)` | 发送@消息 | 群聊@功能 |
400
+ | `.Reply(message_id: str/int, content: str)` | 发送回复消息 | 消息回复功能 |
401
+ | `.Sticker(file_id: str)` | 发送表情包/贴纸 | 表情包功能 |
402
+ | `.Location(lat: float, lon: float, title: str, content: str)` | 发送位置 | 位置分享 |
403
+ | `.Raw(message_list: List[Dict])` | 发送原生消息段 | 自定义消息内容 |
404
+ | `.Recall(message_id: str/int)` | 撤回指定消息 | 消息管理 |
405
+ | `.Edit(message_id: str/int, content: Union[str, List[Dict]])` | 编辑消息 | 消息编辑 |
406
+ | `.Batch(target_ids: List[str], message: Union[str, List[Dict]], target_type: str)` | 批量发送消息 | 群发功能 |
407
+
408
+ ---
409
+
410
+ ## 管理接口
411
+
412
+ ```python
413
+ # 获取所有账户信息
414
+ accounts = onebot12.accounts
415
+
416
+ # 检查账户连接状态
417
+ connection_status = {
418
+ account_id: connection is not None and not connection.closed
419
+ for account_id, connection in onebot12.connections.items()
420
+ }
421
+ print(f"账户连接状态: {connection_status}")
422
+
423
+ # 动态启用/禁用账户(需要重启适配器)
424
+ onebot12.accounts["test"].enabled = False
425
+ ```
426
+
427
+ ---
428
+
429
+ ## 错误处理
430
+
431
+ 适配器提供完善的错误处理机制:
432
+
433
+ 1. **网络连接异常自动重连** - 支持每个账户独立重连,间隔30秒
434
+ 2. **API调用超时处理** - 固定30秒超时机制
435
+ 3. **消息发送失败重试** - 最多3次自动重试
436
+ 4. **标准化错误响应** - 统一的错误格式和状态码
437
+
438
+ ---
439
+
440
+ ## 注意事项
441
+
442
+ 1. 生产环境建议启用 Token 认证以保证安全性
443
+ 2. 对于二进制内容(如图片、音频等),支持直接传入 bytes 数据
444
+ 3. 批量发送时建议适当控制并发数量,避免API限制
445
+ 4. 长时间运行的机器人建议监控连接状态,确保服务可用性
446
+
447
+ ---
448
+
449
+ ## 参考链接
450
+
451
+ - [ErisPulse 主库](https://github.com/ErisPulse/ErisPulse/)
452
+ - [OneBot V12 协议文档](https://12.onebot.dev/)
453
+ - [平台特性文档](./platform-features.md)
@@ -0,0 +1,8 @@
1
+ OneBot12Adapter/Core.py,sha256=ccVFH80p_akq3EWZEBu5bMyiBwLuwrBZY_UYrjFVelU,26167
2
+ OneBot12Adapter/__init__.py,sha256=_rVYimIP2koiHYReVGKBJ5o3hFuVvLj1L1RqQ-7n3AA,65
3
+ erispulse_onebot12adapter-1.0.0.dist-info/licenses/LICENSE,sha256=Q0nf8qebrO7_DA7rL4LaoHogeL38vE265P55uTIghmY,1064
4
+ erispulse_onebot12adapter-1.0.0.dist-info/METADATA,sha256=lgDUJ9NYjVZpq2Mx_1KVLwGF3q9qs9GzPZpZKQ2T2cs,12973
5
+ erispulse_onebot12adapter-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ erispulse_onebot12adapter-1.0.0.dist-info/entry_points.txt,sha256=Cjs3qlWJXBl98t8peEsSZYaHN9VTSW3BGJ59kspEaAs,63
7
+ erispulse_onebot12adapter-1.0.0.dist-info/top_level.txt,sha256=uvlftRBGSF_fFElBbBz-fTPUpe4Mlmj1QpOEAtdWTgk,16
8
+ erispulse_onebot12adapter-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [erispulse.adapter]
2
+ onebot12 = OneBot12Adapter:OneBot12Adapter
@@ -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 @@
1
+ OneBot12Adapter