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,14 +1,30 @@
1
- import typing
1
+ import asyncio
2
+ import inspect
3
+ import os
2
4
  import traceback
3
- from .route import Route, Response, RouteContext
5
+
4
6
  from quart import request
5
- from astrbot.core.config.default import CONFIG_METADATA_2, DEFAULT_VALUE_MAP
7
+
8
+ from astrbot.core import file_token_service, logger
6
9
  from astrbot.core.config.astrbot_config import AstrBotConfig
10
+ from astrbot.core.config.default import (
11
+ CONFIG_METADATA_2,
12
+ CONFIG_METADATA_3,
13
+ CONFIG_METADATA_3_SYSTEM,
14
+ DEFAULT_CONFIG,
15
+ DEFAULT_VALUE_MAP,
16
+ )
17
+ from astrbot.core.config.i18n_utils import ConfigMetadataI18n
7
18
  from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
8
- from astrbot.core.platform.register import platform_registry
19
+ from astrbot.core.platform.register import platform_cls_map, platform_registry
20
+ from astrbot.core.provider import Provider
21
+ from astrbot.core.provider.entities import ProviderType
22
+ from astrbot.core.provider.provider import RerankProvider
9
23
  from astrbot.core.provider.register import provider_registry
10
24
  from astrbot.core.star.star import star_registry
11
- from astrbot.core import logger
25
+ from astrbot.core.utils.astrbot_path import get_astrbot_path
26
+
27
+ from .route import Response, Route, RouteContext
12
28
 
13
29
 
14
30
  def try_cast(value: str, type_: str):
@@ -21,9 +37,7 @@ def try_cast(value: str, type_: str):
21
37
  type_ == "float"
22
38
  and isinstance(value, str)
23
39
  and value.replace(".", "", 1).isdigit()
24
- ):
25
- return float(value)
26
- elif type_ == "float" and isinstance(value, int):
40
+ ) or (type_ == "float" and isinstance(value, int)):
27
41
  return float(value)
28
42
  elif type_ == "float":
29
43
  try:
@@ -32,32 +46,12 @@ def try_cast(value: str, type_: str):
32
46
  return None
33
47
 
34
48
 
35
- def validate_config(
36
- data, schema: dict, is_core: bool
37
- ) -> typing.Tuple[typing.List[str], typing.Dict]:
49
+ def validate_config(data, schema: dict, is_core: bool) -> tuple[list[str], dict]:
38
50
  errors = []
39
51
 
40
52
  def validate(data: dict, metadata: dict = schema, path=""):
41
53
  for key, value in data.items():
42
54
  if key not in metadata:
43
- # 无 schema 的配置项,执行类型猜测
44
- if isinstance(value, str):
45
- try:
46
- data[key] = int(value)
47
- continue
48
- except ValueError:
49
- pass
50
-
51
- try:
52
- data[key] = float(value)
53
- continue
54
- except ValueError:
55
- pass
56
-
57
- if value.lower() == "true":
58
- data[key] = True
59
- elif value.lower() == "false":
60
- data[key] = False
61
55
  continue
62
56
  meta = metadata[key]
63
57
  if "type" not in meta:
@@ -69,7 +63,7 @@ def validate_config(
69
63
  continue
70
64
  if meta["type"] == "list" and not isinstance(value, list):
71
65
  errors.append(
72
- f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}"
66
+ f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}",
73
67
  )
74
68
  elif (
75
69
  meta["type"] == "list"
@@ -88,40 +82,40 @@ def validate_config(
88
82
  casted = try_cast(value, "int")
89
83
  if casted is None:
90
84
  errors.append(
91
- f"错误的类型 {path}{key}: 期望是 int, 得到了 {type(value).__name__}"
85
+ f"错误的类型 {path}{key}: 期望是 int, 得到了 {type(value).__name__}",
92
86
  )
93
87
  data[key] = casted
94
88
  elif meta["type"] == "float" and not isinstance(value, float):
95
89
  casted = try_cast(value, "float")
96
90
  if casted is None:
97
91
  errors.append(
98
- f"错误的类型 {path}{key}: 期望是 float, 得到了 {type(value).__name__}"
92
+ f"错误的类型 {path}{key}: 期望是 float, 得到了 {type(value).__name__}",
99
93
  )
100
94
  data[key] = casted
101
95
  elif meta["type"] == "bool" and not isinstance(value, bool):
102
96
  errors.append(
103
- f"错误的类型 {path}{key}: 期望是 bool, 得到了 {type(value).__name__}"
97
+ f"错误的类型 {path}{key}: 期望是 bool, 得到了 {type(value).__name__}",
104
98
  )
105
99
  elif meta["type"] in ["string", "text"] and not isinstance(value, str):
106
100
  errors.append(
107
- f"错误的类型 {path}{key}: 期望是 string, 得到了 {type(value).__name__}"
101
+ f"错误的类型 {path}{key}: 期望是 string, 得到了 {type(value).__name__}",
108
102
  )
109
103
  elif meta["type"] == "list" and not isinstance(value, list):
110
104
  errors.append(
111
- f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}"
105
+ f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}",
112
106
  )
