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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/config/default.py +1 -11
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +17 -7
- astrbot/core/pipeline/waking_check/stage.py +0 -1
- astrbot/core/platform/manager.py +0 -4
- astrbot/core/star/filter/platform_adapter_type.py +0 -3
- astrbot/core/star/star_manager.py +17 -0
- astrbot/dashboard/routes/plugin.py +50 -0
- {astrbot-4.11.1.dist-info → astrbot-4.11.2.dist-info}/METADATA +1 -1
- {astrbot-4.11.1.dist-info → astrbot-4.11.2.dist-info}/RECORD +13 -16
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +0 -940
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +0 -178
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +0 -159
- {astrbot-4.11.1.dist-info → astrbot-4.11.2.dist-info}/WHEEL +0 -0
- {astrbot-4.11.1.dist-info → astrbot-4.11.2.dist-info}/entry_points.txt +0 -0
- {astrbot-4.11.1.dist-info → astrbot-4.11.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|