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
src/clients/qianxun.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""千寻框架API封装 - 带重试机制"""
|
|
2
|
+
import asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
import logging
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
|
|
10
|
+
from ..utils.text_processor import TextProcessor
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QianXunClient:
|
|
16
|
+
"""千寻框架HTTP API客户端(带重试)"""
|
|
17
|
+
|
|
18
|
+
# API成功状态码
|
|
19
|
+
SUCCESS_CODES = (0, 200)
|
|
20
|
+
|
|
21
|
+
def __init__(self, api_url: str, max_retries: int = 3, retry_delay: float = 1.0):
|
|
22
|
+
"""初始化千寻客户端
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_url: 千寻API地址,如 http://192.168.17.181:7777/qianxun/httpapi
|
|
26
|
+
max_retries: 最大重试次数,默认3次
|
|
27
|
+
retry_delay: 初始重试延迟(秒),默认1秒,每次递增
|
|
28
|
+
"""
|
|
29
|
+
self.api_url = api_url.rstrip('/')
|
|
30
|
+
self.base_url = '/'.join(api_url.split('/')[:3])
|
|
31
|
+
self.client = httpx.AsyncClient(timeout=30.0)
|
|
32
|
+
self.max_retries = max_retries
|
|
33
|
+
self.retry_delay = retry_delay
|
|
34
|
+
self.text_processor = TextProcessor()
|
|
35
|
+
|
|
36
|
+
def update_api_url(self, new_url: str):
|
|
37
|
+
"""更新API地址(配置热更新)"""
|
|
38
|
+
self.api_url = new_url.rstrip('/')
|
|
39
|
+
self.base_url = '/'.join(new_url.split('/')[:3])
|
|
40
|
+
logger.info(f"千寻API地址已更新: {self.api_url}")
|
|
41
|
+
|
|
42
|
+
def _parse_response(self, resp: httpx.Response) -> Dict[str, Any]:
|
|
43
|
+
"""统一解析API响应JSON
|
|
44
|
+
|
|
45
|
+
使用 strict=False 容忍非法控制字符(群名/昵称可能包含特殊字符)
|
|
46
|
+
"""
|
|
47
|
+
return json.loads(resp.text, strict=False)
|
|
48
|
+
|
|
49
|
+
def _is_success(self, result: Dict[str, Any]) -> bool:
|
|
50
|
+
"""检查API响应是否成功"""
|
|
51
|
+
return result.get("code") in self.SUCCESS_CODES
|
|
52
|
+
|
|
53
|
+
def _get_result_data(self, result: Dict[str, Any]) -> Any:
|
|
54
|
+
"""从响应中提取数据"""
|
|
55
|
+
return result.get("result") or result.get("data")
|
|
56
|
+
|
|
57
|
+
# 需要重试的HTTP状态码
|
|
58
|
+
RETRY_STATUS_CODES = (502, 503, 504, 429)
|
|
59
|
+
|
|
60
|
+
async def _request_with_retry(self, method: str, url: str, **kwargs) -> httpx.Response:
|
|
61
|
+
"""带重试的HTTP请求(支持502等错误重试)
|
|
62
|
+
|
|
63
|
+
重试策略:
|
|
64
|
+
- 网络异常:重试
|
|
65
|
+
- 502/503/504/429状态码:重试
|
|
66
|
+
- 使用递增延迟(1秒、2秒、3秒)
|
|
67
|
+
"""
|
|
68
|
+
last_error: Exception = Exception("请求失败")
|
|
69
|
+
for attempt in range(self.max_retries):
|
|
70
|
+
try:
|
|
71
|
+
if method.upper() == 'GET':
|
|
72
|
+
resp = await self.client.get(url, **kwargs)
|
|
73
|
+
else:
|
|
74
|
+
resp = await self.client.post(url, **kwargs)
|
|
75
|
+
|
|
76
|
+
# 检查是否需要重试的状态码
|
|
77
|
+
if resp.status_code in self.RETRY_STATUS_CODES:
|
|
78
|
+
if attempt < self.max_retries - 1:
|
|
79
|
+
delay = self.retry_delay * (attempt + 1)
|
|
80
|
+
logger.warning(
|
|
81
|
+
f"收到HTTP {resp.status_code} (尝试 {attempt + 1}/{self.max_retries}),"
|
|
82
|
+
f"{delay}秒后重试"
|
|
83
|
+
)
|
|
84
|
+
await asyncio.sleep(delay)
|
|
85
|
+
continue
|
|
86
|
+
else:
|
|
87
|
+
logger.error(
|
|
88
|
+
f"收到HTTP {resp.status_code},已达最大重试次数 ({self.max_retries})"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return resp
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
last_error = e
|
|
95
|
+
if attempt < self.max_retries - 1:
|
|
96
|
+
delay = self.retry_delay * (attempt + 1)
|
|
97
|
+
logger.warning(f"请求异常 (尝试 {attempt + 1}/{self.max_retries}),{delay}秒后重试: {e}")
|
|
98
|
+
await asyncio.sleep(delay)
|
|
99
|
+
else:
|
|
100
|
+
logger.error(f"请求异常,已达最大重试次数 ({self.max_retries}): {e}")
|
|
101
|
+
raise last_error
|
|
102
|
+
|
|
103
|
+
async def _send_message(
|
|
104
|
+
self,
|
|
105
|
+
robot_wxid: str,
|
|
106
|
+
to_wxid: str,
|
|
107
|
+
msg_type: str,
|
|
108
|
+
data: Dict[str, Any],
|
|
109
|
+
operation_name: str
|
|
110
|
+
) -> bool:
|
|
111
|
+
"""通用消息发送方法
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
robot_wxid: 机器人wxid
|
|
115
|
+
to_wxid: 接收者wxid
|
|
116
|
+
msg_type: 消息类型(如 sendText, sendImage)
|
|
117
|
+
data: 消息数据
|
|
118
|
+
operation_name: 操作名称(用于日志)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
是否发送成功
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
payload = {"type": msg_type, "data": {"wxid": to_wxid, **data}}
|
|
125
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
126
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
127
|
+
result = self._parse_response(resp)
|
|
128
|
+
|
|
129
|
+
if self._is_success(result):
|
|
130
|
+
logger.info(f"{operation_name}成功: {to_wxid}")
|
|
131
|
+
return True
|
|
132
|
+
else:
|
|
133
|
+
logger.error(f"{operation_name}失败: {result}")
|
|
134
|
+
return False
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"{operation_name}异常: {e}", exc_info=True)
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def _extract_base64_data(self, data_url: str) -> str:
|
|
140
|
+
"""从 Data URL 中提取 base64 数据并添加千寻框架需要的前缀"""
|
|
141
|
+
if not data_url.startswith('data:'):
|
|
142
|
+
return data_url
|
|
143
|
+
if ';base64,' in data_url:
|
|
144
|
+
return f"base64,{data_url.split(';base64,')[1]}"
|
|
145
|
+
if ',' in data_url:
|
|
146
|
+
return f"base64,{data_url.split(',')[1]}"
|
|
147
|
+
return data_url
|
|
148
|
+
|
|
149
|
+
def _sanitize_filename(self, file_name: str) -> str:
|
|
150
|
+
"""清理文件名中的特殊字符"""
|
|
151
|
+
if not file_name:
|
|
152
|
+
return file_name
|
|
153
|
+
original_name = file_name
|
|
154
|
+
# 替换 Windows 非法字符和空格
|
|
155
|
+
for char in ['\\', '/', ':', '*', '?', '"', '<', '>', '|', ' ']:
|
|
156
|
+
file_name = file_name.replace(char, '_')
|
|
157
|
+
# 只保留字母、数字、中文、下划线、点、横线
|
|
158
|
+
file_name = re.sub(r'[^\w\u4e00-\u9fff.\-]', '_', file_name)
|
|
159
|
+
if original_name != file_name:
|
|
160
|
+
logger.info(f"文件名已处理: {repr(original_name)} -> {repr(file_name)}")
|
|
161
|
+
return file_name
|
|
162
|
+
|
|
163
|
+
async def send_text(self, robot_wxid: str, to_wxid: str, msg: str) -> bool:
|
|
164
|
+
"""发送文本消息"""
|
|
165
|
+
msg = self.text_processor.encode_for_qianxun(msg)
|
|
166
|
+
return await self._send_message(robot_wxid, to_wxid, "sendText", {"msg": msg}, "发送消息")
|
|
167
|
+
|
|
168
|
+
async def send_image(self, robot_wxid: str, to_wxid: str, image_path: str, file_name: str = "") -> bool:
|
|
169
|
+
"""发送图片消息"""
|
|
170
|
+
image_path = self._extract_base64_data(image_path)
|
|
171
|
+
return await self._send_message(
|
|
172
|
+
robot_wxid, to_wxid, "sendImage",
|
|
173
|
+
{"path": image_path, "fileName": file_name},
|
|
174
|
+
"发送图片"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def send_file(self, robot_wxid: str, to_wxid: str, file_path: str, file_name: str = "") -> bool:
|
|
178
|
+
"""发送文件"""
|
|
179
|
+
file_path = self._extract_base64_data(file_path)
|
|
180
|
+
file_name = self._sanitize_filename(file_name)
|
|
181
|
+
logger.info(f"发送文件: to={to_wxid}, fileName={repr(file_name)}")
|
|
182
|
+
return await self._send_message(
|
|
183
|
+
robot_wxid, to_wxid, "sendFile",
|
|
184
|
+
{"path": file_path, "fileName": file_name},
|
|
185
|
+
"发送文件"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
async def send_share_url(
|
|
189
|
+
self, robot_wxid: str, to_wxid: str, title: str, content: str,
|
|
190
|
+
jump_url: str, thumb_path: str = "", app: str = ""
|
|
191
|
+
) -> bool:
|
|
192
|
+
"""发送分享链接"""
|
|
193
|
+
return await self._send_message(
|
|
194
|
+
robot_wxid, to_wxid, "sendShareUrl",
|
|
195
|
+
{"title": title, "content": content, "jumpUrl": jump_url, "path": thumb_path, "app": app},
|
|
196
|
+
"发送分享链接"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
async def send_applet(
|
|
200
|
+
self, robot_wxid: str, to_wxid: str, title: str, content: str,
|
|
201
|
+
jump_path: str, gh: str, thumb_path: str = ""
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""发送小程序"""
|
|
204
|
+
return await self._send_message(
|
|
205
|
+
robot_wxid, to_wxid, "sendApplet",
|
|
206
|
+
{"title": title, "content": content, "jumpPath": jump_path, "gh": gh, "path": thumb_path},
|
|
207
|
+
"发送小程序"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def get_contact_info(self, robot_wxid: str, wxid: str) -> Optional[dict]:
|
|
211
|
+
"""获取联系人信息"""
|
|
212
|
+
try:
|
|
213
|
+
payload = {"type": "getContactInfo", "data": {"wxid": wxid}}
|
|
214
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
215
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
216
|
+
result = self._parse_response(resp)
|
|
217
|
+
return self._get_result_data(result) if self._is_success(result) else None
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(f"获取联系人信息异常: {e}", exc_info=True)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
async def get_self_info(self, robot_wxid: str) -> Optional[dict]:
|
|
223
|
+
"""获取机器人自身信息"""
|
|
224
|
+
try:
|
|
225
|
+
payload = {"type": "getSelfInfo", "data": {"type": "2"}}
|
|
226
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
227
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
228
|
+
result = self._parse_response(resp)
|
|
229
|
+
logger.info(f"获取机器人信息响应: {result}")
|
|
230
|
+
|
|
231
|
+
if self._is_success(result):
|
|
232
|
+
data = self._get_result_data(result)
|
|
233
|
+
if data:
|
|
234
|
+
for key in ("nick", "nickname"):
|
|
235
|
+
if data.get(key):
|
|
236
|
+
data[key] = self.text_processor.decode_emoji(data[key])
|
|
237
|
+
return data
|
|
238
|
+
return None
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"获取机器人信息异常: {e}", exc_info=True)
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
async def close(self):
|
|
244
|
+
"""关闭客户端"""
|
|
245
|
+
await self.client.aclose()
|
|
246
|
+
|
|
247
|
+
async def get_contact_list(self, robot_wxid: str) -> list:
|
|
248
|
+
"""获取联系人列表(包括好友和群)"""
|
|
249
|
+
try:
|
|
250
|
+
payload = {"type": "getContactList", "data": {}}
|
|
251
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
252
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
253
|
+
result = self._parse_response(resp)
|
|
254
|
+
logger.debug(f"获取联系人列表响应: {result}")
|
|
255
|
+
|
|
256
|
+
if self._is_success(result):
|
|
257
|
+
contacts = self._get_result_data(result) or []
|
|
258
|
+
logger.info(f"获取联系人列表: 共 {len(contacts)} 个")
|
|
259
|
+
return contacts
|
|
260
|
+
logger.error(f"获取联系人列表失败: {result}")
|
|
261
|
+
return []
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"获取联系人列表异常: {e}", exc_info=True)
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
async def get_chatroom_list(self, robot_wxid: str, refresh: bool = True) -> list:
|
|
267
|
+
"""获取群聊列表"""
|
|
268
|
+
try:
|
|
269
|
+
payload = {"type": "getGroupList", "data": {"type": "2" if refresh else "1"}}
|
|
270
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
271
|
+
logger.info(f"请求群聊列表: {url}")
|
|
272
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
273
|
+
result = self._parse_response(resp)
|
|
274
|
+
logger.debug(f"获取群聊列表响应: {result}")
|
|
275
|
+
|
|
276
|
+
if self._is_success(result):
|
|
277
|
+
chatrooms = result.get("result", [])
|
|
278
|
+
formatted = [{
|
|
279
|
+
"wxid": c.get("wxid", ""),
|
|
280
|
+
"nickname": self.text_processor.decode_emoji(c.get("nick", "") or c.get("wxid", "")),
|
|
281
|
+
"memberCount": c.get("groupMemberNum", 0)
|
|
282
|
+
} for c in chatrooms]
|
|
283
|
+
logger.info(f"获取群聊列表: 共 {len(formatted)} 个群")
|
|
284
|
+
return formatted
|
|
285
|
+
logger.error(f"获取群聊列表失败: {result}")
|
|
286
|
+
return []
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error(f"获取群聊列表异常: {e}", exc_info=True)
|
|
289
|
+
return []
|
|
290
|
+
|
|
291
|
+
async def get_friend_list(self, robot_wxid: str, refresh: bool = True) -> list:
|
|
292
|
+
"""获取好友列表"""
|
|
293
|
+
try:
|
|
294
|
+
payload = {"type": "getFriendList", "data": {"type": "2" if refresh else "1"}}
|
|
295
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
296
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
297
|
+
result = self._parse_response(resp)
|
|
298
|
+
logger.debug(f"获取好友列表响应: {result}")
|
|
299
|
+
|
|
300
|
+
if self._is_success(result):
|
|
301
|
+
friends = result.get("result", [])
|
|
302
|
+
formatted = [{
|
|
303
|
+
"wxid": f.get("wxid", ""),
|
|
304
|
+
"nickname": self.text_processor.decode_emoji(
|
|
305
|
+
f.get("remark", "") or f.get("nick", "") or f.get("wxid", "")
|
|
306
|
+
)
|
|
307
|
+
} for f in friends]
|
|
308
|
+
logger.info(f"获取好友列表: 共 {len(formatted)} 个好友")
|
|
309
|
+
return formatted
|
|
310
|
+
logger.error(f"获取好友列表失败: {result}")
|
|
311
|
+
return []
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"获取好友列表异常: {e}", exc_info=True)
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
async def get_group_member_list(
|
|
317
|
+
self, robot_wxid: str, group_wxid: str, get_nick: bool = True, refresh: bool = False
|
|
318
|
+
) -> list:
|
|
319
|
+
"""获取群成员列表"""
|
|
320
|
+
try:
|
|
321
|
+
payload = {
|
|
322
|
+
"type": "getMemberList",
|
|
323
|
+
"data": {
|
|
324
|
+
"wxid": group_wxid,
|
|
325
|
+
"type": "2" if refresh else "1",
|
|
326
|
+
"getNick": "2" if get_nick else "1"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
url = f"{self.api_url}?wxid={robot_wxid}"
|
|
330
|
+
resp = await self._request_with_retry('POST', url, json=payload)
|
|
331
|
+
result = self._parse_response(resp)
|
|
332
|
+
logger.debug(f"获取群成员列表响应: {result}")
|
|
333
|
+
|
|
334
|
+
if self._is_success(result):
|
|
335
|
+
members = self._get_result_data(result) or []
|
|
336
|
+
for m in members:
|
|
337
|
+
for key in ("groupNick", "nickname"):
|
|
338
|
+
if m.get(key):
|
|
339
|
+
m[key] = self.text_processor.decode_emoji(m[key])
|
|
340
|
+
logger.info(f"获取群成员列表: {group_wxid}, 共 {len(members)} 人")
|
|
341
|
+
return members
|
|
342
|
+
logger.error(f"获取群成员列表失败: {result}")
|
|
343
|
+
return []
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.error(f"获取群成员列表异常: {e}", exc_info=True)
|
|
346
|
+
return []
|
|
347
|
+
|
|
348
|
+
async def download_image(self, image_path: str) -> Optional[str]:
|
|
349
|
+
"""下载图片并返回 base64 编码"""
|
|
350
|
+
try:
|
|
351
|
+
url = f"{self.base_url}/qianxun/httpapi/file"
|
|
352
|
+
params = {"path": image_path}
|
|
353
|
+
logger.info(f"下载图片: {image_path}")
|
|
354
|
+
resp = await self._request_with_retry('GET', url, params=params)
|
|
355
|
+
|
|
356
|
+
if resp.status_code == 200:
|
|
357
|
+
image_data = resp.content
|
|
358
|
+
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
|
359
|
+
logger.info(f"图片下载成功,大小: {len(image_data)} bytes")
|
|
360
|
+
return image_base64
|
|
361
|
+
logger.error(f"下载图片失败: HTTP {resp.status_code}")
|
|
362
|
+
return None
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"下载图片异常: {e}", exc_info=True)
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
def parse_image_path(self, msg: str) -> Optional[str]:
|
|
368
|
+
"""从消息中解析图片路径"""
|
|
369
|
+
match = re.search(r'\[pic=([^,\]]+)', msg)
|
|
370
|
+
return match.group(1) if match else None
|
|
371
|
+
|
|
372
|
+
def get_image_url(self, image_path: str) -> str:
|
|
373
|
+
"""生成千寻图片下载URL"""
|
|
374
|
+
from urllib.parse import urlencode
|
|
375
|
+
return f"{self.base_url}/qianxun/httpapi/file?{urlencode({'path': image_path})}"
|
|
376
|
+
|
|
377
|
+
def parse_voice_info(self, xml_content: str) -> Optional[dict]:
|
|
378
|
+
"""从XML消息中解析语音信息"""
|
|
379
|
+
try:
|
|
380
|
+
import xml.etree.ElementTree as ET
|
|
381
|
+
root = ET.fromstring(xml_content)
|
|
382
|
+
voicemsg = root.find('voicemsg')
|
|
383
|
+
if voicemsg is not None:
|
|
384
|
+
return {'voicelength': int(voicemsg.get('voicelength', 0))}
|
|
385
|
+
return None
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.error(f"解析语音XML失败: {e}", exc_info=True)
|
|
388
|
+
return None
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Core services layer
|
|
2
|
+
"""Core services for the middleware including config management, state storage, and logging."""
|
|
3
|
+
|
|
4
|
+
from .config_manager import ConfigManager
|
|
5
|
+
from .state_store import StateStore
|
|
6
|
+
from .log_collector import LogCollector, log_private_message, log_group_message, log_error, log_system
|
|
7
|
+
from .message_queue import MessageQueue, MessagePriority
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'ConfigManager',
|
|
11
|
+
'StateStore',
|
|
12
|
+
'LogCollector',
|
|
13
|
+
'MessageQueue',
|
|
14
|
+
'MessagePriority',
|
|
15
|
+
'log_private_message',
|
|
16
|
+
'log_group_message',
|
|
17
|
+
'log_error',
|
|
18
|
+
'log_system',
|
|
19
|
+
]
|