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
@@ -1,17 +1,26 @@
1
+ import asyncio
2
+ import base64
3
+ import os
4
+ import random
5
+ import uuid
6
+
7
+ import aiofiles
1
8
  import botpy
2
9
  import botpy.message
3
10
  import botpy.types
4
11
  import botpy.types.message
5
- import asyncio
6
- from astrbot.core.utils.io import file_to_base64, download_image_by_url
7
- from astrbot.api.event import AstrMessageEvent, MessageChain
8
- from astrbot.api.platform import AstrBotMessage, PlatformMetadata
9
- from astrbot.api.message_components import Plain, Image
10
12
  from botpy import Client
11
13
  from botpy.http import Route
12
- from astrbot.api import logger
13
14
  from botpy.types import message
14
- import random
15
+ from botpy.types.message import Media
16
+
17
+ from astrbot.api import logger
18
+ from astrbot.api.event import AstrMessageEvent, MessageChain
19
+ from astrbot.api.message_components import Image, Plain, Record
20
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
21
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
22
+ from astrbot.core.utils.io import download_image_by_url, file_to_base64
23
+ from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
15
24
 
16
25
 
17
26
  class QQOfficialMessageEvent(AstrMessageEvent):
@@ -28,16 +37,15 @@ class QQOfficialMessageEvent(AstrMessageEvent):
28
37
  self.send_buffer = None
29
38
 
30
39
  async def send(self, message: MessageChain):
31
- if not self.send_buffer:
32
- self.send_buffer = message
33
- else:
34
- self.send_buffer.chain.extend(message.chain)
40
+ self.send_buffer = message
41
+ await self._post_send()
35
42
 
36
43
  async def send_streaming(self, generator, use_fallback: bool = False):
37
44
  """流式输出仅支持消息列表私聊"""
38
45
  stream_payload = {"state": 1, "id": None, "index": 0, "reset": False}
39
46
  last_edit_time = 0 # 上次编辑消息的时间
40
47
  throttle_interval = 1 # 编辑消息的间隔时间 (秒)
48
+ ret = None
41
49
  try:
42
50
  async for chain in generator:
43
51
  source = self.message_obj.raw_message
@@ -68,9 +76,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
68
76
 
69
77
  return await super().send_streaming(generator, use_fallback)
70
78
 
71
- async def _post_send(self, stream: dict = None):
79
+ async def _post_send(self, stream: dict | None = None):
72
80
  if not self.send_buffer:
73
- return
81
+ return None
74
82
 
75
83
  source = self.message_obj.raw_message
76
84
  assert isinstance(
@@ -87,10 +95,16 @@ class QQOfficialMessageEvent(AstrMessageEvent):
87
95
  plain_text,
88
96
  image_base64,
89
97
  image_path,
98
+ record_file_path,
90
99
  ) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
91
100
 
92
- if not plain_text and not image_base64 and not image_path:
93
- return
101
+ if (
102
+ not plain_text
103
+ and not image_base64
104
+ and not image_path
105
+ and not record_file_path
106
+ ):
107
+ return None
94
108
 
95
109
  payload = {
96
110
  "content": plain_text,
@@ -100,21 +114,44 @@ class QQOfficialMessageEvent(AstrMessageEvent):
100
114
  if not isinstance(source, (botpy.message.Message, botpy.message.DirectMessage)):
101
115
  payload["msg_seq"] = random.randint(1, 10000)
102
116
 
117
+ ret = None
118
+
103
119
  match type(source):
104
120
  case botpy.message.GroupMessage:
105
121
  if image_base64:
106
122
  media = await self.upload_group_and_c2c_image(
107
- image_base64, 1, group_openid=source.group_openid
123
+ image_base64,
124
+ 1,
125
+ group_openid=source.group_openid,
126
+ )
127
+ payload["media"] = media
128
+ payload["msg_type"] = 7
129
+ if record_file_path: # group record msg
130
+ media = await self.upload_group_and_c2c_record(
131
+ record_file_path,
132
+ 3,
133
+ group_openid=source.group_openid,
108
134
  )
109
135
  payload["media"] = media
110
136
  payload["msg_type"] = 7
111
137
  ret = await self.bot.api.post_group_message(
112
- group_openid=source.group_openid, **payload
138
+ group_openid=source.group_openid,
139
+ **payload,
113
140
  )
114
141
  case botpy.message.C2CMessage:
115
142
  if image_base64:
116
143
  media = await self.upload_group_and_c2c_image(
117
- image_base64, 1, openid=source.author.user_openid
144
+ image_base64,
145
+ 1,
146
+ openid=source.author.user_openid,
147
+ )
148
+ payload["media"] = media
149
+ payload["msg_type"] = 7
150
+ if record_file_path: # c2c record
151
+ media = await self.upload_group_and_c2c_record(
152
+ record_file_path,
153
+ 3,
154
+ openid=source.author.user_openid,
118
155
  )
119
156
  payload["media"] = media
120
157
  payload["msg_type"] = 7
@@ -126,14 +163,16 @@ class QQOfficialMessageEvent(AstrMessageEvent):
126
163
  )
