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.
@@ -0,0 +1,325 @@
1
+ """消息解析器 - 统一消息类型判断和解析逻辑
2
+
3
+ 支持消息类型:
4
+ - text(1): 文本消息
5
+ - image(3): 图片消息
6
+ - voice(34): 语音消息
7
+ - quote(49): 引用消息
8
+ - pat(10002): 拍一拍消息
9
+ - system(10000): 系统消息(入群等)
10
+
11
+ 集成 XMLParser 和 TextProcessor
12
+ """
13
+ import logging
14
+ from dataclasses import dataclass
15
+ from typing import Optional, TYPE_CHECKING
16
+
17
+ from ..utils.xml_parser import XMLParser, QuoteMessageResult, PatMessageResult, VoiceInfo
18
+ from ..utils.text_processor import TextProcessor
19
+
20
+ if TYPE_CHECKING:
21
+ from ..clients.qianxun import QianXunClient
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class ParsedMessage:
28
+ """解析后的消息"""
29
+ type: str # text, image, voice, quote, pat, system, unknown
30
+ content: str # 处理后的消息内容
31
+ image_url: Optional[str] = None # 图片URL(图片消息或引用图片)
32
+ quoted_text: Optional[str] = None # 被引用的消息内容
33
+ quoted_sender: Optional[str] = None # 被引用消息的发送者昵称
34
+ quoted_image_path: Optional[str] = None # 引用的图片路径
35
+ is_quote_to_bot: bool = False # 是否引用机器人的消息
36
+ voice_duration: Optional[float] = None # 语音时长(秒)
37
+ should_process: bool = True # 是否需要继续处理(发送到LangBot)
38
+ is_join_group: bool = False # 是否是入群消息
39
+ pat_info: Optional[PatMessageResult] = None # 拍一拍信息
40
+
41
+
42
+ class MessageParser:
43
+ """消息解析器"""
44
+
45
+ # 消息类型常量
46
+ MSG_TYPE_TEXT = 1
47
+ MSG_TYPE_IMAGE = 3
48
+ MSG_TYPE_VOICE = 34
49
+ MSG_TYPE_QUOTE = 49
50
+ MSG_TYPE_SYSTEM = 10000
51
+ MSG_TYPE_PAT = 10002
52
+
53
+ def __init__(self, qianxun_client: "QianXunClient"):
54
+ """初始化消息解析器
55
+
56
+ Args:
57
+ qianxun_client: 千寻客户端,用于获取图片URL等
58
+ """
59
+ self.qianxun = qianxun_client
60
+ self.xml_parser = XMLParser()
61
+ self.text_processor = TextProcessor()
62
+
63
+ def parse_message(
64
+ self,
65
+ msg_type: int,
66
+ content: str,
67
+ robot_wxid: str,
68
+ at_wxid_list: Optional[list] = None
69
+ ) -> ParsedMessage:
70
+ """解析消息,返回统一格式
71
+
72
+ Args:
73
+ msg_type: 消息类型
74
+ content: 消息内容
75
+ robot_wxid: 机器人wxid
76
+ at_wxid_list: @的wxid列表(群聊消息)
77
+
78
+ Returns:
79
+ ParsedMessage 解析结果
80
+ """
81
+ content = content.strip()
82
+
83
+ if msg_type == self.MSG_TYPE_TEXT:
84
+ return self._parse_text_message(content)
85
+
86
+ elif msg_type == self.MSG_TYPE_IMAGE:
87
+ return self._parse_image_message(content)
88
+
89
+ elif msg_type == self.MSG_TYPE_VOICE:
90
+ return self._parse_voice_message(content)
91
+
92
+ elif msg_type == self.MSG_TYPE_QUOTE:
93
+ return self._parse_quote_message(content, robot_wxid)
94
+
95
+ elif msg_type == self.MSG_TYPE_PAT:
96
+ return self._parse_pat_message(content, robot_wxid)
97
+
98
+ elif msg_type == self.MSG_TYPE_SYSTEM:
99
+ return self._parse_system_message(content, robot_wxid)
100
+
101
+ else:
102
+ # 未知消息类型
103
+ logger.debug(f"未知消息类型: {msg_type}")
104
+ return ParsedMessage(
105
+ type="unknown",
106
+ content=content,
107
+ should_process=False
108
+ )
109
+
110
+ def _parse_text_message(self, content: str) -> ParsedMessage:
111
+ """解析文本消息"""
112
+ # 处理千寻框架的特殊@格式:把 \u2005 转换成普通空格
113
+ content = content.replace('\\u2005', ' ')
114
+ content = content.replace('\u2005', ' ')
115
+
116
+ return ParsedMessage(
117
+ type="text",
118
+ content=content,
119
+ should_process=True
120
+ )
121
+
122
+ def _parse_image_message(self, content: str) -> ParsedMessage:
123
+ """解析图片消息"""
124
+ image_path = self.qianxun.parse_image_path(content)
125
+
126
+ if image_path:
127
+ image_url = self.qianxun.get_image_url(image_path)
128
+ logger.debug(f"解析图片消息: path={image_path}, url={image_url}")
129
+ return ParsedMessage(
130
+ type="image",
131
+ content="[图片]",
132
+ image_url=image_url,
133
+ should_process=True
134
+ )
135
+
136
+ # 解析失败
137
+ logger.warning(f"图片消息解析失败: {content}")
138
+ return ParsedMessage(
139
+ type="image",
140
+ content="[用户发送了一张图片,但无法获取]",
141
+ should_process=True
142
+ )
143
+
144
+ def _parse_voice_message(self, content: str) -> ParsedMessage:
145
+ """解析语音消息"""
146
+ voice_info = self.xml_parser.parse_voice_info(content)
147
+
148
+ if voice_info:
149
+ duration_sec = voice_info.voicelength / 1000
150
+ logger.debug(f"解析语音消息: 长度={duration_sec:.1f}秒")
151
+ return ParsedMessage(
152
+ type="voice",
153
+ content=f"[用户发送了一条{duration_sec:.1f}秒的语音消息]",
154
+ voice_duration=duration_sec,
155
+ should_process=True
156
+ )
157
+
158
+ return ParsedMessage(
159
+ type="voice",
160
+ content="[用户发送了一条语音消息]",
161
+ should_process=True
162
+ )
163
+
164
+ def _parse_quote_message(self, content: str, robot_wxid: str) -> ParsedMessage:
165
+ """解析引用消息"""
166
+ # 先尝试解析引用机器人的消息
167
+ quote_result = self.xml_parser.parse_quote_message(content, robot_wxid)
168
+
169
+ if quote_result:
170
+ # 引用机器人消息
171
+ user_msg = quote_result.user_msg
172
+ quoted_text = quote_result.quoted_text
173
+ sender_name = quote_result.sender_name
174
+
175
+ if quoted_text:
176
+ final_content = f"[引用 {sender_name} 的消息: {quoted_text}]\n{user_msg}"
177
+ else:
178
+ final_content = user_msg
179
+
180
+ logger.debug(f"解析引用消息(机器人): {final_content}")
181
+ return ParsedMessage(
182
+ type="quote",
183
+ content=final_content,
184
+ quoted_text=quoted_text,
185
+ quoted_sender=sender_name,
186
+ is_quote_to_bot=True,
187
+ should_process=True
188
+ )
189
+
190
+ # 尝试解析引用非机器人的消息
191
+ quote_any = self.xml_parser.parse_quote_message_any(content)
192
+
193
+ if quote_any:
194
+ user_msg = quote_any.user_msg
195
+ quoted_text = quote_any.quoted_text
196
+ sender_name = quote_any.sender_name
197
+ quoted_image_path = quote_any.quoted_image_path
198
+
199
+ # 检查是否引用了图片消息
200
+ if quoted_image_path:
201
+ image_url = self.qianxun.get_image_url(quoted_image_path)
202
+ logger.debug(f"解析引用图片消息: {image_url}")
203
+ return ParsedMessage(
204
+ type="quote",
205
+ content=user_msg,
206
+ image_url=image_url,
207
+ quoted_image_path=quoted_image_path,
208
+ quoted_sender=sender_name,
209
+ is_quote_to_bot=True, # 引用图片消息视为需要机器人处理
210
+ should_process=True
211
+ )
212
+
213
+ # 普通引用消息
214
+ if quoted_text:
215
+ final_content = f"[引用 {sender_name} 的消息: {quoted_text}]\n{user_msg}"
216
+ else:
217
+ final_content = user_msg
218
+
219
+ logger.debug(f"解析引用消息(非机器人): {final_content}")
220
+ return ParsedMessage(
221
+ type="quote",
222
+ content=final_content,
223
+ quoted_text=quoted_text,
224
+ quoted_sender=sender_name,
225
+ is_quote_to_bot=False,
226
+ should_process=True
227
+ )
228
+
229
+ # 无法解析
230
+ logger.debug("无法解析引用消息")
231
+ return ParsedMessage(
232
+ type="quote",
233
+ content=content,
234
+ should_process=False
235
+ )
236
+
237
+ def _parse_pat_message(self, content: str, robot_wxid: str) -> ParsedMessage:
238
+ """解析拍一拍消息(XML格式,私聊或部分群聊)"""
239
+ pat_info = self.xml_parser.parse_pat_message(content, robot_wxid)
240
+
241
+ if pat_info and pat_info.is_pat_me:
242
+ logger.debug(f"解析拍一拍消息: {pat_info.from_user} 拍了拍我")
243
+ return ParsedMessage(
244
+ type="pat",
245
+ content="[用户拍了拍我]",
246
+ pat_info=pat_info,
247
+ should_process=True
248
+ )
249
+
250
+ logger.debug(f"忽略拍一拍消息: {pat_info}")
251
+ return ParsedMessage(
252
+ type="pat",
253
+ content=content,
254
+ pat_info=pat_info,
255
+ should_process=False
256
+ )
257
+
258
+ def _parse_system_message(self, content: str, robot_wxid: str) -> ParsedMessage:
259
+ """解析系统消息(入群、拍一拍等)"""
260
+ # 检查是否是入群消息
261
+ if "加入了群聊" in content or "加入群聊" in content:
262
+ logger.debug(f"解析入群消息: {content}")
263
+ return ParsedMessage(
264
+ type="system",
265
+ content=content,
266
+ is_join_group=True,
267
+ should_process=True
268
+ )
269
+
270
+ # 检查是否是文本格式的拍一拍消息(群聊)
271
+ if "拍了拍" in content:
272
+ pat_info = self.xml_parser.parse_pat_text(content, robot_wxid)
273
+ if pat_info and pat_info.is_pat_me:
274
+ logger.debug(f"解析群聊拍一拍消息: {content}")
275
+ return ParsedMessage(
276
+ type="pat",
277
+ content="[用户拍了拍我]",
278
+ pat_info=pat_info,
279
+ should_process=True
280
+ )
281
+
282
+ # 其他系统消息,不处理
283
+ logger.debug(f"忽略系统消息: {content}")
284
+ return ParsedMessage(
285
+ type="system",
286
+ content=content,
287
+ should_process=False
288
+ )
289
+
290
+ def check_at_bot(self, content: str, robot_wxid: str, at_wxid_list: Optional[list] = None) -> bool:
291
+ """检查消息中是否@了机器人
292
+
293
+ Args:
294
+ content: 消息内容
295
+ robot_wxid: 机器人wxid
296
+ at_wxid_list: 千寻框架提供的@列表
297
+
298
+ Returns:
299
+ 是否@了机器人
300
+ """
301
+ # 优先使用千寻框架提供的atWxidList字段(最准确)
302
+ if at_wxid_list:
303
+ if robot_wxid in at_wxid_list:
304
+ return True
305
+ # 如果 at_wxid_list 存在但不包含机器人,说明@的是其他人
306
+ # 不需要继续检查内容
307
+ return False
308
+
309
+ # 如果没有 at_wxid_list,检查消息内容中是否包含机器人wxid
310
+ # 千寻框架的@格式: [@,wxid=xxx,...]
311
+ if f'wxid={robot_wxid}' in content:
312
+ return True
313
+
314
+ return False
315
+
316
+ def check_at_all(self, at_wxid_list: Optional[list] = None) -> bool:
317
+ """检查是否@所有人
318
+
319
+ Args:
320
+ at_wxid_list: 千寻框架提供的@列表
321
+
322
+ Returns:
323
+ 是否@所有人
324
+ """
325
+ return at_wxid_list is not None and "notify@all" in at_wxid_list