113
107
  elif meta["type"] == "object" and not isinstance(value, dict):
114
108
  errors.append(
115
- f"错误的类型 {path}{key}: 期望是 dict, 得到了 {type(value).__name__}"
109
+ f"错误的类型 {path}{key}: 期望是 dict, 得到了 {type(value).__name__}",
116
110
  )
117
111
 
118
112
  if is_core:
119
- for key, group in schema.items():
120
- group_meta = group.get("metadata")
121
- if not group_meta:
122
- continue
123
- # logger.info(f"验证配置: 组 {key} ...")
124
- validate(data, group_meta, path=f"{key}.")
113
+ meta_all = {
114
+ **schema["platform_group"]["metadata"],
115
+ **schema["provider_group"]["metadata"],
116
+ **schema["misc_config_group"]["metadata"],
117
+ }
118
+ validate(data, meta_all)
125
119
  else:
126
120
  validate(data, schema)
127
121
 
@@ -131,42 +125,450 @@ def validate_config(
131
125
  def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False):
132
126
  """验证并保存配置"""
133
127
  errors = None
128
+ logger.info(f"Saving config, is_core={is_core}")
134
129
  try:
135
130
  if is_core:
136
131
  errors, post_config = validate_config(
137
- post_config, CONFIG_METADATA_2, is_core
132
+ post_config,
133
+ CONFIG_METADATA_2,
134
+ is_core,
138
135
  )
139
136
  else:
140
- errors, post_config = validate_config(post_config, config.schema, is_core)
137
+ errors, post_config = validate_config(
138
+ post_config, getattr(config, "schema", {}), is_core
139
+ )
141
140
  except BaseException as e:
142
141
  logger.error(traceback.format_exc())
143
142
  logger.warning(f"验证配置时出现异常: {e}")
144
143
  raise ValueError(f"验证配置时出现异常: {e}")
145
144
  if errors:
146
145
  raise ValueError(f"格式校验未通过: {errors}")
146
+
147
147
  config.save_config(post_config)
148
148
 
149
149
 
150
150
  class ConfigRoute(Route):
151
151
  def __init__(
152
- self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle
152
+ self,
153
+ context: RouteContext,
154
+ core_lifecycle: AstrBotCoreLifecycle,
153
155
  ) -> None:
154
156
  super().__init__(context)
155
157
  self.core_lifecycle = core_lifecycle
158
+ self.config: AstrBotConfig = core_lifecycle.astrbot_config
159
+ self._logo_token_cache = {} # 缓存logo token,避免重复注册
160
+ self.acm = core_lifecycle.astrbot_config_mgr
161
+ self.ucr = core_lifecycle.umop_config_router
156
162
  self.routes = {
163
+ "/config/abconf/new": ("POST", self.create_abconf),
164
+ "/config/abconf": ("GET", self.get_abconf),
165
+ "/config/abconfs": ("GET", self.get_abconf_list),
166
+ "/config/abconf/delete": ("POST", self.delete_abconf),
167
+ "/config/abconf/update": ("POST", self.update_abconf),
168
+ "/config/umo_abconf_routes": ("GET", self.get_uc_table),
169
+ "/config/umo_abconf_route/update_all": ("POST", self.update_ucr_all),
170
+ "/config/umo_abconf_route/update": ("POST", self.update_ucr),
171
+ "/config/umo_abconf_route/delete": ("POST", self.delete_ucr),
157
172
  "/config/get": ("GET", self.get_configs),
173
+ "/config/default": ("GET", self.get_default_config),
158
174
  "/config/astrbot/update": ("POST", self.post_astrbot_configs),
159
175
  "/config/plugin/update": ("POST", self.post_plugin_configs),
160
176
  "/config/platform/new": ("POST", self.post_new_platform),
161
177
  "/config/platform/update": ("POST", self.post_update_platform),
162
178
  "/config/platform/delete": ("POST", self.post_delete_platform),
179
+ "/config/platform/list": ("GET", self.get_platform_list),
163
180
  "/config/provider/new": ("POST", self.post_new_provider),
164
181
  "/config/provider/update": ("POST", self.post_update_provider),
165
182
  "/config/provider/delete": ("POST", self.post_delete_provider),
166
- "/config/llmtools": ("GET", self.get_llm_tools),
183
+ "/config/provider/check_one": ("GET", self.check_one_provider_status),
184
+ "/config/provider/list": ("GET", self.get_provider_config_list),
185
+ "/config/provider/model_list": ("GET", self.get_provider_model_list),
186
+ "/config/provider/get_embedding_dim": ("POST", self.get_embedding_dim),
167
187
  }
