feat(model): 添加自定义模型支持(Fix #304)
This commit is contained in:
parent
967014a72d
commit
9a8f1c4ae3
@ -11,7 +11,6 @@
|
||||
- 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
|
||||
- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
|
||||
- 集成 neo4j mcp (或者自己构建工具)
|
||||
- 工具组件重构以支持 todo,files 等渲染。
|
||||
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接
|
||||
|
||||
### Bugs
|
||||
@ -24,10 +23,11 @@
|
||||
- 新增对于上传文件的智能体中间件
|
||||
- 增强文件下载功能
|
||||
- 新增多模态模型支持(当前仅支持图片,详见文档)
|
||||
- 新建 DeepAgents 智能体(Demo)
|
||||
- 新建 DeepAgents 智能体(Demo),支持 todo,files 等渲染。
|
||||
- 新增基于知识库文件生成思维导图功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425))
|
||||
- 新增基于知识库文件生成示例问题功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425))
|
||||
- 新增知识库支持文件夹/压缩包上传的功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425))
|
||||
- 新增自定义模型支持
|
||||
|
||||
### 修复
|
||||
- 修复重排序模型实际未生效的问题
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
|
||||
系统支持多种大语言模型服务商,通过配置对应的 API 密钥即可使用:
|
||||
|
||||
| 服务商 | 环境变量 | 特点 |
|
||||
|--------|----------|------|
|
||||
| 服务商 | 环境变量 | 特点 |
|
||||
| ------------------------------------------------ | ----------------------- | --------------------- |
|
||||
| [硅基流动](https://cloud.siliconflow.cn/i/Eo5yTHGJ) | `SILICONFLOW_API_KEY` | 🆓 免费额度,默认推荐 |
|
||||
| OpenAI | `OPENAI_API_KEY` | GPT 系列模型 |
|
||||
| DeepSeek | `DEEPSEEK_API_KEY` | 国产大模型 |
|
||||
| OpenRouter | `OPENROUTER_API_KEY` | 多模型聚合平台 |
|
||||
| 智谱清言 | `ZHIPUAI_API_KEY` | GLM 系列模型 |
|
||||
| 阿里云百炼 | `DASHSCOPE_API_KEY` | 通义千问系列 |
|
||||
| OpenAI | `OPENAI_API_KEY` | GPT 系列模型 |
|
||||
| DeepSeek | `DEEPSEEK_API_KEY` | 国产大模型 |
|
||||
| OpenRouter | `OPENROUTER_API_KEY` | 多模型聚合平台 |
|
||||
| 智谱清言 | `ZHIPUAI_API_KEY` | GLM 系列模型 |
|
||||
| 阿里云百炼 | `DASHSCOPE_API_KEY` | 通义千问系列 |
|
||||
|
||||
其余还支持火山、Together、vLLM、Ollama 等。
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
在 `.env` 文件中添加对应的环境变量:
|
||||
|
||||
<<< @/../.env.template#model_provider{bash 2}
|
||||
<<< @/../.env.template#model_provider{bash 5}
|
||||
|
||||
### 默认对话模型格式
|
||||
|
||||
@ -29,8 +29,7 @@
|
||||
default_model: siliconflow/deepseek-ai/DeepSeek-V3.2-Exp
|
||||
```
|
||||
|
||||
在 Web 界面中选择模型时也会自动按照这一格式保存,无需手动拆分提供商和模型名称。
|
||||
|
||||
在 Web 界面中选择模型时也会自动按照这一格式保存。
|
||||
|
||||
::: tip 免费获取 API Key
|
||||
[硅基流动](https://cloud.siliconflow.cn/i/Eo5yTHGJ) 注册即送 14 元额度,支持多种开源模型。
|
||||
@ -38,93 +37,123 @@ default_model: siliconflow/deepseek-ai/DeepSeek-V3.2-Exp
|
||||
|
||||
## 自定义模型供应商
|
||||
|
||||
::: warning
|
||||
原本网页中的自定义模型已在 `0.3.x` 版本移除,请在 `src/config/static/models.py` 中按如下方式配置,并重启服务后选择并使用。此外,这里也推荐一下团队的另外一个小工具 [mvllm (Manage and Route vLLM Servers)](https://github.com/xerrors/mvllm)。
|
||||
:::
|
||||
|
||||
::: tip 配置系统升级 (v0.3.x)
|
||||
从 `v0.3.x` 版本开始,模型配置系统已升级为基于 Pydantic BaseModel 的类型安全配置,支持 TOML 格式的用户配置文件。
|
||||
|
||||
- **默认配置**: `src/config/static/models.py` (Python 代码)
|
||||
- **用户配置**: `saves/config/base.toml` (TOML 格式,仅保存用户修改)
|
||||
:::
|
||||
- **自定义供应商**: `saves/config/custom_providers.toml` (独立配置文件)
|
||||
:::
|
||||
|
||||
系统理论上兼容任何 OpenAI 兼容的模型服务,包括:
|
||||
系统提供了完整的自定义供应商管理功能,支持通过 Web 界面直接添加、编辑、测试和删除自定义模型供应商。
|
||||
|
||||
- **vLLM**: 高性能推理服务
|
||||
- **Ollama**: 本地模型管理
|
||||
- **API 中转服务**: 各种代理和聚合服务
|
||||
### 使用方法
|
||||
|
||||
如需添加新的模型供应商,请按以下步骤操作:
|
||||
系统支持任何 OpenAI 兼容的云服务提供商
|
||||
|
||||
### 1. 编辑模型配置文件
|
||||
#### 1. Web 界面操作(推荐)
|
||||
|
||||
**方式一:修改默认配置(推荐)**
|
||||
编辑 `src/config/static/models.py` 文件中的 `DEFAULT_CHAT_MODEL_PROVIDERS` 字典
|
||||
访问 **系统设置 > 模型配置**,在"自定义供应商"部分点击 **添加自定义供应商**。这里的密钥可以直接填写也可以填写对应的环境变量名称。
|
||||
|
||||
在 `src/config/static/models.py` 中添加新的模型供应商:
|
||||
#### 2. 配置文件操作
|
||||
|
||||
```python
|
||||
DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
|
||||
# ... 现有配置 ...
|
||||
如需通过配置文件管理,编辑 `saves/config/custom_providers.toml`:
|
||||
|
||||
"custom-provider": ChatModelProvider(
|
||||
name="自定义提供商",
|
||||
url="https://your-provider.com/docs",
|
||||
base_url="https://api.your-provider.com/v1",
|
||||
default="custom-model-name",
|
||||
env="CUSTOM_API_KEY_ENV_NAME",
|
||||
models=[
|
||||
"supported-model-name",
|
||||
"another-model-name",
|
||||
],
|
||||
),
|
||||
```toml
|
||||
[model_names.local-vllm]
|
||||
name = "本地 vLLM 服务"
|
||||
url = "https://docs.vllm.ai"
|
||||
base_url = "http://localhost:8000/v1"
|
||||
default = "Qwen/Qwen2.5-7B-Instruct"
|
||||
env = "LOCAL_VLLM_API_KEY"
|
||||
models = [
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2.5-14B-Instruct",
|
||||
]
|
||||
custom = true
|
||||
|
||||
# 本地 Ollama 服务
|
||||
"local-ollama": ChatModelProvider(
|
||||
name="Local Ollama",
|
||||
url="https://ollama.com",
|
||||
base_url="http://localhost:11434/v1",
|
||||
default="llama3.2",
|
||||
env="NO_API_KEY", # 对于不需要API Key的服务,使用NO_API_KEY
|
||||
models=["llama3.2", "qwen2.5"],
|
||||
),
|
||||
|
||||
# 本地 vLLM 服务
|
||||
"local-vllm": ChatModelProvider(
|
||||
name="Local vLLM",
|
||||
url="https://docs.vllm.ai",
|
||||
base_url="http://localhost:8000/v1",
|
||||
default="Qwen/Qwen2.5-7B-Instruct",
|
||||
env="NO_API_KEY",
|
||||
models=[
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2.5-14B-Instruct",
|
||||
],
|
||||
),
|
||||
}
|
||||
[model_names.local-ollama]
|
||||
name = "本地 Ollama"
|
||||
url = "https://ollama.com"
|
||||
base_url = "http://localhost:11434/v1"
|
||||
default = "llama3.2"
|
||||
env = "NO_API_KEY"
|
||||
models = ["llama3.2", "qwen2.5"]
|
||||
custom = true
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
然后在 `.env` 文件中添加对应的环境变量:
|
||||
|
||||
在 `.env` 文件中添加对应的环境变量:
|
||||
```env
|
||||
CUSTOM_API_KEY_ENV_NAME=your_api_key_here
|
||||
LOCAL_VLLM_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
### 3. 重新部署
|
||||
### API 端点
|
||||
|
||||
```bash
|
||||
docker compose restart api-dev
|
||||
系统提供以下 API 端点管理自定义供应商:
|
||||
|
||||
- `GET /api/system/custom-providers` - 获取所有自定义供应商
|
||||
- `POST /api/system/custom-providers` - 添加自定义供应商
|
||||
- `PUT /api/system/custom-providers/{provider_id}` - 更新自定义供应商
|
||||
- `DELETE /api/system/custom-providers/{provider_id}` - 删除自定义供应商
|
||||
- `POST /api/system/custom-providers/{provider_id}/test` - 测试供应商连接
|
||||
|
||||
### 常见配置示例
|
||||
|
||||
#### vLLM 本地服务
|
||||
|
||||
```toml
|
||||
[model_names.vllm-local]
|
||||
name = "vLLM 本地服务"
|
||||
base_url = "http://localhost:8000/v1"
|
||||
default = "Qwen/Qwen2.5-7B-Instruct"
|
||||
env = "NO_API_KEY"
|
||||
models = [
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2.5-14B-Instruct",
|
||||
"meta-llama/Llama-3.1-8B-Instruct"
|
||||
]
|
||||
```
|
||||
|
||||
#### Ollama 本地服务
|
||||
|
||||
```toml
|
||||
[model_names.ollama-local]
|
||||
name = "Ollama 本地服务"
|
||||
base_url = "http://localhost:11434/v1"
|
||||
default = "llama3.2"
|
||||
env = "NO_API_KEY"
|
||||
models = [
|
||||
"llama3.2:latest",
|
||||
"qwen2.5:latest",
|
||||
"codellama:latest"
|
||||
]
|
||||
```
|
||||
|
||||
#### 第三方 API 中转服务
|
||||
|
||||
```toml
|
||||
[model_names.api-proxy]
|
||||
name = "API 中转服务"
|
||||
base_url = "https://api-proxy.example.com/v1"
|
||||
default = "gpt-3.5-turbo"
|
||||
env = "API_PROXY_KEY"
|
||||
models = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4",
|
||||
"claude-3-sonnet"
|
||||
]
|
||||
```
|
||||
|
||||
### 故障排除
|
||||
|
||||
1. **测试连接失败**: 检查 API 地址格式和 API 密钥配置
|
||||
2. **模型不可用**: 确认模型名称拼写和服务端是否支持该模型
|
||||
3. **权限错误**: 确保用户具有管理员权限
|
||||
4. **配置未生效**: 检查环境变量配置和服务重启状态
|
||||
|
||||
## 嵌入模型和重排序模型
|
||||
|
||||
::: warning 重要说明
|
||||
从 v0.2 版本开始,项目采用微服务架构,模型部署与项目本身完全解耦。如需使用本地模型,需要先通过 vLLM 或 Ollama 部署为 API 服务。
|
||||
:::
|
||||
|
||||
### 本地模型部署
|
||||
|
||||
#### 1. 配置模型信息
|
||||
|
||||
在 `src/config/static/models.py` 中的默认配置部分添加:
|
||||
@ -195,4 +224,4 @@ vllm serve BAAI/bge-reranker-v2-m3 \
|
||||
--task score \
|
||||
--dtype fp16 \
|
||||
--port 8000
|
||||
```
|
||||
```
|
||||
|
||||
@ -210,3 +210,102 @@ async def get_all_chat_models_status(current_user: User = Depends(get_admin_user
|
||||
except Exception as e:
|
||||
logger.error(f"获取所有聊天模型状态失败: {e}")
|
||||
return {"message": f"获取所有聊天模型状态失败: {e}", "status": {"models": {}, "total": 0, "available": 0}}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === 自定义供应商管理分组 ===
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@system.get("/custom-providers")
|
||||
async def get_custom_providers(current_user: User = Depends(get_admin_user)):
|
||||
"""获取所有自定义供应商"""
|
||||
try:
|
||||
custom_providers = config.get_custom_providers()
|
||||
return {
|
||||
"providers": {provider: info.model_dump() for provider, info in custom_providers.items()},
|
||||
"message": "success"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取自定义供应商失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取自定义供应商失败: {str(e)}")
|
||||
|
||||
|
||||
@system.post("/custom-providers")
|
||||
async def add_custom_provider(
|
||||
provider_id: str = Body(..., description="供应商ID"),
|
||||
provider_data: dict = Body(..., description="供应商配置数据"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""添加自定义供应商"""
|
||||
try:
|
||||
success = config.add_custom_provider(provider_id, provider_data)
|
||||
if success:
|
||||
return {"message": f"自定义供应商 {provider_id} 添加成功"}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"供应商ID {provider_id} 已存在,请使用其他ID")
|
||||
except Exception as e:
|
||||
logger.error(f"添加自定义供应商失败 {provider_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"添加自定义供应商失败: {str(e)}")
|
||||
|
||||
|
||||
@system.put("/custom-providers/{provider_id}")
|
||||
async def update_custom_provider(
|
||||
provider_id: str,
|
||||
provider_data: dict = Body(..., description="供应商配置数据"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""更新自定义供应商"""
|
||||
try:
|
||||
success = config.update_custom_provider(provider_id, provider_data)
|
||||
if success:
|
||||
return {"message": f"自定义供应商 {provider_id} 更新成功"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"自定义供应商 {provider_id} 不存在或更新失败")
|
||||
except Exception as e:
|
||||
logger.error(f"更新自定义供应商失败 {provider_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"更新自定义供应商失败: {str(e)}")
|
||||
|
||||
|
||||
@system.delete("/custom-providers/{provider_id}")
|
||||
async def delete_custom_provider(provider_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""删除自定义供应商"""
|
||||
try:
|
||||
success = config.delete_custom_provider(provider_id)
|
||||
if success:
|
||||
return {"message": f"自定义供应商 {provider_id} 删除成功"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"自定义供应商 {provider_id} 不存在或删除失败")
|
||||
except Exception as e:
|
||||
logger.error(f"删除自定义供应商失败 {provider_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"删除自定义供应商失败: {str(e)}")
|
||||
|
||||
|
||||
@system.post("/custom-providers/{provider_id}/test")
|
||||
async def test_custom_provider(
|
||||
provider_id: str,
|
||||
request: dict = Body(..., description="测试请求"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""测试自定义供应商连接"""
|
||||
try:
|
||||
# 从请求中获取model_name
|
||||
model_name = request.get("model_name")
|
||||
if not model_name:
|
||||
raise HTTPException(status_code=400, detail="缺少model_name参数")
|
||||
|
||||
# 检查供应商是否存在
|
||||
if provider_id not in config.model_names:
|
||||
raise HTTPException(status_code=404, detail=f"供应商 {provider_id} 不存在")
|
||||
|
||||
# 测试模型状态
|
||||
status = await test_chat_model_status(provider_id, model_name)
|
||||
return {"status": status, "message": "测试完成"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"测试自定义供应商失败 {provider_id}/{model_name}: {e}")
|
||||
return {
|
||||
"message": f"测试自定义供应商失败: {e}",
|
||||
"status": {"provider": provider_id, "model_name": model_name, "status": "error", "message": str(e)},
|
||||
}
|
||||
|
||||
@ -26,6 +26,8 @@ from src.config.static.models import (
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""应用配置类"""
|
||||
|
||||
@ -116,6 +118,7 @@ class Config(BaseModel):
|
||||
super().__init__(**data)
|
||||
self._setup_paths()
|
||||
self._load_user_config()
|
||||
self._load_custom_providers()
|
||||
self._handle_environment()
|
||||
|
||||
def _setup_paths(self):
|
||||
@ -166,6 +169,36 @@ class Config(BaseModel):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load model names: {e}")
|
||||
|
||||
def _load_custom_providers(self):
|
||||
"""从独立的TOML文件加载自定义供应商配置"""
|
||||
custom_config_file = self._config_file.parent / "custom_providers.toml"
|
||||
|
||||
if not custom_config_file.exists():
|
||||
logger.info(f"Custom providers config file not found: {custom_config_file}")
|
||||
return
|
||||
|
||||
logger.info(f"Loading custom providers from {custom_config_file}")
|
||||
try:
|
||||
with open(custom_config_file, "rb") as f:
|
||||
custom_config = tomli.load(f)
|
||||
|
||||
# 加载自定义供应商
|
||||
if "model_names" in custom_config:
|
||||
self._load_custom_model_providers(custom_config["model_names"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load custom providers from {custom_config_file}: {e}")
|
||||
|
||||
def _load_custom_model_providers(self, providers_data):
|
||||
"""加载自定义模型供应商"""
|
||||
try:
|
||||
for provider, provider_data in providers_data.items():
|
||||
provider_data["custom"] = True
|
||||
self.model_names[provider] = ChatModelProvider(**provider_data)
|
||||
logger.info(f"Loaded {len(providers_data)} custom model providers")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load custom model providers: {e}")
|
||||
|
||||
def _handle_environment(self):
|
||||
"""处理环境变量和运行时状态"""
|
||||
# 处理模型目录
|
||||
@ -182,10 +215,13 @@ class Config(BaseModel):
|
||||
self.model_provider_status = {}
|
||||
for provider, info in self.model_names.items():
|
||||
env_var = info.env
|
||||
|
||||
if env_var == "NO_API_KEY":
|
||||
self.model_provider_status[provider] = True
|
||||
else:
|
||||
self.model_provider_status[provider] = bool(os.getenv(env_var))
|
||||
api_key = os.environ.get(env_var)
|
||||
# 如果获取到的值与环境变量名不同,说明环境变量存在或配置了直接值
|
||||
self.model_provider_status[provider] = bool(api_key or info.custom)
|
||||
|
||||
# 检查网络搜索
|
||||
if os.getenv("TAVILY_API_KEY"):
|
||||
@ -354,6 +390,170 @@ class Config(BaseModel):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save models config to {self._config_file}: {e}")
|
||||
|
||||
# ============================================================
|
||||
# 自定义供应商管理方法
|
||||
# ============================================================
|
||||
|
||||
def add_custom_provider(self, provider_id: str, provider_data: dict) -> bool:
|
||||
"""添加自定义供应商
|
||||
|
||||
Args:
|
||||
provider_id: 供应商唯一标识符
|
||||
provider_data: 供应商配置数据
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
try:
|
||||
# 处理环境变量,移除 ${} 包裹
|
||||
if "env" in provider_data and provider_data["env"]:
|
||||
env_value = provider_data["env"]
|
||||
if isinstance(env_value, str) and env_value.startswith("${") and env_value.endswith("}"):
|
||||
provider_data["env"] = env_value[2:-1]
|
||||
|
||||
# 确保标记为自定义供应商
|
||||
provider_data["custom"] = True
|
||||
|
||||
# 检查供应商ID是否已存在(无论是内置还是自定义)
|
||||
if provider_id in self.model_names:
|
||||
logger.error(f"Provider ID already exists: {provider_id}")
|
||||
return False
|
||||
|
||||
# 添加到配置中
|
||||
self.model_names[provider_id] = ChatModelProvider(**provider_data)
|
||||
|
||||
# 保存到自定义供应商配置文件
|
||||
self._save_custom_providers()
|
||||
|
||||
# 重新处理环境变量
|
||||
self._handle_environment()
|
||||
|
||||
logger.info(f"Added custom provider: {provider_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add custom provider {provider_id}: {e}")
|
||||
return False
|
||||
|
||||
def update_custom_provider(self, provider_id: str, provider_data: dict) -> bool:
|
||||
"""更新自定义供应商
|
||||
|
||||
Args:
|
||||
provider_id: 供应商唯一标识符
|
||||
provider_data: 新的供应商配置数据
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
try:
|
||||
# 处理环境变量,移除 ${} 包裹
|
||||
if "env" in provider_data and provider_data["env"]:
|
||||
env_value = provider_data["env"]
|
||||
if isinstance(env_value, str) and env_value.startswith("${") and env_value.endswith("}"):
|
||||
provider_data["env"] = env_value[2:-1]
|
||||
|
||||
# 检查供应商是否存在且为自定义供应商
|
||||
if provider_id not in self.model_names:
|
||||
logger.error(f"Provider not found: {provider_id}")
|
||||
return False
|
||||
|
||||
if not self.model_names[provider_id].custom:
|
||||
logger.error(f"Cannot update non-custom provider: {provider_id}")
|
||||
return False
|
||||
|
||||
# 确保保持自定义供应商标记
|
||||
provider_data["custom"] = True
|
||||
|
||||
# 更新供应商配置
|
||||
self.model_names[provider_id] = ChatModelProvider(**provider_data)
|
||||
|
||||
# 保存到自定义供应商配置文件
|
||||
self._save_custom_providers()
|
||||
|
||||
# 重新处理环境变量
|
||||
self._handle_environment()
|
||||
|
||||
logger.info(f"Updated custom provider: {provider_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update custom provider {provider_id}: {e}")
|
||||
return False
|
||||
|
||||
def delete_custom_provider(self, provider_id: str) -> bool:
|
||||
"""删除自定义供应商
|
||||
|
||||
Args:
|
||||
provider_id: 供应商唯一标识符
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
# 检查供应商是否存在且为自定义供应商
|
||||
if provider_id not in self.model_names:
|
||||
logger.error(f"Provider not found: {provider_id}")
|
||||
return False
|
||||
|
||||
if not self.model_names[provider_id].custom:
|
||||
logger.error(f"Cannot delete non-custom provider: {provider_id}")
|
||||
return False
|
||||
|
||||
# 从配置中删除
|
||||
del self.model_names[provider_id]
|
||||
|
||||
# 保存到自定义供应商配置文件
|
||||
self._save_custom_providers()
|
||||
|
||||
# 重新处理环境变量
|
||||
self._handle_environment()
|
||||
|
||||
logger.info(f"Deleted custom provider: {provider_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete custom provider {provider_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_custom_providers(self) -> dict[str, ChatModelProvider]:
|
||||
"""获取所有自定义供应商
|
||||
|
||||
Returns:
|
||||
自定义供应商字典
|
||||
"""
|
||||
return {k: v for k, v in self.model_names.items() if v.custom}
|
||||
|
||||
def _save_custom_providers(self):
|
||||
"""保存自定义供应商到独立配置文件"""
|
||||
if not self._config_file:
|
||||
logger.warning("Config file path not set")
|
||||
return
|
||||
|
||||
custom_config_file = self._config_file.parent / "custom_providers.toml"
|
||||
|
||||
try:
|
||||
# 获取所有自定义供应商
|
||||
custom_providers = self.get_custom_providers()
|
||||
|
||||
# 创建配置数据
|
||||
custom_config = {}
|
||||
if custom_providers:
|
||||
custom_config["model_names"] = {
|
||||
provider: info.model_dump() for provider, info in custom_providers.items()
|
||||
}
|
||||
|
||||
# 确保目录存在
|
||||
custom_config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入配置文件
|
||||
with open(custom_config_file, "wb") as f:
|
||||
tomli_w.dump(custom_config, f)
|
||||
|
||||
logger.info(f"Custom providers saved to {custom_config_file}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save custom providers to {custom_config_file}: {e}")
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
|
||||
@ -19,6 +19,7 @@ class ChatModelProvider(BaseModel):
|
||||
default: str = Field(..., description="默认模型名称")
|
||||
env: str = Field(..., description="API Key 环境变量名")
|
||||
models: list[str] = Field(default_factory=list, description="支持的模型列表")
|
||||
custom: bool = Field(default=False, description="是否为自定义供应商")
|
||||
|
||||
|
||||
class EmbedModelInfo(BaseModel):
|
||||
|
||||
@ -131,7 +131,7 @@ def select_model(model_provider=None, model_name=None, model_spec=None):
|
||||
# 其他模型,默认使用OpenAIBase
|
||||
try:
|
||||
model = OpenAIBase(
|
||||
api_key=os.getenv(model_info.env),
|
||||
api_key=os.environ.get(model_info.env, model_info.env),
|
||||
base_url=model_info.base_url,
|
||||
model_name=model_name,
|
||||
)
|
||||
|
||||
@ -56,6 +56,17 @@ export async function apiRequest(url, options = {}, requiresAuth = true, respons
|
||||
errorData = await response.json()
|
||||
errorMessage = errorData.detail || errorData.message || errorMessage
|
||||
console.log('API错误详情:', errorData);
|
||||
|
||||
// 如果是422错误,打印更详细的信息
|
||||
if (response.status === 422) {
|
||||
console.error('422验证错误详情:', {
|
||||
url,
|
||||
requestMethod: requestOptions.method,
|
||||
requestHeaders: requestOptions.headers,
|
||||
requestBody: requestOptions.body,
|
||||
responseData: errorData
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果无法解析JSON,使用默认错误信息
|
||||
console.log('无法解析错误响应JSON:', e);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { apiGet, apiPost, apiAdminGet, apiAdminPost, apiSuperAdminPost } from './base'
|
||||
import { apiGet, apiPost, apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
|
||||
/**
|
||||
* 系统管理API模块
|
||||
@ -51,7 +51,7 @@ export const configApi = {
|
||||
*/
|
||||
updateConfigBatch: async (items) => apiAdminPost('/api/system/config/update', items),
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取系统日志
|
||||
* @returns {Promise} - 系统日志
|
||||
@ -119,5 +119,58 @@ export const chatModelApi = {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// === 自定义供应商管理分组 ===
|
||||
// =============================================================================
|
||||
|
||||
export const customProviderApi = {
|
||||
/**
|
||||
* 获取所有自定义供应商
|
||||
* @returns {Promise} - 自定义供应商列表
|
||||
*/
|
||||
getCustomProviders: async () => {
|
||||
return apiAdminGet('/api/system/custom-providers')
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加自定义供应商
|
||||
* @param {string} providerId - 供应商ID
|
||||
* @param {Object} providerData - 供应商配置数据
|
||||
* @returns {Promise} - 添加结果
|
||||
*/
|
||||
addCustomProvider: async (providerId, providerData) => {
|
||||
return apiAdminPost('/api/system/custom-providers', { provider_id: providerId, provider_data: providerData })
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新自定义供应商
|
||||
* @param {string} providerId - 供应商ID
|
||||
* @param {Object} providerData - 供应商配置数据
|
||||
* @returns {Promise} - 更新结果
|
||||
*/
|
||||
updateCustomProvider: async (providerId, providerData) => {
|
||||
return apiAdminPut(`/api/system/custom-providers/${encodeURIComponent(providerId)}`, providerData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除自定义供应商
|
||||
* @param {string} providerId - 供应商ID
|
||||
* @returns {Promise} - 删除结果
|
||||
*/
|
||||
deleteCustomProvider: async (providerId) => {
|
||||
return apiAdminDelete(`/api/system/custom-providers/${encodeURIComponent(providerId)}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 测试自定义供应商连接
|
||||
* @param {string} providerId - 供应商ID
|
||||
* @param {string} modelName - 要测试的模型名称
|
||||
* @returns {Promise} - 测试结果
|
||||
*/
|
||||
testCustomProvider: async (providerId, modelName) => {
|
||||
return apiAdminPost(`/api/system/custom-providers/${encodeURIComponent(providerId)}/test`, { model_name: modelName })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,62 +1,156 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-alert message="自定义模型在 0.3.x 的稳定版中移除,只能通过修改 src/config/static/models.py 来添加模型和供应商。" type="warning" />
|
||||
<br>
|
||||
<div class="model-provider-card configured-provider" v-for="(item, key) in modelKeys" :key="key">
|
||||
<div class="card-header" @click="toggleExpand(item)">
|
||||
<div :class="{'model-icon': true, 'available': modelStatus[item]}">
|
||||
<img :src="modelIcons[item] || modelIcons.default" alt="模型图标">
|
||||
</div>
|
||||
<div class="model-title-container">
|
||||
<h3>{{ modelNames[item].name }}</h3>
|
||||
</div>
|
||||
<a-button
|
||||
type="text"
|
||||
class="expand-button"
|
||||
@click.stop="openProviderConfig(item)"
|
||||
title="配置模型提供商"
|
||||
>
|
||||
<SettingOutlined />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
class="expand-button"
|
||||
@click.stop="toggleExpand(item)"
|
||||
>
|
||||
<span class="icon-wrapper" :class="{'rotated': expandedModels[item]}">
|
||||
<DownCircleOutlined />
|
||||
</span>
|
||||
<!-- 自定义供应商管理区域 -->
|
||||
<div class="custom-providers-section">
|
||||
<div class="section-header">
|
||||
<h3>自定义供应商</h3>
|
||||
<a-button type="primary" @click="openAddCustomProviderModal">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加自定义供应商
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="card-body-wrapper" :class="{'expanded': expandedModels[item]}">
|
||||
<div class="card-body" v-if="modelStatus[item]">
|
||||
<div class="card-models" v-for="(model, idx) in modelNames[item].models" :key="idx">
|
||||
<div class="model_name">{{ model }}</div>
|
||||
<p class="section-description">
|
||||
添加自定义的LLM供应商,支持OpenAI兼容的API格式。API密钥支持直接配置或使用环境变量名。
|
||||
</p>
|
||||
|
||||
<!-- 自定义供应商列表 -->
|
||||
<div class="custom-provider-card" v-for="(provider, providerId) in customProviders" :key="providerId">
|
||||
<div class="card-header">
|
||||
<div class="provider-info">
|
||||
<h4>{{ provider.name }}</h4>
|
||||
<span class="provider-id">{{ providerId }}</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<a-button type="text" size="small" @click="testCustomProvider(providerId, provider.default)">
|
||||
<template #icon>
|
||||
<ApiOutlined />
|
||||
</template>
|
||||
测试连接
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="openEditCustomProviderModal(providerId, provider)">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个自定义供应商吗?"
|
||||
@confirm="deleteCustomProvider(providerId)"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-button type="text" size="small" danger>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="provider-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">API地址:</span>
|
||||
<span class="value">{{ provider.base_url }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">默认模型:</span>
|
||||
<span class="value">{{ provider.default }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">可用模型:</span>
|
||||
<span class="value">{{ provider.models?.join(', ') || '无' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无自定义供应商时的提示 -->
|
||||
<div v-if="Object.keys(customProviders).length === 0" class="empty-state">
|
||||
<a-empty description="暂无自定义供应商">
|
||||
<a-button type="primary" @click="openAddCustomProviderModal">添加自定义供应商</a-button>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-provider-card unconfigured-provider" v-for="(item, key) in notModelKeys" :key="key">
|
||||
<div class="card-header">
|
||||
<div class="model-icon">
|
||||
<img :src="modelIcons[item] || modelIcons.default" alt="模型图标">
|
||||
|
||||
<a-divider />
|
||||
|
||||
<!-- 系统内置供应商 -->
|
||||
<div class="builtin-providers-section">
|
||||
<div class="section-header">
|
||||
<h3>系统内置供应商</h3>
|
||||
<div class="providers-stats">
|
||||
<span class="stats-item available">
|
||||
{{ modelKeys.length }} 可用
|
||||
</span>
|
||||
<span class="stats-item unavailable">
|
||||
{{ notModelKeys.length }} 未配置
|
||||
</span>
|
||||
</div>
|
||||
<div class="model-title-container">
|
||||
<h3 style="font-weight: 400">{{ modelNames[item].name }}</h3>
|
||||
<a :href="modelNames[item].url" target="_blank" class="model-url">
|
||||
<InfoCircleOutlined />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 已配置的供应商 -->
|
||||
<div class="model-provider-card configured-provider" v-for="(item, key) in modelKeys" :key="key">
|
||||
<div class="card-header" @click="toggleExpand(item)">
|
||||
<div :class="{'model-icon': true, 'available': modelStatus[item]}">
|
||||
<img :src="modelIcons[item] || modelIcons.default" alt="模型图标">
|
||||
</div>
|
||||
<div class="model-title-container">
|
||||
<h3>{{ modelNames[item].name }}</h3>
|
||||
<div class="provider-meta">
|
||||
<span class="model-count">{{ modelNames[item].models?.length || 0 }} 个模型</span>
|
||||
<span class="provider-id">{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="expand-button"
|
||||
@click.stop="openProviderConfig(item)"
|
||||
title="配置模型"
|
||||
>
|
||||
<SettingOutlined />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="expand-button"
|
||||
@click.stop="toggleExpand(item)"
|
||||
>
|
||||
<span class="icon-wrapper" :class="{'rotated': expandedModels[item]}">
|
||||
<DownCircleOutlined />
|
||||
</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-button
|
||||
type="text"
|
||||
class="config-button"
|
||||
@click.stop="openProviderConfig(item)"
|
||||
title="配置模型提供商"
|
||||
>
|
||||
<SettingOutlined />
|
||||
</a-button>
|
||||
<div class="missing-keys">
|
||||
需配置<span>{{ modelNames[item].env }}</span>
|
||||
<div class="card-body-wrapper" :class="{'expanded': expandedModels[item]}">
|
||||
<div class="card-body" v-if="modelStatus[item]">
|
||||
<div class="card-models" v-for="(model, idx) in modelNames[item].models" :key="idx">
|
||||
<div class="model_name">{{ model }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未配置的供应商 -->
|
||||
<div class="model-provider-card unconfigured-provider" v-for="(item, key) in notModelKeys" :key="key">
|
||||
<div class="card-header">
|
||||
<div class="model-icon">
|
||||
<img :src="modelIcons[item] || modelIcons.default" alt="模型图标">
|
||||
</div>
|
||||
<div class="model-title-container">
|
||||
<h3>{{ modelNames[item].name }}</h3>
|
||||
<a :href="modelNames[item].url" target="_blank" class="model-url">
|
||||
查看信息 <InfoCircleOutlined />
|
||||
</a>
|
||||
</div>
|
||||
<div class="missing-keys">
|
||||
需配置<span>{{ modelNames[item].env }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -128,22 +222,104 @@
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 自定义供应商配置弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="customProviderModal.visible"
|
||||
:title="customProviderModal.isEdit ? '编辑自定义供应商' : '添加自定义供应商'"
|
||||
@ok="saveCustomProvider"
|
||||
@cancel="cancelCustomProvider"
|
||||
:okText="'保存'"
|
||||
:cancelText="'取消'"
|
||||
:ok-type="'primary'"
|
||||
:width="600"
|
||||
:confirmLoading="customProviderModal.loading"
|
||||
>
|
||||
<a-form
|
||||
ref="customProviderForm"
|
||||
:model="customProviderModal.data"
|
||||
:rules="customProviderRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="供应商ID" name="providerId" v-if="!customProviderModal.isEdit">
|
||||
<a-input
|
||||
v-model:value="customProviderModal.data.providerId"
|
||||
placeholder="请输入唯一的供应商标识符(如:my-provider)"
|
||||
:disabled="customProviderModal.isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="供应商名称" name="name">
|
||||
<a-input
|
||||
v-model:value="customProviderModal.data.name"
|
||||
placeholder="请输入供应商显示名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="API地址" name="base_url">
|
||||
<a-input
|
||||
v-model:value="customProviderModal.data.base_url"
|
||||
placeholder="请输入API基础地址(如:https://api.example.com/v1)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="默认模型" name="default">
|
||||
<a-input
|
||||
v-model:value="customProviderModal.data.default"
|
||||
placeholder="请输入默认模型名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="API密钥" name="env">
|
||||
<a-input
|
||||
v-model:value="customProviderModal.data.env"
|
||||
placeholder="请输入API密钥或环境变量名(如:MY_API_KEY)"
|
||||
/>
|
||||
<div class="form-help-text">
|
||||
支持直接输入API密钥,或使用环境变量名(如:MY_API_KEY)
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="支持的模型" name="models">
|
||||
<a-textarea
|
||||
v-model:value="customProviderModal.data.modelsText"
|
||||
placeholder="请输入支持的模型列表,每行一个模型"
|
||||
:rows="4"
|
||||
/>
|
||||
<div class="form-help-text">
|
||||
每行输入一个模型名称,例如:gpt-3.5-turbo
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="文档地址" name="url">
|
||||
<a-input
|
||||
v-model:value="customProviderModal.data.url"
|
||||
placeholder="请输入供应商文档地址(可选)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, watch, h } from 'vue'
|
||||
import { computed, reactive, watch, h, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
SettingOutlined,
|
||||
DownCircleOutlined,
|
||||
LoadingOutlined,
|
||||
SearchOutlined
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ApiOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { modelIcons } from '@/utils/modelIcon';
|
||||
import { agentApi } from '@/apis/agent_api';
|
||||
import { customProviderApi } from '@/apis/system_api';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
@ -151,6 +327,14 @@ const configStore = useConfigStore();
|
||||
const modelNames = computed(() => configStore.config?.model_names);
|
||||
const modelStatus = computed(() => configStore.config?.model_provider_status);
|
||||
|
||||
// 自定义供应商计算属性
|
||||
const customProviders = computed(() => {
|
||||
const providers = configStore.config?.model_names || {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).filter(([key, value]) => value.custom === true)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// 提供商配置相关状态
|
||||
const providerConfig = reactive({
|
||||
@ -166,7 +350,7 @@ const providerConfig = reactive({
|
||||
|
||||
// 筛选 modelStatus 中为真的key
|
||||
const modelKeys = computed(() => {
|
||||
return Object.keys(modelStatus.value || {}).filter(key => modelStatus.value[key]);
|
||||
return Object.keys(modelStatus.value || {}).filter(key => modelStatus.value[key] && !customProviders.value[key]);
|
||||
});
|
||||
|
||||
// 筛选 modelStatus 中为假的key
|
||||
@ -294,9 +478,382 @@ const filteredModels = computed(() => {
|
||||
const searchQuery = providerConfig.searchQuery.toLowerCase();
|
||||
return allModels.filter(model => model.id.toLowerCase().includes(searchQuery));
|
||||
});
|
||||
|
||||
// 自定义供应商管理
|
||||
const customProviderForm = ref();
|
||||
const customProviderModal = reactive({
|
||||
visible: false,
|
||||
isEdit: false,
|
||||
loading: false,
|
||||
data: {
|
||||
providerId: '',
|
||||
name: '',
|
||||
base_url: '',
|
||||
default: '',
|
||||
env: '',
|
||||
modelsText: '',
|
||||
models: [],
|
||||
url: ''
|
||||
}
|
||||
});
|
||||
|
||||
// 自定义供应商表单验证规则
|
||||
const customProviderRules = {
|
||||
providerId: [
|
||||
{ required: true, message: '请输入供应商ID', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '供应商ID只能包含字母、数字、下划线和横线', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
// 检查是否与现有供应商ID重复
|
||||
if (modelNames.value && modelNames.value[value]) {
|
||||
return Promise.reject('供应商ID已存在,请使用其他ID');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入供应商名称', trigger: 'blur' }
|
||||
],
|
||||
base_url: [
|
||||
{ required: true, message: '请输入API地址', trigger: 'blur' },
|
||||
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' }
|
||||
],
|
||||
default: [
|
||||
{ required: true, message: '请输入默认模型', trigger: 'blur' }
|
||||
],
|
||||
env: [
|
||||
{ required: true, message: '请输入API密钥或环境变量', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
// API密钥掩码显示
|
||||
const maskApiKey = (apiKey) => {
|
||||
if (!apiKey) return '未配置';
|
||||
|
||||
// 如果是环境变量格式,直接显示
|
||||
if (apiKey.startsWith('${') && apiKey.endsWith('}')) {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
// 如果是直接的API密钥,进行掩码处理
|
||||
if (apiKey.length > 8) {
|
||||
return apiKey.substring(0, 4) + '***' + apiKey.substring(apiKey.length - 4);
|
||||
}
|
||||
return '***';
|
||||
};
|
||||
|
||||
// 打开添加自定义供应商弹窗
|
||||
const openAddCustomProviderModal = () => {
|
||||
customProviderModal.visible = true;
|
||||
customProviderModal.isEdit = false;
|
||||
resetCustomProviderForm();
|
||||
};
|
||||
|
||||
// 打开编辑自定义供应商弹窗
|
||||
const openEditCustomProviderModal = (providerId, provider) => {
|
||||
customProviderModal.visible = true;
|
||||
customProviderModal.isEdit = true;
|
||||
|
||||
// 填充表单数据
|
||||
customProviderModal.data.providerId = providerId;
|
||||
customProviderModal.data.name = provider.name;
|
||||
customProviderModal.data.base_url = provider.base_url;
|
||||
customProviderModal.data.default = provider.default;
|
||||
customProviderModal.data.env = provider.env;
|
||||
customProviderModal.data.models = provider.models || [];
|
||||
customProviderModal.data.modelsText = (provider.models || []).join('\n');
|
||||
customProviderModal.data.url = provider.url || '';
|
||||
};
|
||||
|
||||
// 重置自定义供应商表单
|
||||
const resetCustomProviderForm = () => {
|
||||
customProviderModal.data = {
|
||||
providerId: '',
|
||||
name: '',
|
||||
base_url: '',
|
||||
default: '',
|
||||
env: '',
|
||||
modelsText: '',
|
||||
models: [],
|
||||
url: ''
|
||||
};
|
||||
customProviderForm.value?.resetFields();
|
||||
};
|
||||
|
||||
// 保存自定义供应商
|
||||
const saveCustomProvider = async () => {
|
||||
try {
|
||||
await customProviderForm.value.validate();
|
||||
customProviderModal.loading = true;
|
||||
|
||||
// 处理模型列表
|
||||
const models = customProviderModal.data.modelsText
|
||||
.split('\n')
|
||||
.map(model => model.trim())
|
||||
.filter(model => model.length > 0);
|
||||
|
||||
const providerData = {
|
||||
name: customProviderModal.data.name,
|
||||
base_url: customProviderModal.data.base_url,
|
||||
default: customProviderModal.data.default,
|
||||
env: customProviderModal.data.env,
|
||||
models: models,
|
||||
url: customProviderModal.data.url,
|
||||
custom: true
|
||||
};
|
||||
|
||||
let result;
|
||||
if (customProviderModal.isEdit) {
|
||||
result = await customProviderApi.updateCustomProvider(
|
||||
customProviderModal.data.providerId,
|
||||
providerData
|
||||
);
|
||||
message.success('自定义供应商更新成功');
|
||||
} else {
|
||||
result = await customProviderApi.addCustomProvider(
|
||||
customProviderModal.data.providerId,
|
||||
providerData
|
||||
);
|
||||
message.success(`自定义供应商 ${customProviderModal.data.providerId} 添加成功`);
|
||||
}
|
||||
|
||||
// 关闭弹窗并刷新配置
|
||||
customProviderModal.visible = false;
|
||||
await configStore.refreshConfig();
|
||||
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证错误
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理API错误响应
|
||||
let errorMessage = '未知错误';
|
||||
if (error.response?.data?.detail) {
|
||||
errorMessage = error.response.data.detail;
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
}
|
||||
|
||||
message.error(`操作失败: ${errorMessage}`);
|
||||
} finally {
|
||||
customProviderModal.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消自定义供应商操作
|
||||
const cancelCustomProvider = () => {
|
||||
customProviderModal.visible = false;
|
||||
resetCustomProviderForm();
|
||||
};
|
||||
|
||||
// 删除自定义供应商
|
||||
const deleteCustomProvider = async (providerId) => {
|
||||
try {
|
||||
const result = await customProviderApi.deleteCustomProvider(providerId);
|
||||
message.success('自定义供应商删除成功');
|
||||
await configStore.refreshConfig();
|
||||
} catch (error) {
|
||||
message.error(`删除失败: ${error.message || error.response?.data?.detail || '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 测试自定义供应商连接
|
||||
const testCustomProvider = async (providerId, modelName) => {
|
||||
try {
|
||||
message.loading({ content: '正在测试连接...', key: 'test-connection', duration: 0 });
|
||||
|
||||
const result = await customProviderApi.testCustomProvider(providerId, modelName);
|
||||
|
||||
if (result.status?.status === 'available') {
|
||||
message.success({ content: '连接测试成功', key: 'test-connection', duration: 2 });
|
||||
} else {
|
||||
message.error({
|
||||
content: `连接测试失败: ${result.status?.message || '未知错误'}`,
|
||||
key: 'test-connection',
|
||||
duration: 3
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error({
|
||||
content: `测试失败: ${error.message || error.response?.data?.detail || '未知错误'}`,
|
||||
key: 'test-connection',
|
||||
duration: 3
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 自定义供应商区域样式
|
||||
.custom-providers-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--gray-600);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.custom-provider-card {
|
||||
border: 1px solid var(--gray-200);
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--gray-300);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
|
||||
.provider-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.provider-id {
|
||||
background: var(--main-color);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 16px 20px;
|
||||
|
||||
.provider-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: var(--gray-900);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--gray-300);
|
||||
}
|
||||
}
|
||||
|
||||
.builtin-providers-section {
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.providers-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
|
||||
.stats-item {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
|
||||
&.available {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&.unavailable {
|
||||
background: rgba(249, 115, 22, 0.1);
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单帮助文本样式
|
||||
.form-help-text {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.model-provider-card {
|
||||
border: 1px solid var(--gray-150);
|
||||
background-color: white;
|
||||
@ -323,14 +880,16 @@ const filteredModels = computed(() => {
|
||||
|
||||
// 已配置provider的样式
|
||||
&.configured-provider {
|
||||
.card-header {
|
||||
.model-icon {
|
||||
&.available {
|
||||
filter: grayscale(0%);
|
||||
}
|
||||
}
|
||||
|
||||
.model-icon {
|
||||
&.available {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.provider-meta {
|
||||
.model-count {
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -339,18 +898,32 @@ const filteredModels = computed(() => {
|
||||
&.unconfigured-provider {
|
||||
.card-header {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
|
||||
h3 {
|
||||
color: var(--gray-700);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.missing-keys {
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fed7aa;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
.model-icon {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.missing-keys {
|
||||
color: var(--gray-700);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
& > span {
|
||||
margin-left: 6px;
|
||||
user-select: all;
|
||||
background-color: rgba(251, 146, 60, 0.15);
|
||||
color: #d97706;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
border: 1px solid rgba(251, 146, 60, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -370,10 +943,47 @@ const filteredModels = computed(() => {
|
||||
|
||||
.model-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.provider-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.model-count {
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.provider-id {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-600);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.provider-link {
|
||||
color: var(--gray-500);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-url {
|
||||
@ -390,11 +1000,11 @@ const filteredModels = computed(() => {
|
||||
.model-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@ -409,11 +1019,31 @@ const filteredModels = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
.expand-button, .config-button {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--gray-500);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-50);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: inline-flex;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@ -764,6 +1394,19 @@ const filteredModels = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加简单的脉冲动画
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-width: 768px) {
|
||||
.model-provider-card {
|
||||
|
||||
@ -32,11 +32,11 @@
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu class="scrollable-menu">
|
||||
<a-menu-item-group v-for="(item, key) in modelKeys" :key="key" :title="modelNames[item]?.name">
|
||||
<a-menu-item-group v-for="(item, key) in modelKeys" :key="key" :title="modelNames[item]?.name">
|
||||
<a-menu-item v-for="(model, idx) in modelNames[item]?.models" :key="`${item}-${idx}`" @click="handleSelectModel(item, model)">
|
||||
{{ model }}
|
||||
</a-menu-item>
|
||||
</a-menu-item-group>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user