wecom-aibot-python-sdk 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.
aibot/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ 企业微信智能机器人 Python SDK
3
+
4
+ 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ from .client import WSClient
10
+ from .api import WeComApiClient
11
+ from .ws import WsConnectionManager
12
+ from .message_handler import MessageHandler
13
+ from .crypto_utils import decrypt_file
14
+ from .logger import DefaultLogger
15
+ from .utils import generate_req_id, generate_random_string
16
+ from .types import (
17
+ MessageType,
18
+ EventType,
19
+ TemplateCardType,
20
+ WsCmd,
21
+ WSClientOptions,
22
+ WsFrame,
23
+ WsFrameHeaders,
24
+ Logger,
25
+ )
26
+
27
+ __all__ = [
28
+ # 版本
29
+ "__version__",
30
+ # 类
31
+ "WSClient",
32
+ "WeComApiClient",
33
+ "WsConnectionManager",
34
+ "MessageHandler",
35
+ "DefaultLogger",
36
+ # 函数
37
+ "decrypt_file",
38
+ "generate_req_id",
39
+ "generate_random_string",
40
+ # 枚举/常量
41
+ "MessageType",
42
+ "EventType",
43
+ "TemplateCardType",
44
+ "WsCmd",
45
+ # 类型
46
+ "WSClientOptions",
47
+ "WsFrame",
48
+ "WsFrameHeaders",
49
+ "Logger",
50
+ ]
aibot/api.py ADDED
@@ -0,0 +1,74 @@
1
+ """
2
+ 企业微信 API 客户端
3
+
4
+ 对标 Node.js SDK src/api.ts
5
+ 仅负责文件下载等 HTTP 辅助功能,消息收发均走 WebSocket 通道。
6
+ """
7
+
8
+ import re
9
+ import ssl
10
+ from typing import Any, Optional, Tuple
11
+ from urllib.parse import unquote
12
+
13
+ import aiohttp
14
+
15
+ try:
16
+ import certifi
17
+ _SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
18
+ except ImportError:
19
+ # 未安装 certifi 时回退到系统默认证书
20
+ _SSL_CONTEXT = ssl.create_default_context()
21
+
22
+
23
+ class WeComApiClient:
24
+ """企业微信 API 客户端"""
25
+
26
+ def __init__(self, logger: Any, timeout: int = 10000):
27
+ self._logger = logger
28
+ self._timeout = aiohttp.ClientTimeout(total=timeout / 1000)
29
+
30
+ async def download_file_raw(self, url: str) -> Tuple[bytes, Optional[str]]:
31
+ """
32
+ 下载文件(返回原始 bytes 及文件名)
33
+
34
+ :param url: 文件下载地址
35
+ :return: (文件数据, 文件名)
36
+ """
37
+ self._logger.info("Downloading file...")
38
+
39
+ try:
40
+ connector = aiohttp.TCPConnector(ssl=_SSL_CONTEXT)
41
+ async with aiohttp.ClientSession(timeout=self._timeout, connector=connector) as session:
42
+ async with session.get(url) as response:
43
+ response.raise_for_status()
44
+ data = await response.read()
45
+
46
+ # 从 Content-Disposition 头中解析文件名
47
+ content_disposition = response.headers.get("Content-Disposition", "")
48
+ filename: Optional[str] = None
49
+
50
+ if content_disposition:
51
+ # 优先匹配 filename*=UTF-8''xxx 格式(RFC 5987)
52
+ utf8_match = re.search(
53
+ r"filename\*=UTF-8''([^;\s]+)",
54
+ content_disposition,
55
+ re.IGNORECASE,
56
+ )
57
+ if utf8_match:
58
+ filename = unquote(utf8_match.group(1))
59
+ else:
60
+ # 匹配 filename="xxx" 或 filename=xxx 格式
61
+ match = re.search(
62
+ r'filename="?([^";\s]+)"?',
63
+ content_disposition,
64
+ re.IGNORECASE,
65
+ )
66
+ if match:
67
+ filename = unquote(match.group(1))
68
+
69
+ self._logger.info("File downloaded successfully")
70
+ return data, filename
71
+
72
+ except Exception as e:
73
+ self._logger.error("File download failed:", str(e))
74
+ raise
aibot/client.py ADDED
@@ -0,0 +1,362 @@
1
+ """
2
+ WSClient 核心客户端
3
+
4
+ 对标 Node.js SDK src/client.ts
5
+ 继承自 pyee.AsyncIOEventEmitter,组合 WsConnectionManager + MessageHandler + WeComApiClient。
6
+ """
7
+
8
+ import asyncio
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from pyee.asyncio import AsyncIOEventEmitter
12
+
13
+ from .api import WeComApiClient
14
+ from .crypto_utils import decrypt_file
15
+ from .logger import DefaultLogger
16
+ from .message_handler import MessageHandler
17
+ from .types import WsCmd, WsFrame, WsFrameHeaders, WSClientOptions
18
+ from .utils import generate_req_id
19
+ from .ws import WsConnectionManager
20
+
21
+
22
+ class WSClient(AsyncIOEventEmitter):
23
+ """
24
+ 企业微信智能机器人 Python SDK 核心客户端
25
+
26
+ 基于 asyncio + pyee 的事件驱动架构,提供 WebSocket 长连接消息收发能力。
27
+ """
28
+
29
+ def __init__(self, options: WSClientOptions) -> None:
30
+ super().__init__()
31
+
32
+ self._options = options
33
+ self._logger = options.logger or DefaultLogger()
34
+ self._started = False
35
+
36
+ # 初始化 API 客户端(仅用于文件下载)
37
+ self._api_client = WeComApiClient(
38
+ self._logger,
39
+ self._options.request_timeout,
40
+ )
41
+
42
+ # 初始化 WebSocket 管理器
43
+ self._ws_manager = WsConnectionManager(
44
+ self._logger,
45
+ heartbeat_interval=self._options.heartbeat_interval,
46
+ reconnect_base_delay=self._options.reconnect_interval,
47
+ max_reconnect_attempts=self._options.max_reconnect_attempts,
48
+ ws_url=self._options.ws_url or None,
49
+ )
50
+
51
+ # 设置认证凭证
52
+ self._ws_manager.set_credentials(self._options.bot_id, self._options.secret)
53
+
54
+ # 初始化消息处理器
55
+ self._message_handler = MessageHandler(self._logger)
56
+
57
+ # 绑定 WebSocket 事件
58
+ self._setup_ws_events()
59
+
60
+ def _setup_ws_events(self) -> None:
61
+ """设置 WebSocket 事件处理"""
62
+ self._ws_manager.on_connected = lambda: self.emit("connected")
63
+
64
+ def _on_authenticated() -> None:
65
+ self._logger.info("Authenticated")
66
+ self.emit("authenticated")
67
+
68
+ self._ws_manager.on_authenticated = _on_authenticated
69
+
70
+ self._ws_manager.on_disconnected = lambda reason: self.emit(
71
+ "disconnected", reason
72
+ )
73
+ self._ws_manager.on_reconnecting = lambda attempt: self.emit(
74
+ "reconnecting", attempt
75
+ )
76
+ self._ws_manager.on_error = lambda error: self.emit("error", error)
77
+ self._ws_manager.on_message = lambda frame: self._message_handler.handle_frame(
78
+ frame, self
79
+ )
80
+
81
+ async def connect(self) -> "WSClient":
82
+ """
83
+ 建立 WebSocket 长连接
84
+
85
+ SDK 使用内置默认地址建立连接,连接成功后自动发送认证帧(bot_id + secret)。
86
+
87
+ :return: 返回 self,支持链式调用
88
+ """
89
+ if self._started:
90
+ self._logger.warn("Client already connected")
91
+ return self
92
+
93
+ self._logger.info("Establishing WebSocket connection...")
94
+ self._started = True
95
+
96
+ await self._ws_manager.connect()
97
+
98
+ return self
99
+
100
+ def disconnect(self) -> None:
101
+ """断开 WebSocket 连接"""
102
+ if not self._started:
103
+ self._logger.warn("Client not connected")
104
+ return
105
+
106
+ self._logger.info("Disconnecting...")
107
+ self._started = False
108
+ self._ws_manager.disconnect()
109
+ self._logger.info("Disconnected")
110
+
111
+ async def reply(
112
+ self,
113
+ frame: WsFrameHeaders,
114
+ body: Dict[str, Any],
115
+ cmd: Optional[str] = None,
116
+ ) -> WsFrame:
117
+ """
118
+ 通过 WebSocket 通道发送回复消息(通用方法)
119
+
120
+ :param frame: 收到的原始 WebSocket 帧,透传 headers.req_id
121
+ :param body: 回复消息体
122
+ :param cmd: 发送的命令类型
123
+ :return: 回执帧
124
+ """
125
+ headers = frame.get("headers", {})
126
+ req_id = headers.get("req_id", "")
127
+ return await self._ws_manager.send_reply(req_id, body, cmd or WsCmd.RESPONSE)
128
+
129
+ async def reply_stream(
130
+ self,
131
+ frame: WsFrameHeaders,
132
+ stream_id: str,
133
+ content: str,
134
+ finish: bool = False,
135
+ msg_item: Optional[List[Dict[str, Any]]] = None,
136
+ feedback: Optional[Dict[str, Any]] = None,
137
+ ) -> WsFrame:
138
+ """
139
+ 发送流式文本回复(便捷方法)
140
+
141
+ :param frame: 收到的原始 WebSocket 帧,透传 headers.req_id
142
+ :param stream_id: 流式消息 ID
143
+ :param content: 回复内容(支持 Markdown)
144
+ :param finish: 是否结束流式消息,默认 False
145
+ :param msg_item: 图文混排项(仅在 finish=True 时有效)
146
+ :param feedback: 反馈信息(仅在首次回复时设置)
147
+ :return: 回执帧
148
+ """
149
+ stream: Dict[str, Any] = {
150
+ "id": stream_id,
151
+ "finish": finish,
152
+ "content": content,
153
+ }
154
+
155
+ # msg_item 仅在 finish=True 时支持
156
+ if finish and msg_item and len(msg_item) > 0:
157
+ stream["msg_item"] = msg_item
158
+
159
+ # feedback 仅在首次回复时设置
160
+ if feedback:
161
+ stream["feedback"] = feedback
162
+
163
+ return await self.reply(
164
+ frame,
165
+ {
166
+ "msgtype": "stream",
167
+ "stream": stream,
168
+ },
169
+ )
170
+
171
+ async def reply_welcome(
172
+ self,
173
+ frame: WsFrameHeaders,
174
+ body: Dict[str, Any],
175
+ ) -> WsFrame:
176
+ """
177
+ 发送欢迎语回复
178
+
179
+ 注意:此方法需要使用对应事件(如 enter_chat)的 req_id 才能调用。
180
+ 收到事件回调后需在 5 秒内发送回复,超时将无法发送欢迎语。
181
+
182
+ :param frame: 对应事件的 WebSocket 帧
183
+ :param body: 欢迎语消息体(支持文本或模板卡片格式)
184
+ :return: 回执帧
185
+ """
186
+ return await self.reply(frame, body, WsCmd.RESPONSE_WELCOME)
187
+
188
+ async def reply_template_card(
189
+ self,
190
+ frame: WsFrameHeaders,
191
+ template_card: Dict[str, Any],
192
+ feedback: Optional[Dict[str, Any]] = None,
193
+ ) -> WsFrame:
194
+ """
195
+ 回复模板卡片消息
196
+
197
+ :param frame: 收到的原始 WebSocket 帧
198
+ :param template_card: 模板卡片内容
199
+ :param feedback: 反馈信息
200
+ :return: 回执帧
201
+ """
202
+ card = {**template_card, "feedback": feedback} if feedback else template_card
203
+ body = {
204
+ "msgtype": "template_card",
205
+ "template_card": card,
206
+ }
207
+ return await self.reply(frame, body)
208
+
209
+ async def reply_stream_with_card(
210
+ self,
211
+ frame: WsFrameHeaders,
212
+ stream_id: str,
213
+ content: str,
214
+ finish: bool = False,
215
+ msg_item: Optional[List[Dict[str, Any]]] = None,
216
+ stream_feedback: Optional[Dict[str, Any]] = None,
217
+ template_card: Optional[Dict[str, Any]] = None,
218
+ card_feedback: Optional[Dict[str, Any]] = None,
219
+ ) -> WsFrame:
220
+ """
221
+ 发送流式消息 + 模板卡片组合回复
222
+
223
+ :param frame: 收到的原始 WebSocket 帧
224
+ :param stream_id: 流式消息 ID
225
+ :param content: 回复内容(支持 Markdown)
226
+ :param finish: 是否结束流式消息,默认 False
227
+ :param msg_item: 图文混排项(仅在 finish=True 时有效)
228
+ :param stream_feedback: 流式消息反馈信息(首次回复时设置)
229
+ :param template_card: 模板卡片内容(同一消息只能回复一次)
230
+ :param card_feedback: 模板卡片反馈信息
231
+ :return: 回执帧
232
+ """
233
+ stream: Dict[str, Any] = {
234
+ "id": stream_id,
235
+ "finish": finish,
236
+ "content": content,
237
+ }
238
+
239
+ if finish and msg_item and len(msg_item) > 0:
240
+ stream["msg_item"] = msg_item
241
+
242
+ if stream_feedback:
243
+ stream["feedback"] = stream_feedback
244
+
245
+ body: Dict[str, Any] = {
246
+ "msgtype": "stream_with_template_card",
247
+ "stream": stream,
248
+ }
249
+
250
+ if template_card:
251
+ card = (
252
+ {**template_card, "feedback": card_feedback}
253
+ if card_feedback
254
+ else template_card
255
+ )
256
+ body["template_card"] = card
257
+
258
+ return await self.reply(frame, body)
259
+
260
+ async def update_template_card(
261
+ self,
262
+ frame: WsFrameHeaders,
263
+ template_card: Dict[str, Any],
264
+ userids: Optional[List[str]] = None,
265
+ ) -> WsFrame:
266
+ """
267
+ 更新模板卡片
268
+
269
+ 注意:此方法需要使用对应事件(template_card_event)的 req_id 才能调用。
270
+ 收到事件回调后需在 5 秒内发送回复,超时将无法更新卡片。
271
+
272
+ :param frame: 对应事件的 WebSocket 帧
273
+ :param template_card: 模板卡片内容(task_id 需跟回调收到的 task_id 一致)
274
+ :param userids: 要替换模版卡片消息的 userid 列表
275
+ :return: 回执帧
276
+ """
277
+ body: Dict[str, Any] = {
278
+ "response_type": "update_template_card",
279
+ "template_card": template_card,
280
+ }
281
+ if userids and len(userids) > 0:
282
+ body["userids"] = userids
283
+
284
+ return await self.reply(frame, body, WsCmd.RESPONSE_UPDATE)
285
+
286
+ async def send_message(
287
+ self,
288
+ chatid: str,
289
+ body: Dict[str, Any],
290
+ ) -> WsFrame:
291
+ """
292
+ 主动发送消息
293
+
294
+ 向指定会话(单聊或群聊)主动推送消息,无需依赖收到的回调帧。
295
+
296
+ :param chatid: 会话 ID,单聊填用户的 userid,群聊填对应群聊的 chatid
297
+ :param body: 消息体(支持 markdown 或 template_card 格式)
298
+ :return: 回执帧
299
+ """
300
+ req_id = generate_req_id(WsCmd.SEND_MSG)
301
+ full_body = {"chatid": chatid, **body}
302
+ return await self._ws_manager.send_reply(req_id, full_body, WsCmd.SEND_MSG)
303
+
304
+ async def download_file(
305
+ self, url: str, aes_key: Optional[str] = None
306
+ ) -> Tuple[bytes, Optional[str]]:
307
+ """
308
+ 下载文件并使用 AES 密钥解密
309
+
310
+ :param url: 文件下载地址
311
+ :param aes_key: AES 解密密钥(Base64 编码),取自消息中 image.aeskey 或 file.aeskey
312
+ :return: (解密后的文件数据, 文件名)
313
+ """
314
+ self._logger.info("Downloading and decrypting file...")
315
+
316
+ try:
317
+ # 下载加密的文件数据
318
+ encrypted_data, filename = await self._api_client.download_file_raw(url)
319
+
320
+ # 如果没有提供 aes_key,直接返回原始数据
321
+ if not aes_key:
322
+ self._logger.warn("No aes_key provided, returning raw file data")
323
+ return encrypted_data, filename
324
+
325
+ # 使用独立的解密模块进行 AES-256-CBC 解密
326
+ decrypted_data = decrypt_file(encrypted_data, aes_key)
327
+
328
+ self._logger.info("File downloaded and decrypted successfully")
329
+ return decrypted_data, filename
330
+
331
+ except Exception as e:
332
+ self._logger.error(f"File download/decrypt failed: {e}")
333
+ raise
334
+
335
+ @property
336
+ def is_connected(self) -> bool:
337
+ """获取当前连接状态"""
338
+ return self._ws_manager.is_connected
339
+
340
+ @property
341
+ def api(self) -> WeComApiClient:
342
+ """获取 API 客户端实例(供高级用途使用)"""
343
+ return self._api_client
344
+
345
+ def run(self) -> None:
346
+ """
347
+ 便捷方法:启动事件循环并连接
348
+
349
+ 等价于:
350
+ asyncio.get_event_loop().run_until_complete(client.connect())
351
+ asyncio.get_event_loop().run_forever()
352
+ """
353
+ loop = asyncio.new_event_loop()
354
+ asyncio.set_event_loop(loop)
355
+
356
+ try:
357
+ loop.run_until_complete(self.connect())
358
+ loop.run_forever()
359
+ except KeyboardInterrupt:
360
+ self.disconnect()
361
+ finally:
362
+ loop.close()
aibot/crypto_utils.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ 加解密工具模块
3
+
4
+ 对标 Node.js SDK src/crypto.ts
5
+ 提供文件加解密相关的功能函数,使用 AES-256-CBC 解密。
6
+ """
7
+
8
+ import base64
9
+
10
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
11
+
12
+
13
+ def decrypt_file(encrypted_data: bytes, aes_key: str) -> bytes:
14
+ """
15
+ 使用 AES-256-CBC 解密文件
16
+
17
+ :param encrypted_data: 加密的文件数据
18
+ :param aes_key: Base64 编码的 AES-256 密钥
19
+ :return: 解密后的文件数据
20
+ :raises ValueError: 参数无效时
21
+ :raises RuntimeError: 解密失败时
22
+ """
23
+ if not encrypted_data:
24
+ raise ValueError("decrypt_file: encrypted_data is empty or not provided")
25
+
26
+ if not aes_key or not isinstance(aes_key, str):
27
+ raise ValueError("decrypt_file: aes_key must be a non-empty string")
28
+
29
+ # 将 Base64 编码的 aesKey 解码为 bytes
30
+ # Node.js 的 Buffer.from(str, 'base64') 会自动容错处理缺少的 '=' padding,
31
+ # 但 Python 的 base64.b64decode 严格要求长度是 4 的倍数,需要手动补齐。
32
+ padded_aes_key = aes_key + '=' * (4 - len(aes_key) % 4) if len(aes_key) % 4 != 0 else aes_key
33
+ key = base64.b64decode(padded_aes_key)
34
+
35
+ # IV 取 aesKey 解码后的前 16 字节
36
+ iv = key[:16]
37
+
38
+ try:
39
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
40
+ decryptor = cipher.decryptor()
41
+
42
+ # 确保加密数据长度是 AES block size (16字节) 的倍数
43
+ # Node.js 的 setAutoPadding(false) 不会对不对齐的数据报错,
44
+ # 但 Python 的 cryptography 库会抛出 "Incorrect padding"。
45
+ # 这里手动补零对齐,后续通过 PKCS#7 去除 padding 来获得正确数据。
46
+ block_size = 16
47
+ remainder = len(encrypted_data) % block_size
48
+ if remainder != 0:
49
+ encrypted_data = encrypted_data + b'\x00' * (block_size - remainder)
50
+
51
+ # 解密(不自动处理 padding)
52
+ decrypted = decryptor.update(encrypted_data) + decryptor.finalize()
53
+
54
+ # 手动去除 PKCS#7 填充(支持 32 字节 block)
55
+ if len(decrypted) == 0:
56
+ raise ValueError("Decrypted data is empty")
57
+
58
+ pad_len = decrypted[-1]
59
+ if pad_len < 1 or pad_len > 32 or pad_len > len(decrypted):
60
+ raise ValueError(f"Invalid PKCS#7 padding value: {pad_len}")
61
+
62
+ # 验证所有 padding 字节是否一致
63
+ for i in range(len(decrypted) - pad_len, len(decrypted)):
64
+ if decrypted[i] != pad_len:
65
+ raise ValueError("Invalid PKCS#7 padding: padding bytes mismatch")
66
+
67
+ return decrypted[: len(decrypted) - pad_len]
68
+
69
+ except Exception as e:
70
+ raise RuntimeError(
71
+ f"decrypt_file: Decryption failed - {e}. "
72
+ "This may indicate corrupted data or an incorrect aesKey."
73
+ ) from e
aibot/logger.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ 默认日志实现
3
+
4
+ 对标 Node.js SDK src/logger.ts
5
+ 带有日志级别和时间戳的控制台日志
6
+ """
7
+
8
+ import sys
9
+ from datetime import datetime, timezone
10
+
11
+
12
+ class DefaultLogger:
13
+ """默认日志实现,带有日志级别和时间戳的控制台日志"""
14
+
15
+ def __init__(self, prefix: str = "AiBotSDK"):
16
+ self._prefix = prefix
17
+
18
+ def _format_time(self) -> str:
19
+ return datetime.now(timezone.utc).isoformat()
20
+
21
+ def debug(self, message: str, *args: object) -> None:
22
+ print(
23
+ f"[{self._format_time()}] [{self._prefix}] [DEBUG] {message}",
24
+ *args,
25
+ file=sys.stderr,
26
+ )
27
+
28
+ def info(self, message: str, *args: object) -> None:
29
+ print(
30
+ f"[{self._format_time()}] [{self._prefix}] [INFO] {message}",
31
+ *args,
32
+ file=sys.stderr,
33
+ )
34
+
35
+ def warn(self, message: str, *args: object) -> None:
36
+ print(
37
+ f"[{self._format_time()}] [{self._prefix}] [WARN] {message}",
38
+ *args,
39
+ file=sys.stderr,
40
+ )
41
+
42
+ def error(self, message: str, *args: object) -> None:
43
+ print(
44
+ f"[{self._format_time()}] [{self._prefix}] [ERROR] {message}",
45
+ *args,
46
+ file=sys.stderr,
47
+ )
@@ -0,0 +1,89 @@
1
+ """
2
+ 消息处理器
3
+
4
+ 对标 Node.js SDK src/message-handler.ts
5
+ 负责解析 WebSocket 帧并分发为具体的消息事件和事件回调。
6
+ """
7
+
8
+ import json
9
+ from typing import Any
10
+
11
+ from .types import MessageType, WsCmd, WsFrame
12
+
13
+
14
+ class MessageHandler:
15
+ """
16
+ 消息处理器
17
+
18
+ 负责解析 WebSocket 帧并分发为具体的消息事件和事件回调。
19
+ """
20
+
21
+ def __init__(self, logger: Any):
22
+ self._logger = logger
23
+
24
+ def handle_frame(self, frame: WsFrame, emitter: Any) -> None:
25
+ """
26
+ 处理收到的 WebSocket 帧,解析并触发对应的消息/事件
27
+
28
+ :param frame: WebSocket 接收帧
29
+ :param emitter: WSClient 实例,用于触发事件
30
+ """
31
+ try:
32
+ body = frame.get("body")
33
+
34
+ if not body or not body.get("msgtype"):
35
+ self._logger.warn(
36
+ f"Received invalid message format: {json.dumps(frame)[:200]}"
37
+ )
38
+ return
39
+
40
+ # 事件推送回调处理
41
+ if frame.get("cmd") == WsCmd.EVENT_CALLBACK:
42
+ self._handle_event_callback(frame, emitter)
43
+ return
44
+
45
+ # 消息推送回调处理
46
+ self._handle_message_callback(frame, emitter)
47
+ except Exception as e:
48
+ self._logger.error(f"Failed to handle message: {e}")
49
+
50
+ def _handle_message_callback(self, frame: WsFrame, emitter: Any) -> None:
51
+ """处理消息推送回调 (aibot_msg_callback)"""
52
+ body = frame.get("body", {})
53
+
54
+ # 触发通用消息事件
55
+ emitter.emit("message", frame)
56
+
57
+ # 根据 body 中的消息类型触发特定事件
58
+ msgtype = body.get("msgtype", "")
59
+
60
+ if msgtype == MessageType.Text:
61
+ emitter.emit("message.text", frame)
62
+ elif msgtype == MessageType.Image:
63
+ emitter.emit("message.image", frame)
64
+ elif msgtype == MessageType.Mixed:
65
+ emitter.emit("message.mixed", frame)
66
+ elif msgtype == MessageType.Voice:
67
+ emitter.emit("message.voice", frame)
68
+ elif msgtype == MessageType.File:
69
+ emitter.emit("message.file", frame)
70
+ else:
71
+ self._logger.debug(f"Received unhandled message type: {msgtype}")
72
+
73
+ def _handle_event_callback(self, frame: WsFrame, emitter: Any) -> None:
74
+ """处理事件推送回调 (aibot_event_callback)"""
75
+ body = frame.get("body", {})
76
+
77
+ # 触发通用事件
78
+ emitter.emit("event", frame)
79
+
80
+ # 根据事件类型触发特定事件
81
+ event = body.get("event", {})
82
+ event_type = event.get("eventtype") if isinstance(event, dict) else None
83
+
84
+ if event_type:
85
+ emitter.emit(f"event.{event_type}", frame)
86
+ else:
87
+ self._logger.debug(
88
+ f"Received event callback without eventtype: {json.dumps(body)[:200]}"
89
+ )