ForcomeBot 2.2.4__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.
- forcomebot-2.2.4.dist-info/METADATA +342 -0
- forcomebot-2.2.4.dist-info/RECORD +36 -0
- forcomebot-2.2.4.dist-info/WHEEL +4 -0
- forcomebot-2.2.4.dist-info/entry_points.txt +4 -0
- src/__init__.py +68 -0
- src/__main__.py +487 -0
- src/api/__init__.py +21 -0
- src/api/routes.py +775 -0
- src/api/websocket.py +280 -0
- src/auth/__init__.py +33 -0
- src/auth/database.py +87 -0
- src/auth/dingtalk.py +373 -0
- src/auth/jwt_handler.py +129 -0
- src/auth/middleware.py +260 -0
- src/auth/models.py +107 -0
- src/auth/routes.py +385 -0
- src/clients/__init__.py +7 -0
- src/clients/langbot.py +710 -0
- src/clients/qianxun.py +388 -0
- src/core/__init__.py +19 -0
- src/core/config_manager.py +411 -0
- src/core/log_collector.py +167 -0
- src/core/message_queue.py +364 -0
- src/core/state_store.py +242 -0
- src/handlers/__init__.py +8 -0
- src/handlers/message_handler.py +833 -0
- src/handlers/message_parser.py +325 -0
- src/handlers/scheduler.py +822 -0
- src/models.py +77 -0
- src/static/assets/index-B4i68B5_.js +50 -0
- src/static/assets/index-BPXisDkw.css +2 -0
- src/static/index.html +14 -0
- src/static/vite.svg +1 -0
- src/utils/__init__.py +13 -0
- src/utils/text_processor.py +166 -0
- src/utils/xml_parser.py +215 -0
src/clients/langbot.py
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
"""LangBot OneBot v11 WebSocket 客户端 - 带指数退避重连
|
|
2
|
+
|
|
3
|
+
重构版本:
|
|
4
|
+
- 实现指数退避重连(1s→2s→4s→...→60s)
|
|
5
|
+
- 集成 StateStore(替换内部映射)
|
|
6
|
+
- 添加 update_connection() 方法(配置热更新)
|
|
7
|
+
- 保持所有原有方法签名不变
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional, Callable, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..core.state_store import StateStore
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LangBotClient:
|
|
22
|
+
"""
|
|
23
|
+
OneBot v11 反向 WebSocket 客户端(带指数退避重连)
|
|
24
|
+
连接到 LangBot 的 aiocqhttp 适配器
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
host: str = "127.0.0.1",
|
|
30
|
+
port: int = 2280,
|
|
31
|
+
access_token: str = "",
|
|
32
|
+
initial_reconnect_delay: float = 1.0,
|
|
33
|
+
max_reconnect_delay: float = 60.0
|
|
34
|
+
):
|
|
35
|
+
"""初始化LangBot客户端
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
host: LangBot主机地址
|
|
39
|
+
port: LangBot端口
|
|
40
|
+
access_token: 访问令牌
|
|
41
|
+
initial_reconnect_delay: 初始重连延迟(秒),默认1秒
|
|
42
|
+
max_reconnect_delay: 最大重连延迟(秒),默认60秒
|
|
43
|
+
"""
|
|
44
|
+
self.host = host
|
|
45
|
+
self.port = port
|
|
46
|
+
self.access_token = access_token
|
|
47
|
+
self.ws = None
|
|
48
|
+
self._connected = False
|
|
49
|
+
self._reconnecting = False
|
|
50
|
+
self._event_callback: Optional[Callable] = None
|
|
51
|
+
self._receive_task: Optional[asyncio.Task] = None
|
|
52
|
+
self._reconnect_task: Optional[asyncio.Task] = None
|
|
53
|
+
self._self_id = "10000" # 模拟的机器人ID
|
|
54
|
+
|
|
55
|
+
# 指数退避重连配置
|
|
56
|
+
self._initial_reconnect_delay = initial_reconnect_delay
|
|
57
|
+
self._max_reconnect_delay = max_reconnect_delay
|
|
58
|
+
self._current_reconnect_delay = initial_reconnect_delay
|
|
59
|
+
|
|
60
|
+
# StateStore 集成(可选)
|
|
61
|
+
self._state_store: Optional["StateStore"] = None
|
|
62
|
+
|
|
63
|
+
# 内部映射(当没有 StateStore 时使用)
|
|
64
|
+
self._group_id_mapping: dict = {}
|
|
65
|
+
self._message_user_mapping: dict = {}
|
|
66
|
+
|
|
67
|
+
def set_state_store(self, store: "StateStore"):
|
|
68
|
+
"""设置状态存储器
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
store: StateStore实例
|
|
72
|
+
"""
|
|
73
|
+
self._state_store = store
|
|
74
|
+
logger.info("LangBot客户端已集成StateStore")
|
|
75
|
+
|
|
76
|
+
async def update_connection(self, host: str, port: int, access_token: str = ""):
|
|
77
|
+
"""更新连接配置并重连(配置热更新)
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
host: 新的主机地址
|
|
81
|
+
port: 新的端口
|
|
82
|
+
access_token: 新的访问令牌
|
|
83
|
+
"""
|
|
84
|
+
logger.info(f"更新LangBot连接配置: {host}:{port}")
|
|
85
|
+
self.host = host
|
|
86
|
+
self.port = port
|
|
87
|
+
self.access_token = access_token
|
|
88
|
+
|
|
89
|
+
# 关闭现有连接
|
|
90
|
+
await self.close()
|
|
91
|
+
|
|
92
|
+
# 重置重连延迟
|
|
93
|
+
self._current_reconnect_delay = self._initial_reconnect_delay
|
|
94
|
+
|
|
95
|
+
# 重新连接
|
|
96
|
+
await self.connect()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def is_connected(self) -> bool:
|
|
100
|
+
"""是否已连接"""
|
|
101
|
+
return self._connected
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_reconnecting(self) -> bool:
|
|
105
|
+
"""是否正在重连"""
|
|
106
|
+
return self._reconnecting
|
|
107
|
+
|
|
108
|
+
async def connect(self) -> bool:
|
|
109
|
+
"""连接到 LangBot
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
是否连接成功
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
import websockets
|
|
116
|
+
|
|
117
|
+
ws_url = f"ws://{self.host}:{self.port}/ws"
|
|
118
|
+
logger.info(f"正在连接 LangBot: {ws_url}")
|
|
119
|
+
|
|
120
|
+
# aiocqhttp 需要特定的请求头
|
|
121
|
+
headers = {
|
|
122
|
+
"X-Self-ID": self._self_id,
|
|
123
|
+
"X-Client-Role": "Universal" # 同时处理事件和API
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if self.access_token:
|
|
127
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
128
|
+
|
|
129
|
+
logger.info(f"连接头: {headers}")
|
|
130
|
+
|
|
131
|
+
# 尝试不同版本的 websockets 库
|
|
132
|
+
try:
|
|
133
|
+
# websockets >= 10.0
|
|
134
|
+
self.ws = await websockets.connect(
|
|
135
|
+
ws_url,
|
|
136
|
+
additional_headers=headers
|
|
137
|
+
)
|
|
138
|
+
except TypeError:
|
|
139
|
+
try:
|
|
140
|
+
# websockets 旧版本
|
|
141
|
+
self.ws = await websockets.connect(
|
|
142
|
+
ws_url,
|
|
143
|
+
extra_headers=headers
|
|
144
|
+
)
|
|
145
|
+
except TypeError:
|
|
146
|
+
# 更旧的版本
|
|
147
|
+
self.ws = await websockets.connect(ws_url)
|
|
148
|
+
|
|
149
|
+
self._connected = True
|
|
150
|
+
self._reconnecting = False
|
|
151
|
+
# 重置重连延迟
|
|
152
|
+
self._current_reconnect_delay = self._initial_reconnect_delay
|
|
153
|
+
|
|
154
|
+
logger.info(f"WebSocket 连接成功: {ws_url}")
|
|
155
|
+
|
|
156
|
+
# 发送生命周期连接事件
|
|
157
|
+
await self._send_lifecycle_event()
|
|
158
|
+
|
|
159
|
+
# 启动消息接收任务
|
|
160
|
+
self._receive_task = asyncio.create_task(self._receive_loop())
|
|
161
|
+
|
|
162
|
+
logger.info("已连接到 LangBot (OneBot v11)")
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"连接 LangBot 失败: {e}", exc_info=True)
|
|
167
|
+
self._connected = False
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
async def reconnect(self):
|
|
171
|
+
"""指数退避重连
|
|
172
|
+
|
|
173
|
+
重连延迟按指数增长:1s→2s→4s→8s→...→60s(最大)
|
|
174
|
+
连接成功后重置延迟为初始值
|
|
175
|
+
"""
|
|
176
|
+
if self._reconnecting:
|
|
177
|
+
logger.debug("已在重连中,跳过")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
self._reconnecting = True
|
|
181
|
+
|
|
182
|
+
while not self._connected:
|
|
183
|
+
try:
|
|
184
|
+
logger.info(f"尝试重连 LangBot...")
|
|
185
|
+
success = await self.connect()
|
|
186
|
+
|
|
187
|
+
if success:
|
|
188
|
+
logger.info("重连成功")
|
|
189
|
+
self._reconnecting = False
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"重连失败: {e}")
|
|
194
|
+
|
|
195
|
+
# 等待后重试
|
|
196
|
+
logger.info(f"将在 {self._current_reconnect_delay} 秒后重试...")
|
|
197
|
+
await asyncio.sleep(self._current_reconnect_delay)
|
|
198
|
+
|
|
199
|
+
# 指数退避,最大不超过 max_reconnect_delay
|
|
200
|
+
self._current_reconnect_delay = min(
|
|
201
|
+
self._current_reconnect_delay * 2,
|
|
202
|
+
self._max_reconnect_delay
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self._reconnecting = False
|
|
206
|
+
|
|
207
|
+
def get_reconnect_delay(self) -> float:
|
|
208
|
+
"""获取当前重连延迟(用于测试)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
当前重连延迟(秒)
|
|
212
|
+
"""
|
|
213
|
+
return self._current_reconnect_delay
|
|
214
|
+
|
|
215
|
+
async def _send_lifecycle_event(self):
|
|
216
|
+
"""发送生命周期连接事件"""
|
|
217
|
+
event = {
|
|
218
|
+
"time": int(time.time()),
|
|
219
|
+
"self_id": int(self._self_id),
|
|
220
|
+
"post_type": "meta_event",
|
|
221
|
+
"meta_event_type": "lifecycle",
|
|
222
|
+
"sub_type": "connect"
|
|
223
|
+
}
|
|
224
|
+
await self._send(event)
|
|
225
|
+
|
|
226
|
+
async def _send(self, data: dict):
|
|
227
|
+
"""发送数据"""
|
|
228
|
+
if self.ws and self._connected:
|
|
229
|
+
try:
|
|
230
|
+
message = json.dumps(data)
|
|
231
|
+
logger.info(f"WebSocket 发送: {message[:500]}") # 限制日志长度
|
|
232
|
+
await self.ws.send(message)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"发送失败: {e}")
|
|
235
|
+
self._connected = False
|
|
236
|
+
else:
|
|
237
|
+
logger.warning(f"WebSocket 未连接,无法发送: connected={self._connected}, ws={self.ws is not None}")
|
|
238
|
+
|
|
239
|
+
async def _receive_loop(self):
|
|
240
|
+
"""接收消息循环"""
|
|
241
|
+
try:
|
|
242
|
+
while self._connected and self.ws:
|
|
243
|
+
try:
|
|
244
|
+
message = await self.ws.recv()
|
|
245
|
+
logger.info(f"WebSocket 收到: {message[:500]}") # 限制日志长度
|
|
246
|
+
data = json.loads(message)
|
|
247
|
+
await self._handle_message(data)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
if self._connected:
|
|
250
|
+
logger.error(f"接收消息异常: {e}")
|
|
251
|
+
self._connected = False
|
|
252
|
+
break
|
|
253
|
+
except asyncio.CancelledError:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
# 连接断开,启动重连
|
|
257
|
+
if not self._connected and not self._reconnecting:
|
|
258
|
+
logger.info("连接断开,启动指数退避重连...")
|
|
259
|
+
self._reconnect_task = asyncio.create_task(self.reconnect())
|
|
260
|
+
|
|
261
|
+
async def _handle_message(self, data: dict):
|
|
262
|
+
"""处理收到的消息"""
|
|
263
|
+
# 检查是否是 API 调用请求(LangBot 发送消息)
|
|
264
|
+
action = data.get("action")
|
|
265
|
+
if action:
|
|
266
|
+
params = data.get("params", {})
|
|
267
|
+
echo = data.get("echo")
|
|
268
|
+
|
|
269
|
+
logger.info(f"收到动作: {action}, 参数: {params}")
|
|
270
|
+
|
|
271
|
+
# 调用回调处理
|
|
272
|
+
result = {}
|
|
273
|
+
if self._event_callback:
|
|
274
|
+
try:
|
|
275
|
+
result = await self._event_callback(action, params) or {}
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error(f"处理动作异常: {e}")
|
|
278
|
+
|
|
279
|
+
# 发送响应
|
|
280
|
+
response = {
|
|
281
|
+
"status": "ok",
|
|
282
|
+
"retcode": 0,
|
|
283
|
+
"data": result
|
|
284
|
+
}
|
|
285
|
+
if echo:
|
|
286
|
+
response["echo"] = echo
|
|
287
|
+
|
|
288
|
+
await self._send(response)
|
|
289
|
+
|
|
290
|
+
def set_event_callback(self, callback: Callable):
|
|
291
|
+
"""设置事件回调函数"""
|
|
292
|
+
self._event_callback = callback
|
|
293
|
+
|
|
294
|
+
def get_original_group_id(self, numeric_group_id: str) -> str:
|
|
295
|
+
"""获取原始的微信群ID
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
numeric_group_id: 数字格式的群ID
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
原始微信群ID
|
|
302
|
+
"""
|
|
303
|
+
# 优先使用 StateStore
|
|
304
|
+
if self._state_store:
|
|
305
|
+
return self._state_store.get_original_group_id(str(numeric_group_id))
|
|
306
|
+
# 回退到内部映射
|
|
307
|
+
return self._group_id_mapping.get(str(numeric_group_id), numeric_group_id)
|
|
308
|
+
|
|
309
|
+
def get_user_id_by_message(self, message_id: str) -> Optional[str]:
|
|
310
|
+
"""根据消息ID获取发送者user_id
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
message_id: 消息ID
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
用户ID,不存在则返回None
|
|
317
|
+
"""
|
|
318
|
+
# 优先使用 StateStore
|
|
319
|
+
if self._state_store:
|
|
320
|
+
return self._state_store.get_user_by_message(str(message_id))
|
|
321
|
+
# 回退到内部映射
|
|
322
|
+
return self._message_user_mapping.get(str(message_id))
|
|
323
|
+
|
|
324
|
+
def _save_group_mapping(self, numeric_id: str, original_id: str):
|
|
325
|
+
"""保存群ID映射
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
numeric_id: 数字ID
|
|
329
|
+
original_id: 原始微信群ID
|
|
330
|
+
"""
|
|
331
|
+
if self._state_store:
|
|
332
|
+
self._state_store.set_group_mapping(str(numeric_id), original_id)
|
|
333
|
+
else:
|
|
334
|
+
self._group_id_mapping[str(numeric_id)] = original_id
|
|
335
|
+
|
|
336
|
+
def _save_message_user_mapping(self, message_id: str, user_id: str):
|
|
337
|
+
"""保存消息-用户映射
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
message_id: 消息ID
|
|
341
|
+
user_id: 用户ID
|
|
342
|
+
"""
|
|
343
|
+
if self._state_store:
|
|
344
|
+
self._state_store.set_message_user(str(message_id), user_id)
|
|
345
|
+
else:
|
|
346
|
+
self._message_user_mapping[str(message_id)] = user_id
|
|
347
|
+
|
|
348
|
+
async def send_private_message(self, user_id: str, message: str) -> None:
|
|
349
|
+
"""发送私聊消息事件到 LangBot
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
user_id: 用户ID
|
|
353
|
+
message: 消息内容
|
|
354
|
+
"""
|
|
355
|
+
if not self._connected:
|
|
356
|
+
logger.warning("未连接到 LangBot")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
message_id = int(time.time() * 1000) % 2147483647
|
|
360
|
+
|
|
361
|
+
event = {
|
|
362
|
+
"time": int(time.time()),
|
|
363
|
+
"self_id": int(self._self_id),
|
|
364
|
+
"post_type": "message",
|
|
365
|
+
"message_type": "private",
|
|
366
|
+
"sub_type": "friend",
|
|
367
|
+
"message_id": message_id,
|
|
368
|
+
"user_id": user_id,
|
|
369
|
+
"message": message,
|
|
370
|
+
"raw_message": message,
|
|
371
|
+
"font": 0,
|
|
372
|
+
"sender": {
|
|
373
|
+
"user_id": user_id,
|
|
374
|
+
"nickname": user_id,
|
|
375
|
+
"sex": "unknown",
|
|
376
|
+
"age": 0
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await self._send(event)
|
|
381
|
+
logger.info(f"已发送私聊消息事件: [{user_id}] {message}")
|
|
382
|
+
|
|
383
|
+
async def send_group_message(
|
|
384
|
+
self,
|
|
385
|
+
group_id: str,
|
|
386
|
+
user_id: str,
|
|
387
|
+
message: str,
|
|
388
|
+
at_bot: bool = False
|
|
389
|
+
) -> None:
|
|
390
|
+
"""发送群聊消息事件到 LangBot
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
group_id: 群ID
|
|
394
|
+
user_id: 发送者ID
|
|
395
|
+
message: 消息内容
|
|
396
|
+
at_bot: 是否标记为@机器人(用于引用消息等场景)
|
|
397
|
+
"""
|
|
398
|
+
if not self._connected:
|
|
399
|
+
logger.warning("未连接到 LangBot")
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
message_id = int(time.time() * 1000) % 2147483647
|
|
403
|
+
|
|
404
|
+
# 尝试将 group_id 转换为数字(微信群ID格式: 57388706417@chatroom)
|
|
405
|
+
numeric_group_id = group_id
|
|
406
|
+
if '@' in group_id:
|
|
407
|
+
numeric_group_id = group_id.split('@')[0]
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
numeric_group_id = int(numeric_group_id)
|
|
411
|
+
except ValueError:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
# 根据 at_bot 参数决定是否添加 @机器人 标记
|
|
415
|
+
cq_message = message
|
|
416
|
+
if at_bot:
|
|
417
|
+
# 引用机器人消息或明确需要触发机器人时,添加@标记
|
|
418
|
+
cq_message = f'[CQ:at,qq={self._self_id}] {message}'
|
|
419
|
+
|
|
420
|
+
event = {
|
|
421
|
+
"time": int(time.time()),
|
|
422
|
+
"self_id": int(self._self_id),
|
|
423
|
+
"post_type": "message",
|
|
424
|
+
"message_type": "group",
|
|
425
|
+
"sub_type": "normal",
|
|
426
|
+
"message_id": message_id,
|
|
427
|
+
"group_id": numeric_group_id,
|
|
428
|
+
"user_id": user_id,
|
|
429
|
+
"message": cq_message,
|
|
430
|
+
"raw_message": message,
|
|
431
|
+
"font": 0,
|
|
432
|
+
"sender": {
|
|
433
|
+
"user_id": user_id,
|
|
434
|
+
"nickname": user_id,
|
|
435
|
+
"card": "",
|
|
436
|
+
"sex": "unknown",
|
|
437
|
+
"age": 0,
|
|
438
|
+
"area": "",
|
|
439
|
+
"level": "0",
|
|
440
|
+
"role": "member",
|
|
441
|
+
"title": ""
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# 保存映射(使用 StateStore 或内部映射)
|
|
446
|
+
self._save_group_mapping(str(numeric_group_id), group_id)
|
|
447
|
+
self._save_message_user_mapping(str(message_id), user_id)
|
|
448
|
+
|
|
449
|
+
logger.info(f"发送群聊事件到 LangBot: group_id={numeric_group_id}, message={cq_message}")
|
|
450
|
+
await self._send(event)
|
|
451
|
+
logger.info(f"已发送群聊消息事件: [{group_id}][{user_id}] {message}")
|
|
452
|
+
|
|
453
|
+
async def health_check(self) -> bool:
|
|
454
|
+
"""健康检查
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
是否健康(已连接)
|
|
458
|
+
"""
|
|
459
|
+
return self._connected and self.ws is not None
|
|
460
|
+
|
|
461
|
+
async def close(self):
|
|
462
|
+
"""关闭客户端"""
|
|
463
|
+
self._connected = False
|
|
464
|
+
|
|
465
|
+
# 取消重连任务
|
|
466
|
+
if self._reconnect_task:
|
|
467
|
+
self._reconnect_task.cancel()
|
|
468
|
+
try:
|
|
469
|
+
await self._reconnect_task
|
|
470
|
+
except asyncio.CancelledError:
|
|
471
|
+
pass
|
|
472
|
+
self._reconnect_task = None
|
|
473
|
+
|
|
474
|
+
# 取消接收任务
|
|
475
|
+
if self._receive_task:
|
|
476
|
+
self._receive_task.cancel()
|
|
477
|
+
try:
|
|
478
|
+
await self._receive_task
|
|
479
|
+
except asyncio.CancelledError:
|
|
480
|
+
pass
|
|
481
|
+
self._receive_task = None
|
|
482
|
+
|
|
483
|
+
# 关闭WebSocket
|
|
484
|
+
if self.ws:
|
|
485
|
+
await self.ws.close()
|
|
486
|
+
self.ws = None
|
|
487
|
+
|
|
488
|
+
self._reconnecting = False
|
|
489
|
+
|
|
490
|
+
async def send_private_message_with_image(self, user_id: str, image_url: str) -> None:
|
|
491
|
+
"""发送带图片的私聊消息事件到 LangBot
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
user_id: 用户ID
|
|
495
|
+
image_url: 图片URL
|
|
496
|
+
"""
|
|
497
|
+
if not self._connected:
|
|
498
|
+
logger.warning("未连接到 LangBot")
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
message_id = int(time.time() * 1000) % 2147483647
|
|
502
|
+
|
|
503
|
+
event = {
|
|
504
|
+
"time": int(time.time()),
|
|
505
|
+
"self_id": int(self._self_id),
|
|
506
|
+
"post_type": "message",
|
|
507
|
+
"message_type": "private",
|
|
508
|
+
"sub_type": "friend",
|
|
509
|
+
"message_id": message_id,
|
|
510
|
+
"user_id": user_id,
|
|
511
|
+
"message": [
|
|
512
|
+
{
|
|
513
|
+
"type": "image",
|
|
514
|
+
"data": {
|
|
515
|
+
"url": image_url
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
"raw_message": "[图片]",
|
|
520
|
+
"font": 0,
|
|
521
|
+
"sender": {
|
|
522
|
+
"user_id": user_id,
|
|
523
|
+
"nickname": user_id,
|
|
524
|
+
"sex": "unknown",
|
|
525
|
+
"age": 0
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
logger.info(f"发送私聊图片事件到 LangBot: [{user_id}], url={image_url}")
|
|
530
|
+
await self._send(event)
|
|
531
|
+
|
|
532
|
+
async def send_private_message_with_image_and_text(
|
|
533
|
+
self,
|
|
534
|
+
user_id: str,
|
|
535
|
+
image_url: str,
|
|
536
|
+
text: str
|
|
537
|
+
) -> None:
|
|
538
|
+
"""发送带图片和文本的私聊消息事件到 LangBot(用于引用图片场景)
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
user_id: 用户ID
|
|
542
|
+
image_url: 图片URL
|
|
543
|
+
text: 文本内容
|
|
544
|
+
"""
|
|
545
|
+
if not self._connected:
|
|
546
|
+
logger.warning("未连接到 LangBot")
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
message_id = int(time.time() * 1000) % 2147483647
|
|
550
|
+
|
|
551
|
+
event = {
|
|
552
|
+
"time": int(time.time()),
|
|
553
|
+
"self_id": int(self._self_id),
|
|
554
|
+
"post_type": "message",
|
|
555
|
+
"message_type": "private",
|
|
556
|
+
"sub_type": "friend",
|
|
557
|
+
"message_id": message_id,
|
|
558
|
+
"user_id": user_id,
|
|
559
|
+
"message": [
|
|
560
|
+
{"type": "image", "data": {"url": image_url}},
|
|
561
|
+
{"type": "text", "data": {"text": text}}
|
|
562
|
+
],
|
|
563
|
+
"raw_message": f"[图片] {text}",
|
|
564
|
+
"font": 0,
|
|
565
|
+
"sender": {
|
|
566
|
+
"user_id": user_id,
|
|
567
|
+
"nickname": user_id,
|
|
568
|
+
"sex": "unknown",
|
|
569
|
+
"age": 0
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
logger.info(f"发送私聊图片+文本事件到 LangBot: [{user_id}], url={image_url}, text={text}")
|
|
574
|
+
await self._send(event)
|
|
575
|
+
|
|
576
|
+
async def send_group_message_with_image(
|
|
577
|
+
self,
|
|
578
|
+
group_id: str,
|
|
579
|
+
user_id: str,
|
|
580
|
+
image_url: str,
|
|
581
|
+
at_bot: bool = False
|
|
582
|
+
) -> None:
|
|
583
|
+
"""发送带图片的群聊消息事件到 LangBot
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
group_id: 群ID
|
|
587
|
+
user_id: 发送者ID
|
|
588
|
+
image_url: 图片URL
|
|
589
|
+
at_bot: 是否标记为@机器人
|
|
590
|
+
"""
|
|
591
|
+
if not self._connected:
|
|
592
|
+
logger.warning("未连接到 LangBot")
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
message_id = int(time.time() * 1000) % 2147483647
|
|
596
|
+
|
|
597
|
+
# 转换 group_id
|
|
598
|
+
numeric_group_id = group_id
|
|
599
|
+
if '@' in group_id:
|
|
600
|
+
numeric_group_id = group_id.split('@')[0]
|
|
601
|
+
try:
|
|
602
|
+
numeric_group_id = int(numeric_group_id)
|
|
603
|
+
except ValueError:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
# 保存映射
|
|
607
|
+
self._save_group_mapping(str(numeric_group_id), group_id)
|
|
608
|
+
|
|
609
|
+
# 构建消息内容
|
|
610
|
+
message_segments = []
|
|
611
|
+
if at_bot:
|
|
612
|
+
message_segments.append({"type": "at", "data": {"qq": str(self._self_id)}})
|
|
613
|
+
message_segments.append({"type": "image", "data": {"url": image_url}})
|
|
614
|
+
|
|
615
|
+
event = {
|
|
616
|
+
"time": int(time.time()),
|
|
617
|
+
"self_id": int(self._self_id),
|
|
618
|
+
"post_type": "message",
|
|
619
|
+
"message_type": "group",
|
|
620
|
+
"sub_type": "normal",
|
|
621
|
+
"message_id": message_id,
|
|
622
|
+
"group_id": numeric_group_id,
|
|
623
|
+
"user_id": user_id,
|
|
624
|
+
"message": message_segments,
|
|
625
|
+
"raw_message": "[图片]",
|
|
626
|
+
"font": 0,
|
|
627
|
+
"sender": {
|
|
628
|
+
"user_id": user_id,
|
|
629
|
+
"nickname": user_id,
|
|
630
|
+
"card": "",
|
|
631
|
+
"sex": "unknown",
|
|
632
|
+
"age": 0,
|
|
633
|
+
"area": "",
|
|
634
|
+
"level": "0",
|
|
635
|
+
"role": "member",
|
|
636
|
+
"title": ""
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
logger.info(f"发送群聊图片事件到 LangBot: [{group_id}][{user_id}], url={image_url}, at_bot={at_bot}")
|
|
641
|
+
await self._send(event)
|
|
642
|
+
|
|
643
|
+
async def send_group_message_with_image_and_text(
|
|
644
|
+
self,
|
|
645
|
+
group_id: str,
|
|
646
|
+
user_id: str,
|
|
647
|
+
image_url: str,
|
|
648
|
+
text: str
|
|
649
|
+
) -> None:
|
|
650
|
+
"""发送带图片和文本的群聊消息事件到 LangBot(用于关联图片缓存场景)
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
group_id: 群ID
|
|
654
|
+
user_id: 发送者ID
|
|
655
|
+
image_url: 图片URL
|
|
656
|
+
text: 文本内容
|
|
657
|
+
"""
|
|
658
|
+
if not self._connected:
|
|
659
|
+
logger.warning("未连接到 LangBot")
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
message_id = int(time.time() * 1000) % 2147483647
|
|
663
|
+
|
|
664
|
+
# 转换 group_id
|
|
665
|
+
numeric_group_id = group_id
|
|
666
|
+
if '@' in group_id:
|
|
667
|
+
numeric_group_id = group_id.split('@')[0]
|
|
668
|
+
try:
|
|
669
|
+
numeric_group_id = int(numeric_group_id)
|
|
670
|
+
except ValueError:
|
|
671
|
+
pass
|
|
672
|
+
|
|
673
|
+
# 保存映射
|
|
674
|
+
self._save_group_mapping(str(numeric_group_id), group_id)
|
|
675
|
+
self._save_message_user_mapping(str(message_id), user_id)
|
|
676
|
+
|
|
677
|
+
# 构建消息:@机器人 + 图片 + 文本
|
|
678
|
+
message_segments = [
|
|
679
|
+
{"type": "at", "data": {"qq": str(self._self_id)}},
|
|
680
|
+
{"type": "image", "data": {"url": image_url}},
|
|
681
|
+
{"type": "text", "data": {"text": text}}
|
|
682
|
+
]
|
|
683
|
+
|
|
684
|
+
event = {
|
|
685
|
+
"time": int(time.time()),
|
|
686
|
+
"self_id": int(self._self_id),
|
|
687
|
+
"post_type": "message",
|
|
688
|
+
"message_type": "group",
|
|
689
|
+
"sub_type": "normal",
|
|
690
|
+
"message_id": message_id,
|
|
691
|
+
"group_id": numeric_group_id,
|
|
692
|
+
"user_id": user_id,
|
|
693
|
+
"message": message_segments,
|
|
694
|
+
"raw_message": f"[图片] {text}",
|
|
695
|
+
"font": 0,
|
|
696
|
+
"sender": {
|
|
697
|
+
"user_id": user_id,
|
|
698
|
+
"nickname": user_id,
|
|
699
|
+
"card": "",
|
|
700
|
+
"sex": "unknown",
|
|
701
|
+
"age": 0,
|
|
702
|
+
"area": "",
|
|
703
|
+
"level": "0",
|
|
704
|
+
"role": "member",
|
|
705
|
+
"title": ""
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
logger.info(f"发送群聊图片+文本事件到 LangBot: [{group_id}][{user_id}], url={image_url}, text={text}")
|
|
710
|
+
await self._send(event)
|