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,5 +1,4 @@
1
- """
2
- MIT License
1
+ """MIT License
3
2
 
4
3
  Copyright (c) 2021 Lxns-Network
5
4
 
@@ -22,73 +21,51 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
21
  SOFTWARE.
23
22
  """
24
23
 
24
+ import asyncio
25
25
  import base64
26
26
  import json
27
27
  import os
28
28
  import uuid
29
- import asyncio
30
- import typing as T
31
29
  from enum import Enum
30
+
32
31
  from pydantic.v1 import BaseModel
33
- from astrbot.core import logger
34
- from astrbot.core.utils.io import download_image_by_url, file_to_base64, download_file
35
-
36
-
37
- class ComponentType(Enum):
38
- Plain = "Plain" # 纯文本消息
39
- Face = "Face" # QQ表情
40
- Record = "Record" # 语音
41
- Video = "Video" # 视频
42
- At = "At" # At
43
- Node = "Node" # 转发消息的一个节点
44
- Nodes = "Nodes" # 转发消息的多个节点
45
- Poke = "Poke" # QQ 戳一戳
46
- Image = "Image" # 图片
47
- Reply = "Reply" # 回复
48
- Forward = "Forward" # 转发消息
49
- File = "File" # 文件
50
32
 
33
+ from astrbot.core import astrbot_config, file_token_service, logger
34
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
35
+ from astrbot.core.utils.io import download_file, download_image_by_url, file_to_base64
36
+
37
+
38
+ class ComponentType(str, Enum):
39
+ # Basic Segment Types
40
+ Plain = "Plain" # plain text message
41
+ Image = "Image" # image
42
+ Record = "Record" # audio
43
+ Video = "Video" # video
44
+ File = "File" # file attachment
45
+
46
+ # IM-specific Segment Types
47
+ Face = "Face" # Emoji segment for Tencent QQ platform
48
+ At = "At" # mention a user in IM apps
49
+ Node = "Node" # a node in a forwarded message
50
+ Nodes = "Nodes" # a forwarded message consisting of multiple nodes
51
+ Poke = "Poke" # a poke message for Tencent QQ platform
52
+ Reply = "Reply" # a reply message segment
53
+ Forward = "Forward" # a forwarded message segment
51
54
  RPS = "RPS" # TODO
52
55
  Dice = "Dice" # TODO
53
56
  Shake = "Shake" # TODO
54
- Anonymous = "Anonymous" # TODO
55
57
  Share = "Share"
56
58
  Contact = "Contact" # TODO
57
59
  Location = "Location" # TODO
58
60
  Music = "Music"
59
- RedBag = "RedBag"
60
- Xml = "Xml"
61
61
  Json = "Json"
62
- CardImage = "CardImage"
63
- TTS = "TTS"
64
62
  Unknown = "Unknown"
65
-
66
63
  WechatEmoji = "WechatEmoji" # Wechat 下的 emoji 表情包
67
64
 
68
65
 
69
66
  class BaseMessageComponent(BaseModel):
70
67
  type: ComponentType
71
68
 
72
- def toString(self):
73
- output = f"[CQ:{self.type.lower()}"
74
- for k, v in self.__dict__.items():
75
- if k == "type" or v is None:
76
- continue
77
- if k == "_type":
78
- k = "type"
79
- if isinstance(v, bool):
80
- v = 1 if v else 0
81
- output += ",%s=%s" % (
82
- k,
83
- str(v)
84
- .replace("&", "&")
85
- .replace(",", ",")
86
- .replace("[", "[")
87
- .replace("]", "]"),
88
- )
89
- output += "]"
90
- return output
91
-
92
69
  def toDict(self):
93
70
  data = {}
94
71
  for k, v in self.__dict__.items():
@@ -99,25 +76,28 @@ class BaseMessageComponent(BaseModel):
99
76
  data[k] = v
100
77
  return {"type": self.type.lower(), "data": data}
101
78
 
79
+ async def to_dict(self) -> dict:
80
+ # 默认情况下,回退到旧的同步 toDict()
81
+ return self.toDict()
82
+
102
83
 
103
84
  class Plain(BaseMessageComponent):
104
- type: ComponentType = "Plain"
85
+ type = ComponentType.Plain
105
86
  text: str
106
- convert: T.Optional[bool] = True # 若为 False 则直接发送未转换 CQ 码的消息
87
+ convert: bool | None = True
107
88
 
108
89
  def __init__(self, text: str, convert: bool = True, **_):
109
90
  super().__init__(text=text, convert=convert, **_)
110
91
 
111
- def toString(self): # 没有 [CQ:plain] 这种东西,所以直接导出纯文本
112
- if not self.convert:
113
- return self.text
114
- return (
115
- self.text.replace("&", "&").replace("[", "[").replace("]", "]")
116
- )
92
+ def toDict(self):
93
+ return {"type": "text", "data": {"text": self.text.strip()}}
94
+
95
+ async def to_dict(self):
96
+ return {"type": "text", "data": {"text": self.text}}
117
97
 
118
98
 
119
99
  class Face(BaseMessageComponent):
120
- type: ComponentType = "Face"
100
+ type = ComponentType.Face
121
101
  id: int
122
102
 
123
103
  def __init__(self, **_):
@@ -125,18 +105,18 @@ class Face(BaseMessageComponent):
125
105
 
126
106
 
127
107
  class Record(BaseMessageComponent):
128
- type: ComponentType = "Record"
129
- file: T.Optional[str] = ""
130
- magic: T.Optional[bool] = False
131
- url: T.Optional[str] = ""
132
- cache: T.Optional[bool] = True
133
- proxy: T.Optional[bool] = True
134
- timeout: T.Optional[int] = 0
108
+ type = ComponentType.Record
109
+ file: str | None = ""
110
+ magic: bool | None = False
111
+ url: str | None = ""
112
+ cache: bool | None = True
113
+ proxy: bool | None = True
114
+ timeout: int | None = 0
135
115
  # 额外
136
- path: T.Optional[str]
116
+ path: str | None
137
117
 
138
- def __init__(self, file: T.Optional[str], **_):
139
- for k in _.keys():
118
+ def __init__(self, file: str | None, **_):
119
+ for k in _:
140
120
  if k == "url":
141
121
  pass
142
122
  # Protocol.warn(f"go-cqhttp doesn't support send {self.type} by {k}")
@@ -152,44 +132,52 @@ class Record(BaseMessageComponent):
152
132
  return Record(file=url, **_)
153
133
  raise Exception("not a valid url")
154
134
 
135
+ @staticmethod
136
+ def fromBase64(bs64_data: str, **_):
137
+ return Record(file=f"base64://{bs64_data}", **_)
138
+
155
139
  async def convert_to_file_path(self) -> str:
156
140
  """将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。