127
164
  else:
128
165
  ret = await self.post_c2c_message(
129
- openid=source.author.user_openid, **payload
166
+ openid=source.author.user_openid,
167
+ **payload,
130
168
  )
131
169
  logger.debug(f"Message sent to C2C: {ret}")
132
170
  case botpy.message.Message:
133
171
  if image_path:
134
172
  payload["file_image"] = image_path
135
173
  ret = await self.bot.api.post_message(
136
- channel_id=source.channel_id, **payload
174
+ channel_id=source.channel_id,
175
+ **payload,
137
176
  )
138
177
  case botpy.message.DirectMessage:
139
178
  if image_path:
@@ -147,7 +186,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
147
186
  return ret
148
187
 
149
188
  async def upload_group_and_c2c_image(
150
- self, image_base64: str, file_type: int, **kwargs
189
+ self,
190
+ image_base64: str,
191
+ file_type: int,
192
+ **kwargs,
151
193
  ) -> botpy.types.message.Media:
152
194
  payload = {
153
195
  "file_data": image_base64,
@@ -158,7 +200,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
158
200
  payload["openid"] = kwargs["openid"]
159
201
  route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
160
202
  return await self.bot.api._http.request(route, json=payload)
161
- elif "group_openid" in kwargs:
203
+ if "group_openid" in kwargs:
162
204
  payload["group_openid"] = kwargs["group_openid"]
163
205
  route = Route(
164
206
  "POST",
@@ -167,21 +209,73 @@ class QQOfficialMessageEvent(AstrMessageEvent):
167
209
  )
168
210
  return await self.bot.api._http.request(route, json=payload)
169
211
 
212
+ async def upload_group_and_c2c_record(
213
+ self,
214
+ file_source: str,
215
+ file_type: int,
216
+ srv_send_msg: bool = False,
217
+ **kwargs,
218
+ ) -> Media | None:
219
+ """上传媒体文件"""
220
+ # 构建基础payload
221
+ payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
222
+
223
+ # 处理文件数据
224
+ if os.path.exists(file_source):
225
+ # 读取本地文件
226
+ async with aiofiles.open(file_source, "rb") as f:
227
+ file_content = await f.read()
228
+ # use base64 encode
229
+ payload["file_data"] = base64.b64encode(file_content).decode("utf-8")
230
+ else:
231
+ # 使用URL
232
+ payload["url"] = file_source
233
+
234
+ # 添加接收者信息和确定路由
235
+ if "openid" in kwargs:
236
+ payload["openid"] = kwargs["openid"]
237
+ route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
238
+ elif "group_openid" in kwargs:
239
+ payload["group_openid"] = kwargs["group_openid"]
240
+ route = Route(
241
+ "POST",
242
+ "/v2/groups/{group_openid}/files",
243
+ group_openid=kwargs["group_openid"],
244
+ )
245
+ else:
246
+ return None
247
+
248
+ try:
249
+ # 使用底层HTTP请求
250
+ result = await self.bot.api._http.request(route, json=payload)
251
+
252
+ if result:
253
+ return Media(
254
+ file_uuid=result.get("file_uuid"),
255
+ file_info=result.get("file_info"),
256
+ ttl=result.get("ttl", 0),
257
+ file_id=result.get("id", ""),
258
+ )
259
+ except Exception as e:
260
+ logger.error(f"上传请求错误: {e}")
261
+
262
+ return None
263
+
170
264
  async def post_c2c_message(
171
265
  self,
172
266
  openid: str,
173
267
  msg_type: int = 0,
174
- content: str = None,
175
- embed: message.Embed = None,
176
- ark: message.Ark = None,
177
- message_reference: message.Reference = None,
178
- media: message.Media = None,
179
- msg_id: str = None,
268
+ content: str | None = None,
269
+ embed: message.Embed | None = None,
270
+ ark: message.Ark | None = None,
271
+ message_reference: message.Reference | None = None,
272
+ media: message.Media | None = None,
273
+ msg_id: str | None = None,
180
274
  msg_seq: str = 1,
181
- event_id: str = None,
182
- markdown: message.MarkdownPayload = None,
183
- keyboard: message.Keyboard = None,
184
- stream: dict = None,
275
+ event_id: str | None = None,
276
+ markdown: message.MarkdownPayload | None = None,
277
+ keyboard: message.Keyboard | None = None,
278
+ stream: dict | None = None,
185
279
  ) -> message.Message:
186
280
  payload = locals()
187
281
  payload.pop("self", None)
@@ -193,6 +287,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
193
287
  plain_text = ""
194
288
  image_base64 = None # only one img supported
195
289
  image_file_path = None
290
+ record_file_path = None
196
291
  for i in message.chain:
197
292
  if isinstance(i, Plain):
198
293
  plain_text += i.text
@@ -208,6 +303,27 @@ class QQOfficialMessageEvent(AstrMessageEvent):
208
303
  else:
209
304
  image_base64 = file_to_base64(i.file)
210
305
  image_base64 = image_base64.removeprefix("base64://")
306
+ elif isinstance(i, Record):
307
+ if i.file:
308
+ record_wav_path = await i.convert_to_file_path() # wav 路径
309
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
310
+ record_tecent_silk_path = os.path.join(
311
+ temp_dir,
312
+ f"{uuid.uuid4()}.silk",
313
+ )
314
+ try:
315
+ duration = await wav_to_tencent_silk(
316
+ record_wav_path,
317
+ record_tecent_silk_path,
318
+ )
319
+ if duration > 0:
320
+ record_file_path = record_tecent_silk_path
321
+ else:
322
+ record_file_path = None
323
+ logger.error("转换音频格式时出错:音频时长不大于0")
324
+ except Exception as e:
325
+ logger.error(f"处理语音时出错: {e}")
326
+ record_file_path = None
211
327
  else:
212
328
  logger.debug(f"qq_official 忽略 {i.type}")
213
- return plain_text, image_base64, image_file_path
329
+ return plain_text, image_base64, image_file_path, record_file_path
@@ -1,30 +1,31 @@
1
1
  from __future__ import annotations
2
2
 
3
- import botpy
3
+ import asyncio
4
4
  import logging
5
+ import os
5
6
  import time
6
- import asyncio
7
+
8
+ import botpy
7
9
  import botpy.message
8
10
  import botpy.types
9
11
  import botpy.types.message
10
- import os
11
-
12
12
  from botpy import Client
13
+
14
+ from astrbot import logger
15
+ from astrbot.api.event import MessageChain
16
+ from astrbot.api.message_components import At, Image, Plain
13
17
  from astrbot.api.platform import (
14
- Platform,
15
18
  AstrBotMessage,
16
19
  MessageMember,
17
20
  MessageType,
21
+ Platform,
18
22
  PlatformMetadata,
19
23
  )
20
- from astrbot import logger
21
- from astrbot.api.event import MessageChain
22
- from typing import Union, List
23
- from astrbot.api.message_components import Image, Plain, At
24
+ from astrbot.core.message.components import BaseMessageComponent
24
25
  from astrbot.core.platform.astr_message_event import MessageSesion
25
- from .qqofficial_message_event import QQOfficialMessageEvent
26
+
26
27
  from ...register import register_platform_adapter
27
- from astrbot.core.message.components import BaseMessageComponent
28
+ from .qqofficial_message_event import QQOfficialMessageEvent
28
29
 
29
30
  # remove logger handler
30
31
  for handler in logging.root.handlers[:]:
@@ -33,13 +34,14 @@ for handler in logging.root.handlers[:]:
33
34
 
34
35
  # QQ 机器人官方框架
35
36
  class botClient(Client):
36
- def set_platform(self, platform: "QQOfficialPlatformAdapter"):
37
+ def set_platform(self, platform: QQOfficialPlatformAdapter):
37
38
  self.platform = platform
38
39
 
39
40
  # 收到群消息
40
41
  async def on_group_at_message_create(self, message: botpy.message.GroupMessage):
41
42
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
42
- message, MessageType.GROUP_MESSAGE
43
+ message,
44
+ MessageType.GROUP_MESSAGE,
43
45
  )