168
188
  self.register_routes()
169
189
 
190
+ async def get_uc_table(self):
191
+ """获取 UMOP 配置路由表"""
192
+ return Response().ok({"routing": self.ucr.umop_to_conf_id}).__dict__
193
+
194
+ async def update_ucr_all(self):
195
+ """更新 UMOP 配置路由表的全部内容"""
196
+ post_data = await request.json
197
+ if not post_data:
198
+ return Response().error("缺少配置数据").__dict__
199
+
200
+ new_routing = post_data.get("routing", None)
201
+
202
+ if not new_routing or not isinstance(new_routing, dict):
203
+ return Response().error("缺少或错误的路由表数据").__dict__
204
+
205
+ try:
206
+ await self.ucr.update_routing_data(new_routing)
207
+ return Response().ok(message="更新成功").__dict__
208
+ except Exception as e:
209
+ logger.error(traceback.format_exc())
210
+ return Response().error(f"更新路由表失败: {e!s}").__dict__
211
+
212
+ async def update_ucr(self):
213
+ """更新 UMOP 配置路由表"""
214
+ post_data = await request.json
215
+ if not post_data:
216
+ return Response().error("缺少配置数据").__dict__
217
+
218
+ umo = post_data.get("umo", None)
219
+ conf_id = post_data.get("conf_id", None)
220
+
221
+ if not umo or not conf_id:
222
+ return Response().error("缺少 UMO 或配置文件 ID").__dict__
223
+
224
+ try:
225
+ await self.ucr.update_route(umo, conf_id)
226
+ return Response().ok(message="更新成功").__dict__
227
+ except Exception as e:
228
+ logger.error(traceback.format_exc())
229
+ return Response().error(f"更新路由表失败: {e!s}").__dict__
230
+
231
+ async def delete_ucr(self):
232
+ """删除 UMOP 配置路由表中的一项"""
233
+ post_data = await request.json
234
+ if not post_data:
235
+ return Response().error("缺少配置数据").__dict__
236
+
237
+ umo = post_data.get("umo", None)
238
+
239
+ if not umo:
240
+ return Response().error("缺少 UMO").__dict__
241
+
242
+ try:
243
+ if umo in self.ucr.umop_to_conf_id:
244
+ del self.ucr.umop_to_conf_id[umo]
245
+ await self.ucr.update_routing_data(self.ucr.umop_to_conf_id)
246
+ return Response().ok(message="删除成功").__dict__
247
+ except Exception as e:
248
+ logger.error(traceback.format_exc())
249
+ return Response().error(f"删除路由表项失败: {e!s}").__dict__
250
+
251
+ async def get_default_config(self):
252
+ """获取默认配置文件"""
253
+ metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3)
254
+ return Response().ok({"config": DEFAULT_CONFIG, "metadata": metadata}).__dict__
255
+
256
+ async def get_abconf_list(self):
257
+ """获取所有 AstrBot 配置文件的列表"""
258
+ abconf_list = self.acm.get_conf_list()
259
+ return Response().ok({"info_list": abconf_list}).__dict__
260
+
261
+ async def create_abconf(self):
262
+ """创建新的 AstrBot 配置文件"""
263
+ post_data = await request.json
264
+ if not post_data:
265
+ return Response().error("缺少配置数据").__dict__
266
+ name = post_data.get("name", None)
267
+ config = post_data.get("config", DEFAULT_CONFIG)
268
+
269
+ try:
270
+ conf_id = self.acm.create_conf(name=name, config=config)
271
+ return Response().ok(message="创建成功", data={"conf_id": conf_id}).__dict__
272
+ except ValueError as e:
273
+ return Response().error(str(e)).__dict__
274
+
275
+ async def get_abconf(self):
276
+ """获取指定 AstrBot 配置文件"""
277
+ abconf_id = request.args.get("id")
278
+ system_config = request.args.get("system_config", "0").lower() == "1"
279
+ if not abconf_id and not system_config:
280
+ return Response().error("缺少配置文件 ID").__dict__
281
+
282
+ try:
283
+ if system_config:
284
+ abconf = self.acm.confs["default"]
285
+ metadata = ConfigMetadataI18n.convert_to_i18n_keys(
286
+ CONFIG_METADATA_3_SYSTEM
287
+ )
288
+ return Response().ok({"config": abconf, "metadata": metadata}).__dict__
289
+ if abconf_id is None:
290
+ raise ValueError("abconf_id cannot be None")
291
+ abconf = self.acm.confs[abconf_id]
292
+ metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3)
293
+ return Response().ok({"config": abconf, "metadata": metadata}).__dict__
294
+ except ValueError as e:
295
+ return Response().error(str(e)).__dict__
296
+
297
+ async def delete_abconf(self):
298
+ """删除指定 AstrBot 配置文件"""
299
+ post_data = await request.json
300
+ if not post_data:
301
+ return Response().error("缺少配置数据").__dict__
302
+
303
+ conf_id = post_data.get("id")
304
+ if not conf_id:
305
+ return Response().error("缺少配置文件 ID").__dict__
306
+
307
+ try:
308
+ success = self.acm.delete_conf(conf_id)
309
+ if success:
310
+ return Response().ok(message="删除成功").__dict__
311
+ return Response().error("删除失败").__dict__
312
+ except ValueError as e:
313
+ return Response().error(str(e)).__dict__
314
+ except Exception as e:
315
+ logger.error(traceback.format_exc())
316
+ return Response().error(f"删除配置文件失败: {e!s}").__dict__
317
+
318
+ async def update_abconf(self):
319
+ """更新指定 AstrBot 配置文件信息"""
320
+ post_data = await request.json
321
+ if not post_data:
322
+ return Response().error("缺少配置数据").__dict__
323
+
324
+ conf_id = post_data.get("id")
325
+ if not conf_id:
326
+ return Response().error("缺少配置文件 ID").__dict__
327
+
328
+ name = post_data.get("name")
329
+
330
+ try:
331
+ success = self.acm.update_conf_info(conf_id, name=name)
332
+ if success:
333
+ return Response().ok(message="更新成功").__dict__
334
+ return Response().error("更新失败").__dict__
335
+ except ValueError as e:
336
+ return Response().error(str(e)).__dict__
337
+ except Exception as e:
338
+ logger.error(traceback.format_exc())
339
+ return Response().error(f"更新配置文件失败: {e!s}").__dict__
340
+
341
+ async def _test_single_provider(self, provider):
342
+ """辅助函数:测试单个 provider 的可用性"""
343
+ meta = provider.meta()
344
+ provider_name = provider.provider_config.get("id", "Unknown Provider")
345
+ provider_capability_type = meta.provider_type
346
+
347
+ status_info = {
348
+ "id": getattr(meta, "id", "Unknown ID"),
349
+ "model": getattr(meta, "model", "Unknown Model"),
350
+ "type": provider_capability_type.value,
351
+ "name": provider_name,
352
+ "status": "unavailable", # 默认为不可用
353
+ "error": None,
354
+ }
355
+ logger.debug(
356
+ f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})",
357
+ )
358
+
359
+ if provider_capability_type == ProviderType.CHAT_COMPLETION:
360
+ try:
361
+ logger.debug(f"Sending 'Ping' to provider: {status_info['name']}")
362
+ response = await asyncio.wait_for(
363
+ provider.text_chat(prompt="REPLY `PONG` ONLY"),
364
+ timeout=45.0,
365
+ )
366
+ logger.debug(
367
+ f"Received response from {status_info['name']}: {response}",
368
+ )
369
+ if response is not None:
370
+ status_info["status"] = "available"
371
+ response_text_snippet = ""
372
+ if (
373
+ hasattr(response, "completion_text")
374
+ and response.completion_text
375
+ ):
376
+ response_text_snippet = (
377
+ response.completion_text[:70] + "..."
378
+ if len(response.completion_text) > 70
379
+ else response.completion_text
380
+ )
381
+ elif hasattr(response, "result_chain") and response.result_chain:
382
+ try:
383
+ response_text_snippet = (
384
+ response.result_chain.get_plain_text()[:70] + "..."
385
+ if len(response.result_chain.get_plain_text()) > 70
386
+ else response.result_chain.get_plain_text()
387
+ )
388
+ except Exception as _:
389
+ pass
390
+ logger.info(
391
+ f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'",
392
+ )
393
+ else:
394
+ status_info["error"] = (
395
+ "Test call returned None, but expected an LLMResponse object."
396
+ )
397
+ logger.warning(
398
+ f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.",
399
+ )
400
+
401
+ except asyncio.TimeoutError:
402
+ status_info["error"] = (
403
+ "Connection timed out after 45 seconds during test call."
404
+ )
405
+ logger.warning(
406
+ f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.",
407
+ )
408
+ except Exception as e:
409
+ error_message = str(e)
410
+ status_info["error"] = error_message
411
+ logger.warning(
412
+ f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
413
+ )
414
+ logger.debug(
415
+ f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
416
+ )
417
+
418
+ elif provider_capability_type == ProviderType.EMBEDDING:
419
+ try:
420
+ # For embedding, we can call the get_embedding method with a short prompt.
421
+ embedding_result = await provider.get_embedding("health_check")
422
+ if isinstance(embedding_result, list) and (
423
+ not embedding_result or isinstance(embedding_result[0], float)
424
+ ):
425
+ status_info["status"] = "available"
426
+ else:
427
+ status_info["status"] = "unavailable"
428
+ status_info["error"] = (
429
+ f"Embedding test failed: unexpected result type {type(embedding_result)}"
430
+ )
431
+ except Exception as e:
432
+ logger.error(
433
+ f"Error testing embedding provider {provider_name}: {e}",
434
+ exc_info=True,
435
+ )
436
+ status_info["status"] = "unavailable"
437
+ status_info["error"] = f"Embedding test failed: {e!s}"
438
+
439
+ elif provider_capability_type == ProviderType.TEXT_TO_SPEECH:
440
+ try:
441
+ # For TTS, we can call the get_audio method with a short prompt.
442
+ audio_result = await provider.get_audio("你好")
443
+ if isinstance(audio_result, str) and audio_result:
444
+ status_info["status"] = "available"
445
+ else:
446
+ status_info["status"] = "unavailable"
447
+ status_info["error"] = (
448
+ f"TTS test failed: unexpected result type {type(audio_result)}"
449
+ )
450
+ except Exception as e:
451
+ logger.error(
452
+ f"Error testing TTS provider {provider_name}: {e}",
453
+ exc_info=True,
454
+ )
455
+ status_info["status"] = "unavailable"
456
+ status_info["error"] = f"TTS test failed: {e!s}"
457
+ elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
458
+ try:
459
+ logger.debug(
460
+ f"Sending health check audio to provider: {status_info['name']}",
461
+ )
462
+ sample_audio_path = os.path.join(
463
+ get_astrbot_path(),
464
+ "samples",
465
+ "stt_health_check.wav",
466
+ )
467
+ if not os.path.exists(sample_audio_path):
468
+ status_info["status"] = "unavailable"
469
+ status_info["error"] = (
470
+ "STT test failed: sample audio file not found."
471
+ )
472
+ logger.warning(
473
+ f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}",
474
+ )
475
+ else:
476
+ text_result = await provider.get_text(sample_audio_path)
477
+ if isinstance(text_result, str) and text_result:
478
+ status_info["status"] = "available"
479
+ snippet = (
480
+ text_result[:70] + "..."
481
+ if len(text_result) > 70
482
+ else text_result
483
+ )
484
+ logger.info(
485
+ f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'",
486
+ )
487
+ else:
488
+ status_info["status"] = "unavailable"
489
+ status_info["error"] = (
490
+ f"STT test failed: unexpected result type {type(text_result)}"
491
+ )
492
+ logger.warning(
493
+ f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}",
494
+ )
495
+ except Exception as e:
496
+ logger.error(
497
+ f"Error testing STT provider {provider_name}: {e}",
498
+ exc_info=True,
499
+ )
500
+ status_info["status"] = "unavailable"
501
+ status_info["error"] = f"STT test failed: {e!s}"
502
+ elif provider_capability_type == ProviderType.RERANK:
503
+ try:
504
+ assert isinstance(provider, RerankProvider)
505
+ await provider.rerank("Apple", documents=["apple", "banana"])
506
+ status_info["status"] = "available"
507
+ except Exception as e:
508
+ logger.error(
509
+ f"Error testing rerank provider {provider_name}: {e}",
510
+ exc_info=True,
511
+ )
512
+ status_info["status"] = "unavailable"
513
+ status_info["error"] = f"Rerank test failed: {e!s}"
514
+
515
+ else:
516
+ logger.debug(
517
+ f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}",
518
+ )
519
+ status_info["status"] = "available"
520
+ status_info["error"] = (
521
+ "This provider type is not tested and is assumed to be available."
522
+ )
523
+
524
+ return status_info
525
+
526
+ def _error_response(
527
+ self,
528
+ message: str,
529
+ status_code: int = 500,
530
+ log_fn=logger.error,
531
+ ):
532
+ log_fn(message)
533
+ # 记录更详细的traceback信息,但只在是严重错误时
534
+ if status_code == 500:
535
+ log_fn(traceback.format_exc())
536
+ return Response().error(message).__dict__
537
+
538
+ async def check_one_provider_status(self):
539
+ """API: check a single LLM Provider's status by id"""
540
+ provider_id = request.args.get("id")
541
+ if not provider_id:
542
+ return self._error_response(
543
+ "Missing provider_id parameter",
544
+ 400,
545
+ logger.warning,
546
+ )
547
+
548
+ logger.info(f"API call: /config/provider/check_one id={provider_id}")
549
+ try:
550
+ prov_mgr = self.core_lifecycle.provider_manager
551
+ target = prov_mgr.inst_map.get(provider_id)
552
+
553
+ if not target:
554
+ logger.warning(
555
+ f"Provider with id '{provider_id}' not found in provider_manager.",
556
+ )
557
+ return (
558
+ Response()
559
+ .error(f"Provider with id '{provider_id}' not found")
560
+ .__dict__
561
+ )
562
+
563
+ result = await self._test_single_provider(target)
564
+ return Response().ok(result).__dict__
565
+
566
+ except Exception as e:
567
+ return self._error_response(
568
+ f"Critical error checking provider {provider_id}: {e}",
569
+ 500,
570
+ )
571
+
170
572
  async def get_configs(self):