157
141
 
158
142
  Returns:
159
143
  str: 语音的本地路径,以绝对路径表示。
144
+
160
145
  """
161
- if self.file and self.file.startswith("file:///"):
162
- file_path = self.file[8:]
163
- return file_path
164
- elif self.file and self.file.startswith("http"):
146
+ if not self.file:
147
+ raise Exception(f"not a valid file: {self.file}")
148
+ if self.file.startswith("file:///"):
149
+ return self.file[8:]
150
+ if self.file.startswith("http"):
165
151
  file_path = await download_image_by_url(self.file)
166
152
  return os.path.abspath(file_path)
167
- elif self.file and self.file.startswith("base64://"):
153
+ if self.file.startswith("base64://"):
168
154
  bs64_data = self.file.removeprefix("base64://")
169
155
  image_bytes = base64.b64decode(bs64_data)
170
- file_path = f"data/temp/{uuid.uuid4()}.jpg"
156
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
157
+ file_path = os.path.join(temp_dir, f"{uuid.uuid4()}.jpg")
171
158
  with open(file_path, "wb") as f:
172
159
  f.write(image_bytes)
173
160
  return os.path.abspath(file_path)
174
- elif os.path.exists(self.file):
175
- file_path = self.file
176
- return os.path.abspath(file_path)
177
- else:
178
- raise Exception(f"not a valid file: {self.file}")
161
+ if os.path.exists(self.file):
162
+ return os.path.abspath(self.file)
163
+ raise Exception(f"not a valid file: {self.file}")
179
164
 
180
165
  async def convert_to_base64(self) -> str:
181
166
  """将语音统一转换为 base64 编码。这个方法避免了手动判断语音数据类型,直接返回语音数据的 base64 编码。
182
167
 
183
168
  Returns:
184
169
  str: 语音的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