44
46
  abm.session_id = (
45
47
  abm.sender.user_id if self.platform.unique_session else message.group_openid
@@ -49,7 +51,8 @@ class botClient(Client):
49
51
  # 收到频道消息
50
52
  async def on_at_message_create(self, message: botpy.message.Message):
51
53
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
52
- message, MessageType.GROUP_MESSAGE
54
+ message,
55
+ MessageType.GROUP_MESSAGE,
53
56
  )
54
57
  abm.session_id = (
55
58
  abm.sender.user_id if self.platform.unique_session else message.channel_id
@@ -59,7 +62,8 @@ class botClient(Client):
59
62
  # 收到私聊消息
60
63
  async def on_direct_message_create(self, message: botpy.message.DirectMessage):
61
64
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
62
- message, MessageType.FRIEND_MESSAGE
65
+ message,
66
+ MessageType.FRIEND_MESSAGE,
63
67
  )
64
68
  abm.session_id = abm.sender.user_id
65
69
  self._commit(abm)
@@ -67,7 +71,8 @@ class botClient(Client):
67
71
  # 收到 C2C 消息
68
72
  async def on_c2c_message_create(self, message: botpy.message.C2CMessage):
69
73
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
70
- message, MessageType.FRIEND_MESSAGE
74
+ message,
75
+ MessageType.FRIEND_MESSAGE,
71
76
  )