171
573
  # plugin_name 为空时返回 AstrBot 配置
172
574
  # 否则返回指定 plugin_name 的插件配置
@@ -175,11 +577,116 @@ class ConfigRoute(Route):
175
577
  return Response().ok(await self._get_astrbot_config()).__dict__
176
578
  return Response().ok(await self._get_plugin_config(plugin_name)).__dict__
177
579
 
580
+ async def get_provider_config_list(self):
581
+ provider_type = request.args.get("provider_type", None)
582
+ if not provider_type:
583
+ return Response().error("缺少参数 provider_type").__dict__
584
+ provider_type_ls = provider_type.split(",")
585
+ provider_list = []
586
+ astrbot_config = self.core_lifecycle.astrbot_config
587
+ for provider in astrbot_config["provider"]:
588
+ if provider.get("provider_type", None) in provider_type_ls:
589
+ provider_list.append(provider)
590
+ return Response().ok(provider_list).__dict__
591
+
592
+ async def get_provider_model_list(self):
593
+ """获取指定提供商的模型列表"""
594
+ provider_id = request.args.get("provider_id", None)
595
+ if not provider_id:
596
+ return Response().error("缺少参数 provider_id").__dict__
597
+
598
+ prov_mgr = self.core_lifecycle.provider_manager
599
+ provider = prov_mgr.inst_map.get(provider_id, None)
600
+ if not provider:
601
+ return Response().error(f"未找到 ID 为 {provider_id} 的提供商").__dict__
602
+ if not isinstance(provider, Provider):
603
+ return (
604
+ Response()
605
+ .error(f"提供商 {provider_id} 类型不支持获取模型列表")
606
+ .__dict__
607
+ )
608
+
609
+ try:
610
+ models = await provider.get_models()
611
+ ret = {
612
+ "models": models,
613
+ "provider_id": provider_id,
614
+ }
615
+ return Response().ok(ret).__dict__
616
+ except Exception as e:
617
+ logger.error(traceback.format_exc())
618
+ return Response().error(str(e)).__dict__
619
+
620
+ async def get_embedding_dim(self):
621
+ """获取嵌入模型的维度"""
622
+ post_data = await request.json
623
+ provider_config = post_data.get("provider_config", None)
624
+ if not provider_config:
625
+ return Response().error("缺少参数 provider_config").__dict__
626
+
627
+ try:
628
+ # 动态导入 EmbeddingProvider
629
+ from astrbot.core.provider.provider import EmbeddingProvider
630
+ from astrbot.core.provider.register import provider_cls_map
631
+
632
+ # 获取 provider 类型
633
+ provider_type = provider_config.get("type", None)
634
+ if not provider_type:
635
+ return Response().error("provider_config 缺少 type 字段").__dict__
636
+
637
+ # 获取对应的 provider 类
638
+ if provider_type not in provider_cls_map:
639
+ return (
640
+ Response()
641
+ .error(f"未找到适用于 {provider_type} 的提供商适配器")
642
+ .__dict__
643
+ )
644
+
645
+ provider_metadata = provider_cls_map[provider_type]
646
+ cls_type = provider_metadata.cls_type
647
+
648
+ if not cls_type:
649
+ return Response().error(f"无法找到 {provider_type} 的类").__dict__
650
+
651
+ # 实例化 provider
652
+ inst = cls_type(provider_config, {})
653
+
654
+ # 检查是否是 EmbeddingProvider
655
+ if not isinstance(inst, EmbeddingProvider):
656
+ return Response().error("提供商不是 EmbeddingProvider 类型").__dict__
657
+
658
+ # 初始化
659
+ if getattr(inst, "initialize", None):
660
+ await inst.initialize()
661
+
662
+ # 获取嵌入向量维度
663
+ vec = await inst.get_embedding("echo")
664
+ dim = len(vec)
665
+
666
+ logger.info(
667
+ f"检测到 {provider_config.get('id', 'unknown')} 的嵌入向量维度为 {dim}",
668
+ )
669
+
670
+ return Response().ok({"embedding_dimensions": dim}).__dict__
671
+ except Exception as e:
672
+ logger.error(traceback.format_exc())
673
+ return Response().error(f"获取嵌入维度失败: {e!s}").__dict__
674
+
675
+ async def get_platform_list(self):
676
+ """获取所有平台的列表"""
677
+ platform_list = []
678
+ for platform in self.config["platform"]:
679
+ platform_list.append(platform)
680
+ return Response().ok({"platforms": platform_list}).__dict__
681
+
178
682
  async def post_astrbot_configs(self):