170
+
185
171
  """
186
172
  # convert to base64
187
- if self.file and self.file.startswith("file:///"):
173
+ if not self.file:
174
+ raise Exception(f"not a valid file: {self.file}")
175
+ if self.file.startswith("file:///"):
188
176
  bs64_data = file_to_base64(self.file[8:])
189
- elif self.file and self.file.startswith("http"):
177
+ elif self.file.startswith("http"):
190
178
  file_path = await download_image_by_url(self.file)
191
179
  bs64_data = file_to_base64(file_path)
192
- elif self.file and self.file.startswith("base64://"):
180
+ elif self.file.startswith("base64://"):
193
181
  bs64_data = self.file
194
182
  elif os.path.exists(self.file):
195
183
  bs64_data = file_to_base64(self.file)
@@ -198,19 +186,39 @@ class Record(BaseMessageComponent):
198
186
  bs64_data = bs64_data.removeprefix("base64://")
199
187
  return bs64_data
200
188
 
189
+ async def register_to_file_service(self) -> str:
190
+ """将语音注册到文件服务。
191
+
192
+ Returns:
193
+ str: 注册后的URL
194
+
195
+ Raises:
196
+ Exception: 如果未配置 callback_api_base
197
+
198
+ """
199
+ callback_host = astrbot_config.get("callback_api_base")
200
+
201
+ if not callback_host:
202
+ raise Exception("未配置 callback_api_base,文件服务不可用")
203
+
204
+ file_path = await self.convert_to_file_path()
205
+
206
+ token = await file_token_service.register_file(file_path)
207
+
208
+ logger.debug(f"已注册:{callback_host}/api/file/{token}")
209
+
210
+ return f"{callback_host}/api/file/{token}"
211
+
201
212
 
202
213
  class Video(BaseMessageComponent):
203
- type: ComponentType = "Video"
214
+ type = ComponentType.Video
204
215
  file: str
205
- cover: T.Optional[str] = ""
206
- c: T.Optional[int] = 2
216
+ cover: str | None = ""
217
+ c: int | None = 2
207
218
  # 额外
208
- path: T.Optional[str] = ""
219
+ path: str | None = ""
209
220
 
210
221
  def __init__(self, file: str, **_):
211
- # for k in _.keys():
212
- # if k == "c" and _[k] not in [2, 3]:
213
- # logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
214
222
  super().__init__(file=file, **_)
215
223
 
216
224
  @staticmethod
@@ -223,15 +231,84 @@ class Video(BaseMessageComponent):
223
231
  return Video(file=url, **_)
224
232
  raise Exception("not a valid url")
225
233
 
234
+ async def convert_to_file_path(self) -> str:
235
+ """将这个视频统一转换为本地文件路径。这个方法避免了手动判断视频数据类型,直接返回视频数据的本地路径(如果是网络 URL,则会自动进行下载)。
236
+
237
+ Returns:
238
+ str: 视频的本地路径,以绝对路径表示。
239
+
240
+ """
241
+ url = self.file
242
+ if url and url.startswith("file:///"):
243
+ return url[8:]
244
+ if url and url.startswith("http"):
245
+ download_dir = os.path.join(get_astrbot_data_path(), "temp")
246
+ video_file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
247
+ await download_file(url, video_file_path)
248
+ if os.path.exists(video_file_path):
249
+ return os.path.abspath(video_file_path)
250
+ raise Exception(f"download failed: {url}")
251
+ if os.path.exists(url):
252
+ return os.path.abspath(url)
253
+ raise Exception(f"not a valid file: {url}")
254
+
255
+ async def register_to_file_service(self):
256
+ """将视频注册到文件服务。
257
+
258
+ Returns:
259
+ str: 注册后的URL
260
+
261
+ Raises:
262
+ Exception: 如果未配置 callback_api_base
263
+
264
+ """
265
+ callback_host = astrbot_config.get("callback_api_base")
266
+
267
+ if not callback_host:
268
+ raise Exception("未配置 callback_api_base,文件服务不可用")
269
+
270
+ file_path = await self.convert_to_file_path()
271
+
272
+ token = await file_token_service.register_file(file_path)
273
+
274
+ logger.debug(f"已注册:{callback_host}/api/file/{token}")
275
+
276
+ return f"{callback_host}/api/file/{token}"
277
+
278
+ async def to_dict(self):
279
+ """需要和 toDict 区分开,toDict 是同步方法"""
280
+ url_or_path = self.file
281
+ if url_or_path.startswith("http"):
282
+ payload_file = url_or_path
283
+ elif callback_host := astrbot_config.get("callback_api_base"):
284
+ callback_host = str(callback_host).removesuffix("/")
285
+ token = await file_token_service.register_file(url_or_path)
286
+ payload_file = f"{callback_host}/api/file/{token}"
287
+ logger.debug(f"Generated video file callback link: {payload_file}")
288
+ else:
289
+ payload_file = url_or_path
290
+ return {
291
+ "type": "video",
292
+ "data": {
293
+ "file": payload_file,
294
+ },
295
+ }
296
+
226
297
 
227
298
  class At(BaseMessageComponent):
228
- type: ComponentType = "At"
229
- qq: T.Union[int, str] # 此处str为all时代表所有人
230
- name: T.Optional[str] = ""
299
+ type = ComponentType.At
300
+ qq: int | str # 此处str为all时代表所有人
301
+ name: str | None = ""
231
302
 
232
303
  def __init__(self, **_):
233
304
  super().__init__(**_)
234
305
 
306
+ def toDict(self):
307
+ return {
308
+ "type": "at",
309
+ "data": {"qq": str(self.qq)},
310
+ }
311
+
235
312
 
236
313
  class AtAll(At):
237
314
  qq: str = "all"
@@ -241,74 +318,66 @@ class AtAll(At):
241
318
 
242
319
 
243
320
  class RPS(BaseMessageComponent): # TODO
244
- type: ComponentType = "RPS"
321
+ type = ComponentType.RPS
245
322
 
246
323
  def __init__(self, **_):
247
324
  super().__init__(**_)
248
325
 
249
326
 
250
327
  class Dice(BaseMessageComponent): # TODO
251
- type: ComponentType = "Dice"
328
+ type = ComponentType.Dice
252
329
 
253
330
  def __init__(self, **_):
254
331
  super().__init__(**_)
255
332
 
256
333
 
257
334
  class Shake(BaseMessageComponent): # TODO
258
- type: ComponentType = "Shake"
259
-
260
- def __init__(self, **_):
261
- super().__init__(**_)
262
-
263
-
264
- class Anonymous(BaseMessageComponent): # TODO
265
- type: ComponentType = "Anonymous"
266
- ignore: T.Optional[bool] = False
335
+ type = ComponentType.Shake
267
336
 
268
337
  def __init__(self, **_):
269
338
  super().__init__(**_)
270
339
 
271
340
 
272
341
  class Share(BaseMessageComponent):
273
- type: ComponentType = "Share"
342
+ type = ComponentType.Share
274
343
  url: str
275
344
  title: str
276
- content: T.Optional[str] = ""
277
- image: T.Optional[str] = ""
345
+ content: str | None = ""
346
+ image: str | None = ""
278
347
 
279
348
  def __init__(self, **_):
280
349
  super().__init__(**_)
281
350
 
282
351
 
283
352
  class Contact(BaseMessageComponent): # TODO
284
- type: ComponentType = "Contact"
353
+ type = ComponentType.Contact
285
354
  _type: str # type 字段冲突
286
- id: T.Optional[int] = 0
355
+ id: int | None = 0
287
356
 
288
357
  def __init__(self, **_):
289
358
  super().__init__(**_)
290
359
 
291
360
 
292
361
  class Location(BaseMessageComponent): # TODO
293
- type: ComponentType = "Location"
362
+ type = ComponentType.Location
294
363
  lat: float
295
364
  lon: float
296
- title: T.Optional[str] = ""
297
- content: T.Optional[str] = ""
365
+ title: str | None = ""
366
+ content: str | None = ""
298
367
 
299
368
  def __init__(self, **_):
300
369
  super().__init__(**_)
301
370
 
302
371
 
303
372
  class Music(BaseMessageComponent):
304
- type: ComponentType = "Music"
373
+ type = ComponentType.Music
305
374
  _type: str
306
- id: T.Optional[int] = 0
307
- url: T.Optional[str] = ""
308
- audio: T.Optional[str] = ""
309
- title: T.Optional[str] = ""
310
- content: T.Optional[str] = ""
311
- image: T.Optional[str] = ""
375
+ id: int | None = 0
376
+ url: str | None = ""
377
+ audio: str | None = ""
378
+ title: str | None = ""
379
+ content: str | None = ""
380
+ image: str | None = ""
312
381
 
313
382
  def __init__(self, **_):
314
383
  # for k in _.keys():
@@ -318,19 +387,19 @@ class Music(BaseMessageComponent):
318
387
 
319
388
 
320
389
  class Image(BaseMessageComponent):
321
- type: ComponentType = "Image"
322
- file: T.Optional[str] = ""
323
- _type: T.Optional[str] = ""
324
- subType: T.Optional[int] = 0
325
- url: T.Optional[str] = ""
326
- cache: T.Optional[bool] = True
327
- id: T.Optional[int] = 40000
328
- c: T.Optional[int] = 2
390
+ type = ComponentType.Image
391
+ file: str | None = ""
392
+ _type: str | None = ""
393
+ subType: int | None = 0
394
+ url: str | None = ""
395
+ cache: bool | None = True
396
+ id: int | None = 40000
397
+ c: int | None = 2
329
398
  # 额外
330
- path: T.Optional[str] = ""
331
- file_unique: T.Optional[str] = "" # 某些平台可能有图片缓存的唯一标识
399
+ path: str | None = ""
400
+ file_unique: str | None = "" # 某些平台可能有图片缓存的唯一标识
332
401
 
333
- def __init__(self, file: T.Optional[str], **_):
402
+ def __init__(self, file: str | None, **_):
334
403
  super().__init__(file=file, **_)
335
404
 
336
405
  @staticmethod
@@ -360,41 +429,45 @@ class Image(BaseMessageComponent):
360
429
 
361
430
  Returns:
362
431
  str: 图片的本地路径,以绝对路径表示。
432
+
363
433
  """
