From 67c33b842db6bc06d56f73ccfcc2129c35f37706 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 16 Dec 2025 16:11:56 +0800 Subject: [PATCH] feat: add new provider icons and improve provider source handling - Added icons for 'modelstack', 'tokenpony', and 'compshare' in providerUtils.js. - Updated ProviderPage.vue to display the correct count of displayed provider sources. - Enhanced the logic for displaying provider sources to include placeholders for unselected templates. - Improved the display name for provider sources to show template keys for placeholders. - Adjusted styles for better layout and overflow handling in provider source list and cards. - Refactored source selection logic to handle placeholder sources correctly. - Updated error handling in provider testing to provide clearer messages. --- astrbot/core/config/default.py | 208 ++++++++-------- astrbot/core/provider/manager.py | 222 ++++++++++-------- astrbot/dashboard/routes/config.py | 32 ++- .../images/provider_logos/modelstack.svg | 1 + dashboard/src/utils/providerUtils.js | 3 + dashboard/src/views/ProviderPage.vue | 81 +++++-- 6 files changed, 319 insertions(+), 228 deletions(-) create mode 100644 dashboard/src/assets/images/provider_logos/modelstack.svg diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index f8dbbf469..c5462084c 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -873,77 +873,9 @@ CONFIG_METADATA_2 = { "api_base": "https://api.openai.com/v1", "timeout": 120, "custom_headers": {}, - "hint": "也兼容所有与 OpenAI API 兼容的服务。", }, - "Azure OpenAI": { - "id": "azure", - "provider": "azure", - "type": "openai_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "api_version": "2024-05-01-preview", - "key": [], - "api_base": "", - "timeout": 120, - "custom_headers": {}, - }, - "xAI": { - "id": "xai", - "provider": "xai", - "type": "openai_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "api_base": "https://api.x.ai/v1", - "timeout": 120, - "custom_headers": {}, - "xai_native_search": False, - }, - "Anthropic": { - "hint": "注意Claude系列模型的温度调节范围为0到1.0,超出可能导致报错", - "id": "claude", - "provider": "anthropic", - "type": "anthropic_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "api_base": "https://api.anthropic.com/v1", - "timeout": 120, - }, - "Ollama": { - "hint": "启用前请确保已正确安装并运行 Ollama 服务端,Ollama默认不带鉴权,无需修改key", - "id": "ollama_default", - "provider": "ollama", - "type": "openai_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": ["ollama"], # ollama 的 key 默认是 ollama - "api_base": "http://localhost:11434/v1", - "custom_headers": {}, - }, - "LM Studio": { - "id": "lm_studio", - "provider": "lm_studio", - "type": "openai_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": ["lmstudio"], - "api_base": "http://localhost:1234/v1", - "custom_headers": {}, - }, - "Gemini(OpenAI兼容)": { - "id": "gemini_default", - "provider": "google", - "type": "openai_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "api_base": "https://generativelanguage.googleapis.com/v1beta/openai/", - "timeout": 120, - "custom_headers": {}, - }, - "Gemini": { - "id": "gemini_default", + "Google Gemini": { + "id": "google_gemini", "provider": "google", "type": "googlegenai_chat_completion", "provider_type": "chat_completion", @@ -965,8 +897,41 @@ CONFIG_METADATA_2 = { "budget": 0, }, }, + "Anthropic": { + "id": "anthropic", + "provider": "anthropic", + "type": "anthropic_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://api.anthropic.com/v1", + "timeout": 120, + }, + "Moonshot": { + "id": "moonshot", + "provider": "moonshot", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "timeout": 120, + "api_base": "https://api.moonshot.cn/v1", + "custom_headers": {}, + }, + "xAI": { + "id": "xai", + "provider": "xai", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://api.x.ai/v1", + "timeout": 120, + "custom_headers": {}, + "xai_native_search": False, + }, "DeepSeek": { - "id": "deepseek_default", + "id": "deepseek", "provider": "deepseek", "type": "openai_chat_completion", "provider_type": "chat_completion", @@ -976,8 +941,73 @@ CONFIG_METADATA_2 = { "timeout": 120, "custom_headers": {}, }, + "Zhipu": { + "id": "zhipu", + "provider": "zhipu", + "type": "zhipu_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "timeout": 120, + "api_base": "https://open.bigmodel.cn/api/paas/v4/", + "custom_headers": {}, + }, + "Azure OpenAI": { + "id": "azure_openai", + "provider": "azure", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "api_version": "2024-05-01-preview", + "key": [], + "api_base": "", + "timeout": 120, + "custom_headers": {}, + }, + "Ollama": { + "id": "ollama", + "provider": "ollama", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": ["ollama"], # ollama 的 key 默认是 ollama + "api_base": "http://127.0.0.1:11434/v1", + "custom_headers": {}, + }, + "LM Studio": { + "id": "lm_studio", + "provider": "lm_studio", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": ["lmstudio"], + "api_base": "http://127.0.0.1:1234/v1", + "custom_headers": {}, + }, + "ModelStack": { + "id": "modelstack", + "provider": "modelstack", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://modelstack.app/v1", + "timeout": 120, + "custom_headers": {}, + }, + "Gemini_OpenAI_API": { + "id": "google_gemini_openai", + "provider": "google", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://generativelanguage.googleapis.com/v1beta/openai/", + "timeout": 120, + "custom_headers": {}, + }, "Groq": { - "id": "groq_default", + "id": "groq", "provider": "groq", "type": "groq_chat_completion", "provider_type": "chat_completion", @@ -998,7 +1028,7 @@ CONFIG_METADATA_2 = { "timeout": 120, "custom_headers": {}, }, - "硅基流动": { + "SiliconFlow": { "id": "siliconflow", "provider": "siliconflow", "type": "openai_chat_completion", @@ -1009,7 +1039,7 @@ CONFIG_METADATA_2 = { "api_base": "https://api.siliconflow.cn/v1", "custom_headers": {}, }, - "PPIO派欧云": { + "PPIO": { "id": "ppio", "provider": "ppio", "type": "openai_chat_completion", @@ -1020,7 +1050,7 @@ CONFIG_METADATA_2 = { "timeout": 120, "custom_headers": {}, }, - "小马算力": { + "TokenPony": { "id": "tokenpony", "provider": "tokenpony", "type": "openai_chat_completion", @@ -1031,7 +1061,7 @@ CONFIG_METADATA_2 = { "timeout": 120, "custom_headers": {}, }, - "优云智算": { + "Compshare": { "id": "compshare", "provider": "compshare", "type": "openai_chat_completion", @@ -1042,28 +1072,6 @@ CONFIG_METADATA_2 = { "timeout": 120, "custom_headers": {}, }, - "Kimi": { - "id": "moonshot", - "provider": "moonshot", - "type": "openai_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "timeout": 120, - "api_base": "https://api.moonshot.cn/v1", - "custom_headers": {}, - }, - "智谱 AI": { - "id": "zhipu_default", - "provider": "zhipu", - "type": "zhipu_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "timeout": 120, - "api_base": "https://open.bigmodel.cn/api/paas/v4/", - "custom_headers": {}, - }, "ModelScope": { "id": "modelscope", "provider": "modelscope", @@ -1088,7 +1096,6 @@ CONFIG_METADATA_2 = { "dify_query_input_key": "astrbot_text_query", "variables": {}, "timeout": 60, - "hint": "请确保你在 AstrBot 里设置的 APP 类型和 Dify 里面创建的应用的类型一致!", }, "Coze": { "id": "coze", @@ -1142,7 +1149,6 @@ CONFIG_METADATA_2 = { "model": "whisper-1", }, "Whisper(Local)": { - "hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", "provider": "openai", "type": "openai_whisper_selfhost", "provider_type": "speech_to_text", @@ -1151,7 +1157,6 @@ CONFIG_METADATA_2 = { "model": "tiny", }, "SenseVoice(Local)": { - "hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", "type": "sensevoice_stt_selfhost", "provider": "sensevoice", "provider_type": "speech_to_text", @@ -1173,7 +1178,6 @@ CONFIG_METADATA_2 = { "timeout": "20", }, "Edge TTS": { - "hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。", "id": "edge_tts", "provider": "microsoft", "type": "edge_tts", diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 57ecf22fd..bb6308a4b 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -1,4 +1,5 @@ import asyncio +import copy import traceback from typing import Protocol, runtime_checkable @@ -37,8 +38,6 @@ class ProviderManager: config = acm.confs["default"] self.providers_config: list = config["provider"] self.provider_sources_config: list = config.get("provider_sources", []) - self.merged_provider_config: dict = {} - """合并 provider 和 provider_sources 配置后的结果""" self.provider_settings: dict = config["provider_settings"] self.provider_stt_settings: dict = config.get("provider_stt_settings", {}) self.provider_tts_settings: dict = config.get("provider_tts_settings", {}) @@ -254,9 +253,117 @@ class ProviderManager: # 初始化 MCP Client 连接 asyncio.create_task(self.llm_tools.init_mcp_clients(), name="init_mcp_clients") - async def load_provider(self, provider_config: dict): - # 如果 provider_source_id 存在且不为空,则从 provider_sources 中找到对应的配置并合并 - provider_source_id = provider_config.get("provider_source_id", "") + def dynamic_import_provider(self, type: str): + """动态导入提供商适配器模块 + + Args: + type (str): 提供商请求类型。 + + Raises: + ImportError: 如果提供商类型未知或无法导入对应模块,则抛出异常。 + """ + match type: + case "openai_chat_completion": + from .sources.openai_source import ( + ProviderOpenAIOfficial as ProviderOpenAIOfficial, + ) + case "zhipu_chat_completion": + from .sources.zhipu_source import ProviderZhipu as ProviderZhipu + case "groq_chat_completion": + from .sources.groq_source import ProviderGroq as ProviderGroq + case "anthropic_chat_completion": + from .sources.anthropic_source import ( + ProviderAnthropic as ProviderAnthropic, + ) + case "googlegenai_chat_completion": + from .sources.gemini_source import ( + ProviderGoogleGenAI as ProviderGoogleGenAI, + ) + case "sensevoice_stt_selfhost": + from .sources.sensevoice_selfhosted_source import ( + ProviderSenseVoiceSTTSelfHost as ProviderSenseVoiceSTTSelfHost, + ) + case "openai_whisper_api": + from .sources.whisper_api_source import ( + ProviderOpenAIWhisperAPI as ProviderOpenAIWhisperAPI, + ) + case "openai_whisper_selfhost": + from .sources.whisper_selfhosted_source import ( + ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost, + ) + case "xinference_stt": + from .sources.xinference_stt_provider import ( + ProviderXinferenceSTT as ProviderXinferenceSTT, + ) + case "openai_tts_api": + from .sources.openai_tts_api_source import ( + ProviderOpenAITTSAPI as ProviderOpenAITTSAPI, + ) + case "edge_tts": + from .sources.edge_tts_source import ( + ProviderEdgeTTS as ProviderEdgeTTS, + ) + case "gsv_tts_selfhost": + from .sources.gsv_selfhosted_source import ( + ProviderGSVTTS as ProviderGSVTTS, + ) + case "gsvi_tts_api": + from .sources.gsvi_tts_source import ( + ProviderGSVITTS as ProviderGSVITTS, + ) + case "fishaudio_tts_api": + from .sources.fishaudio_tts_api_source import ( + ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI, + ) + case "dashscope_tts": + from .sources.dashscope_tts import ( + ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI, + ) + case "azure_tts": + from .sources.azure_tts_source import ( + AzureTTSProvider as AzureTTSProvider, + ) + case "minimax_tts_api": + from .sources.minimax_tts_api_source import ( + ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI, + ) + case "volcengine_tts": + from .sources.volcengine_tts import ( + ProviderVolcengineTTS as ProviderVolcengineTTS, + ) + case "gemini_tts": + from .sources.gemini_tts_source import ( + ProviderGeminiTTSAPI as ProviderGeminiTTSAPI, + ) + case "openai_embedding": + from .sources.openai_embedding_source import ( + OpenAIEmbeddingProvider as OpenAIEmbeddingProvider, + ) + case "gemini_embedding": + from .sources.gemini_embedding_source import ( + GeminiEmbeddingProvider as GeminiEmbeddingProvider, + ) + case "vllm_rerank": + from .sources.vllm_rerank_source import ( + VLLMRerankProvider as VLLMRerankProvider, + ) + case "xinference_rerank": + from .sources.xinference_rerank_source import ( + XinferenceRerankProvider as XinferenceRerankProvider, + ) + case "bailian_rerank": + from .sources.bailian_rerank_source import ( + BailianRerankProvider as BailianRerankProvider, + ) + + def get_merged_provider_config(self, provider_config: dict) -> dict: + """获取 provider 配置和 provider_source 配置合并后的结果 + + Returns: + dict: 合并后的 provider 配置,key 为 provider id,value 为合并后的配置字典 + """ + pc = copy.deepcopy(provider_config) + provider_source_id = pc.get("provider_source_id", "") if provider_source_id: provider_source = None for ps in self.provider_sources_config: @@ -266,12 +373,15 @@ class ProviderManager: if provider_source: # 合并配置,provider 的配置优先级更高 - merged_config = {**provider_source, **provider_config} + merged_config = {**provider_source, **pc} # 保持 id 为 provider 的 id,而不是 source 的 id - merged_config["id"] = provider_config["id"] - provider_config = merged_config + merged_config["id"] = pc["id"] + pc = merged_config + return pc - self.merged_provider_config[provider_config["id"]] = provider_config + async def load_provider(self, provider_config: dict): + # 如果 provider_source_id 存在且不为空,则从 provider_sources 中找到对应的配置并合并 + provider_config = self.get_merged_provider_config(provider_config) if not provider_config["enable"]: logger.info(f"Provider {provider_config['id']} is disabled, skipping") @@ -285,99 +395,7 @@ class ProviderManager: # 动态导入 try: - match provider_config["type"]: - case "openai_chat_completion": - from .sources.openai_source import ( - ProviderOpenAIOfficial as ProviderOpenAIOfficial, - ) - case "zhipu_chat_completion": - from .sources.zhipu_source import ProviderZhipu as ProviderZhipu - case "groq_chat_completion": - from .sources.groq_source import ProviderGroq as ProviderGroq - case "anthropic_chat_completion": - from .sources.anthropic_source import ( - ProviderAnthropic as ProviderAnthropic, - ) - case "googlegenai_chat_completion": - from .sources.gemini_source import ( - ProviderGoogleGenAI as ProviderGoogleGenAI, - ) - case "sensevoice_stt_selfhost": - from .sources.sensevoice_selfhosted_source import ( - ProviderSenseVoiceSTTSelfHost as ProviderSenseVoiceSTTSelfHost, - ) - case "openai_whisper_api": - from .sources.whisper_api_source import ( - ProviderOpenAIWhisperAPI as ProviderOpenAIWhisperAPI, - ) - case "openai_whisper_selfhost": - from .sources.whisper_selfhosted_source import ( - ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost, - ) - case "xinference_stt": - from .sources.xinference_stt_provider import ( - ProviderXinferenceSTT as ProviderXinferenceSTT, - ) - case "openai_tts_api": - from .sources.openai_tts_api_source import ( - ProviderOpenAITTSAPI as ProviderOpenAITTSAPI, - ) - case "edge_tts": - from .sources.edge_tts_source import ( - ProviderEdgeTTS as ProviderEdgeTTS, - ) - case "gsv_tts_selfhost": - from .sources.gsv_selfhosted_source import ( - ProviderGSVTTS as ProviderGSVTTS, - ) - case "gsvi_tts_api": - from .sources.gsvi_tts_source import ( - ProviderGSVITTS as ProviderGSVITTS, - ) - case "fishaudio_tts_api": - from .sources.fishaudio_tts_api_source import ( - ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI, - ) - case "dashscope_tts": - from .sources.dashscope_tts import ( - ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI, - ) - case "azure_tts": - from .sources.azure_tts_source import ( - AzureTTSProvider as AzureTTSProvider, - ) - case "minimax_tts_api": - from .sources.minimax_tts_api_source import ( - ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI, - ) - case "volcengine_tts": - from .sources.volcengine_tts import ( - ProviderVolcengineTTS as ProviderVolcengineTTS, - ) - case "gemini_tts": - from .sources.gemini_tts_source import ( - ProviderGeminiTTSAPI as ProviderGeminiTTSAPI, - ) - case "openai_embedding": - from .sources.openai_embedding_source import ( - OpenAIEmbeddingProvider as OpenAIEmbeddingProvider, - ) - case "gemini_embedding": - from .sources.gemini_embedding_source import ( - GeminiEmbeddingProvider as GeminiEmbeddingProvider, - ) - case "vllm_rerank": - from .sources.vllm_rerank_source import ( - VLLMRerankProvider as VLLMRerankProvider, - ) - case "xinference_rerank": - from .sources.xinference_rerank_source import ( - XinferenceRerankProvider as XinferenceRerankProvider, - ) - case "bailian_rerank": - from .sources.bailian_rerank_source import ( - BailianRerankProvider as BailianRerankProvider, - ) + self.dynamic_import_provider(provider_config["type"]) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。", diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index cd80cb961..f0502dcf8 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -521,9 +521,25 @@ class ConfigRoute(Route): return Response().error("缺少参数 provider_type").__dict__ provider_type_ls = provider_type.split(",") provider_list = [] - pc = self.core_lifecycle.provider_manager.merged_provider_config - for provider in pc.values(): - if provider.get("provider_type", None) in provider_type_ls: + ps = self.core_lifecycle.provider_manager.providers_config + p_source_pt = { + psrc["id"]: psrc["provider_type"] + for psrc in self.core_lifecycle.provider_manager.provider_sources_config + } + for provider in ps: + ps_id = provider.get("provider_source_id", None) + if ( + ps_id + and ps_id in p_source_pt + and p_source_pt[ps_id] in provider_type_ls + ): + # chat + prov = self.core_lifecycle.provider_manager.get_merged_provider_config( + provider + ) + provider_list.append(prov) + elif not ps_id and provider.get("provider_type", None) in provider_type_ls: + # agent runner, embedding, etc provider_list.append(provider) return Response().ok(provider_list).__dict__ @@ -647,6 +663,14 @@ class ConfigRoute(Route): if not provider_type: return Response().error("provider_source 缺少 type 字段").__dict__ + try: + self.core_lifecycle.provider_manager.dynamic_import_provider( + provider_type + ) + except ImportError as e: + logger.error(traceback.format_exc()) + return Response().error(f"动态导入提供商适配器失败: {e!s}").__dict__ + # 获取对应的 provider 类 if provider_type not in provider_cls_map: return ( @@ -842,7 +866,7 @@ class ConfigRoute(Route): async def post_delete_provider(self): provider_id = await request.json - provider_id = provider_id.get("id") + provider_id = provider_id.get("id", "") for i, provider in enumerate(self.config["provider"]): if provider["id"] == provider_id: del self.config["provider"][i] diff --git a/dashboard/src/assets/images/provider_logos/modelstack.svg b/dashboard/src/assets/images/provider_logos/modelstack.svg new file mode 100644 index 000000000..940ac8799 --- /dev/null +++ b/dashboard/src/assets/images/provider_logos/modelstack.svg @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/dashboard/src/utils/providerUtils.js b/dashboard/src/utils/providerUtils.js index da9a2acc3..e9d8f21f1 100644 --- a/dashboard/src/utils/providerUtils.js +++ b/dashboard/src/utils/providerUtils.js @@ -32,6 +32,9 @@ export function getProviderIcon(type) { 'microsoft': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/microsoft.svg', 'vllm': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/vllm.svg', 'groq': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq.svg', + "modelstack": new URL('@/assets/images/provider_logos/modelstack.svg', import.meta.url).href, + "tokenpony": "https://tokenpony.cn/tokenpony-web/logo.png", + "compshare": "https://compshare.cn/favicon.ico" }; return icons[type] || ''; } diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index c5512cba5..a86b0bb85 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -29,14 +29,14 @@ -
- +
+

{{ tm('providerSources.title') }}

- {{ filteredProviderSources.length }} + {{ displayedProviderSources.length }}