179
- post_configs = await request.json
683
+ data = await request.json
684
+ config = data.get("config", None)
685
+ conf_id = data.get("conf_id", None)
180
686
  try:
181
- await self._save_astrbot_configs(post_configs)
182
- return Response().ok(None, "保存成功~ 机器人正在重载配置。").__dict__
687
+ await self._save_astrbot_configs(config, conf_id)
688
+ await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
689
+ return Response().ok(None, "保存成功~").__dict__
183
690
  except Exception as e:
184
691
  logger.error(traceback.format_exc())
185
692
  return Response().error(str(e)).__dict__
@@ -204,7 +711,7 @@ class ConfigRoute(Route):
204
711
  try:
205
712
  save_config(self.config, self.config, is_core=True)
206
713
  await self.core_lifecycle.platform_manager.load_platform(
207
- new_platform_config
714
+ new_platform_config,
208
715
  )
209
716
  except Exception as e:
210
717
  return Response().error(str(e)).__dict__
@@ -216,7 +723,7 @@ class ConfigRoute(Route):
216
723
  try:
217
724
  save_config(self.config, self.config, is_core=True)
218
725
  await self.core_lifecycle.provider_manager.load_provider(
219
- new_provider_config
726
+ new_provider_config,
220
727
  )
221
728
  except Exception as e:
222
729
  return Response().error(str(e)).__dict__
@@ -302,6 +809,73 @@ class ConfigRoute(Route):
302
809
  tools = tool_mgr.get_func_desc_openai_style()
303
810
  return Response().ok(tools).__dict__
304
811
 
812
+ async def _register_platform_logo(self, platform, platform_default_tmpl):
813
+ """注册平台logo文件并生成访问令牌"""
814
+ if not platform.logo_path:
815
+ return
816
+
817
+ try:
818
+ # 检查缓存
819
+ cache_key = f"{platform.name}:{platform.logo_path}"
820
+ if cache_key in self._logo_token_cache:
821
+ cached_token = self._logo_token_cache[cache_key]
822
+ # 确保platform_default_tmpl[platform.name]存在且为字典
823
+ if platform.name not in platform_default_tmpl or not isinstance(
824
+ platform_default_tmpl[platform.name], dict
825
+ ):
826
+ platform_default_tmpl[platform.name] = {}
827
+ platform_default_tmpl[platform.name]["logo_token"] = cached_token
828
+ logger.debug(f"Using cached logo token for platform {platform.name}")
829
+ return
830
+
831
+ # 获取平台适配器类
832
+ platform_cls = platform_cls_map.get(platform.name)
833
+ if not platform_cls:
834
+ logger.warning(f"Platform class not found for {platform.name}")
835
+ return
836
+
837
+ # 获取插件目录路径
838
+ module_file = inspect.getfile(platform_cls)
839
+ plugin_dir = os.path.dirname(module_file)
840
+
841
+ # 解析logo文件路径
842
+ logo_file_path = os.path.join(plugin_dir, platform.logo_path)
843
+
844
+ # 检查文件是否存在并注册令牌
845
+ if os.path.exists(logo_file_path):
846
+ logo_token = await file_token_service.register_file(
847
+ logo_file_path,
848
+ timeout=3600,
849
+ )
850
+
851
+ # 确保platform_default_tmpl[platform.name]存在且为字典
852
+ if platform.name not in platform_default_tmpl or not isinstance(
853
+ platform_default_tmpl[platform.name], dict
854
+ ):
855
+ platform_default_tmpl[platform.name] = {}
856
+
857
+ platform_default_tmpl[platform.name]["logo_token"] = logo_token
858
+
859
+ # 缓存token
860
+ self._logo_token_cache[cache_key] = logo_token
861
+
862
+ logger.debug(f"Logo token registered for platform {platform.name}")
863
+ else:
864
+ logger.warning(
865
+ f"Platform {platform.name} logo file not found: {logo_file_path}",
866
+ )
867
+
868
+ except (ImportError, AttributeError) as e:
869
+ logger.warning(
870
+ f"Failed to import required modules for platform {platform.name}: {e}",
871
+ )
872
+ except OSError as e:
873
+ logger.warning(f"File system error for platform {platform.name} logo: {e}")
874
+ except Exception as e:
875
+ logger.warning(
876
+ f"Unexpected error registering logo for platform {platform.name}: {e}",
877
+ )
878
+
305
879
  async def _get_astrbot_config(self):