364
- url = self.url if self.url else self.file
365
- if url and url.startswith("file:///"):
366
- image_file_path = url[8:]
367
- return image_file_path
368
- elif url and url.startswith("http"):
434
+ url = self.url or self.file
435
+ if not url:
436
+ raise ValueError("No valid file or URL provided")
437
+ if url.startswith("file:///"):
438
+ return url[8:]
439
+ if url.startswith("http"):
369
440
  image_file_path = await download_image_by_url(url)
370
441
  return os.path.abspath(image_file_path)
371
- elif url and url.startswith("base64://"):
442
+ if url.startswith("base64://"):
372
443
  bs64_data = url.removeprefix("base64://")
373
444
  image_bytes = base64.b64decode(bs64_data)
374
- image_file_path = f"data/temp/{uuid.uuid4()}.jpg"
445
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
446
+ image_file_path = os.path.join(temp_dir, f"{uuid.uuid4()}.jpg")
375
447
  with open(image_file_path, "wb") as f:
376
448
  f.write(image_bytes)
377
449
  return os.path.abspath(image_file_path)
378
- elif os.path.exists(url):
379
- image_file_path = url
380
- return os.path.abspath(image_file_path)
381
- else:
382
- raise Exception(f"not a valid file: {url}")
450
+ if os.path.exists(url):
451
+ return os.path.abspath(url)
452
+ raise Exception(f"not a valid file: {url}")
383
453
 