72
77
  abm.session_id = abm.sender.user_id
73
78
  self._commit(abm)
@@ -80,14 +85,17 @@ class botClient(Client):
80
85
  self.platform.meta(),
81
86
  abm.session_id,
82
87
  self.platform.client,
83
- )
88
+ ),
84
89
  )
85
90
 
86
91
 
87
92
  @register_platform_adapter("qq_official", "QQ 机器人官方 API 适配器")
88
93
  class QQOfficialPlatformAdapter(Platform):
89
94
  def __init__(
90
- self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
95
+ self,
96
+ platform_config: dict,
97
+ platform_settings: dict,
98
+ event_queue: asyncio.Queue,
91
99
  ) -> None:
92
100
  super().__init__(event_queue)
93
101
 
@@ -107,7 +115,8 @@ class QQOfficialPlatformAdapter(Platform):
107
115
  )
108
116
  else:
109
117
  self.intents = botpy.Intents(
110
- public_guild_messages=True, direct_message=guild_dm
118
+ public_guild_messages=True,
119
+ direct_message=guild_dm,
111
120
  )
112
121
  self.client = botClient(
113
122
  intents=self.intents,
@@ -120,7 +129,9 @@ class QQOfficialPlatformAdapter(Platform):
120
129
  self.test_mode = os.environ.get("TEST_MODE", "off") == "on"
121
130
 
122
131
  async def send_by_session(
123
- self, session: MessageSesion, message_chain: MessageChain
132
+ self,
133
+ session: MessageSesion,
134
+ message_chain: MessageChain,
124
135
  ):
125
136
  raise NotImplementedError("QQ 机器人官方 API 适配器不支持 send_by_session")
126
137
 
@@ -133,7 +144,7 @@ class QQOfficialPlatformAdapter(Platform):
133
144
 
134
145
  @staticmethod
135
146
  def _parse_from_qqofficial(
136
- message: Union[botpy.message.Message, botpy.message.GroupMessage],
147
+ message: botpy.message.Message | botpy.message.GroupMessage,
137
148
  message_type: MessageType,
138
149
  ):
139
150
  abm = AstrBotMessage()
@@ -142,10 +153,11 @@ class QQOfficialPlatformAdapter(Platform):
142
153
  abm.raw_message = message
143
154
  abm.message_id = message.id
144
155
  abm.tag = "qq_official"
145
- msg: List[BaseMessageComponent] = []
156
+ msg: list[BaseMessageComponent] = []
146
157
 
147
158
  if isinstance(message, botpy.message.GroupMessage) or isinstance(
148
- message, botpy.message.C2CMessage
159
+ message,
160
+ botpy.message.C2CMessage,
149
161
  ):
150
162
  if isinstance(message, botpy.message.GroupMessage):
151
163
  abm.sender = MessageMember(message.author.member_openid, "")
@@ -167,7 +179,8 @@ class QQOfficialPlatformAdapter(Platform):
167
179
  abm.message = msg
168
180
 
169
181
  elif isinstance(message, botpy.message.Message) or isinstance(
170
- message, botpy.message.DirectMessage
182
+ message,
183
+ botpy.message.DirectMessage,
171
184
  ):
172
185
  try:
173
186
  abm.self_id = str(message.mentions[0].id)
@@ -175,7 +188,8 @@ class QQOfficialPlatformAdapter(Platform):
175
188
  abm.self_id = ""
176
189
 
177
190
  plain_content = message.content.replace(
178
- "<@!" + str(abm.self_id) + ">", ""
191
+ "<@!" + str(abm.self_id) + ">",
192
+ "",
179
193
  ).strip()
180
194
 
181
195
  if message.attachments:
@@ -189,7 +203,8 @@ class QQOfficialPlatformAdapter(Platform):
189
203
  abm.message = msg
190
204
  abm.message_str = plain_content
191
205
  abm.sender = MessageMember(
192
- str(message.author.id), str(message.author.username)
206
+ str(message.author.id),
207
+ str(message.author.username),
193
208
  )
194
209
  msg.append(At(qq="qq_official"))
195
210
  msg.append(Plain(plain_content))
@@ -1,19 +1,21 @@
1
- import botpy
2
- import logging
3
1
  import asyncio
2
+ import logging
3
+
4
+ import botpy
4
5
  import botpy.message
5
6
  import botpy.types
6
7
  import botpy.types.message
7
-
8
8
  from botpy import Client
9
- from astrbot.api.platform import Platform, AstrBotMessage, MessageType, PlatformMetadata
9
+
10
+ from astrbot import logger
10
11
  from astrbot.api.event import MessageChain
12
+ from astrbot.api.platform import AstrBotMessage, MessageType, Platform, PlatformMetadata
11
13
  from astrbot.core.platform.astr_message_event import MessageSesion
12
- from .qo_webhook_event import QQOfficialWebhookMessageEvent
14
+
13
15
  from ...register import register_platform_adapter
14
- from .qo_webhook_server import QQOfficialWebhook
15
16
  from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
16
- from astrbot import logger
17
+ from .qo_webhook_event import QQOfficialWebhookMessageEvent
18
+ from .qo_webhook_server import QQOfficialWebhook
17
19
 
18
20
  # remove logger handler
19
21
  for handler in logging.root.handlers[:]:
@@ -28,7 +30,8 @@ class botClient(Client):
28
30
  # 收到群消息
29
31
  async def on_group_at_message_create(self, message: botpy.message.GroupMessage):
30
32
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
31
- message, MessageType.GROUP_MESSAGE
33
+ message,
34
+ MessageType.GROUP_MESSAGE,
32
35
  )
