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.
- astrbot/api/__init__.py +16 -4
- astrbot/api/all.py +2 -1
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -34
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +8 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__init__.py +1 -0
- astrbot/cli/__main__.py +18 -197
- astrbot/cli/commands/__init__.py +6 -0
- astrbot/cli/commands/cmd_conf.py +209 -0
- astrbot/cli/commands/cmd_init.py +56 -0
- astrbot/cli/commands/cmd_plug.py +245 -0
- astrbot/cli/commands/cmd_run.py +62 -0
- astrbot/cli/utils/__init__.py +18 -0
- astrbot/cli/utils/basic.py +76 -0
- astrbot/cli/utils/plugin.py +246 -0
- astrbot/cli/utils/version_comparator.py +90 -0
- astrbot/core/__init__.py +17 -19
- astrbot/core/agent/agent.py +14 -0
- astrbot/core/agent/handoff.py +38 -0
- astrbot/core/agent/hooks.py +30 -0
- astrbot/core/agent/mcp_client.py +385 -0
- astrbot/core/agent/message.py +175 -0
- astrbot/core/agent/response.py +14 -0
- astrbot/core/agent/run_context.py +22 -0
- astrbot/core/agent/runners/__init__.py +3 -0
- astrbot/core/agent/runners/base.py +65 -0
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
- astrbot/core/agent/tool.py +285 -0
- astrbot/core/agent/tool_executor.py +17 -0
- astrbot/core/astr_agent_context.py +19 -0
- astrbot/core/astr_agent_hooks.py +36 -0
- astrbot/core/astr_agent_run_util.py +80 -0
- astrbot/core/astr_agent_tool_exec.py +246 -0
- astrbot/core/astrbot_config_mgr.py +275 -0
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +60 -20
- astrbot/core/config/default.py +1972 -453
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/conversation_mgr.py +285 -75
- astrbot/core/core_lifecycle.py +167 -62
- astrbot/core/db/__init__.py +305 -102
- astrbot/core/db/migration/helper.py +69 -0
- astrbot/core/db/migration/migra_3_to_4.py +357 -0
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/migration/migra_webchat_session.py +131 -0
- astrbot/core/db/migration/shared_preferences_v3.py +48 -0
- astrbot/core/db/migration/sqlite_v3.py +497 -0
- astrbot/core/db/po.py +259 -55
- astrbot/core/db/sqlite.py +773 -528
- astrbot/core/db/vec_db/base.py +73 -0
- astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
- astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
- astrbot/core/event_bus.py +26 -22
- astrbot/core/exceptions.py +9 -0
- astrbot/core/file_token_service.py +98 -0
- astrbot/core/initial_loader.py +19 -10
- astrbot/core/knowledge_base/chunking/__init__.py +9 -0
- astrbot/core/knowledge_base/chunking/base.py +25 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
- astrbot/core/knowledge_base/chunking/recursive.py +161 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
- astrbot/core/knowledge_base/kb_helper.py +642 -0
- astrbot/core/knowledge_base/kb_mgr.py +330 -0
- astrbot/core/knowledge_base/models.py +120 -0
- astrbot/core/knowledge_base/parsers/__init__.py +13 -0
- astrbot/core/knowledge_base/parsers/base.py +51 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
- astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/prompts.py +65 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +276 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
- astrbot/core/log.py +21 -15
- astrbot/core/message/components.py +413 -287
- astrbot/core/message/message_event_result.py +35 -24
- astrbot/core/persona_mgr.py +192 -0
- astrbot/core/pipeline/__init__.py +14 -14
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +7 -1
- astrbot/core/pipeline/context_utils.py +107 -0
- astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
- astrbot/core/pipeline/process_stage/stage.py +21 -15
- astrbot/core/pipeline/process_stage/utils.py +125 -0
- astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
- astrbot/core/pipeline/respond/stage.py +142 -101
- astrbot/core/pipeline/result_decorate/stage.py +124 -57
- astrbot/core/pipeline/scheduler.py +21 -16
- astrbot/core/pipeline/session_status_check/stage.py +37 -0
- astrbot/core/pipeline/stage.py +11 -76
- astrbot/core/pipeline/waking_check/stage.py +69 -33
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +107 -129
- astrbot/core/platform/astrbot_message.py +32 -12
- astrbot/core/platform/manager.py +62 -18
- astrbot/core/platform/message_session.py +30 -0
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +9 -4
- astrbot/core/platform/register.py +12 -7
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
- astrbot/core/platform/sources/discord/client.py +129 -0
- astrbot/core/platform/sources/discord/components.py +139 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
- astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
- astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
- astrbot/core/platform/sources/lark/lark_event.py +39 -13
- astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
- astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
- astrbot/core/platform/sources/satori/satori_event.py +432 -0
- astrbot/core/platform/sources/slack/client.py +164 -0
- astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
- astrbot/core/platform/sources/slack/slack_event.py +253 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
- astrbot/core/platform/sources/telegram/tg_event.py +136 -36
- astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
- astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
- astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
- astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
- astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
- astrbot/core/platform_message_history_mgr.py +49 -0
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +154 -98
- astrbot/core/provider/func_tool_manager.py +446 -458
- astrbot/core/provider/manager.py +345 -207
- astrbot/core/provider/provider.py +188 -73
- astrbot/core/provider/register.py +9 -7
- astrbot/core/provider/sources/anthropic_source.py +295 -115
- astrbot/core/provider/sources/azure_tts_source.py +224 -0
- astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
- astrbot/core/provider/sources/dashscope_tts.py +138 -14
- astrbot/core/provider/sources/edge_tts_source.py +24 -19
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
- astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
- astrbot/core/provider/sources/gemini_source.py +310 -132
- astrbot/core/provider/sources/gemini_tts_source.py +81 -0
- astrbot/core/provider/sources/groq_source.py +15 -0
- astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
- astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
- astrbot/core/provider/sources/openai_embedding_source.py +40 -0
- astrbot/core/provider/sources/openai_source.py +241 -145
- astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
- astrbot/core/provider/sources/volcengine_tts.py +115 -0
- astrbot/core/provider/sources/whisper_api_source.py +18 -13
- astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/provider/sources/zhipu_source.py +6 -73
- astrbot/core/star/__init__.py +43 -11
- astrbot/core/star/config.py +17 -18
- astrbot/core/star/context.py +362 -138
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +111 -35
- astrbot/core/star/filter/command_group.py +46 -34
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +45 -12
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -15
- astrbot/core/star/register/star.py +41 -13
- astrbot/core/star/register/star_handler.py +236 -86
- astrbot/core/star/session_llm_manager.py +280 -0
- astrbot/core/star/session_plugin_manager.py +170 -0
- astrbot/core/star/star.py +36 -43
- astrbot/core/star/star_handler.py +47 -85
- astrbot/core/star/star_manager.py +442 -260
- astrbot/core/star/star_tools.py +167 -45
- astrbot/core/star/updator.py +17 -20
- astrbot/core/umop_config_router.py +106 -0
- astrbot/core/updator.py +38 -13
- astrbot/core/utils/astrbot_path.py +39 -0
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/core/utils/io.py +119 -60
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +11 -10
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/path_util.py +63 -62
- astrbot/core/utils/pip_installer.py +37 -15
- astrbot/core/utils/session_lock.py +29 -0
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +174 -34
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/local_strategy.py +386 -238
- astrbot/core/utils/t2i/network_strategy.py +109 -49
- astrbot/core/utils/t2i/renderer.py +29 -14
- astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
- astrbot/core/utils/t2i/template_manager.py +111 -0
- astrbot/core/utils/tencent_record_helper.py +115 -1
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +112 -65
- astrbot/dashboard/routes/__init__.py +20 -13
- astrbot/dashboard/routes/auth.py +20 -9
- astrbot/dashboard/routes/chat.py +297 -141
- astrbot/dashboard/routes/config.py +652 -55
- astrbot/dashboard/routes/conversation.py +107 -37
- astrbot/dashboard/routes/file.py +26 -0
- astrbot/dashboard/routes/knowledge_base.py +1244 -0
- astrbot/dashboard/routes/log.py +27 -2
- astrbot/dashboard/routes/persona.py +202 -0
- astrbot/dashboard/routes/plugin.py +197 -139
- astrbot/dashboard/routes/route.py +27 -7
- astrbot/dashboard/routes/session_management.py +354 -0
- astrbot/dashboard/routes/stat.py +85 -18
- astrbot/dashboard/routes/static_file.py +5 -2
- astrbot/dashboard/routes/t2i.py +233 -0
- astrbot/dashboard/routes/tools.py +184 -120
- astrbot/dashboard/routes/update.py +59 -36
- astrbot/dashboard/server.py +96 -36
- astrbot/dashboard/utils.py +165 -0
- astrbot-4.7.0.dist-info/METADATA +294 -0
- astrbot-4.7.0.dist-info/RECORD +274 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
- astrbot/core/db/plugin/sqlite_impl.py +0 -112
- astrbot/core/db/sqlite_init.sql +0 -50
- astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
- astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
- astrbot/core/platform/sources/gewechat/client.py +0 -806
- astrbot/core/platform/sources/gewechat/downloader.py +0 -55
- astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
- astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
- astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
- astrbot/core/provider/sources/dashscope_source.py +0 -203
- astrbot/core/provider/sources/dify_source.py +0 -281
- astrbot/core/provider/sources/llmtuner_source.py +0 -132
- astrbot/core/rag/embedding/openai_source.py +0 -20
- astrbot/core/rag/knowledge_db_mgr.py +0 -94
- astrbot/core/rag/store/__init__.py +0 -9
- astrbot/core/rag/store/chroma_db.py +0 -42
- astrbot/core/utils/dify_api_client.py +0 -152
- astrbot-3.5.6.dist-info/METADATA +0 -249
- astrbot-3.5.6.dist-info/RECORD +0 -158
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
2
4
|
import traceback
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
from quart import request
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,
|
|
132
|
+
post_config,
|
|
133
|
+
CONFIG_METADATA_2,
|
|
134
|
+
is_core,
|
|
138
135
|
)
|
|
139
136
|
else:
|
|
140
|
-
errors, post_config = validate_config(
|
|
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,
|
|
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/
|
|
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
|
-
|
|
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(
|
|
182
|
-
|
|
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(
|
|
933
|
+
async def _save_astrbot_configs(
|
|
934
|
+
self, post_configs: dict, conf_id: str | None = None
|
|
935
|
+
):
|
|
348
936
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|