384
454
  async def convert_to_base64(self) -> str:
385
455
  """将这个图片统一转换为 base64 编码。这个方法避免了手动判断图片数据类型,直接返回图片数据的 base64 编码。
386
456
 
387
457
  Returns:
388
458
  str: 图片的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
459
+
389
460
  """
390
461
  # convert to base64
391
- url = self.url if self.url else self.file
392
- if url and url.startswith("file:///"):
462
+ url = self.url or self.file
463
+ if not url:
464
+ raise ValueError("No valid file or URL provided")
465
+ if url.startswith("file:///"):
393
466
  bs64_data = file_to_base64(url[8:])
394
- elif url and url.startswith("http"):
467
+ elif url.startswith("http"):
395
468
  image_file_path = await download_image_by_url(url)
396
469
  bs64_data = file_to_base64(image_file_path)
397
- elif url and url.startswith("base64://"):
470
+ elif url.startswith("base64://"):
398
471
  bs64_data = url
399
472
  elif os.path.exists(url):
400
473
  bs64_data = file_to_base64(url)
@@ -403,45 +476,60 @@ class Image(BaseMessageComponent):
403
476
  bs64_data = bs64_data.removeprefix("base64://")
404
477
  return bs64_data
405
478
 
479
+ async def register_to_file_service(self) -> str:
480
+ """将图片注册到文件服务。
481
+
482
+ Returns:
483
+ str: 注册后的URL
484
+
485
+ Raises:
486
+ Exception: 如果未配置 callback_api_base
487
+
488
+ """
489
+ callback_host = astrbot_config.get("callback_api_base")
490
+
491
+ if not callback_host:
492
+ raise Exception("未配置 callback_api_base,文件服务不可用")
493
+
494
+ file_path = await self.convert_to_file_path()
495
+
496
+ token = await file_token_service.register_file(file_path)
497
+
498
+ logger.debug(f"已注册:{callback_host}/api/file/{token}")
499
+
500
+ return f"{callback_host}/api/file/{token}"
501
+
406
502
 
407
503
  class Reply(BaseMessageComponent):
408
- type: ComponentType = "Reply"
409
- id: T.Union[str, int]
504
+ type = ComponentType.Reply
505
+ id: str | int
410
506
  """所引用的消息 ID"""
411
- chain: T.Optional[T.List["BaseMessageComponent"]] = []
507
+ chain: list["BaseMessageComponent"] | None = []
412
508
  """被引用的消息段列表"""
413
- sender_id: T.Optional[int] | T.Optional[str] = 0
509
+ sender_id: int | None | str = 0
414
510
  """被引用的消息对应的发送者的 ID"""
415
- sender_nickname: T.Optional[str] = ""
511
+ sender_nickname: str | None = ""
416
512
  """被引用的消息对应的发送者的昵称"""
417
- time: T.Optional[int] = 0
513
+ time: int | None = 0
418
514
  """被引用的消息发送时间"""
419
- message_str: T.Optional[str] = ""
515
+ message_str: str | None = ""
420
516
  """被引用的消息解析后的纯文本消息字符串"""
421
517
 
422
- text: T.Optional[str] = ""
518
+ text: str | None = ""
423
519
  """deprecated"""
424
- qq: T.Optional[int] = 0
520
+ qq: int | None = 0
425
521
  """deprecated"""
426
- seq: T.Optional[int] = 0
522
+ seq: int | None = 0
427
523
  """deprecated"""
428
524
 
429
525
  def __init__(self, **_):
430
526
  super().__init__(**_)
431
527
 
432
528
 
433
- class RedBag(BaseMessageComponent):
434
- type: ComponentType = "RedBag"
435
- title: str
436
-
437
- def __init__(self, **_):
438
- super().__init__(**_)
439
-
440
-
441
529
  class Poke(BaseMessageComponent):
442
- type: str = ""
443
- id: T.Optional[int] = 0
444
- qq: T.Optional[int] = 0
530
+ type: str = ComponentType.Poke
531
+ id: int | None = 0
532
+ qq: int | None = 0
445
533
 
446
534
  def __init__(self, type: str, **_):
