AstrBot 3.5.6__py3-none-any.whl → 4.7.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.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,472 @@
1
+ """企业微信智能机器人平台适配器
2
+ 基于企业微信智能机器人 API 的消息平台适配器,支持 HTTP 回调
3
+ 参考webchat_adapter.py的队列机制,实现异步消息处理和流式响应
4
+ """
5
+
6
+ import asyncio
7
+ import base64
8
+ import hashlib
9
+ import time
10
+ import uuid
11
+ from collections.abc import Awaitable, Callable
12
+ from typing import Any
13
+
14
+ from astrbot.api import logger
15
+ from astrbot.api.event import MessageChain
16
+ from astrbot.api.message_components import At, Image, Plain
17
+ from astrbot.api.platform import (
18
+ AstrBotMessage,
19
+ MessageMember,
20
+ MessageType,
21
+ Platform,
22
+ PlatformMetadata,
23
+ )
24
+ from astrbot.core.platform.astr_message_event import MessageSesion
25
+
26
+ from ...register import register_platform_adapter
27
+ from .wecomai_api import (
28
+ WecomAIBotAPIClient,
29
+ WecomAIBotMessageParser,
30
+ WecomAIBotStreamMessageBuilder,
31
+ )
32
+ from .wecomai_event import WecomAIBotMessageEvent
33
+ from .wecomai_queue_mgr import WecomAIQueueMgr
34
+ from .wecomai_server import WecomAIBotServer
35
+ from .wecomai_utils import (
36
+ WecomAIBotConstants,
37
+ format_session_id,
38
+ generate_random_string,
39
+ process_encrypted_image,
40
+ )
41
+
42
+
43
+ class WecomAIQueueListener:
44
+ """企业微信智能机器人队列监听器,参考webchat的QueueListener设计"""
45
+
46
+ def __init__(
47
+ self,
48
+ queue_mgr: WecomAIQueueMgr,
49
+ callback: Callable[[dict], Awaitable[None]],
50
+ ) -> None:
51
+ self.queue_mgr = queue_mgr
52
+ self.callback = callback
53
+ self.running_tasks = set()
54
+
55
+ async def listen_to_queue(self, session_id: str):
56
+ """监听特定会话的队列"""
57
+ queue = self.queue_mgr.get_or_create_queue(session_id)
58
+ while True:
59
+ try:
60
+ data = await queue.get()
61
+ await self.callback(data)
62
+ except Exception as e:
63
+ logger.error(f"处理会话 {session_id} 消息时发生错误: {e}")
64
+ break
65
+
66
+ async def run(self):
67
+ """监控新会话队列并启动监听器"""
68
+ monitored_sessions = set()
69
+
70
+ while True:
71
+ # 检查新会话
72
+ current_sessions = set(self.queue_mgr.queues.keys())
73
+ new_sessions = current_sessions - monitored_sessions
74
+
75
+ # 为新会话启动监听器
76
+ for session_id in new_sessions:
77
+ task = asyncio.create_task(self.listen_to_queue(session_id))
78
+ self.running_tasks.add(task)
79
+ task.add_done_callback(self.running_tasks.discard)
80
+ monitored_sessions.add(session_id)
81
+ logger.debug(f"[WecomAI] 为会话启动监听器: {session_id}")
82
+
83
+ # 清理已不存在的会话
84
+ removed_sessions = monitored_sessions - current_sessions
85
+ monitored_sessions -= removed_sessions
86
+
87
+ # 清理过期的待处理响应
88
+ self.queue_mgr.cleanup_expired_responses()
89
+
90
+ await asyncio.sleep(1) # 每秒检查一次新会话
91
+
92
+
93
+ @register_platform_adapter(
94
+ "wecom_ai_bot",
95
+ "企业微信智能机器人适配器,支持 HTTP 回调接收消息",
96
+ )
97
+ class WecomAIBotAdapter(Platform):
98
+ """企业微信智能机器人适配器"""
99
+
100
+ def __init__(
101
+ self,
102
+ platform_config: dict,
103
+ platform_settings: dict,
104
+ event_queue: asyncio.Queue,
105
+ ) -> None:
106
+ super().__init__(event_queue)
107
+
108
+ self.config = platform_config
109
+ self.settings = platform_settings
110
+
111
+ # 初始化配置参数
112
+ self.token = self.config["token"]
113
+ self.encoding_aes_key = self.config["encoding_aes_key"]
114
+ self.port = int(self.config["port"])
115
+ self.host = self.config.get("callback_server_host", "0.0.0.0")
116
+ self.bot_name = self.config.get("wecom_ai_bot_name", "")
117
+ self.initial_respond_text = self.config.get(
118
+ "wecomaibot_init_respond_text",
119
+ "💭 思考中...",
120
+ )
121
+ self.friend_message_welcome_text = self.config.get(
122
+ "wecomaibot_friend_message_welcome_text",
123
+ "",
124
+ )
125
+
126
+ # 平台元数据
127
+ self.metadata = PlatformMetadata(
128
+ name="wecom_ai_bot",
129
+ description="企业微信智能机器人适配器,支持 HTTP 回调接收消息",
130
+ id=self.config.get("id", "wecom_ai_bot"),
131
+ )
132
+
133
+ # 初始化 API 客户端
134
+ self.api_client = WecomAIBotAPIClient(self.token, self.encoding_aes_key)
135
+
136
+ # 初始化 HTTP 服务器
137
+ self.server = WecomAIBotServer(
138
+ host=self.host,
139
+ port=self.port,
140
+ api_client=self.api_client,
141
+ message_handler=self._process_message,
142
+ )
143
+
144
+ # 事件循环和关闭信号
145
+ self.shutdown_event = asyncio.Event()
146
+
147
+ # 队列管理器
148
+ self.queue_mgr = WecomAIQueueMgr()
149
+
150
+ # 队列监听器
151
+ self.queue_listener = WecomAIQueueListener(
152
+ self.queue_mgr,
153
+ self._handle_queued_message,
154
+ )
155
+
156
+ async def _handle_queued_message(self, data: dict):
157
+ """处理队列中的消息,类似webchat的callback"""
158
+ try:
159
+ abm = await self.convert_message(data)
160
+ await self.handle_msg(abm)
161
+ except Exception as e:
162
+ logger.error(f"处理队列消息时发生异常: {e}")
163
+
164
+ async def _process_message(
165
+ self,
166
+ message_data: dict[str, Any],
167
+ callback_params: dict[str, str],
168
+ ) -> str | None:
169
+ """处理接收到的消息
170
+
171
+ Args:
172
+ message_data: 解密后的消息数据
173
+ callback_params: 回调参数 (nonce, timestamp)
174
+
175
+ Returns:
176
+ 加密后的响应消息,无需响应时返回 None
177
+
178
+ """
179
+ msgtype = message_data.get("msgtype")
180
+ if not msgtype:
181
+ logger.warning(f"消息类型未知,忽略: {message_data}")
182
+ return None
183
+ session_id = self._extract_session_id(message_data)
184
+ if msgtype in ("text", "image", "mixed"):
185
+ # user sent a text / image / mixed message
186
+ try:
187
+ # create a brand-new unique stream_id for this message session
188
+ stream_id = f"{session_id}_{generate_random_string(10)}"
189
+ await self._enqueue_message(
190
+ message_data,
191
+ callback_params,
192
+ stream_id,
193
+ session_id,
194
+ )
195
+ self.queue_mgr.set_pending_response(stream_id, callback_params)
196
+
197
+ resp = WecomAIBotStreamMessageBuilder.make_text_stream(
198
+ stream_id,
199
+ self.initial_respond_text,
200
+ False,
201
+ )
202
+ return await self.api_client.encrypt_message(
203
+ resp,
204
+ callback_params["nonce"],
205
+ callback_params["timestamp"],
206
+ )
207
+ except Exception as e:
208
+ logger.error("处理消息时发生异常: %s", e)
209
+ return None
210
+ elif msgtype == "stream":
211
+ # wechat server is requesting for updates of a stream
212
+ stream_id = message_data["stream"]["id"]
213
+ if not self.queue_mgr.has_back_queue(stream_id):
214
+ logger.error(f"Cannot find back queue for stream_id: {stream_id}")
215
+
216
+ # 返回结束标志,告诉微信服务器流已结束
217
+ end_message = WecomAIBotStreamMessageBuilder.make_text_stream(
218
+ stream_id,
219
+ "",
220
+ True,
221
+ )
222
+ resp = await self.api_client.encrypt_message(
223
+ end_message,
224
+ callback_params["nonce"],
225
+ callback_params["timestamp"],
226
+ )
227
+ return resp
228
+ queue = self.queue_mgr.get_or_create_back_queue(stream_id)
229
+ if queue.empty():
230
+ logger.debug(
231
+ f"No new messages in back queue for stream_id: {stream_id}",
232
+ )
233
+ return None
234
+
235
+ # aggregate all delta chains in the back queue
236
+ latest_plain_content = ""
237
+ image_base64 = []
238
+ finish = False
239
+ while not queue.empty():
240
+ msg = await queue.get()
241
+ if msg["type"] == "plain":
242
+ latest_plain_content = msg["data"] or ""
243
+ elif msg["type"] == "image":
244
+ image_base64.append(msg["image_data"])
245
+ elif msg["type"] == "end":
246
+ # stream end
247
+ finish = True
248
+ self.queue_mgr.remove_queues(stream_id)
249
+ break
250
+
251
+ logger.debug(
252
+ f"Aggregated content: {latest_plain_content}, image: {len(image_base64)}, finish: {finish}",
253
+ )
254
+ if latest_plain_content or image_base64:
255
+ msg_items = []
256
+ if finish and image_base64:
257
+ for img_b64 in image_base64:
258
+ # get md5 of image
259
+ img_data = base64.b64decode(img_b64)
260
+ img_md5 = hashlib.md5(img_data).hexdigest()
261
+ msg_items.append(
262
+ {
263
+ "msgtype": WecomAIBotConstants.MSG_TYPE_IMAGE,
264
+ "image": {"base64": img_b64, "md5": img_md5},
265
+ },
266
+ )
267
+ image_base64 = []
268
+
269
+ plain_message = WecomAIBotStreamMessageBuilder.make_mixed_stream(
270
+ stream_id,
271
+ latest_plain_content,
272
+ msg_items,
273
+ finish,
274
+ )
275
+ encrypted_message = await self.api_client.encrypt_message(
276
+ plain_message,
277
+ callback_params["nonce"],
278
+ callback_params["timestamp"],
279
+ )
280
+ if encrypted_message:
281
+ logger.debug(
282
+ f"Stream message sent successfully, stream_id: {stream_id}",
283
+ )
284
+ else:
285
+ logger.error("消息加密失败")
286
+ return encrypted_message
287
+ return None
288
+ elif msgtype == "event":
289
+ event = message_data.get("event")
290
+ if event == "enter_chat" and self.friend_message_welcome_text:
291
+ # 用户进入会话,发送欢迎消息
292
+ try:
293
+ resp = WecomAIBotStreamMessageBuilder.make_text(
294
+ self.friend_message_welcome_text,
295
+ )
296
+ return await self.api_client.encrypt_message(
297
+ resp,
298
+ callback_params["nonce"],
299
+ callback_params["timestamp"],
300
+ )
301
+ except Exception as e:
302
+ logger.error("处理欢迎消息时发生异常: %s", e)
303
+ return None
304
+
305
+ def _extract_session_id(self, message_data: dict[str, Any]) -> str:
306
+ """从消息数据中提取会话ID"""
307
+ user_id = message_data.get("from", {}).get("userid", "default_user")
308
+ return format_session_id("wecomai", user_id)
309
+
310
+ async def _enqueue_message(
311
+ self,
312
+ message_data: dict[str, Any],
313
+ callback_params: dict[str, str],
314
+ stream_id: str,
315
+ session_id: str,
316
+ ):
317
+ """将消息放入队列进行异步处理"""
318
+ input_queue = self.queue_mgr.get_or_create_queue(stream_id)
319
+ _ = self.queue_mgr.get_or_create_back_queue(stream_id)
320
+ message_payload = {
321
+ "message_data": message_data,
322
+ "callback_params": callback_params,
323
+ "session_id": session_id,
324
+ "stream_id": stream_id,
325
+ }
326
+ await input_queue.put(message_payload)
327
+ logger.debug(f"[WecomAI] 消息已入队: {stream_id}")
328
+
329
+ async def convert_message(self, payload: dict) -> AstrBotMessage:
330
+ """转换队列中的消息数据为AstrBotMessage,类似webchat的convert_message"""
331
+ message_data = payload["message_data"]
332
+ session_id = payload["session_id"]
333
+ # callback_params = payload["callback_params"] # 保留但暂时不使用
334
+
335
+ # 解析消息内容
336
+ msgtype = message_data.get("msgtype")
337
+ content = ""
338
+ image_base64 = []
339
+
340
+ _img_url_to_process = []
341
+ msg_items = []
342
+
343
+ if msgtype == WecomAIBotConstants.MSG_TYPE_TEXT:
344
+ content = WecomAIBotMessageParser.parse_text_message(message_data)
345
+ elif msgtype == WecomAIBotConstants.MSG_TYPE_IMAGE:
346
+ _img_url_to_process.append(
347
+ WecomAIBotMessageParser.parse_image_message(message_data),
348
+ )
349
+ elif msgtype == WecomAIBotConstants.MSG_TYPE_MIXED:
350
+ # 提取混合消息中的文本内容
351
+ msg_items = WecomAIBotMessageParser.parse_mixed_message(message_data)
352
+ text_parts = []
353
+ for item in msg_items or []:
354
+ if item.get("msgtype") == WecomAIBotConstants.MSG_TYPE_TEXT:
355
+ text_content = item.get("text", {}).get("content", "")
356
+ if text_content:
357
+ text_parts.append(text_content)
358
+ elif item.get("msgtype") == WecomAIBotConstants.MSG_TYPE_IMAGE:
359
+ image_url = item.get("image", {}).get("url", "")
360
+ if image_url:
361
+ _img_url_to_process.append(image_url)
362
+ content = " ".join(text_parts) if text_parts else ""
363
+ else:
364
+ content = f"[{msgtype}消息]"
365
+
366
+ # 并行处理图片下载和解密
367
+ if _img_url_to_process:
368
+ tasks = [
369
+ process_encrypted_image(url, self.encoding_aes_key)
370
+ for url in _img_url_to_process
371
+ ]
372
+ results = await asyncio.gather(*tasks)
373
+ for success, result in results:
374
+ if success:
375
+ image_base64.append(result)
376
+ else:
377
+ logger.error(f"处理加密图片失败: {result}")
378
+
379
+ # 构建 AstrBotMessage
380
+ abm = AstrBotMessage()
381
+ abm.self_id = self.bot_name
382
+ abm.message_str = content or "[未知消息]"
383
+ abm.message_id = str(uuid.uuid4())
384
+ abm.timestamp = int(time.time())
385
+ abm.raw_message = payload
386
+
387
+ # 发送者信息
388
+ abm.sender = MessageMember(
389
+ user_id=message_data.get("from", {}).get("userid", "unknown"),
390
+ nickname=message_data.get("from", {}).get("userid", "unknown"),
391
+ )
392
+
393
+ # 消息类型
394
+ abm.type = (
395
+ MessageType.GROUP_MESSAGE
396
+ if message_data.get("chattype") == "group"
397
+ else MessageType.FRIEND_MESSAGE
398
+ )
399
+ abm.session_id = session_id
400
+
401
+ # 消息内容
402
+ abm.message = []
403
+
404
+ # 处理 At
405
+ if self.bot_name and f"@{self.bot_name}" in abm.message_str:
406
+ abm.message_str = abm.message_str.replace(f"@{self.bot_name}", "").strip()
407
+ abm.message.append(At(qq=self.bot_name, name=self.bot_name))
408
+ abm.message.append(Plain(abm.message_str))
409
+ if image_base64:
410
+ for img_b64 in image_base64:
411
+ abm.message.append(Image.fromBase64(img_b64))
412
+
413
+ logger.debug(f"WecomAIAdapter: {abm.message}")
414
+ return abm
415
+
416
+ async def send_by_session(
417
+ self,
418
+ session: MessageSesion,
419
+ message_chain: MessageChain,
420
+ ):
421
+ """通过会话发送消息"""
422
+ # 企业微信智能机器人主要通过回调响应,这里记录日志
423
+ logger.info("会话发送消息: %s -> %s", session.session_id, message_chain)
424
+ await super().send_by_session(session, message_chain)
425
+
426
+ def run(self) -> Awaitable[Any]:
427
+ """运行适配器,同时启动HTTP服务器和队列监听器"""
428
+ logger.info("启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port)
429
+
430
+ async def run_both():
431
+ # 同时运行HTTP服务器和队列监听器
432
+ await asyncio.gather(
433
+ self.server.start_server(),
434
+ self.queue_listener.run(),
435
+ )
436
+
437
+ return run_both()
438
+
439
+ async def terminate(self):
440
+ """终止适配器"""
441
+ logger.info("企业微信智能机器人适配器正在关闭...")
442
+ self.shutdown_event.set()
443
+ await self.server.shutdown()
444
+
445
+ def meta(self) -> PlatformMetadata:
446
+ """获取平台元数据"""
447
+ return self.metadata
448
+
449
+ async def handle_msg(self, message: AstrBotMessage):
450
+ """处理消息,创建消息事件并提交到事件队列"""
451
+ try:
452
+ message_event = WecomAIBotMessageEvent(
453
+ message_str=message.message_str,
454
+ message_obj=message,
455
+ platform_meta=self.meta(),
456
+ session_id=message.session_id,
457
+ api_client=self.api_client,
458
+ queue_mgr=self.queue_mgr,
459
+ )
460
+
461
+ self.commit_event(message_event)
462
+
463
+ except Exception as e:
464
+ logger.error("处理消息时发生异常: %s", e)
465
+
466
+ def get_client(self) -> WecomAIBotAPIClient:
467
+ """获取 API 客户端"""
468
+ return self.api_client
469
+
470
+ def get_server(self) -> WecomAIBotServer:
471
+ """获取 HTTP 服务器实例"""
472
+ return self.server