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,153 @@
1
+ """企业微信智能机器人队列管理器
2
+ 参考 webchat_queue_mgr.py,为企业微信智能机器人实现队列机制
3
+ 支持异步消息处理和流式响应
4
+ """
5
+
6
+ import asyncio
7
+ from typing import Any
8
+
9
+ from astrbot.api import logger
10
+
11
+
12
+ class WecomAIQueueMgr:
13
+ """企业微信智能机器人队列管理器"""
14
+
15
+ def __init__(self) -> None:
16
+ self.queues: dict[str, asyncio.Queue] = {}
17
+ """StreamID 到输入队列的映射 - 用于接收用户消息"""
18
+
19
+ self.back_queues: dict[str, asyncio.Queue] = {}
20
+ """StreamID 到输出队列的映射 - 用于发送机器人响应"""
21
+
22
+ self.pending_responses: dict[str, dict[str, Any]] = {}
23
+ """待处理的响应缓存,用于流式响应"""
24
+
25
+ def get_or_create_queue(self, session_id: str) -> asyncio.Queue:
26
+ """获取或创建指定会话的输入队列
27
+
28
+ Args:
29
+ session_id: 会话ID
30
+
31
+ Returns:
32
+ 输入队列实例
33
+
34
+ """
35
+ if session_id not in self.queues:
36
+ self.queues[session_id] = asyncio.Queue()
37
+ logger.debug(f"[WecomAI] 创建输入队列: {session_id}")
38
+ return self.queues[session_id]
39
+
40
+ def get_or_create_back_queue(self, session_id: str) -> asyncio.Queue:
41
+ """获取或创建指定会话的输出队列
42
+
43
+ Args:
44
+ session_id: 会话ID
45
+
46
+ Returns:
47
+ 输出队列实例
48
+
49
+ """
50
+ if session_id not in self.back_queues:
51
+ self.back_queues[session_id] = asyncio.Queue()
52
+ logger.debug(f"[WecomAI] 创建输出队列: {session_id}")
53
+ return self.back_queues[session_id]
54
+
55
+ def remove_queues(self, session_id: str):
56
+ """移除指定会话的所有队列
57
+
58
+ Args:
59
+ session_id: 会话ID
60
+
61
+ """
62
+ if session_id in self.queues:
63
+ del self.queues[session_id]
64
+ logger.debug(f"[WecomAI] 移除输入队列: {session_id}")
65
+
66
+ if session_id in self.back_queues:
67
+ del self.back_queues[session_id]
68
+ logger.debug(f"[WecomAI] 移除输出队列: {session_id}")
69
+
70
+ if session_id in self.pending_responses:
71
+ del self.pending_responses[session_id]
72
+ logger.debug(f"[WecomAI] 移除待处理响应: {session_id}")
73
+
74
+ def has_queue(self, session_id: str) -> bool:
75
+ """检查是否存在指定会话的队列
76
+
77
+ Args:
78
+ session_id: 会话ID
79
+
80
+ Returns:
81
+ 是否存在队列
82
+
83
+ """
84
+ return session_id in self.queues
85
+
86
+ def has_back_queue(self, session_id: str) -> bool:
87
+ """检查是否存在指定会话的输出队列
88
+
89
+ Args:
90
+ session_id: 会话ID
91
+
92
+ Returns:
93
+ 是否存在输出队列
94
+
95
+ """
96
+ return session_id in self.back_queues
97
+
98
+ def set_pending_response(self, session_id: str, callback_params: dict[str, str]):
99
+ """设置待处理的响应参数
100
+
101
+ Args:
102
+ session_id: 会话ID
103
+ callback_params: 回调参数(nonce, timestamp等)
104
+
105
+ """
106
+ self.pending_responses[session_id] = {
107
+ "callback_params": callback_params,
108
+ "timestamp": asyncio.get_event_loop().time(),
109
+ }
110
+ logger.debug(f"[WecomAI] 设置待处理响应: {session_id}")
111
+
112
+ def get_pending_response(self, session_id: str) -> dict[str, Any] | None:
113
+ """获取待处理的响应参数
114
+
115
+ Args:
116
+ session_id: 会话ID
117
+
118
+ Returns:
119
+ 响应参数,如果不存在则返回None
120
+
121
+ """
122
+ return self.pending_responses.get(session_id)
123
+
124
+ def cleanup_expired_responses(self, max_age_seconds: int = 300):
125
+ """清理过期的待处理响应
126
+
127
+ Args:
128
+ max_age_seconds: 最大存活时间(秒)
129
+
130
+ """
131
+ current_time = asyncio.get_event_loop().time()
132
+ expired_sessions = []
133
+
134
+ for session_id, response_data in self.pending_responses.items():
135
+ if current_time - response_data["timestamp"] > max_age_seconds:
136
+ expired_sessions.append(session_id)
137
+
138
+ for session_id in expired_sessions:
139
+ del self.pending_responses[session_id]
140
+ logger.debug(f"[WecomAI] 清理过期响应: {session_id}")
141
+
142
+ def get_stats(self) -> dict[str, int]:
143
+ """获取队列统计信息
144
+
145
+ Returns:
146
+ 统计信息字典
147
+
148
+ """
149
+ return {
150
+ "input_queues": len(self.queues),
151
+ "output_queues": len(self.back_queues),
152
+ "pending_responses": len(self.pending_responses),
153
+ }
@@ -0,0 +1,168 @@
1
+ """企业微信智能机器人 HTTP 服务器
2
+ 处理企业微信智能机器人的 HTTP 回调请求
3
+ """
4
+
5
+ import asyncio
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ import quart
10
+
11
+ from astrbot.api import logger
12
+
13
+ from .wecomai_api import WecomAIBotAPIClient
14
+ from .wecomai_utils import WecomAIBotConstants
15
+
16
+
17
+ class WecomAIBotServer:
18
+ """企业微信智能机器人 HTTP 服务器"""
19
+
20
+ def __init__(
21
+ self,
22
+ host: str,
23
+ port: int,
24
+ api_client: WecomAIBotAPIClient,
25
+ message_handler: Callable[[dict[str, Any], dict[str, str]], Any] | None = None,
26
+ ):
27
+ """初始化服务器
28
+
29
+ Args:
30
+ host: 监听地址
31
+ port: 监听端口
32
+ api_client: API客户端实例
33
+ message_handler: 消息处理回调函数
34
+
35
+ """
36
+ self.host = host
37
+ self.port = port
38
+ self.api_client = api_client
39
+ self.message_handler = message_handler
40
+
41
+ self.app = quart.Quart(__name__)
42
+ self._setup_routes()
43
+
44
+ self.shutdown_event = asyncio.Event()
45
+
46
+ def _setup_routes(self):
47
+ """设置 Quart 路由"""
48
+ # 使用 Quart 的 add_url_rule 方法添加路由
49
+ self.app.add_url_rule(
50
+ "/webhook/wecom-ai-bot",
51
+ view_func=self.verify_url,
52
+ methods=["GET"],
53
+ )
54
+
55
+ self.app.add_url_rule(
56
+ "/webhook/wecom-ai-bot",
57
+ view_func=self.handle_message,
58
+ methods=["POST"],
59
+ )
60
+
61
+ async def verify_url(self):
62
+ """验证回调 URL"""
63
+ args = quart.request.args
64
+ msg_signature = args.get("msg_signature")
65
+ timestamp = args.get("timestamp")
66
+ nonce = args.get("nonce")
67
+ echostr = args.get("echostr")
68
+
69
+ if not all([msg_signature, timestamp, nonce, echostr]):
70
+ logger.error("URL 验证参数缺失")
71
+ return "verify fail", 400
72
+
73
+ # 类型检查确保不为 None
74
+ assert msg_signature is not None
75
+ assert timestamp is not None
76
+ assert nonce is not None
77
+ assert echostr is not None
78
+
79
+ logger.info("收到企业微信智能机器人 WebHook URL 验证请求。")
80
+ result = self.api_client.verify_url(msg_signature, timestamp, nonce, echostr)
81
+ return result, 200, {"Content-Type": "text/plain"}
82
+
83
+ async def handle_message(self):
84
+ """处理消息回调"""
85
+ args = quart.request.args
86
+ msg_signature = args.get("msg_signature")
87
+ timestamp = args.get("timestamp")
88
+ nonce = args.get("nonce")
89
+
90
+ if not all([msg_signature, timestamp, nonce]):
91
+ logger.error("消息回调参数缺失")
92
+ return "缺少必要参数", 400
93
+
94
+ # 类型检查确保不为 None
95
+ assert msg_signature is not None
96
+ assert timestamp is not None
97
+ assert nonce is not None
98
+
99
+ logger.debug(
100
+ f"收到消息回调,msg_signature={msg_signature}, timestamp={timestamp}, nonce={nonce}",
101
+ )
102
+
103
+ try:
104
+ # 获取请求体
105
+ post_data = await quart.request.get_data()
106
+
107
+ # 确保 post_data 是 bytes 类型
108
+ if isinstance(post_data, str):
109
+ post_data = post_data.encode("utf-8")
110
+
111
+ # 解密消息
112
+ ret_code, message_data = await self.api_client.decrypt_message(
113
+ post_data,
114
+ msg_signature,
115
+ timestamp,
116
+ nonce,
117
+ )
118
+
119
+ if ret_code != WecomAIBotConstants.SUCCESS or not message_data:
120
+ logger.error("消息解密失败,错误码: %d", ret_code)
121
+ return "消息解密失败", 400
122
+
123
+ # 调用消息处理器
124
+ response = None
125
+ if self.message_handler:
126
+ try:
127
+ response = await self.message_handler(
128
+ message_data,
129
+ {"nonce": nonce, "timestamp": timestamp},
130
+ )
131
+ except Exception as e:
132
+ logger.error("消息处理器执行异常: %s", e)
133
+ return "消息处理异常", 500
134
+
135
+ if response:
136
+ return response, 200, {"Content-Type": "text/plain"}
137
+ return "success", 200, {"Content-Type": "text/plain"}
138
+
139
+ except Exception as e:
140
+ logger.error("处理消息时发生异常: %s", e)
141
+ return "内部服务器错误", 500
142
+
143
+ async def start_server(self):
144
+ """启动服务器"""
145
+ logger.info("启动企业微信智能机器人服务器,监听 %s:%d", self.host, self.port)
146
+
147
+ try:
148
+ await self.app.run_task(
149
+ host=self.host,
150
+ port=self.port,
151
+ shutdown_trigger=self.shutdown_trigger,
152
+ )
153
+ except Exception as e:
154
+ logger.error("服务器运行异常: %s", e)
155
+ raise
156
+
157
+ async def shutdown_trigger(self):
158
+ """关闭触发器"""
159
+ await self.shutdown_event.wait()
160
+
161
+ async def shutdown(self):
162
+ """关闭服务器"""
163
+ logger.info("企业微信智能机器人服务器正在关闭...")
164
+ self.shutdown_event.set()
165
+
166
+ def get_app(self):
167
+ """获取 Quart 应用实例"""
168
+ return self.app
@@ -0,0 +1,209 @@
1
+ """企业微信智能机器人工具模块
2
+ 提供常量定义、工具函数和辅助方法
3
+ """
4
+
5
+ import asyncio
6
+ import base64
7
+ import hashlib
8
+ import secrets
9
+ import string
10
+ from typing import Any
11
+
12
+ import aiohttp
13
+ from Crypto.Cipher import AES
14
+
15
+ from astrbot.api import logger
16
+
17
+
18
+ # 常量定义
19
+ class WecomAIBotConstants:
20
+ """企业微信智能机器人常量"""
21
+
22
+ # 消息类型
23
+ MSG_TYPE_TEXT = "text"
24
+ MSG_TYPE_IMAGE = "image"
25
+ MSG_TYPE_MIXED = "mixed"
26
+ MSG_TYPE_STREAM = "stream"
27
+ MSG_TYPE_EVENT = "event"
28
+
29
+ # 流消息状态
30
+ STREAM_CONTINUE = False
31
+ STREAM_FINISH = True
32
+
33
+ # 错误码
34
+ SUCCESS = 0
35
+ DECRYPT_ERROR = -40001
36
+ VALIDATE_SIGNATURE_ERROR = -40002
37
+ PARSE_XML_ERROR = -40003
38
+ COMPUTE_SIGNATURE_ERROR = -40004
39
+ ILLEGAL_AES_KEY = -40005
40
+ VALIDATE_APPID_ERROR = -40006
41
+ ENCRYPT_AES_ERROR = -40007
42
+ ILLEGAL_BUFFER = -40008
43
+
44
+
45
+ def generate_random_string(length: int = 10) -> str:
46
+ """生成随机字符串
47
+
48
+ Args:
49
+ length: 字符串长度,默认为 10
50
+
51
+ Returns:
52
+ 随机字符串
53
+
54
+ """
55
+ letters = string.ascii_letters + string.digits
56
+ return "".join(secrets.choice(letters) for _ in range(length))
57
+
58
+
59
+ def calculate_image_md5(image_data: bytes) -> str:
60
+ """计算图片数据的 MD5 值
61
+
62
+ Args:
63
+ image_data: 图片二进制数据
64
+
65
+ Returns:
66
+ MD5 哈希值(十六进制字符串)
67
+
68
+ """
69
+ return hashlib.md5(image_data).hexdigest()
70
+
71
+
72
+ def encode_image_base64(image_data: bytes) -> str:
73
+ """将图片数据编码为 Base64
74
+
75
+ Args:
76
+ image_data: 图片二进制数据
77
+
78
+ Returns:
79
+ Base64 编码的字符串
80
+
81
+ """
82
+ return base64.b64encode(image_data).decode("utf-8")
83
+
84
+
85
+ def format_session_id(session_type: str, session_id: str) -> str:
86
+ """格式化会话 ID
87
+
88
+ Args:
89
+ session_type: 会话类型 ("user", "group")
90
+ session_id: 原始会话 ID
91
+
92
+ Returns:
93
+ 格式化后的会话 ID
94
+
95
+ """
96
+ return f"wecom_ai_bot_{session_type}_{session_id}"
97
+
98
+
99
+ def parse_session_id(formatted_session_id: str) -> tuple[str, str]:
100
+ """解析格式化的会话 ID
101
+
102
+ Args:
103
+ formatted_session_id: 格式化的会话 ID
104
+
105
+ Returns:
106
+ (会话类型, 原始会话ID)
107
+
108
+ """
109
+ parts = formatted_session_id.split("_", 3)
110
+ if (
111
+ len(parts) >= 4
112
+ and parts[0] == "wecom"
113
+ and parts[1] == "ai"
114
+ and parts[2] == "bot"
115
+ ):
116
+ return parts[3], "_".join(parts[4:]) if len(parts) > 4 else ""
117
+ return "user", formatted_session_id
118
+
119
+
120
+ def safe_json_loads(json_str: str, default: Any = None) -> Any:
121
+ """安全地解析 JSON 字符串
122
+
123
+ Args:
124
+ json_str: JSON 字符串
125
+ default: 解析失败时的默认值
126
+
127
+ Returns:
128
+ 解析结果或默认值
129
+
130
+ """
131
+ import json
132
+
133
+ try:
134
+ return json.loads(json_str)
135
+ except (json.JSONDecodeError, TypeError) as e:
136
+ logger.warning(f"JSON 解析失败: {e}, 原始字符串: {json_str}")
137
+ return default
138
+
139
+
140
+ def format_error_response(error_code: int, error_msg: str) -> str:
141
+ """格式化错误响应
142
+
143
+ Args:
144
+ error_code: 错误码
145
+ error_msg: 错误信息
146
+
147
+ Returns:
148
+ 格式化的错误响应字符串
149
+
150
+ """
151
+ return f"Error {error_code}: {error_msg}"
152
+
153
+
154
+ async def process_encrypted_image(
155
+ image_url: str,
156
+ aes_key_base64: str,
157
+ ) -> tuple[bool, str]:
158
+ """下载并解密加密图片
159
+
160
+ Args:
161
+ image_url: 加密图片的URL
162
+ aes_key_base64: Base64编码的AES密钥(与回调加解密相同)
163
+
164
+ Returns:
165
+ Tuple[bool, str]: status 为 True 时 data 是解密后的图片数据的 base64 编码,
166
+ status 为 False 时 data 是错误信息
167
+
168
+ """
169
+ # 1. 下载加密图片
170
+ logger.info("开始下载加密图片: %s", image_url)
171
+ try:
172
+ async with aiohttp.ClientSession() as session:
173
+ async with session.get(image_url, timeout=15) as response:
174
+ response.raise_for_status()
175
+ encrypted_data = await response.read()
176
+ logger.info("图片下载成功,大小: %d 字节", len(encrypted_data))
177
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
178
+ error_msg = f"下载图片失败: {e!s}"
179
+ logger.error(error_msg)
180
+ return False, error_msg
181
+
182
+ # 2. 准备AES密钥和IV
183
+ if not aes_key_base64:
184
+ raise ValueError("AES密钥不能为空")
185
+
186
+ # Base64解码密钥 (自动处理填充)
187
+ aes_key = base64.b64decode(aes_key_base64 + "=" * (-len(aes_key_base64) % 4))
188
+ if len(aes_key) != 32:
189
+ raise ValueError("无效的AES密钥长度: 应为32字节")
190
+
191
+ iv = aes_key[:16] # 初始向量为密钥前16字节
192
+
193
+ # 3. 解密图片数据
194
+ cipher = AES.new(aes_key, AES.MODE_CBC, iv)
195
+ decrypted_data = cipher.decrypt(encrypted_data)
196
+
197
+ # 4. 去除PKCS#7填充 (Python 3兼容写法)
198
+ pad_len = decrypted_data[-1] # 直接获取最后一个字节的整数值
199
+ if pad_len > 32: # AES-256块大小为32字节
200
+ raise ValueError("无效的填充长度 (大于32字节)")
201
+
202
+ decrypted_data = decrypted_data[:-pad_len]
203
+ logger.info("图片解密成功,解密后大小: %d 字节", len(decrypted_data))
204
+
205
+ # 5. 转换为base64编码
206
+ base64_data = base64.b64encode(decrypted_data).decode("utf-8")
207
+ logger.info("图片已转换为base64编码,编码后长度: %d", len(base64_data))
208
+
209
+ return True, base64_data