447
535
  type = f"Poke:{type}"
@@ -449,7 +537,7 @@ class Poke(BaseMessageComponent):
449
537
 
450
538
 
451
539
  class Forward(BaseMessageComponent):
452
- type: ComponentType = "Forward"
540
+ type = ComponentType.Forward
453
541
  id: str
454
542
 
455
543
  def __init__(self, **_):
@@ -459,57 +547,87 @@ class Forward(BaseMessageComponent):
459
547
  class Node(BaseMessageComponent):
460
548
  """群合并转发消息"""
461
549
 
462
- type: ComponentType = "Node"
463
- id: T.Optional[int] = 0 # 忽略
464
- name: T.Optional[str] = "" # qq昵称
465
- uin: T.Optional[int] = 0 # qq号
466
- content: T.Optional[T.Union[str, list, dict]] = "" # 子消息段列表
467
- seq: T.Optional[T.Union[str, list]] = "" # 忽略
468
- time: T.Optional[int] = 0
469
-
470
- def __init__(self, content: T.Union[str, list, dict, "Node", T.List["Node"]], **_):
471
- if isinstance(content, list):
472
- _content = None
473
- if all(isinstance(item, Node) for item in content):
474
- _content = [node.toDict() for node in content]
475
- else:
476
- _content = ""
477
- for chain in content:
478
- _content += chain.toString()
479
- content = _content
480
- elif isinstance(content, Node):
481
- content = content.toDict()
550
+ type = ComponentType.Node
551
+ id: int | None = 0 # 忽略
552
+ name: str | None = "" # qq昵称
553
+ uin: str | None = "0" # qq号
554
+ content: list[BaseMessageComponent] | None = []
555
+ seq: str | list | None = "" # 忽略
556
+ time: int | None = 0 # 忽略
557
+
558
+ def __init__(self, content: list[BaseMessageComponent], **_):
559
+ if isinstance(content, Node):
560
+ # back
561
+ content = [content]
482
562
  super().__init__(content=content, **_)
483
563
 
484
- def toString(self):
485
- # logger.warn("Protocol: node doesn't support stringify")
486
- return ""
564
+ async def to_dict(self):
565
+ data_content = []
566
+ for comp in self.content:
567
+ if isinstance(comp, (Image, Record)):
568
+ # For Image and Record segments, we convert them to base64
569
+ bs64 = await comp.convert_to_base64()
570
+ data_content.append(
571
+ {
572
+ "type": comp.type.lower(),
573
+ "data": {"file": f"base64://{bs64}"},
574
+ },
575
+ )
576
+ elif isinstance(comp, Plain):
577
+ # For Plain segments, we need to handle the plain differently
578
+ d = await comp.to_dict()
579
+ data_content.append(d)
580
+ elif isinstance(comp, File):
581
+ # For File segments, we need to handle the file differently
582
+ d = await comp.to_dict()
583
+ data_content.append(d)
584
+ elif isinstance(comp, (Node, Nodes)):
585
+ # For Node segments, we recursively convert them to dict
586
+ d = await comp.to_dict()
587
+ data_content.append(d)
588
+ else:
589
+ d = comp.toDict()
590
+ data_content.append(d)
591
+ return {
592
+ "type": "node",
593
+ "data": {
594
+ "user_id": str(self.uin),
595
+ "nickname": self.name,
596
+ "content": data_content,
597
+ },
598
+ }
487
599
 
488
600
 
489
601
  class Nodes(BaseMessageComponent):
490
- type: ComponentType = "Nodes"
491
- nodes: T.List[Node]
602
+ type = ComponentType.Nodes
603
+ nodes: list[Node]
492
604
 
493
- def __init__(self, nodes: T.List[Node], **_):
605
+ def __init__(self, nodes: list[Node], **_):
494
606
  super().__init__(nodes=nodes, **_)
495
607
 
496
608
  def toDict(self):
497
- return {"messages": [node.toDict() for node in self.nodes]}
498
-
499
-
500
- class Xml(BaseMessageComponent):
501
- type: ComponentType = "Xml"
502
- data: str
503
- resid: T.Optional[int] = 0
504
-
505
- def __init__(self, **_):
506
- super().__init__(**_)
609
+ """Deprecated. Use to_dict instead"""
610
+ ret = {
611
+ "messages": [],
612
+ }
613
+ for node in self.nodes:
614
+ d = node.toDict()
615
+ ret["messages"].append(d)
616
+ return ret
617
+
618
+ async def to_dict(self):
619
+ """将 Nodes 转换为字典格式,适用于 OneBot JSON 格式"""
620
+ ret = {"messages": []}
621
+ for node in self.nodes:
622
+ d = await node.to_dict()
623
+ ret["messages"].append(d)
624
+ return ret
507
625
 
508
626
 
509
627
  class Json(BaseMessageComponent):