33
36
  abm.session_id = (
34
37
  abm.sender.user_id if self.platform.unique_session else message.group_openid
@@ -38,7 +41,8 @@ class botClient(Client):
38
41
  # 收到频道消息
39
42
  async def on_at_message_create(self, message: botpy.message.Message):
40
43
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
41
- message, MessageType.GROUP_MESSAGE
44
+ message,
45
+ MessageType.GROUP_MESSAGE,
42
46
  )
43
47
  abm.session_id = (
44
48
  abm.sender.user_id if self.platform.unique_session else message.channel_id
@@ -48,7 +52,8 @@ class botClient(Client):
48
52
  # 收到私聊消息
49
53
  async def on_direct_message_create(self, message: botpy.message.DirectMessage):
50
54
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
51
- message, MessageType.FRIEND_MESSAGE
55
+ message,
56
+ MessageType.FRIEND_MESSAGE,
52
57
  )
53
58
  abm.session_id = abm.sender.user_id
54
59
  self._commit(abm)
@@ -56,7 +61,8 @@ class botClient(Client):
56
61
  # 收到 C2C 消息
57
62
  async def on_c2c_message_create(self, message: botpy.message.C2CMessage):
58
63
  abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
59
- message, MessageType.FRIEND_MESSAGE
64
+ message,
65
+ MessageType.FRIEND_MESSAGE,
60
66
  )
61
67
  abm.session_id = abm.sender.user_id
62
68
  self._commit(abm)
@@ -64,15 +70,22 @@ class botClient(Client):
64
70
  def _commit(self, abm: AstrBotMessage):
65
71
  self.platform.commit_event(
66
72
  QQOfficialWebhookMessageEvent(
67
- abm.message_str, abm, self.platform.meta(), abm.session_id, self
68
- )
73
+ abm.message_str,
74
+ abm,
75
+ self.platform.meta(),
76
+ abm.session_id,
77
+ self,
78
+ ),
69
79
  )
70
80
 
71
81
 
72
82
  @register_platform_adapter("qq_official_webhook", "QQ 机器人官方 API 适配器(Webhook)")
73
83
  class QQOfficialWebhookPlatformAdapter(Platform):