306
880
  config = self.config
307
881
 
@@ -309,9 +883,21 @@ class ConfigRoute(Route):
309
883
  platform_default_tmpl = CONFIG_METADATA_2["platform_group"]["metadata"][
310
884
  "platform"
311
885
  ]["config_template"]
886
+
887
+ # 收集需要注册logo的平台
888
+ logo_registration_tasks = []
312
889
  for platform in platform_registry:
313
890
  if platform.default_config_tmpl:
314
891
  platform_default_tmpl[platform.name] = platform.default_config_tmpl
892
+ # 收集logo注册任务
893
+ if platform.logo_path:
894
+ logo_registration_tasks.append(
895
+ self._register_platform_logo(platform, platform_default_tmpl),
896
+ )
897
+
898
+ # 并行执行logo注册
899
+ if logo_registration_tasks:
900
+ await asyncio.gather(*logo_registration_tasks, return_exceptions=True)
315
901
 
316
902
  # 服务提供商的默认配置模板注入
317
903
  provider_default_tmpl = CONFIG_METADATA_2["provider_group"]["metadata"][
@@ -338,16 +924,27 @@ class ConfigRoute(Route):
338
924
  "description": f"{plugin_name} 配置",
339
925
  "type": "object",
340
926
  "items": plugin_md.config.schema, # 初始化时通过 __setattr__ 存入了 schema
341
- }
927
+ },
342
928
  }
343
929
  break
344
930
 
345
931
  return ret
346
932
 
347
- async def _save_astrbot_configs(self, post_configs: dict):
933
+ async def _save_astrbot_configs(
934
+ self, post_configs: dict, conf_id: str | None = None
935
+ ):
348
936
  try:
349
- save_config(post_configs, self.config, is_core=True)
350
- await self.core_lifecycle.restart()
937
+ if conf_id not in self.acm.confs:
938
+ raise ValueError(f"配置文件 {conf_id} 不存在")
939
+ astrbot_config = self.acm.confs[conf_id]
940
+
941
+ # 保留服务端的 t2i_active_template 值
942
+ if "t2i_active_template" in astrbot_config:
943
+ post_configs["t2i_active_template"] = astrbot_config[
944
+ "t2i_active_template"
945
+ ]
946
+
947
+ save_config(post_configs, astrbot_config, is_core=True)
351
948
  except Exception as e:
352
949
  raise e
353
950