510
- type: ComponentType = "Json"
511
- data: T.Union[str, dict]
512
- resid: T.Optional[int] = 0
628
+ type = ComponentType.Json
629
+ data: str | dict
630
+ resid: int | None = 0
513
631
 
514
632
  def __init__(self, data, **_):
515
633
  if isinstance(data, dict):
@@ -517,80 +635,49 @@ class Json(BaseMessageComponent):
517
635
  super().__init__(data=data, **_)
518
636
 
519
637
 
520
- class CardImage(BaseMessageComponent):
521
- type: ComponentType = "CardImage"
522
- file: str
523
- cache: T.Optional[bool] = True
524
- minwidth: T.Optional[int] = 400
525
- minheight: T.Optional[int] = 400
526
- maxwidth: T.Optional[int] = 500
527
- maxheight: T.Optional[int] = 500
528
- source: T.Optional[str] = ""
529
- icon: T.Optional[str] = ""
530
-
531
- def __init__(self, **_):
532
- super().__init__(**_)
533
-
534
- @staticmethod
535
- def fromFileSystem(path, **_):
536
- return CardImage(file=f"file:///{os.path.abspath(path)}", **_)
537
-
538
-
539
- class TTS(BaseMessageComponent):
540
- type: ComponentType = "TTS"
541
- text: str
542
-
543
- def __init__(self, **_):
544
- super().__init__(**_)
545
-
546
-
547
638
  class Unknown(BaseMessageComponent):
548
- type: ComponentType = "Unknown"
639
+ type = ComponentType.Unknown
549
640
  text: str
550
641
 
551
- def toString(self):
552
- return ""
553
-
554
642
 
555
643
  class File(BaseMessageComponent):
556
- """
557
- 文件消息段
558
- """
644
+ """文件消息段"""
559
645
 
560
- type: ComponentType = "File"
561
- name: T.Optional[str] = "" # 名字
562
- _file: T.Optional[str] = "" # 本地路径
563
- url: T.Optional[str] = "" # url
564
- _downloaded: bool = False # 是否已经下载
646
+ type = ComponentType.File
647
+ name: str | None = "" # 名字
648
+ file_: str | None = "" # 本地路径
649
+ url: str | None = "" # url
565
650
 
566
- def __init__(self, name: str = "", file: str = "", url: str = ""):
567
- super().__init__(name=name, _file=file, url=url)
651
+ def __init__(self, name: str, file: str = "", url: str = ""):
652
+ """文件消息段。"""
653
+ super().__init__(name=name, file_=file, url=url)
568
654
 
569
655
  @property
570
656
  def file(self) -> str:
571
- """
572
- 获取文件路径,如果文件不存在但有URL,则同步下载文件
657
+ """获取文件路径,如果文件不存在但有URL,则同步下载文件
573
658
 
574
659
  Returns:
575
660
  str: 文件路径
661
+
576
662
  """
577
- if self._file and os.path.exists(self._file):
578
- return self._file
663
+ if self.file_ and os.path.exists(self.file_):
664
+ return os.path.abspath(self.file_)
579
665
 
580
- if self.url and not self._downloaded:
666
+ if self.url:
581
667
  try:
582
668
  loop = asyncio.get_event_loop()
583
669
  if loop.is_running():
584
670
  logger.warning(
585
- "不可以在异步上下文中同步等待下载! 请使用 await get_file() 代替"
671
+ "不可以在异步上下文中同步等待下载! "
672
+ "这个警告通常发生于某些逻辑试图通过 <File>.file 获取文件消息段的文件内容。"
673
+ "请使用 await get_file() 代替直接获取 <File>.file 字段",
586
674
  )
587
675
  return ""
588
- else:
589
- # 等待下载完成
590
- loop.run_until_complete(self._download_file())
676
+ # 等待下载完成
677
+ loop.run_until_complete(self._download_file())
591
678
 
592
- if self._file and os.path.exists(self._file):
593
- return self._file
679
+ if self.file_ and os.path.exists(self.file_):
680
+ return os.path.abspath(self.file_)
594
681
  except Exception as e:
595
682
  logger.error(f"文件下载失败: {e}")
596
683
 
@@ -598,86 +685,125 @@ class File(BaseMessageComponent):
598
685
 
599
686
  @file.setter
600
687
  def file(self, value: str):
601
- """
602
- 向前兼容, 设置file属性, 传入的参数可能是文件路径或URL
688
+ """向前兼容, 设置file属性, 传入的参数可能是文件路径或URL
603
689
 
604
690
  Args:
605
691
  value (str): 文件路径或URL
692
+
606
693
  """
607
694
  if value.startswith("http://") or value.startswith("https://"):
608
695
  self.url = value
609
696
  else:
610
- self._file = value
697
+ self.file_ = value
611
698
 
612
- async def get_file(self) -> str:
613
- """
614
- 异步获取文件
615
- To 插件开发者: 请注意在使用后清理下载的文件, 以免占用过多空间
699
+ async def get_file(self, allow_return_url: bool = False) -> str:
700
+ """异步获取文件。请注意在使用后清理下载的文件, 以免占用过多空间
616
701
 