74
84
  def __init__(
75
- self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
85
+ self,
86
+ platform_config: dict,
87
+ platform_settings: dict,
88
+ event_queue: asyncio.Queue,
76
89
  ) -> None:
77
90
  super().__init__(event_queue)
78
91
 
@@ -83,7 +96,9 @@ class QQOfficialWebhookPlatformAdapter(Platform):
83
96
  self.unique_session = platform_settings["unique_session"]
84
97
 
85
98
  intents = botpy.Intents(
86
- public_messages=True, public_guild_messages=True, direct_message=True
99
+ public_messages=True,
100
+ public_guild_messages=True,
101
+ direct_message=True,
87
102
  )
88
103
  self.client = botClient(
89
104
  intents=intents, # 已经无用
@@ -93,7 +108,9 @@ class QQOfficialWebhookPlatformAdapter(Platform):
93
108
  self.client.set_platform(self)
94
109
 
95
110
  async def send_by_session(
96
- self, session: MessageSesion, message_chain: MessageChain
111
+ self,
112
+ session: MessageSesion,
113
+ message_chain: MessageChain,
97
114
  ):
98
115
  raise NotImplementedError("QQ 机器人官方 API 适配器不支持 send_by_session")
99
116
 
@@ -106,7 +123,9 @@ class QQOfficialWebhookPlatformAdapter(Platform):
106
123
 
107
124
  async def run(self):
108
125
  self.webhook_helper = QQOfficialWebhook(
109
- self.config, self._event_queue, self.client
126
+ self.config,
127
+ self._event_queue,
128
+ self.client,
110
129
  )
111
130
  await self.webhook_helper.initialize()
112
131
  await self.webhook_helper.start_polling()
@@ -1,5 +1,7 @@
1
- from astrbot.api.platform import AstrBotMessage, PlatformMetadata
2
1
  from botpy import Client
2
+
3
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
4
+
3
5
  from ..qqofficial.qqofficial_message_event import QQOfficialMessageEvent
4
6
 
5
7
 
@@ -1,10 +1,12 @@
1
- import quart
2
- import logging
3
1
  import asyncio
4
- from botpy import BotAPI, BotHttp, Client, Token, BotWebSocket, ConnectionSession
5
- from astrbot.api import logger
2
+ import logging
3
+
4
+ import quart
5
+ from botpy import BotAPI, BotHttp, BotWebSocket, Client, ConnectionSession, Token
6
6
  from cryptography.hazmat.primitives.asymmetric import ed25519
7
7
 
8
+ from astrbot.api import logger
9
+
8
10
  # remove logger handler
9
11
  for handler in logging.root.handlers[:]:
10
12
  logging.root.removeHandler(handler)
@@ -15,18 +17,21 @@ class QQOfficialWebhook:
15
17
  self.appid = config["appid"]
16
18
  self.secret = config["secret"]
17
19
  self.port = config.get("port", 6196)
20
+ self.is_sandbox = config.get("is_sandbox", False)
18
21
  self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
19
22
 
20
23
  if isinstance(self.port, str):
21
24
  self.port = int(self.port)
22
25
 
23
- self.http: BotHttp = BotHttp(timeout=300)
26
+ self.http: BotHttp = BotHttp(timeout=300, is_sandbox=self.is_sandbox)
24
27
  self.api: BotAPI = BotAPI(http=self.http)
25
28
  self.token = Token(self.appid, self.secret)
26
29
 
27
30
  self.server = quart.Quart(__name__)
28
31
  self.server.add_url_rule(
29
- "/astrbot-qo-webhook/callback", view_func=self.callback, methods=["POST"]
32
+ "/astrbot-qo-webhook/callback",
33
+ view_func=self.callback,
34
+ methods=["POST"],
30
35
  )
31
36
  self.client = botpy_client
32
37
  self.event_queue = event_queue
@@ -61,7 +66,8 @@ class QQOfficialWebhook:
61
66
  seed = await self.repeat_seed(self.secret)
62
67
  private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed)
63
68
  msg = validation_payload.get("event_ts", "") + validation_payload.get(
64
- "plain_token", ""
69
+ "plain_token",
70
+ "",
65
71
  )
66
72
  # sign
67
73
  signature = private_key.sign(msg.encode()).hex()
@@ -98,7 +104,7 @@ class QQOfficialWebhook:
98
104
 
99
105
  async def start_polling(self):
100
106
  logger.info(
101
- f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。"
107
+ f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。",
102
108
  )
103
109
  await self.server.run_task(
104
110
  host=self.callback_server_host,