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,306 @@
1
+ import asyncio
2
+ import sys
3
+ import uuid
4
+
5
+ import quart
6
+ from requests import Response
7
+ from wechatpy import WeChatClient, parse_message
8
+ from wechatpy.crypto import WeChatCrypto
9
+ from wechatpy.exceptions import InvalidSignatureException
10
+ from wechatpy.messages import BaseMessage, ImageMessage, TextMessage, VoiceMessage
11
+ from wechatpy.utils import check_signature
12
+
13
+ from astrbot.api.event import MessageChain
14
+ from astrbot.api.message_components import Image, Plain, Record
15
+ from astrbot.api.platform import (
16
+ AstrBotMessage,
17
+ MessageMember,
18
+ MessageType,
19
+ Platform,
20
+ PlatformMetadata,
21
+ register_platform_adapter,
22
+ )
23
+ from astrbot.core import logger
24
+ from astrbot.core.platform.astr_message_event import MessageSesion
25
+
26
+ from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
27
+
28
+ if sys.version_info >= (3, 12):
29
+ from typing import override
30
+ else:
31
+ from typing_extensions import override
32
+
33
+
34
+ class WecomServer:
35
+ def __init__(self, event_queue: asyncio.Queue, config: dict):
36
+ self.server = quart.Quart(__name__)
37
+ self.port = int(config.get("port"))
38
+ self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
39
+ self.token = config.get("token")
40
+ self.encoding_aes_key = config.get("encoding_aes_key")
41
+ self.appid = config.get("appid")
42
+ self.server.add_url_rule(
43
+ "/callback/command",
44
+ view_func=self.verify,
45
+ methods=["GET"],
46
+ )
47
+ self.server.add_url_rule(
48
+ "/callback/command",
49
+ view_func=self.callback_command,
50
+ methods=["POST"],
51
+ )
52
+ self.crypto = WeChatCrypto(self.token, self.encoding_aes_key, self.appid)
53
+
54
+ self.event_queue = event_queue
55
+
56
+ self.callback = None
57
+ self.shutdown_event = asyncio.Event()
58
+
59
+ async def verify(self):
60
+ logger.info(f"验证请求有效性: {quart.request.args}")
61
+
62
+ args = quart.request.args
63
+ if not args.get("signature", None):
64
+ logger.error("未知的响应,请检查回调地址是否填写正确。")
65
+ return "err"
66
+ try:
67
+ check_signature(
68
+ self.token,
69
+ args.get("signature"),
70
+ args.get("timestamp"),
71
+ args.get("nonce"),
72
+ )
73
+ logger.info("验证请求有效性成功。")
74
+ return args.get("echostr", "empty")
75
+ except InvalidSignatureException:
76
+ logger.error("验证请求有效性失败,签名异常,请检查配置。")
77
+ return "err"
78
+
79
+ async def callback_command(self):
80
+ data = await quart.request.get_data()
81
+ msg_signature = quart.request.args.get("msg_signature")
82
+ timestamp = quart.request.args.get("timestamp")
83
+ nonce = quart.request.args.get("nonce")
84
+ try:
85
+ xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
86
+ except InvalidSignatureException:
87
+ logger.error("解密失败,签名异常,请检查配置。")
88
+ raise
89
+ else:
90
+ msg = parse_message(xml)
91
+ logger.info(f"解析成功: {msg}")
92
+
93
+ if self.callback:
94
+ result_xml = await self.callback(msg)
95
+ if not result_xml:
96
+ return "success"
97
+ if isinstance(result_xml, str):
98
+ return result_xml
99
+
100
+ return "success"
101
+
102
+ async def start_polling(self):
103
+ logger.info(
104
+ f"将在 {self.callback_server_host}:{self.port} 端口启动 微信公众平台 适配器。",
105
+ )
106
+ await self.server.run_task(
107
+ host=self.callback_server_host,
108
+ port=self.port,
109
+ shutdown_trigger=self.shutdown_trigger,
110
+ )
111
+
112
+ async def shutdown_trigger(self):
113
+ await self.shutdown_event.wait()
114
+
115
+
116
+ @register_platform_adapter(
117
+ "weixin_official_account", "微信公众平台 适配器", support_streaming_message=False
118
+ )
119
+ class WeixinOfficialAccountPlatformAdapter(Platform):
120
+ def __init__(
121
+ self,
122
+ platform_config: dict,
123
+ platform_settings: dict,
124
+ event_queue: asyncio.Queue,
125
+ ) -> None:
126
+ super().__init__(event_queue)
127
+ self.config = platform_config
128
+ self.settingss = platform_settings
129
+ self.client_self_id = uuid.uuid4().hex[:8]
130
+ self.api_base_url = platform_config.get(
131
+ "api_base_url",
132
+ "https://api.weixin.qq.com/cgi-bin/",
133
+ )
134
+ self.active_send_mode = self.config.get("active_send_mode", False)
135
+
136
+ if not self.api_base_url:
137
+ self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
138
+
139
+ self.api_base_url = self.api_base_url.removesuffix("/")
140
+ if not self.api_base_url.endswith("/cgi-bin"):
141
+ self.api_base_url += "/cgi-bin"
142
+
143
+ if not self.api_base_url.endswith("/"):
144
+ self.api_base_url += "/"
145
+
146
+ self.server = WecomServer(self._event_queue, self.config)
147
+
148
+ self.client = WeChatClient(
149
+ self.config["appid"].strip(),
150
+ self.config["secret"].strip(),
151
+ )
152
+
153
+ self.client.API_BASE_URL = self.api_base_url
154
+
155
+ # 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重
156
+ # msgid -> Future
157
+ self.wexin_event_workers: dict[str, asyncio.Future] = {}
158
+
159
+ async def callback(msg: BaseMessage):
160
+ try:
161
+ if self.active_send_mode:
162
+ await self.convert_message(msg, None)
163
+ else:
164
+ if msg.id in self.wexin_event_workers:
165
+ future = self.wexin_event_workers[msg.id]
166
+ logger.debug(f"duplicate message id checked: {msg.id}")
167
+ else:
168
+ future = asyncio.get_event_loop().create_future()
169
+ self.wexin_event_workers[msg.id] = future
170
+ await self.convert_message(msg, future)
171
+ # I love shield so much!
172
+ result = await asyncio.wait_for(
173
+ asyncio.shield(future),
174
+ 60,
175
+ ) # wait for 60s
176
+ logger.debug(f"Got future result: {result}")
177
+ self.wexin_event_workers.pop(msg.id, None)
178
+ return result # xml. see weixin_offacc_event.py
179
+ except asyncio.TimeoutError:
180
+ pass
181
+ except Exception as e:
182
+ logger.error(f"转换消息时出现异常: {e}")
183
+
184
+ self.server.callback = callback
185
+
186
+ @override
187
+ async def send_by_session(
188
+ self,
189
+ session: MessageSesion,
190
+ message_chain: MessageChain,
191
+ ):
192
+ await super().send_by_session(session, message_chain)
193
+
194
+ @override
195
+ def meta(self) -> PlatformMetadata:
196
+ return PlatformMetadata(
197
+ "weixin_official_account",
198
+ "微信公众平台 适配器",
199
+ id=self.config.get("id", "weixin_official_account"),
200
+ support_streaming_message=False,
201
+ )
202
+
203
+ @override
204
+ async def run(self):
205
+ await self.server.start_polling()
206
+
207
+ async def convert_message(
208
+ self,
209
+ msg,
210
+ future: asyncio.Future = None,
211
+ ) -> AstrBotMessage | None:
212
+ abm = AstrBotMessage()
213
+ if isinstance(msg, TextMessage):
214
+ abm.message_str = msg.content
215
+ abm.self_id = str(msg.target)
216
+ abm.message = [Plain(msg.content)]
217
+ abm.type = MessageType.FRIEND_MESSAGE
218
+ abm.sender = MessageMember(
219
+ msg.source,
220
+ msg.source,
221
+ )
222
+ abm.message_id = msg.id
223
+ abm.timestamp = msg.time
224
+ abm.session_id = abm.sender.user_id
225
+ elif msg.type == "image":
226
+ assert isinstance(msg, ImageMessage)
227
+ abm.message_str = "[图片]"
228
+ abm.self_id = str(msg.target)
229
+ abm.message = [Image(file=msg.image, url=msg.image)]
230
+ abm.type = MessageType.FRIEND_MESSAGE
231
+ abm.sender = MessageMember(
232
+ msg.source,
233
+ msg.source,
234
+ )
235
+ abm.message_id = msg.id
236
+ abm.timestamp = msg.time
237
+ abm.session_id = abm.sender.user_id
238
+ elif msg.type == "voice":
239
+ assert isinstance(msg, VoiceMessage)
240
+
241
+ resp: Response = await asyncio.get_event_loop().run_in_executor(
242
+ None,
243
+ self.client.media.download,
244
+ msg.media_id,
245
+ )
246
+ path = f"data/temp/wecom_{msg.media_id}.amr"
247
+ with open(path, "wb") as f:
248
+ f.write(resp.content)
249
+
250
+ try:
251
+ from pydub import AudioSegment
252
+
253
+ path_wav = f"data/temp/wecom_{msg.media_id}.wav"
254
+ audio = AudioSegment.from_file(path)
255
+ audio.export(path_wav, format="wav")
256
+ except Exception as e:
257
+ logger.error(
258
+ f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。",
259
+ )
260
+ path_wav = path
261
+ return
262
+
263
+ abm.message_str = ""
264
+ abm.self_id = str(msg.target)
265
+ abm.message = [Record(file=path_wav, url=path_wav)]
266
+ abm.type = MessageType.FRIEND_MESSAGE
267
+ abm.sender = MessageMember(
268
+ msg.source,
269
+ msg.source,
270
+ )
271
+ abm.message_id = msg.id
272
+ abm.timestamp = msg.time
273
+ abm.session_id = abm.sender.user_id
274
+ else:
275
+ logger.warning(f"暂未实现的事件: {msg.type}")
276
+ future.set_result(None)
277
+ return
278
+ # 很不优雅 :(
279
+ abm.raw_message = {
280
+ "message": msg,
281
+ "future": future,
282
+ "active_send_mode": self.active_send_mode,
283
+ }
284
+ logger.info(f"abm: {abm}")
285
+ await self.handle_msg(abm)
286
+
287
+ async def handle_msg(self, message: AstrBotMessage):
288
+ message_event = WeixinOfficialAccountPlatformEvent(
289
+ message_str=message.message_str,
290
+ message_obj=message,
291
+ platform_meta=self.meta(),
292
+ session_id=message.session_id,
293
+ client=self.client,
294
+ )
295
+ self.commit_event(message_event)
296
+
297
+ def get_client(self) -> WeChatClient:
298
+ return self.client
299
+
300
+ async def terminate(self):
301
+ self.server.shutdown_event.set()
302
+ try:
303
+ await self.server.server.shutdown()
304
+ except Exception as _:
305
+ pass
306
+ logger.info("微信公众平台 适配器已被优雅地关闭")
@@ -0,0 +1,186 @@
1
+ import asyncio
2
+ import uuid
3
+
4
+ from wechatpy import WeChatClient
5
+ from wechatpy.replies import ImageReply, TextReply, VoiceReply
6
+
7
+ from astrbot.api import logger
8
+ from astrbot.api.event import AstrMessageEvent, MessageChain
9
+ from astrbot.api.message_components import Image, Plain, Record
10
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
11
+
12
+ try:
13
+ import pydub
14
+ except Exception:
15
+ logger.warning(
16
+ "检测到 pydub 库未安装,微信公众平台将无法语音收发。如需使用语音,请前往管理面板 -> 控制台 -> 安装 Pip 库安装 pydub。",
17
+ )
18
+
19
+
20
+ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
21
+ def __init__(
22
+ self,
23
+ message_str: str,
24
+ message_obj: AstrBotMessage,
25
+ platform_meta: PlatformMetadata,
26
+ session_id: str,
27
+ client: WeChatClient,
28
+ ):
29
+ super().__init__(message_str, message_obj, platform_meta, session_id)
30
+ self.client = client
31
+
32
+ @staticmethod
33
+ async def send_with_client(
34
+ client: WeChatClient,
35
+ message: MessageChain,
36
+ user_name: str,
37
+ ):
38
+ pass
39
+
40
+ async def split_plain(self, plain: str) -> list[str]:
41
+ """将长文本分割成多个小文本, 每个小文本长度不超过 2048 字符
42
+
43
+ Args:
44
+ plain (str): 要分割的长文本
45
+ Returns:
46
+ list[str]: 分割后的文本列表
47
+
48
+ """
49
+ if len(plain) <= 2048:
50
+ return [plain]
51
+ result = []
52
+ start = 0
53
+ while start < len(plain):
54
+ # 剩下的字符串长度<2048时结束
55
+ if start + 2048 >= len(plain):
56
+ result.append(plain[start:])
57
+ break
58
+
59
+ # 向前搜索分割标点符号
60
+ end = min(start + 2048, len(plain))
61
+ cut_position = end
62
+ for i in range(end, start, -1):
63
+ if i < len(plain) and plain[i - 1] in [
64
+ "。",
65
+ "!",
66
+ "?",
67
+ ".",
68
+ "!",
69
+ "?",
70
+ "\n",
71
+ ";",
72
+ ";",
73
+ ]:
74
+ cut_position = i
75
+ break
76
+
77
+ # 没找到合适的位置分割, 直接切分
78
+ if cut_position == end and end < len(plain):
79
+ cut_position = end
80
+
81
+ result.append(plain[start:cut_position])
82
+ start = cut_position
83
+
84
+ return result
85
+
86
+ async def send(self, message: MessageChain):
87
+ message_obj = self.message_obj
88
+ active_send_mode = message_obj.raw_message.get("active_send_mode", False)
89
+ for comp in message.chain:
90
+ if isinstance(comp, Plain):
91
+ # Split long text messages if needed
92
+ plain_chunks = await self.split_plain(comp.text)
93
+ for chunk in plain_chunks:
94
+ if active_send_mode:
95
+ self.client.message.send_text(message_obj.sender.user_id, chunk)
96
+ else:
97
+ reply = TextReply(
98
+ content=chunk,
99
+ message=self.message_obj.raw_message["message"],
100
+ )
101
+ xml = reply.render()
102
+ future = self.message_obj.raw_message["future"]
103
+ assert isinstance(future, asyncio.Future)
104
+ future.set_result(xml)
105
+ await asyncio.sleep(0.5) # Avoid sending too fast
106
+ elif isinstance(comp, Image):
107
+ img_path = await comp.convert_to_file_path()
108
+
109
+ with open(img_path, "rb") as f:
110
+ try:
111
+ response = self.client.media.upload("image", f)
112
+ except Exception as e:
113
+ logger.error(f"微信公众平台上传图片失败: {e}")
114
+ await self.send(
115
+ MessageChain().message(f"微信公众平台上传图片失败: {e}"),
116
+ )
117
+ return
118
+ logger.debug(f"微信公众平台上传图片返回: {response}")
119
+
120
+ if active_send_mode:
121
+ self.client.message.send_image(
122
+ message_obj.sender.user_id,
123
+ response["media_id"],
124
+ )
125
+ else:
126
+ reply = ImageReply(
127
+ media_id=response["media_id"],
128
+ message=self.message_obj.raw_message["message"],
129
+ )
130
+ xml = reply.render()
131
+ future = self.message_obj.raw_message["future"]
132
+ assert isinstance(future, asyncio.Future)
133
+ future.set_result(xml)
134
+
135
+ elif isinstance(comp, Record):
136
+ record_path = await comp.convert_to_file_path()
137
+ # 转成amr
138
+ record_path_amr = f"data/temp/{uuid.uuid4()}.amr"
139
+ pydub.AudioSegment.from_wav(record_path).export(
140
+ record_path_amr,
141
+ format="amr",
142
+ )
143
+
144
+ with open(record_path_amr, "rb") as f:
145
+ try:
146
+ response = self.client.media.upload("voice", f)
147
+ except Exception as e:
148
+ logger.error(f"微信公众平台上传语音失败: {e}")
149
+ await self.send(
150
+ MessageChain().message(f"微信公众平台上传语音失败: {e}"),
151
+ )
152
+ return
153
+ logger.info(f"微信公众平台上传语音返回: {response}")
154
+
155
+ if active_send_mode:
156
+ self.client.message.send_voice(
157
+ message_obj.sender.user_id,
158
+ response["media_id"],
159
+ )
160
+ else:
161
+ reply = VoiceReply(
162
+ media_id=response["media_id"],
163
+ message=self.message_obj.raw_message["message"],
164
+ )
165
+ xml = reply.render()
166
+ future = self.message_obj.raw_message["future"]
167
+ assert isinstance(future, asyncio.Future)
168
+ future.set_result(xml)
169
+
170
+ else:
171
+ logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。")
172
+
173
+ await super().send(message)
174
+
175
+ async def send_streaming(self, generator, use_fallback: bool = False):
176
+ buffer = None
177
+ async for chain in generator:
178
+ if not buffer:
179
+ buffer = chain
180
+ else:
181
+ buffer.chain.extend(chain.chain)
182
+ if not buffer:
183
+ return None
184
+ buffer.squash_plain()
185
+ await self.send(buffer)
186
+ return await super().send_streaming(generator, use_fallback)
@@ -0,0 +1,49 @@
1
+ from astrbot.core.db import BaseDatabase
2
+ from astrbot.core.db.po import PlatformMessageHistory
3
+
4
+
5
+ class PlatformMessageHistoryManager:
6
+ def __init__(self, db_helper: BaseDatabase):
7
+ self.db = db_helper
8
+
9
+ async def insert(
10
+ self,
11
+ platform_id: str,
12
+ user_id: str,
13
+ content: list[dict], # TODO: parse from message chain
14
+ sender_id: str | None = None,
15
+ sender_name: str | None = None,
16
+ ):
17
+ """Insert a new platform message history record."""
18
+ await self.db.insert_platform_message_history(
19
+ platform_id=platform_id,
20
+ user_id=user_id,
21
+ content=content,
22
+ sender_id=sender_id,
23
+ sender_name=sender_name,
24
+ )
25
+
26
+ async def get(
27
+ self,
28
+ platform_id: str,
29
+ user_id: str,
30
+ page: int = 1,
31
+ page_size: int = 200,
32
+ ) -> list[PlatformMessageHistory]:
33
+ """Get platform message history for a specific user."""
34
+ history = await self.db.get_platform_message_history(
35
+ platform_id=platform_id,
36
+ user_id=user_id,
37
+ page=page,
38
+ page_size=page_size,
39
+ )
40
+ history.reverse()
41
+ return history
42
+
43
+ async def delete(self, platform_id: str, user_id: str, offset_sec: int = 86400):
44
+ """Delete platform message history records older than the specified offset."""
45
+ await self.db.delete_platform_message_offset(
46
+ platform_id=platform_id,
47
+ user_id=user_id,
48
+ offset_sec=offset_sec,
49
+ )
@@ -1,5 +1,4 @@
1
- from .provider import Provider, Personality, STTProvider
2
-
3
1
  from .entities import ProviderMetaData
2
+ from .provider import Provider, STTProvider
4
3
 
5
- __all__ = ["Provider", "Personality", "ProviderMetaData", "STTProvider"]
4
+ __all__ = ["Provider", "ProviderMetaData", "STTProvider"]
@@ -1,19 +1,19 @@
1
1
  from astrbot.core.provider.entities import (
2
+ AssistantMessageSegment,
3
+ LLMResponse,
4
+ ProviderMetaData,
2
5
  ProviderRequest,
3
6
  ProviderType,
4
- ProviderMetaData,
5
- ToolCallsResult,
6
- AssistantMessageSegment,
7
7
  ToolCallMessageSegment,
8
- LLMResponse,
8
+ ToolCallsResult,
9
9
  )
10
10
 
11
11
  __all__ = [
12
+ "AssistantMessageSegment",
13
+ "LLMResponse",
14
+ "ProviderMetaData",
12
15
  "ProviderRequest",
13
16
  "ProviderType",
14
- "ProviderMetaData",
15
- "ToolCallsResult",
16
- "AssistantMessageSegment",
17
17
  "ToolCallMessageSegment",
18
- "LLMResponse",
18
+ "ToolCallsResult",
19
19
  ]