ForcomeBot 2.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forcomebot-2.2.4.dist-info/METADATA +342 -0
- forcomebot-2.2.4.dist-info/RECORD +36 -0
- forcomebot-2.2.4.dist-info/WHEEL +4 -0
- forcomebot-2.2.4.dist-info/entry_points.txt +4 -0
- src/__init__.py +68 -0
- src/__main__.py +487 -0
- src/api/__init__.py +21 -0
- src/api/routes.py +775 -0
- src/api/websocket.py +280 -0
- src/auth/__init__.py +33 -0
- src/auth/database.py +87 -0
- src/auth/dingtalk.py +373 -0
- src/auth/jwt_handler.py +129 -0
- src/auth/middleware.py +260 -0
- src/auth/models.py +107 -0
- src/auth/routes.py +385 -0
- src/clients/__init__.py +7 -0
- src/clients/langbot.py +710 -0
- src/clients/qianxun.py +388 -0
- src/core/__init__.py +19 -0
- src/core/config_manager.py +411 -0
- src/core/log_collector.py +167 -0
- src/core/message_queue.py +364 -0
- src/core/state_store.py +242 -0
- src/handlers/__init__.py +8 -0
- src/handlers/message_handler.py +833 -0
- src/handlers/message_parser.py +325 -0
- src/handlers/scheduler.py +822 -0
- src/models.py +77 -0
- src/static/assets/index-B4i68B5_.js +50 -0
- src/static/assets/index-BPXisDkw.css +2 -0
- src/static/index.html +14 -0
- src/static/vite.svg +1 -0
- src/utils/__init__.py +13 -0
- src/utils/text_processor.py +166 -0
- src/utils/xml_parser.py +215 -0
|
@@ -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
|