AstrBot 4.11.1__py3-none-any.whl → 4.11.2__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.
@@ -1,940 +0,0 @@
1
- import asyncio
2
- import base64
3
- import json
4
- import os
5
- import time
6
- import traceback
7
- from typing import cast
8
-
9
- import aiohttp
10
- import anyio
11
- import websockets
12
-
13
- from astrbot import logger
14
- from astrbot.api.message_components import At, Image, Plain, Record
15
- from astrbot.api.platform import Platform, PlatformMetadata
16
- from astrbot.core.message.message_event_result import MessageChain
17
- from astrbot.core.platform.astr_message_event import MessageSesion
18
- from astrbot.core.platform.astrbot_message import (
19
- AstrBotMessage,
20
- MessageMember,
21
- MessageType,
22
- )
23
- from astrbot.core.utils.astrbot_path import get_astrbot_data_path
24
-
25
- from ...register import register_platform_adapter
26
- from .wechatpadpro_message_event import WeChatPadProMessageEvent
27
-
28
- try:
29
- from .xml_data_parser import GeweDataParser
30
- except ImportError as e:
31
- logger.warning(
32
- f"警告: 可能未安装 defusedxml 依赖库,将导致无法解析微信的 表情包、引用 类型的消息: {e!s}",
33
- )
34
-
35
-
36
- @register_platform_adapter(
37
- "wechatpadpro", "WeChatPadPro 消息平台适配器", support_streaming_message=False
38
- )
39
- class WeChatPadProAdapter(Platform):
40
- def __init__(
41
- self,
42
- platform_config: dict,
43
- platform_settings: dict,
44
- event_queue: asyncio.Queue,
45
- ) -> None:
46
- super().__init__(platform_config, event_queue)
47
- self._shutdown_event = None
48
- self.wxnewpass = None
49
- self.settings = platform_settings
50
-
51
- self.metadata = PlatformMetadata(
52
- name="wechatpadpro",
53
- description="WeChatPadPro 消息平台适配器",
54
- id=self.config.get("id", "wechatpadpro"),
55
- support_streaming_message=False,
56
- )
57
-
58
- # 保存配置信息
59
- self.admin_key = self.config.get("admin_key")
60
- self.host = self.config.get("host")
61
- self.port = self.config.get("port")
62
- self.active_mesasge_poll: bool = self.config.get(
63
- "wpp_active_message_poll",
64
- False,
65
- )
66
- self.active_message_poll_interval: int = self.config.get(
67
- "wpp_active_message_poll_interval",
68
- 5,
69
- )
70
- self.base_url = f"http://{self.host}:{self.port}"
71
- self.auth_key = None # 用于保存生成的授权码
72
- self.wxid: str | None = None # 用于保存登录成功后的 wxid
73
- self.credentials_file = os.path.join(
74
- get_astrbot_data_path(),
75
- "wechatpadpro_credentials.json",
76
- ) # 持久化文件路径
77
- self.ws_handle_task = None
78
-
79
- # 添加图片消息缓存,用于引用消息处理
80
- self.cached_images = {}
81
- """缓存图片消息。key是NewMsgId (对应引用消息的svrid),value是图片的base64数据"""
82
- # 设置缓存大小限制,避免内存占用过大
83
- self.max_image_cache = 50
84
-
85
- # 添加文本消息缓存,用于引用消息处理
86
- self.cached_texts = {}
87
- """缓存文本消息。key是NewMsgId (对应引用消息的svrid),value是消息文本内容"""
88
- # 设置文本缓存大小限制
89
- self.max_text_cache = 100
90
-
91
- async def run(self) -> None:
92
- """启动平台适配器的运行实例。"""
93
- logger.info("WeChatPadPro 适配器正在启动...")
94
-
95
- if loaded_credentials := self.load_credentials():
96
- self.auth_key = loaded_credentials.get("auth_key")
97
- self.wxid = loaded_credentials.get("wxid")
98
-
99
- isLoginIn = await self.check_online_status()
100
-
101
- # 检查在线状态
102
- if self.auth_key and isLoginIn:
103
- logger.info("WeChatPadPro 设备已在线,凭据存在,跳过扫码登录。")
104
- # 如果在线,连接 WebSocket 接收消息
105
- self.ws_handle_task = asyncio.create_task(self.connect_websocket())
106
- else:
107
- # 1. 生成授权码
108
- if not self.auth_key:
109
- logger.info("WeChatPadPro 无可用凭据,将生成新的授权码。")
110
- await self.generate_auth_key()
111
-
112
- # 2. 获取登录二维码
113
- if not isLoginIn:
114
- logger.info("WeChatPadPro 设备已离线,开始扫码登录。")
115
- qr_code_url = await self.get_login_qr_code()
116
-
117
- if qr_code_url:
118
- logger.info(f"请扫描以下二维码登录: {qr_code_url}")
119
- else:
120
- logger.error("无法获取登录二维码。")
121
- return
122
-
123
- # 3. 检测扫码状态
124
- login_successful = await self.check_login_status()
125
-
126
- if login_successful:
127
- logger.info("登录成功,WeChatPadPro适配器已连接。")
128
- else:
129
- logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。")
130
- await self.terminate()
131
- return
132
-
133
- # 登录成功后,连接 WebSocket 接收消息
134
- self.ws_handle_task = asyncio.create_task(self.connect_websocket())
135
-
136
- self._shutdown_event = asyncio.Event()
137
- await self._shutdown_event.wait()
138
- logger.info("WeChatPadPro 适配器已停止。")
139
-
140
- def load_credentials(self):
141
- """从文件中加载 auth_key 和 wxid。"""
142
- if os.path.exists(self.credentials_file):
143
- try:
144
- with open(self.credentials_file) as f:
145
- credentials = json.load(f)
146
- logger.info("成功加载 WeChatPadPro 凭据。")
147
- return credentials
148
- except Exception as e:
149
- logger.error(f"加载 WeChatPadPro 凭据失败: {e}")
150
- return None
151
-
152
- def save_credentials(self):
153
- """将 auth_key 和 wxid 保存到文件。"""
154
- credentials = {
155
- "auth_key": self.auth_key,
156
- "wxid": self.wxid,
157
- }
158
- try:
159
- # 确保数据目录存在
160
- data_dir = os.path.dirname(self.credentials_file)
161
- os.makedirs(data_dir, exist_ok=True)
162
- with open(self.credentials_file, "w") as f:
163
- json.dump(credentials, f)
164
- except Exception as e:
165
- logger.error(f"保存 WeChatPadPro 凭据失败: {e}")
166
-
167
- async def check_online_status(self):
168
- """检查 WeChatPadPro 设备是否在线。"""
169
- if not self.auth_key:
170
- return False
171
- url = f"{self.base_url}/login/GetLoginStatus"
172
- params = {"key": self.auth_key}
173
-
174
- async with aiohttp.ClientSession() as session:
175
- try:
176
- async with session.get(url, params=params) as response:
177
- response_data = await response.json()
178
- # 根据提供的在线接口返回示例,成功状态码是 200,loginState 为 1 表示在线
179
- if response.status == 200 and response_data.get("Code") == 200:
180
- login_state = response_data.get("Data", {}).get("loginState")
181
- if login_state == 1:
182
- logger.info("WeChatPadPro 设备当前在线。")
183
- return True
184
- # login_state == 3 为离线状态
185
- if login_state == 3:
186
- logger.info("WeChatPadPro 设备不在线。")
187
- return False
188
- logger.error(f"未知的在线状态: {response_data}")
189
- return False
190
- # Code == 300 为微信退出状态。
191
- if response.status == 200 and response_data.get("Code") == 300:
192
- logger.info("WeChatPadPro 设备已退出。")
193
- return False
194
- if response.status == 200 and response_data.get("Code") == -2:
195
- # 该链接不存在
196
- self.auth_key = None
197
- return False
198
- logger.error(
199
- f"检查在线状态失败: {response.status}, {response_data}",
200
- )
201
- return False
202
-
203
- except aiohttp.ClientConnectorError as e:
204
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
205
- return False
206
- except Exception as e:
207
- logger.error(f"检查在线状态时发生错误: {e}")
208
- logger.error(traceback.format_exc())
209
- return False
210
-
211
- def _extract_auth_key(self, data):
212
- """Helper method to extract auth_key from response data."""
213
- if isinstance(data, dict):
214
- auth_keys = data.get("authKeys") # 新接口
215
- if isinstance(auth_keys, list) and auth_keys:
216
- return auth_keys[0]
217
- elif isinstance(data, list) and data: # 旧接口
218
- return data[0]
219
- return None
220
-
221
- async def generate_auth_key(self):
222
- """生成授权码。"""
223
- url = f"{self.base_url}/admin/GenAuthKey1"
224
- params = {"key": self.admin_key}
225
- payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码
226
-
227
- self.auth_key = None # Reset auth_key before generating a new one
228
-
229
- async with aiohttp.ClientSession() as session:
230
- try:
231
- async with session.post(url, params=params, json=payload) as response:
232
- if response.status != 200:
233
- logger.error(
234
- f"生成授权码失败: {response.status}, {await response.text()}",
235
- )
236
- return
237
-
238
- response_data = await response.json()
239
- if response_data.get("Code") == 200:
240
- if data := response_data.get("Data"):
241
- self.auth_key = self._extract_auth_key(data)
242
-
243
- if self.auth_key:
244
- logger.info("成功获取授权码")
245
- else:
246
- logger.error(
247
- f"生成授权码成功但未找到授权码: {response_data}",
248
- )
249
- else:
250
- logger.error(f"生成授权码失败: {response_data}")
251
- except aiohttp.ClientConnectorError as e:
252
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
253
- except Exception as e:
254
- logger.error(f"生成授权码时发生错误: {e}")
255
-
256
- async def get_login_qr_code(self):
257
- """获取登录二维码地址。"""
258
- url = f"{self.base_url}/login/GetLoginQrCodeNew"
259
- params = {"key": self.auth_key}
260
- payload = {} # 根据文档,这个接口的 body 可以为空
261
-
262
- async with aiohttp.ClientSession() as session:
263
- try:
264
- async with session.post(url, params=params, json=payload) as response:
265
- response_data = await response.json()
266
- if response.status == 200 and response_data.get("Code") == 200:
267
- # 二维码地址在 Data.QrCodeUrl 字段中
268
- if response_data.get("Data") and response_data["Data"].get(
269
- "QrCodeUrl",
270
- ):
271
- return response_data["Data"]["QrCodeUrl"]
272
- logger.error(
273
- f"获取登录二维码成功但未找到二维码地址: {response_data}",
274
- )
275
- return None
276
- if "该 key 无效" in response_data.get("Text"):
277
- logger.error(
278
- "授权码无效,已经清除。请重新启动 AstrBot 或者本消息适配器。原因也可能是 WeChatPadPro 的 MySQL 服务没有启动成功,请检查 WeChatPadPro 服务的日志。",
279
- )
280
- self.auth_key = None
281
- self.save_credentials()
282
- return None
283
- logger.error(
284
- f"获取登录二维码失败: {response.status}, {response_data}",
285
- )
286
- return None
287
- except aiohttp.ClientConnectorError as e:
288
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
289
- return None
290
- except Exception as e:
291
- logger.error(f"获取登录二维码时发生错误: {e}")
292
- return None
293
-
294
- async def check_login_status(self):
295
- """循环检测扫码状态。
296
- 尝试 6 次后跳出循环,添加倒计时。
297
- 返回 True 如果登录成功,否则返回 False。
298
- """
299
- url = f"{self.base_url}/login/CheckLoginStatus"
300
- params = {"key": self.auth_key}
301
-
302
- attempts = 0 # 初始化尝试次数
303
- max_attempts = 36 # 最大尝试次数
304
- countdown = 180 # 倒计时时长
305
- logger.info(f"请在 {countdown} 秒内扫码登录。")
306
- while attempts < max_attempts:
307
- async with aiohttp.ClientSession() as session:
308
- try:
309
- async with session.get(url, params=params) as response:
310
- response_data = await response.json()
311
- # 成功判断条件和数据提取路径
312
- if response.status == 200 and response_data.get("Code") == 200:
313
- if (
314
- response_data.get("Data")
315
- and response_data["Data"].get("state") is not None
316
- ):
317
- status = response_data["Data"]["state"]
318
- logger.info(
319
- f"第 {attempts + 1} 次尝试,当前登录状态: {status},还剩{countdown - attempts * 5}秒",
320
- )
321
- if status == 2: # 状态 2 表示登录成功
322
- self.wxid = response_data["Data"].get("wxid")
323
- self.wxnewpass = response_data["Data"].get(
324
- "wxnewpass",
325
- )
326
- logger.info(
327
- f"登录成功,wxid: {self.wxid}, wxnewpass: {self.wxnewpass}",
328
- )
329
- self.save_credentials() # 登录成功后保存凭据
330
- return True
331
- if status == -2: # 二维码过期
332
- logger.error("二维码已过期,请重新获取。")
333
- return False
334
- else:
335
- logger.error(
336
- f"检测登录状态成功但未找到登录状态: {response_data}",
337
- )
338
- elif response_data.get("Code") == 300:
339
- # "不存在状态"
340
- pass
341
- else:
342
- logger.info(
343
- f"检测登录状态失败: {response.status}, {response_data}",
344
- )
345
-
346
- except aiohttp.ClientConnectorError as e:
347
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
348
- await asyncio.sleep(5)
349
- attempts += 1
350
- continue
351
- except Exception as e:
352
- logger.error(f"检测登录状态时发生错误: {e}")
353
- attempts += 1
354
- continue
355
-
356
- attempts += 1
357
- await asyncio.sleep(5) # 每隔5秒检测一次
358
- logger.warning("登录检测超过最大尝试次数,退出检测。")
359
- return False
360
-
361
- async def connect_websocket(self):
362
- """建立 WebSocket 连接并处理接收到的消息。"""
363
- os.environ["no_proxy"] = f"localhost,127.0.0.1,{self.host}"
364
- ws_url = f"ws://{self.host}:{self.port}/ws/GetSyncMsg?key={self.auth_key}"
365
- logger.info(
366
- f"正在连接 WebSocket: ws://{self.host}:{self.port}/ws/GetSyncMsg?key=***",
367
- )
368
- while True:
369
- try:
370
- async with websockets.connect(ws_url) as websocket:
371
- logger.debug("WebSocket 连接成功。")
372
- # 设置空闲超时重连
373
- wait_time = (
374
- self.active_message_poll_interval
375
- if self.active_mesasge_poll
376
- else 120
377
- )
378
- while True:
379
- try:
380
- message = await asyncio.wait_for(
381
- websocket.recv(),
382
- timeout=wait_time,
383
- )
384
- # logger.debug(message) # 不显示原始消息内容
385
- asyncio.create_task(self.handle_websocket_message(message))
386
- except asyncio.TimeoutError:
387
- logger.debug(f"WebSocket 连接空闲超过 {wait_time} s")
388
- break
389
- except websockets.exceptions.ConnectionClosedOK:
390
- logger.info("WebSocket 连接正常关闭。")
391
- break
392
- except Exception as e:
393
- logger.error(f"处理 WebSocket 消息时发生错误: {e}")
394
- break
395
- except Exception as e:
396
- logger.error(
397
- f"WebSocket 连接失败: {e}, 请检查WeChatPadPro服务状态,或尝试重启WeChatPadPro适配器。",
398
- )
399
- await asyncio.sleep(5)
400
-
401
- async def handle_websocket_message(self, message: str | bytes):
402
- """处理从 WebSocket 接收到的消息。"""
403
- logger.debug(f"收到 WebSocket 消息: {message}")
404
- try:
405
- message_data = json.loads(message)
406
- if (
407
- message_data.get("msg_id") is not None
408
- and message_data.get("from_user_name") is not None
409
- ):
410
- abm = await self.convert_message(message_data)
411
- if abm:
412
- # 创建 WeChatPadProMessageEvent 实例
413
- message_event = WeChatPadProMessageEvent(
414
- message_str=abm.message_str,
415
- message_obj=abm,
416
- platform_meta=self.meta(),
417
- session_id=abm.session_id,
418
- # 传递适配器实例,以便在事件中调用 send 方法
419
- adapter=self,
420
- )
421
- # 提交事件到事件队列
422
- self.commit_event(message_event)
423
- else:
424
- logger.warning(f"收到未知结构的 WebSocket 消息: {message_data}")
425
-
426
- except json.JSONDecodeError:
427
- logger.error(f"无法解析 WebSocket 消息为 JSON: {message}")
428
- except Exception as e:
429
- logger.error(f"处理 WebSocket 消息时发生错误: {e}")
430
-
431
- async def convert_message(self, raw_message: dict) -> AstrBotMessage | None:
432
- """将 WeChatPadPro 原始消息转换为 AstrBotMessage。"""
433
- if self.wxid is None:
434
- logger.error("WeChatPadPro 适配器未登录或未获取到 wxid,无法处理消息。")
435
- return None
436
- abm = AstrBotMessage()
437
- abm.raw_message = raw_message
438
- abm.message_id = str(raw_message.get("msg_id"))
439
- abm.timestamp = cast(int, raw_message.get("create_time"))
440
- abm.self_id = self.wxid
441
-
442
- if int(time.time()) - abm.timestamp > 180:
443
- logger.warning(
444
- f"忽略 3 分钟前的旧消息:消息时间戳 {abm.timestamp} 超过当前时间 {int(time.time())}。",
445
- )
446
- return None
447
-
448
- from_user_name = raw_message.get("from_user_name", {}).get("str", "")
449
- to_user_name = raw_message.get("to_user_name", {}).get("str", "")
450
- content = raw_message.get("content", {}).get("str", "")
451
- push_content = raw_message.get("push_content", "")
452
- msg_type = cast(int, raw_message.get("msg_type"))
453
-
454
- abm.message_str = ""
455
- abm.message = []
456
-
457
- # 如果是机器人自己发送的消息、回显消息或系统消息,忽略
458
- if from_user_name == self.wxid:
459
- logger.info("忽略来自自己的消息。")
460
- return None
461
-
462
- if from_user_name in ["weixin", "newsapp", "newsapp_wechat"]:
463
- logger.info("忽略来自微信团队的消息。")
464
- return None
465
-
466
- # 先判断群聊/私聊并设置基本属性
467
- if await self._process_chat_type(
468
- abm,
469
- raw_message,
470
- from_user_name,
471
- to_user_name,
472
- content,
473
- push_content,
474
- ):
475
- # 再根据消息类型处理消息内容
476
- await self._process_message_content(abm, raw_message, msg_type, content)
477
-
478
- return abm
479
- return None
480
-
481
- async def _process_chat_type(
482
- self,
483
- abm: AstrBotMessage,
484
- raw_message: dict,
485
- from_user_name: str,
486
- to_user_name: str,
487
- content: str,
488
- push_content: str,
489
- ):
490
- """判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。"""
491
- if from_user_name == "weixin":
492
- return False
493
- at_me = False
494
- if "@chatroom" in from_user_name:
495
- abm.type = MessageType.GROUP_MESSAGE
496
- abm.group_id = from_user_name
497
-
498
- parts = content.split(":\n", 1)
499
- sender_wxid = parts[0] if len(parts) == 2 else ""
500
- abm.sender = MessageMember(user_id=sender_wxid, nickname="")
501
-
502
- # 获取群聊发送者的nickname
503
- if sender_wxid:
504
- accurate_nickname = await self._get_group_member_nickname(
505
- abm.group_id,
506
- sender_wxid,
507
- )
508
- if accurate_nickname:
509
- abm.sender.nickname = accurate_nickname
510
-
511
- if abm.type == MessageType.GROUP_MESSAGE:
512
- abm.session_id = abm.group_id
513
- else:
514
- abm.session_id = abm.sender.user_id
515
-
516
- msg_source = raw_message.get("msg_source", "")
517
- if self.wxid in msg_source:
518
- at_me = True
519
- if "在群聊中@了你" in raw_message.get("push_content", ""):
520
- at_me = True
521
- if at_me:
522
- abm.message.insert(0, At(qq=abm.self_id, name=""))
523
- else:
524
- abm.type = MessageType.FRIEND_MESSAGE
525
- abm.group_id = ""
526
- nick_name = ""
527
- if push_content and " : " in push_content:
528
- nick_name = push_content.split(" : ")[0]
529
- abm.sender = MessageMember(user_id=from_user_name, nickname=nick_name)
530
- abm.session_id = from_user_name
531
- return True
532
-
533
- async def _get_group_member_nickname(
534
- self,
535
- group_id: str,
536
- member_wxid: str,
537
- ) -> str | None:
538
- """通过接口获取群成员的昵称。"""
539
- url = f"{self.base_url}/group/GetChatroomMemberDetail"
540
- params = {"key": self.auth_key}
541
- payload = {
542
- "ChatRoomName": group_id,
543
- }
544
-
545
- async with aiohttp.ClientSession() as session:
546
- try:
547
- async with session.post(url, params=params, json=payload) as response:
548
- response_data = await response.json()
549
- if response.status == 200 and response_data.get("Code") == 200:
550
- # 从返回数据中查找对应成员的昵称
551
- member_list = (
552
- response_data.get("Data", {})
553
- .get("member_data", {})
554
- .get("chatroom_member_list", [])
555
- )
556
- for member in member_list:
557
- if member.get("user_name") == member_wxid:
558
- return member.get("nick_name")
559
- logger.warning(
560
- f"在群 {group_id} 中未找到成员 {member_wxid} 的昵称",
561
- )
562
- else:
563
- logger.error(
564
- f"获取群成员详情失败: {response.status}, {response_data}",
565
- )
566
- return None
567
- except aiohttp.ClientConnectorError as e:
568
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
569
- return None
570
- except Exception as e:
571
- logger.error(f"获取群成员详情时发生错误: {e}")
572
- return None
573
-
574
- async def _download_raw_image(
575
- self,
576
- from_user_name: str,
577
- to_user_name: str,
578
- msg_id: int,
579
- ) -> dict | None:
580
- """下载原始图片。"""
581
- url = f"{self.base_url}/message/GetMsgBigImg"
582
- params = {"key": self.auth_key}
583
- payload = {
584
- "CompressType": 0,
585
- "FromUserName": from_user_name,
586
- "MsgId": msg_id,
587
- "Section": {"DataLen": 61440, "StartPos": 0},
588
- "ToUserName": to_user_name,
589
- "TotalLen": 0,
590
- }
591
- async with aiohttp.ClientSession() as session:
592
- try:
593
- async with session.post(url, params=params, json=payload) as response:
594
- if response.status == 200:
595
- return await response.json()
596
- logger.error(f"下载图片失败: {response.status}")
597
- return None
598
- except aiohttp.ClientConnectorError as e:
599
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
600
- return None
601
- except Exception as e:
602
- logger.error(f"下载图片时发生错误: {e}")
603
- return None
604
-
605
- async def download_voice(
606
- self,
607
- to_user_name: str,
608
- new_msg_id: str,
609
- bufid: str,
610
- length: int,
611
- ):
612
- """下载原始音频。"""
613
- url = f"{self.base_url}/message/GetMsgVoice"
614
- params = {"key": self.auth_key}
615
- payload = {
616
- "Bufid": bufid,
617
- "ToUserName": to_user_name,
618
- "NewMsgId": new_msg_id,
619
- "Length": length,
620
- }
621
- async with aiohttp.ClientSession() as session:
622
- try:
623
- async with session.post(url, params=params, json=payload) as response:
624
- if response.status == 200:
625
- return await response.json()
626
- logger.error(f"下载音频失败: {response.status}")
627
- return None
628
- except aiohttp.ClientConnectorError as e:
629
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
630
- return None
631
- except Exception as e:
632
- logger.error(f"下载音频时发生错误: {e}")
633
- return None
634
-
635
- async def _process_message_content(
636
- self,
637
- abm: AstrBotMessage,
638
- raw_message: dict,
639
- msg_type: int,
640
- content: str,
641
- ):
642
- """根据消息类型处理消息内容,填充 AstrBotMessage 的 message 列表。"""
643
- if msg_type == 1: # 文本消息
644
- abm.message_str = content
645
- if abm.type == MessageType.GROUP_MESSAGE:
646
- parts = content.split(":\n", 1)
647
- if len(parts) == 2:
648
- message_content = parts[1]
649
- abm.message_str = message_content
650
-
651
- # 检查是否@了机器人,参考 gewechat 的实现方式
652
- # 微信大部分客户端在@用户昵称后面,紧接着是一个\u2005字符(四分之一空格)
653
- at_me = False
654
-
655
- # 检查 msg_source 中是否包含机器人的 wxid
656
- # wechatpadpro 的格式: <atuserlist>wxid</atuserlist>
657
- # gewechat 的格式: <atuserlist><![CDATA[wxid]]></atuserlist>
658
- msg_source = raw_message.get("msg_source", "")
659
- if (
660
- f"<atuserlist>{abm.self_id}</atuserlist>" in msg_source
661
- or f"<atuserlist>{abm.self_id}," in msg_source
662
- or f",{abm.self_id}</atuserlist>" in msg_source
663
- ):
664
- at_me = True
665
-
666
- # 也检查 push_content 中是否有@提示
667
- push_content = raw_message.get("push_content", "")
668
- if "在群聊中@了你" in push_content:
669
- at_me = True
670
-
671
- if at_me:
672
- # 被@了,在消息开头插入At组件(参考gewechat的做法)
673
- bot_nickname = await self._get_group_member_nickname(
674
- abm.group_id,
675
- abm.self_id,
676
- )
677
- abm.message.insert(
678
- 0,
679
- At(qq=abm.self_id, name=bot_nickname or abm.self_id),
680
- )
681
-
682
- # 只有当消息内容不仅仅是@时才添加Plain组件
683
- if "\u2005" in message_content:
684
- # 检查@之后是否还有其他内容
685
- parts = message_content.split("\u2005")
686
- if len(parts) > 1 and any(
687
- part.strip() for part in parts[1:]
688
- ):
689
- abm.message.append(Plain(message_content))
690
- else:
691
- # 检查是否只包含@机器人
692
- is_pure_at = False
693
- if (
694
- bot_nickname
695
- and message_content.strip() == f"@{bot_nickname}"
696
- ):
697
- is_pure_at = True
698
- if not is_pure_at:
699
- abm.message.append(Plain(message_content))
700
- else:
701
- # 没有@机器人,作为普通文本处理
702
- abm.message.append(Plain(message_content))
703
- else:
704
- abm.message.append(Plain(abm.message_str))
705
- else: # 私聊消息
706
- abm.message.append(Plain(abm.message_str))
707
-
708
- # 缓存文本消息,以便引用消息可以查找
709
- try:
710
- # 获取msg_id作为缓存的key
711
- new_msg_id = raw_message.get("new_msg_id")
712
- if new_msg_id:
713
- # 限制缓存大小
714
- if (
715
- len(self.cached_texts) >= self.max_text_cache
716
- and self.cached_texts
717
- ):
718
- # 删除最早的一条缓存
719
- oldest_key = next(iter(self.cached_texts))
720
- self.cached_texts.pop(oldest_key)
721
-
722
- logger.debug(f"缓存文本消息,new_msg_id={new_msg_id}")
723
- self.cached_texts[str(new_msg_id)] = content
724
- except Exception as e:
725
- logger.error(f"缓存文本消息失败: {e}")
726
- elif msg_type == 3:
727
- # 图片消息
728
- from_user_name = raw_message.get("from_user_name", {}).get("str", "")
729
- to_user_name = raw_message.get("to_user_name", {}).get("str", "")
730
- msg_id = cast(int, raw_message.get("msg_id"))
731
- image_resp = await self._download_raw_image(
732
- from_user_name,
733
- to_user_name,
734
- msg_id,
735
- )
736
- if image_resp is None:
737
- logger.error(f"下载图片失败: msg_id={msg_id}")
738
- return
739
- image_bs64_data = (
740
- image_resp.get("Data", {}).get("Data", {}).get("Buffer", None)
741
- )
742
- if image_bs64_data:
743
- abm.message.append(Image.fromBase64(image_bs64_data))
744
- # 缓存图片,以便引用消息可以查找
745
- try:
746
- # 获取msg_id作为缓存的key
747
- new_msg_id = raw_message.get("new_msg_id")
748
- if new_msg_id:
749
- # 限制缓存大小
750
- if (
751
- len(self.cached_images) >= self.max_image_cache
752
- and self.cached_images
753
- ):
754
- # 删除最早的一条缓存
755
- oldest_key = next(iter(self.cached_images))
756
- self.cached_images.pop(oldest_key)
757
-
758
- logger.debug(f"缓存图片消息,new_msg_id={new_msg_id}")
759
- self.cached_images[str(new_msg_id)] = image_bs64_data
760
- except Exception as e:
761
- logger.error(f"缓存图片消息失败: {e}")
762
- elif msg_type == 47:
763
- # 视频消息 (注意:表情消息也是 47,需要区分)
764
- data_parser = GeweDataParser(
765
- content=content,
766
- is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
767
- raw_message=raw_message,
768
- )
769
- emoji_message = data_parser.parse_emoji()
770
- if emoji_message is not None:
771
- abm.message.append(emoji_message)
772
- elif msg_type == 50:
773
- logger.warning("收到语音/视频消息,待实现。")
774
- elif msg_type == 34:
775
- # 语音消息
776
- bufid = 0
777
- to_user_name = raw_message.get("to_user_name", {}).get("str", "")
778
- new_msg_id = raw_message.get("new_msg_id")
779
- if new_msg_id is None:
780
- logger.error("语音消息缺少 new_msg_id")
781
- return
782
- data_parser = GeweDataParser(
783
- content=content,
784
- is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
785
- raw_message=raw_message,
786
- )
787
-
788
- voicemsg = data_parser._format_to_xml().find("voicemsg")
789
- if voicemsg is None:
790
- logger.error("无法从 XML 解析 voicemsg 节点")
791
- return
792
- bufid = voicemsg.get("bufid") or "0"
793
- length = int(voicemsg.get("length") or 0)
794
- voice_resp = await self.download_voice(
795
- to_user_name=to_user_name,
796
- new_msg_id=new_msg_id,
797
- bufid=bufid,
798
- length=length,
799
- )
800
- if voice_resp is None:
801
- logger.error(f"下载语音失败: new_msg_id={new_msg_id}")
802
- return
803
- voice_bs64_data = voice_resp.get("Data", {}).get("Base64", None)
804
- if voice_bs64_data:
805
- voice_bs64_data = base64.b64decode(voice_bs64_data)
806
- temp_dir = os.path.join(get_astrbot_data_path(), "temp")
807
- file_path = os.path.join(
808
- temp_dir,
809
- f"wechatpadpro_voice_{abm.message_id}.silk",
810
- )
811
-
812
- async with await anyio.open_file(file_path, "wb") as f:
813
- await f.write(voice_bs64_data)
814
- abm.message.append(Record(file=file_path, url=file_path))
815
- elif msg_type == 49:
816
- try:
817
- parser = GeweDataParser(
818
- content=content,
819
- is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
820
- cached_texts=self.cached_texts,
821
- cached_images=self.cached_images,
822
- raw_message=raw_message,
823
- downloader=self._download_raw_image,
824
- )
825
- components = await parser.parse_mutil_49()
826
- if components:
827
- abm.message.extend(components)
828
- abm.message_str = "\n".join(
829
- c.text for c in components if isinstance(c, Plain)
830
- )
831
- except Exception as e:
832
- logger.warning(f"msg_type 49 处理失败: {e}")
833
- abm.message.append(Plain("[XML 消息处理失败]"))
834
- abm.message_str = "[XML 消息处理失败]"
835
- else:
836
- logger.warning(f"收到未处理的消息类型: {msg_type}。")
837
-
838
- async def terminate(self):
839
- """终止一个平台的运行实例。"""
840
- logger.info("终止 WeChatPadPro 适配器。")
841
- try:
842
- if self.ws_handle_task:
843
- self.ws_handle_task.cancel()
844
- if self._shutdown_event is not None:
845
- self._shutdown_event.set()
846
- except Exception:
847
- pass
848
-
849
- def meta(self) -> PlatformMetadata:
850
- """得到一个平台的元数据。"""
851
- return self.metadata
852
-
853
- async def send_by_session(
854
- self,
855
- session: MessageSesion,
856
- message_chain: MessageChain,
857
- ):
858
- dummy_message_obj = AstrBotMessage()
859
- dummy_message_obj.session_id = session.session_id
860
- # 根据 session_id 判断消息类型
861
- if "@chatroom" in session.session_id:
862
- dummy_message_obj.type = MessageType.GROUP_MESSAGE
863
- if "#" in session.session_id:
864
- dummy_message_obj.group_id = session.session_id.split("#")[0]
865
- else:
866
- dummy_message_obj.group_id = session.session_id
867
- dummy_message_obj.sender = MessageMember(user_id="", nickname="")
868
- else:
869
- dummy_message_obj.type = MessageType.FRIEND_MESSAGE
870
- dummy_message_obj.group_id = ""
871
- dummy_message_obj.sender = MessageMember(user_id="", nickname="")
872
- sending_event = WeChatPadProMessageEvent(
873
- message_str="",
874
- message_obj=dummy_message_obj,
875
- platform_meta=self.meta(),
876
- session_id=session.session_id,
877
- adapter=self,
878
- )
879
- # 调用实例方法 send
880
- await sending_event.send(message_chain)
881
-
882
- async def get_contact_list(self):
883
- """获取联系人列表。"""
884
- url = f"{self.base_url}/friend/GetContactList"
885
- params = {"key": self.auth_key}
886
- payload = {"CurrentChatRoomContactSeq": 0, "CurrentWxcontactSeq": 0}
887
- async with aiohttp.ClientSession() as session:
888
- try:
889
- async with session.post(url, params=params, json=payload) as response:
890
- if response.status != 200:
891
- logger.error(f"获取联系人列表失败: {response.status}")
892
- return None
893
- result = await response.json()
894
- if result.get("Code") == 200 and result.get("Data"):
895
- contact_list = (
896
- result.get("Data", {})
897
- .get("ContactList", {})
898
- .get("contactUsernameList", [])
899
- )
900
- return contact_list
901
- logger.error(f"获取联系人列表失败: {result}")
902
- return None
903
- except aiohttp.ClientConnectorError as e:
904
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
905
- return None
906
- except Exception as e:
907
- logger.error(f"获取联系人列表时发生错误: {e}")
908
- return None
909
-
910
- async def get_contact_details_list(
911
- self,
912
- room_wx_id_list: list[str] | None = None,
913
- user_names: list[str] | None = None,
914
- ) -> dict | None:
915
- """获取联系人详情列表。"""
916
- if room_wx_id_list is None:
917
- room_wx_id_list = []
918
- if user_names is None:
919
- user_names = []
920
- url = f"{self.base_url}/friend/GetContactDetailsList"
921
- params = {"key": self.auth_key}
922
- payload = {"RoomWxIDList": room_wx_id_list, "UserNames": user_names}
923
- async with aiohttp.ClientSession() as session:
924
- try:
925
- async with session.post(url, params=params, json=payload) as response:
926
- if response.status != 200:
927
- logger.error(f"获取联系人详情列表失败: {response.status}")
928
- return None
929
- result = await response.json()
930
- if result.get("Code") == 200 and result.get("Data"):
931
- contact_list = result.get("Data", {}).get("contactList", {})
932
- return contact_list
933
- logger.error(f"获取联系人详情列表失败: {result}")
934
- return None
935
- except aiohttp.ClientConnectorError as e:
936
- logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
937
- return None
938
- except Exception as e:
939
- logger.error(f"获取联系人详情列表时发生错误: {e}")
940
- return None