From bb45d9cb54ee47df142acff9a0c9c9cdc14b4e6d Mon Sep 17 00:00:00 2001
From: Soulter <905617992@qq.com>
Date: Sat, 13 Dec 2025 17:16:07 +0800
Subject: [PATCH] stage
---
PROVIDER_REFACTOR_INSTRUCTION.md | 56 +
astrbot/core/config/default.py | 128 +-
astrbot/core/provider/manager.py | 19 +
.../core/provider/sources/anthropic_source.py | 12 +-
astrbot/core/utils/migra_helper.py | 92 +
astrbot/dashboard/routes/config.py | 93 +-
.../src/components/shared/AstrBotConfig.vue | 1 +
.../i18n/locales/en-US/features/provider.json | 37 +
.../i18n/locales/zh-CN/features/provider.json | 37 +
dashboard/src/views/ProviderPage.vue | 1613 +++++++++--------
dashboard/src/views/ProviderPage.vue.backup | 892 +++++++++
11 files changed, 2156 insertions(+), 824 deletions(-)
create mode 100644 PROVIDER_REFACTOR_INSTRUCTION.md
create mode 100644 dashboard/src/views/ProviderPage.vue.backup
diff --git a/PROVIDER_REFACTOR_INSTRUCTION.md b/PROVIDER_REFACTOR_INSTRUCTION.md
new file mode 100644
index 000000000..73fbacdbb
--- /dev/null
+++ b/PROVIDER_REFACTOR_INSTRUCTION.md
@@ -0,0 +1,56 @@
+总览:对于 chat_completion 的提供商,目前需要将 provider 分成 provider_source 和 provider 两个部分,这样可以更好地支持同一 provider_source 下的多模型添加。
+
+目前已经在 astrbot/core/config/default.py 中对 provider 配置进行了拆分,接下来需要修改:
+
+## provider/manager 部分:
+
+需要传入 provider_sources 给 ProviderManager,然后在创建 provider 的时候:
+
+1. 如果 provider.provider_source_id 存在且不为空,则从 provider_sources 中找到对应的 provider_source,并将 provider_source 的配置合并到 provider_config 中。provider 的配置优先级更高。
+2. 如果 provider.provider_source_id 不存在或为空,则使用 provider 自身的配置。
+
+## WebUI 部分:
+
+我们主要需要大量修改 ProviderPage.vue 文件。
+
+将整体页面换成左右多栏布局。
+
+分为 三个 栏目:
+
+1. 左第一个栏目用于选择 provider type。
+2. 中间第二个栏目用于选择 provider source。
+3. 右第三个栏目用于配置 provider。
+
+中间的栏目最上面有一个添加按钮,点击后会出现一个 dropdown,列出所有的 provider source type,选择后会在中间栏目添加一个 provider source。
+
+选中某个 provider source 后,右侧栏目会显示该 provider source 的配置。指向这个 provider source 的 provider 本质上是 model 的配置。在 provider source 配置下面,会有一个“获取模型”的按钮(如果没有保存/更新 source 配置,则写“保存并获取模型”),点击后会在下方有一个 v-list 列出该 provider source 支持的所有模型,选择后会在下方增加一个新的模型配置(也就是provider的配置)。整个过程从设计上看不要有任何 dialog。
+
+有一个保存按钮,用于保存当前 provider source 和 provider 的配置。
+
+### 模型列表的获取
+
+需要增加一个 API,用于获取某个 provider source 支持的模型列表。前端在点击“添加模型”按钮时,调用该 API 获取模型列表并展示在 dropdown 中。
+
+GET /config/provider_sources//models
+
+本质上这个会初始化一下 Provider,然后调用 Provider.get_models() 来获取模型列表,然后销毁 Provider 实例。
+
+### 测试模型
+
+在 provider 的配置那里,增加一个“测试模型”按钮,类同现在的测试功能。
+
+## 迁移
+
+需要编写一个迁移脚本,将现有的 provider 配置拆分成 provider_source 和 provider 两部分,provider_source 的配置从现有的 provider 中提取公共部分,provider 则只保留模型相关的配置。
+
+仅对 chat_completion 类型的 provider 进行迁移,其他类型的 provider 保持不变。
+
+可以在 astrbot/core/utils/migra_helper.py 的 migra 方法中添加迁移逻辑。
+
+## UI 风格
+
+整体风格需要现代化、简洁化,不要使用任何渐变。
+
+## 其他
+
+请最小化改动。
\ No newline at end of file
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index daa2c4fac..6e7f919fd 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -1,6 +1,7 @@
"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。"""
import os
+from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
@@ -61,7 +62,8 @@ DEFAULT_CONFIG = {
"ignore_bot_self_message": False,
"ignore_at_all": False,
},
- "provider": [],
+ "provider_sources": [], # provider sources
+ "provider": [], # models from provider_sources
"provider_settings": {
"enable": True,
"default_provider_id": "",
@@ -170,6 +172,22 @@ DEFAULT_CONFIG = {
}
+class ChatProviderTemplate(TypedDict):
+ id: str
+ provider_source_id: str
+ model: str
+ modalities: list
+ custom_extra_body: dict[str, Any]
+
+
+CHAT_PROVIDER_TEMPLATE = {
+ "id": "",
+ "provide_source_id": "",
+ "model": "",
+ "modalities": [],
+ "custom_extra_body": {},
+}
+
"""
AstrBot v3 时代的配置元数据,目前仅承担以下功能:
@@ -843,6 +861,7 @@ CONFIG_METADATA_2 = {
"metadata": {
"provider": {
"type": "list",
+ # provider sources templates
"config_template": {
"OpenAI": {
"id": "openai",
@@ -853,10 +872,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.openai.com/v1",
"timeout": 120,
- "model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
"hint": "也兼容所有与 OpenAI API 兼容的服务。",
},
"Azure OpenAI": {
@@ -869,10 +885,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "",
"timeout": 120,
- "model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"xAI": {
"id": "xai",
@@ -883,11 +896,8 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.x.ai/v1",
"timeout": 120,
- "model_config": {"model": "grok-2-latest", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
"xai_native_search": False,
- "modalities": ["text", "image", "tool_use"],
},
"Anthropic": {
"hint": "注意Claude系列模型的温度调节范围为0到1.0,超出可能导致报错",
@@ -899,12 +909,6 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.anthropic.com/v1",
"timeout": 120,
- "model_config": {
- "model": "claude-3-5-sonnet-latest",
- "max_tokens": 4096,
- "temperature": 0.2,
- },
- "modalities": ["text", "image", "tool_use"],
},
"Ollama": {
"hint": "启用前请确保已正确安装并运行 Ollama 服务端,Ollama默认不带鉴权,无需修改key",
@@ -915,10 +919,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": ["ollama"], # ollama 的 key 默认是 ollama
"api_base": "http://localhost:11434/v1",
- "model_config": {"model": "llama3.1-8b", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"LM Studio": {
"id": "lm_studio",
@@ -928,12 +929,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": ["lmstudio"],
"api_base": "http://localhost:1234/v1",
- "model_config": {
- "model": "llama-3.1-8b",
- },
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"Gemini(OpenAI兼容)": {
"id": "gemini_default",
@@ -944,13 +940,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
"timeout": 120,
- "model_config": {
- "model": "gemini-1.5-flash",
- "temperature": 0.4,
- },
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"Gemini": {
"id": "gemini_default",
@@ -961,10 +951,6 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://generativelanguage.googleapis.com/",
"timeout": 120,
- "model_config": {
- "model": "gemini-2.0-flash-exp",
- "temperature": 0.4,
- },
"gm_resp_image_modal": False,
"gm_native_search": False,
"gm_native_coderunner": False,
@@ -978,7 +964,6 @@ CONFIG_METADATA_2 = {
"gm_thinking_config": {
"budget": 0,
},
- "modalities": ["text", "image", "tool_use"],
},
"DeepSeek": {
"id": "deepseek_default",
@@ -989,10 +974,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.deepseek.com/v1",
"timeout": 120,
- "model_config": {"model": "deepseek-chat", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "tool_use"],
},
"Groq": {
"id": "groq_default",
@@ -1003,13 +985,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.groq.com/openai/v1",
"timeout": 120,
- "model_config": {
- "model": "openai/gpt-oss-20b",
- "temperature": 0.4,
- },
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "tool_use"],
},
"302.AI": {
"id": "302ai",
@@ -1020,10 +996,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.302.ai/v1",
"timeout": 120,
- "model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"硅基流动": {
"id": "siliconflow",
@@ -1034,13 +1007,7 @@ CONFIG_METADATA_2 = {
"key": [],
"timeout": 120,
"api_base": "https://api.siliconflow.cn/v1",
- "model_config": {
- "model": "deepseek-ai/DeepSeek-V3",
- "temperature": 0.4,
- },
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"PPIO派欧云": {
"id": "ppio",
@@ -1051,12 +1018,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.ppinfra.com/v3/openai",
"timeout": 120,
- "model_config": {
- "model": "deepseek/deepseek-r1",
- "temperature": 0.4,
- },
"custom_headers": {},
- "custom_extra_body": {},
},
"小马算力": {
"id": "tokenpony",
@@ -1067,12 +1029,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.tokenpony.cn/v1",
"timeout": 120,
- "model_config": {
- "model": "kimi-k2-instruct-0905",
- "temperature": 0.7,
- },
"custom_headers": {},
- "custom_extra_body": {},
},
"优云智算": {
"id": "compshare",
@@ -1083,12 +1040,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.modelverse.cn/v1",
"timeout": 120,
- "model_config": {
- "model": "moonshotai/Kimi-K2-Instruct",
- },
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"Kimi": {
"id": "moonshot",
@@ -1099,10 +1051,7 @@ CONFIG_METADATA_2 = {
"key": [],
"timeout": 120,
"api_base": "https://api.moonshot.cn/v1",
- "model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
},
"智谱 AI": {
"id": "zhipu_default",
@@ -1113,12 +1062,18 @@ CONFIG_METADATA_2 = {
"key": [],
"timeout": 120,
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
- "model_config": {
- "model": "glm-4-flash",
- },
"custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
+ },
+ "ModelScope": {
+ "id": "modelscope",
+ "provider": "modelscope",
+ "type": "openai_chat_completion",
+ "provider_type": "chat_completion",
+ "enable": True,
+ "key": [],
+ "timeout": 120,
+ "api_base": "https://api-inference.modelscope.cn/v1",
+ "custom_headers": {},
},
"Dify": {
"id": "dify_app_default",
@@ -1164,20 +1119,6 @@ CONFIG_METADATA_2 = {
"variables": {},
"timeout": 60,
},
- "ModelScope": {
- "id": "modelscope",
- "provider": "modelscope",
- "type": "openai_chat_completion",
- "provider_type": "chat_completion",
- "enable": True,
- "key": [],
- "timeout": 120,
- "api_base": "https://api-inference.modelscope.cn/v1",
- "model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
- "custom_headers": {},
- "custom_extra_body": {},
- "modalities": ["text", "image", "tool_use"],
- },
"FastGPT": {
"id": "fastgpt",
"provider": "fastgpt",
@@ -1448,6 +1389,10 @@ CONFIG_METADATA_2 = {
},
},
"items": {
+ "provider_source_id": {
+ "invisible": True,
+ "type": "string",
+ },
"xai_native_search": {
"description": "启用原生搜索功能",
"type": "bool",
@@ -2005,7 +1950,6 @@ CONFIG_METADATA_2 = {
"id": {
"description": "ID",
"type": "string",
- "hint": "模型提供商名字。",
},
"type": {
"description": "模型提供商种类",
@@ -2025,12 +1969,10 @@ CONFIG_METADATA_2 = {
"description": "API Key",
"type": "list",
"items": {"type": "string"},
- "hint": "提供商 API Key。",
},
"api_base": {
"description": "API Base URL",
"type": "string",
- "hint": "API Base URL 请在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
},
"model_config": {
"description": "模型配置",
diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py
index be8edc282..e3e0e7595 100644
--- a/astrbot/core/provider/manager.py
+++ b/astrbot/core/provider/manager.py
@@ -36,6 +36,7 @@ class ProviderManager:
self.acm = acm
config = acm.confs["default"]
self.providers_config: list = config["provider"]
+ self.provider_sources_config: list = config.get("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", {})
@@ -252,6 +253,23 @@ class ProviderManager:
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", "")
+ if provider_source_id:
+ provider_source = None
+ for ps in self.provider_sources_config:
+ if ps.get("id") == provider_source_id:
+ provider_source = ps
+ break
+
+ if provider_source:
+ # 合并配置,provider 的配置优先级更高
+ merged_config = {**provider_source, **provider_config}
+ # 保持 id 为 provider 的 id,而不是 source 的 id
+ merged_config["id"] = provider_config["id"]
+ provider_config = merged_config
+
if not provider_config["enable"]:
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
return
@@ -499,6 +517,7 @@ class ProviderManager:
# 和配置文件保持同步
self.providers_config = astrbot_config["provider"]
+ self.provider_sources_config = astrbot_config.get("provider_sources", [])
config_ids = [provider["id"] for provider in self.providers_config]
logger.info(f"providers in user's config: {config_ids}")
for key in list(self.inst_map.keys()):
diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py
index bd0f06fba..f2b7fac6f 100644
--- a/astrbot/core/provider/sources/anthropic_source.py
+++ b/astrbot/core/provider/sources/anthropic_source.py
@@ -112,7 +112,11 @@ class ProviderAnthropic(Provider):
if tool_list := tools.get_func_desc_anthropic_style():
payloads["tools"] = tool_list
- completion = await self.client.messages.create(**payloads, stream=False)
+ extra_body = self.provider_config.get("custom_extra_body", {})
+
+ completion = await self.client.messages.create(
+ **payloads, stream=False, extra_body=extra_body
+ )
assert isinstance(completion, Message)
logger.debug(f"completion: {completion}")
@@ -152,7 +156,11 @@ class ProviderAnthropic(Provider):
final_text = ""
final_tool_calls = []
- async with self.client.messages.stream(**payloads) as stream:
+ extra_body = self.provider_config.get("custom_extra_body", {})
+
+ async with self.client.messages.stream(
+ **payloads, extra_body=extra_body
+ ) as stream:
assert isinstance(stream, anthropic.AsyncMessageStream)
async for event in stream:
if event.type == "content_block_start":
diff --git a/astrbot/core/utils/migra_helper.py b/astrbot/core/utils/migra_helper.py
index 5642d606e..52715caf9 100644
--- a/astrbot/core/utils/migra_helper.py
+++ b/astrbot/core/utils/migra_helper.py
@@ -32,6 +32,91 @@ def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
logger.error(traceback.format_exc())
+def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
+ """
+ Migrate old provider structure to new provider-source separation.
+ Provider only keeps: id, provider_source_id, model, modalities, custom_extra_body
+ All other fields move to provider_sources.
+ """
+ providers = conf.get("provider", [])
+ provider_sources = conf.get("provider_sources", [])
+
+ # Track if any migration happened
+ migrated = False
+
+ # Provider-only fields that should stay in provider
+ provider_only_fields = {
+ "id",
+ "provider_source_id",
+ "model",
+ "modalities",
+ "custom_extra_body",
+ }
+
+ # Fields that should not go to source
+ source_exclude_fields = provider_only_fields | {"model_config"}
+
+ for provider in providers:
+ # Skip if already has provider_source_id
+ if provider.get("provider_source_id"):
+ continue
+
+ # Skip non-chat-completion types (they don't need source separation)
+ provider_type = provider.get("provider_type", "")
+ if provider_type != "chat_completion":
+ # For old types without provider_type, check type field
+ old_type = provider.get("type", "")
+ if not ("chat_completion" in old_type):
+ continue
+
+ migrated = True
+ logger.info(f"Migrating provider {provider.get('id')} to new structure")
+
+ # Extract source fields from provider
+ source_fields = {}
+ for key, value in list(provider.items()):
+ if key not in source_exclude_fields:
+ source_fields[key] = value
+
+ # Create new provider_source
+ source_id = provider.get("id", "") + "_source"
+ new_source = {"id": source_id, **source_fields}
+
+ # Update provider to only keep necessary fields
+ provider["provider_source_id"] = source_id
+
+ # Extract model from model_config if exists
+ if "model_config" in provider and isinstance(provider["model_config"], dict):
+ model_config = provider["model_config"]
+ provider["model"] = model_config.get("model", "")
+
+ # Put other model_config fields into custom_extra_body
+ extra_body_fields = {k: v for k, v in model_config.items() if k != "model"}
+ if extra_body_fields:
+ if "custom_extra_body" not in provider:
+ provider["custom_extra_body"] = {}
+ provider["custom_extra_body"].update(extra_body_fields)
+
+ # Initialize new fields if not present
+ if "modalities" not in provider:
+ provider["modalities"] = []
+ if "custom_extra_body" not in provider:
+ provider["custom_extra_body"] = {}
+
+ # Remove fields that should be in source
+ keys_to_remove = [k for k in provider.keys() if k not in provider_only_fields]
+ for key in keys_to_remove:
+ del provider[key]
+
+ # Add source to provider_sources
+ provider_sources.append(new_source)
+
+ if migrated:
+ conf["provider_sources"] = provider_sources
+ conf.save_config()
+ logger.info("Provider-source structure migration completed")
+
+
async def migra(
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
) -> None:
@@ -71,3 +156,10 @@ async def migra(
for conf in acm.confs.values():
_migra_agent_runner_configs(conf, ids_map)
+
+ # Migrate providers to new structure: extract source fields to provider_sources
+ try:
+ _migra_provider_to_source_structure(astrbot_config)
+ except Exception as e:
+ logger.error(f"Migration for provider-source structure failed: {e!s}")
+ logger.error(traceback.format_exc())
diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py
index 0edbe8377..22a24892e 100644
--- a/astrbot/dashboard/routes/config.py
+++ b/astrbot/dashboard/routes/config.py
@@ -6,7 +6,7 @@ from typing import Any
from quart import request
-from astrbot.core import file_token_service, logger
+from astrbot.core import astrbot_config, file_token_service, logger
from astrbot.core.config.astrbot_config import AstrBotConfig
from astrbot.core.config.default import (
CONFIG_METADATA_2,
@@ -179,13 +179,29 @@ class ConfigRoute(Route):
"/config/provider/new": ("POST", self.post_new_provider),
"/config/provider/update": ("POST", self.post_update_provider),
"/config/provider/delete": ("POST", self.post_delete_provider),
+ "/config/provider/template": ("GET", self.get_provider_template),
"/config/provider/check_one": ("GET", self.check_one_provider_status),
"/config/provider/list": ("GET", self.get_provider_config_list),
"/config/provider/model_list": ("GET", self.get_provider_model_list),
"/config/provider/get_embedding_dim": ("POST", self.get_embedding_dim),
+ "/config/provider_sources//models": (
+ "GET",
+ self.get_provider_source_models,
+ ),
}
self.register_routes()
+ async def get_provider_template(self):
+ provider_config = astrbot_config["provider"]
+ config_schema = {
+ "provider": CONFIG_METADATA_2["provider_group"]["metadata"]["provider"]
+ }
+ data = {
+ "config_schema": config_schema,
+ "providers": provider_config,
+ }
+ return Response().ok(data=data).__dict__
+
async def get_uc_table(self):
"""获取 UMOP 配置路由表"""
return Response().ok({"routing": self.ucr.umop_to_conf_id}).__dict__
@@ -522,6 +538,81 @@ class ConfigRoute(Route):
logger.error(traceback.format_exc())
return Response().error(f"获取嵌入维度失败: {e!s}").__dict__
+ async def get_provider_source_models(self, provider_source_id: str):
+ """获取指定 provider_source 支持的模型列表
+
+ 本质上会临时初始化一个 Provider 实例,调用 get_models() 获取模型列表,然后销毁实例
+ """
+ try:
+ from astrbot.core.provider.register import provider_cls_map
+
+ # 从配置中查找对应的 provider_source
+ provider_sources = self.config.get("provider_sources", [])
+ provider_source = None
+ for ps in provider_sources:
+ if ps.get("id") == provider_source_id:
+ provider_source = ps
+ break
+
+ if not provider_source:
+ return (
+ Response()
+ .error(f"未找到 ID 为 {provider_source_id} 的 provider_source")
+ .__dict__
+ )
+
+ # 获取 provider 类型
+ provider_type = provider_source.get("type", None)
+ if not provider_type:
+ return Response().error("provider_source 缺少 type 字段").__dict__
+
+ # 获取对应的 provider 类
+ if provider_type not in provider_cls_map:
+ return (
+ Response()
+ .error(f"未找到适用于 {provider_type} 的提供商适配器")
+ .__dict__
+ )
+
+ provider_metadata = provider_cls_map[provider_type]
+ cls_type = provider_metadata.cls_type
+
+ if not cls_type:
+ return Response().error(f"无法找到 {provider_type} 的类").__dict__
+
+ # 检查是否是 Provider 类型
+ if not issubclass(cls_type, Provider):
+ return (
+ Response()
+ .error(f"提供商 {provider_type} 不支持获取模型列表")
+ .__dict__
+ )
+
+ # 临时实例化 provider
+ inst = cls_type(provider_source, {})
+
+ # 如果有 initialize 方法,调用它
+ init_fn = getattr(inst, "initialize", None)
+ if inspect.iscoroutinefunction(init_fn):
+ await init_fn()
+
+ # 获取模型列表
+ models = await inst.get_models()
+
+ # 销毁实例(如果有 terminate 方法)
+ terminate_fn = getattr(inst, "terminate", None)
+ if inspect.iscoroutinefunction(terminate_fn):
+ await terminate_fn()
+
+ logger.info(
+ f"获取到 provider_source {provider_source_id} 的模型列表: {models}",
+ )
+
+ return Response().ok({"models": models}).__dict__
+ except Exception as e:
+ logger.error(traceback.format_exc())
+ return Response().error(f"获取模型列表失败: {e!s}").__dict__
+
async def get_platform_list(self):
"""获取所有平台的列表"""
platform_list = []
diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue
index d6c6fee9c..85a7c7f07 100644
--- a/dashboard/src/components/shared/AstrBotConfig.vue
+++ b/dashboard/src/components/shared/AstrBotConfig.vue
@@ -508,6 +508,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
font-size: 0.85em;
opacity: 0.7;
font-weight: normal;
+ display: none;
}
.important-hint {
diff --git a/dashboard/src/i18n/locales/en-US/features/provider.json b/dashboard/src/i18n/locales/en-US/features/provider.json
index e08177d3d..2f1ffeb63 100644
--- a/dashboard/src/i18n/locales/en-US/features/provider.json
+++ b/dashboard/src/i18n/locales/en-US/features/provider.json
@@ -85,5 +85,42 @@
"confirm": {
"delete": "Are you sure you want to delete service provider {id}?"
}
+ },
+ "providerTypes": {
+ "title": "Provider Types"
+ },
+ "providerSources": {
+ "title": "Provider Sources",
+ "empty": "No provider sources",
+ "selectHint": "Please select a provider source",
+ "save": "Save Configuration",
+ "saveAndFetchModels": "Save and Fetch Models",
+ "fetchModels": "Fetch Model List",
+ "saveSuccess": "Provider source saved successfully",
+ "saveError": "Failed to save provider source",
+ "deleteConfirm": "Are you sure you want to delete provider source {id}? This will also delete all associated model configurations.",
+ "deleteSuccess": "Provider source deleted successfully",
+ "deleteError": "Failed to delete provider source",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "advancedConfig": "Advanced Configuration...",
+ "fields": {
+ "name": "Name",
+ "apiKey": "API Key",
+ "baseUrl": "Base URL"
+ }
+ },
+ "models": {
+ "available": "Available Models",
+ "configured": "Configured Models",
+ "empty": "No configured models yet. Click \"Fetch Models\" above to add.",
+ "noModelsFound": "No available models found",
+ "fetchError": "Failed to fetch models",
+ "addSuccess": "Model {model} added successfully",
+ "deleteConfirm": "Are you sure you want to delete model {id}?",
+ "deleteSuccess": "Model deleted successfully",
+ "deleteError": "Failed to delete model",
+ "testSuccess": "Model {id} test passed",
+ "testError": "Model test failed"
}
}
\ No newline at end of file
diff --git a/dashboard/src/i18n/locales/zh-CN/features/provider.json b/dashboard/src/i18n/locales/zh-CN/features/provider.json
index 234018829..8733c6e52 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/provider.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/provider.json
@@ -86,5 +86,42 @@
"confirm": {
"delete": "确定要删除模型提供商 {id} 吗?"
}
+ },
+ "providerTypes": {
+ "title": "提供商类型"
+ },
+ "providerSources": {
+ "title": "提供商源",
+ "empty": "暂无提供商源",
+ "selectHint": "请选择一个提供商源",
+ "save": "保存配置",
+ "saveAndFetchModels": "保存并获取模型",
+ "fetchModels": "获取模型列表",
+ "saveSuccess": "提供商源保存成功",
+ "saveError": "提供商源保存失败",
+ "deleteConfirm": "确定要删除提供商源 {id} 吗?这将同时删除关联的所有模型配置。",
+ "deleteSuccess": "提供商源删除成功",
+ "deleteError": "提供商源删除失败",
+ "enabled": "已启用",
+ "disabled": "已禁用",
+ "advancedConfig": "高级配置...",
+ "fields": {
+ "name": "名称",
+ "apiKey": "API Key",
+ "baseUrl": "Base URL"
+ }
+ },
+ "models": {
+ "available": "可用模型",
+ "configured": "已配置的模型",
+ "empty": "暂无已配置的模型,点击上方的\"获取模型列表\"添加",
+ "noModelsFound": "未找到可用模型",
+ "fetchError": "获取模型列表失败",
+ "addSuccess": "模型 {model} 添加成功",
+ "deleteConfirm": "确定要删除模型 {id} 吗?",
+ "deleteSuccess": "模型删除成功",
+ "deleteError": "模型删除失败",
+ "testSuccess": "模型 {id} 测试通过",
+ "testError": "模型测试失败"
}
}
\ No newline at end of file
diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue
index 643beaac6..afdd02c2e 100644
--- a/dashboard/src/views/ProviderPage.vue
+++ b/dashboard/src/views/ProviderPage.vue
@@ -11,7 +11,7 @@
{{ tm('subtitle') }}
-
+
{{ tm('providers.addProvider') }}
@@ -20,88 +20,236 @@
-
-
-
- mdi-filter-variant
- {{ tm('providers.tabs.all') }}
-
-
- mdi-message-text
- {{ tm('providers.tabs.chatCompletion') }}
-
-
- mdi-message-text
- {{ tm('providers.tabs.agentRunner') }}
-
-
- mdi-microphone-message
- {{ tm('providers.tabs.speechToText') }}
-
-
- mdi-volume-high
- {{ tm('providers.tabs.textToSpeech') }}
-
-
- mdi-code-json
- {{ tm('providers.tabs.embedding') }}
-
-
- mdi-compare-vertical
- {{ tm('providers.tabs.rerank') }}
+
+
+
+ {{ type.icon }}
+ {{ type.label }}
-
-
-
- mdi-api-off
- {{ getEmptyText() }}
-
-
-
-
-
{{ group.label }}
-
-
-
-
-
-
-
-
-
- {{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
- getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
- 'mdi-clock-outline' }}
-
- {{ getStatusText(getProviderStatus(item.id).status) }}
-
-
-
- {{ getProviderStatus(item.id).error }}
-
- {{ getStatusText(getProviderStatus(item.id).status) }}
-
-
-
-
- {{ tm('availability.test') }}
+
+
+
+
+
+
+
+ {{ tm('providerSources.title') }}
+
+
+
+
+
+
+
+ {{ sourceType.label }}
+
+
+
+
+
+
+
+
+
+
+ {{ source.id }}
+
+
-
-
-
-
-
-
-
-
+
+
+
+
mdi-api-off
+
{{ tm('providerSources.empty') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedProviderSource.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tm('providerSources.advancedConfig') }}
+
+
+
+
+
+
+
+
+
+
+ {{ isSourceModified ? tm('providerSources.saveAndFetchModels') : tm('providerSources.fetchModels') }}
+
+
+ {{ tm('providerSources.save') }}
+
+
+
+
+
+
{{ tm('models.available') }}
+
+
+ {{ model }}
+
+
+
+
+
+
+
+
+
+
+
{{ tm('models.configured') }}
+
+
+
+
+
+
+ {{ provider.id }}
+ {{ provider.model_config?.model }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
mdi-package-variant
+
{{ tm('models.empty') }}
+
+
+
+
+
+
mdi-cursor-default-click
+
{{ tm('providerSources.selectHint') }}
+
+
+
+
+
+
@@ -146,25 +294,6 @@
-
-
-
-
- mdi-console-line
- {{ tm('logs.title') }}
-
-
- {{ showConsole ? tm('logs.collapse') : tm('logs.expand') }}
- {{ showConsole ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
-
-
-
-
-
-
-
-
-
@@ -195,13 +324,14 @@
-
- {{ save_message }}
+ {{ snackbar.message }}
-
-
@@ -225,657 +355,672 @@
-
-
-
-
-
- mdi-alert-circle-outline
- ID 冲突警告
-
-
- 检测到 ID "{{ conflictId }}" 重复。请使用一个新的 ID。
-
-
-
- 好的
-
-
-
-
-
-
-
-
- mdi-alert-circle-outline
- 确认保存
-
-
- 您没有填写 API Key,确定要保存吗?这可能会导致该模型无法正常工作。
-
-
-
- 取消
- 确定
-
-
-
-
diff --git a/dashboard/src/views/ProviderPage.vue.backup b/dashboard/src/views/ProviderPage.vue.backup
new file mode 100644
index 000000000..643beaac6
--- /dev/null
+++ b/dashboard/src/views/ProviderPage.vue.backup
@@ -0,0 +1,892 @@
+
+
+
+
+
+
+
+ mdi-creation{{ tm('title') }}
+
+
+ {{ tm('subtitle') }}
+
+
+
+
+ {{ tm('providers.addProvider') }}
+
+
+
+
+
+
+
+
+ mdi-filter-variant
+ {{ tm('providers.tabs.all') }}
+
+
+ mdi-message-text
+ {{ tm('providers.tabs.chatCompletion') }}
+
+
+ mdi-message-text
+ {{ tm('providers.tabs.agentRunner') }}
+
+
+ mdi-microphone-message
+ {{ tm('providers.tabs.speechToText') }}
+
+
+ mdi-volume-high
+ {{ tm('providers.tabs.textToSpeech') }}
+
+
+ mdi-code-json
+ {{ tm('providers.tabs.embedding') }}
+
+
+ mdi-compare-vertical
+ {{ tm('providers.tabs.rerank') }}
+
+
+
+
+
+
+ mdi-api-off
+ {{ getEmptyText() }}
+
+
+
+
+
{{ group.label }}
+
+
+
+
+
+
+
+
+
+ {{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
+ getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
+ 'mdi-clock-outline' }}
+
+ {{ getStatusText(getProviderStatus(item.id).status) }}
+
+
+
+ {{ getProviderStatus(item.id).error }}
+
+ {{ getStatusText(getProviderStatus(item.id).status) }}
+
+
+
+
+ {{ tm('availability.test') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-api-off
+ {{ getEmptyText() }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
+ getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
+ 'mdi-clock-outline' }}
+
+ {{ getStatusText(getProviderStatus(item.id).status) }}
+
+
+
+ {{ getProviderStatus(item.id).error }}
+
+ {{ getStatusText(getProviderStatus(item.id).status) }}
+
+
+
+
+ {{ tm('availability.test') }}
+
+
+
+
+
+
+
+
+
+
+
+ mdi-console-line
+ {{ tm('logs.title') }}
+
+
+ {{ showConsole ? tm('logs.collapse') : tm('logs.expand') }}
+ {{ showConsole ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tm('dialogs.config.cancel') }}
+
+
+ {{ tm('dialogs.config.save') }}
+
+
+
+
+
+
+
+ {{ save_message }}
+
+
+
+
+
+
+
+
+ mdi-information
+ 请前往「配置文件」页测试 Agent 执行器
+
+
+ Agent 执行器的测试请在「配置文件」页进行。
+
+ - 找到对应的配置文件并打开。
+ - 找到 Agent 执行方式部分,修改执行器后点击保存。
+ - 点击右下角的 💬 聊天按钮进行测试。
+
+ 要让机器人应用这个 Agent 执行器,你也需要前往修改 Agent 执行器。
+
+
+
+ 好的
+ 点击前往
+
+
+
+
+
+
+
+
+ mdi-alert-circle-outline
+ ID 冲突警告
+
+
+ 检测到 ID "{{ conflictId }}" 重复。请使用一个新的 ID。
+
+
+
+ 好的
+
+
+
+
+
+
+
+
+ mdi-alert-circle-outline
+ 确认保存
+
+
+ 您没有填写 API Key,确定要保存吗?这可能会导致该模型无法正常工作。
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+