702
+ Args:
703
+ allow_return_url: 是否允许以文件 http 下载链接的形式返回,这允许您自行控制是否需要下载文件。
704
+ 注意,如果为 True,也可能返回文件路径。
617
705
  Returns:
618
- str: 文件路径
706
+ str: 文件路径或者 http 下载链接
707
+
619
708
  """
620
- if self._file and os.path.exists(self._file):
621
- return self._file
709
+ if allow_return_url and self.url:
710
+ return self.url
711
+
712
+ if self.file_ and os.path.exists(self.file_):
713
+ return os.path.abspath(self.file_)
622
714
 
623
715
  if self.url:
624
716
  await self._download_file()
625
- return self._file
717
+ return os.path.abspath(self.file_)
626
718
 
627
719
  return ""
628
720
 
629
721
  async def _download_file(self):
630
722
  """下载文件"""
631
- if self._downloaded:
632
- return
723
+ download_dir = os.path.join(get_astrbot_data_path(), "temp")
724
+ os.makedirs(download_dir, exist_ok=True)
725
+ file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
726
+ await download_file(self.url, file_path)
727
+ self.file_ = os.path.abspath(file_path)
633
728
 
634
- os.makedirs("data/download", exist_ok=True)
635
- filename = self.name or f"{uuid.uuid4().hex}"
636
- file_path = f"data/download/{filename}"
729
+ async def register_to_file_service(self):
730
+ """将文件注册到文件服务。
637
731
 
638
- await download_file(self.url, file_path)
732
+ Returns:
733
+ str: 注册后的URL
734
+
735
+ Raises:
736
+ Exception: 如果未配置 callback_api_base
737
+
738
+ """
739
+ callback_host = astrbot_config.get("callback_api_base")
639
740
 
640
- self._file = file_path
641
- self._downloaded = True
741
+ if not callback_host:
742
+ raise Exception("未配置 callback_api_base,文件服务不可用")
743
+
744
+ file_path = await self.get_file()
745
+
746
+ token = await file_token_service.register_file(file_path)
747
+
748
+ logger.debug(f"已注册:{callback_host}/api/file/{token}")
749
+
750
+ return f"{callback_host}/api/file/{token}"
751
+
752
+ async def to_dict(self):
753
+ """需要和 toDict 区分开,toDict 是同步方法"""
754
+ url_or_path = await self.get_file(allow_return_url=True)
755
+ if url_or_path.startswith("http"):
756
+ payload_file = url_or_path
757
+ elif callback_host := astrbot_config.get("callback_api_base"):
758
+ callback_host = str(callback_host).removesuffix("/")
759
+ token = await file_token_service.register_file(url_or_path)
760
+ payload_file = f"{callback_host}/api/file/{token}"
761
+ logger.debug(f"Generated file callback link: {payload_file}")
762
+ else:
763
+ payload_file = url_or_path
764
+ return {
765
+ "type": "file",
766
+ "data": {
767
+ "name": self.name,
768
+ "file": payload_file,
769
+ },
770
+ }
642
771
 
643
772
 
644
773
  class WechatEmoji(BaseMessageComponent):
645
- type: ComponentType = "WechatEmoji"
646
- md5: T.Optional[str] = ""
647
- md5_len: T.Optional[int] = 0
648
- cdnurl: T.Optional[str] = ""
774
+ type = ComponentType.WechatEmoji
775
+ md5: str | None = ""
776
+ md5_len: int | None = 0
777
+ cdnurl: str | None = ""
649
778
 
650
779
  def __init__(self, **_):
651
780
  super().__init__(**_)
652
781
 
653
782
 
654
783
  ComponentTypes = {
784
+ # Basic Message Segments
655
785
  "plain": Plain,
656
786
  "text": Plain,
657
- "face": Face,
787
+ "image": Image,
658
788
  "record": Record,
659
789
  "video": Video,
790
+ "file": File,
791
+ # IM-specific Message Segments
792
+ "face": Face,
660
793
  "at": At,
661
794
  "rps": RPS,
662
795
  "dice": Dice,
663
796
  "shake": Shake,
664
- "anonymous": Anonymous,
665
797
  "share": Share,
666
798
  "contact": Contact,
667
799
  "location": Location,
668
800
  "music": Music,
669
- "image": Image,
670
801
  "reply": Reply,
671
- "redbag": RedBag,
672
802
  "poke": Poke,
673
803
  "forward": Forward,
674
804
  "node": Node,
675
805
  "nodes": Nodes,
676
- "xml": Xml,
677
806
  "json": Json,
678
- "cardimage": CardImage,
679
- "tts": TTS,
680
807
  "unknown": Unknown,
681
- "file": File,
682
808
  "WechatEmoji": WechatEmoji,
683
809
  }