完成世界书、骰子、apiconfig页面处理
This commit is contained in:
389
backend/api/routes/apiConfigRoute.py
Normal file
389
backend/api/routes/apiConfigRoute.py
Normal file
@@ -0,0 +1,389 @@
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Optional, List, Any
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from core.config import settings
|
||||
from cryptography.fernet import Fernet
|
||||
import base64
|
||||
from services.comfyui_workflow_manager import workflow_manager
|
||||
from services.llm_model_service import LLMModelService
|
||||
|
||||
router = APIRouter(prefix="/api-config", tags=["API Configuration"])
|
||||
|
||||
# 加密密钥(实际项目中应该从环境变量读取)
|
||||
ENCRYPTION_KEY = os.getenv('API_ENCRYPTION_KEY', Fernet.generate_key().decode())
|
||||
fernet = Fernet(ENCRYPTION_KEY.encode() if isinstance(ENCRYPTION_KEY, str) else ENCRYPTION_KEY)
|
||||
|
||||
# 配置文件路径
|
||||
CONFIG_DIR = Path(settings.DATA_PATH) / "apiconfig"
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class ApiConfigItem(BaseModel):
|
||||
"""单个 API 配置项"""
|
||||
id: Optional[str] = None
|
||||
name: Optional[str] = ""
|
||||
category: Optional[str] = None # mainLLM, imageModel, secondaryLLM, ragEmbedding
|
||||
apiUrl: Optional[str] = ""
|
||||
apiKey: Optional[str] = None # 前端传入的可能是明文或空
|
||||
model: Optional[str] = ""
|
||||
|
||||
# 生图模型的特殊字段
|
||||
mode: Optional[str] = None # 'local' | 'cloud'
|
||||
local: Optional[dict] = None
|
||||
cloud: Optional[dict] = None
|
||||
|
||||
|
||||
class ProfileSaveRequest(BaseModel):
|
||||
"""保存配置文件的请求"""
|
||||
profileId: str
|
||||
name: Optional[str] = None
|
||||
apis: Dict[str, ApiConfigItem] # key 是 category,value 是配置
|
||||
|
||||
|
||||
class ProfileResponse(BaseModel):
|
||||
"""配置文件响应(不包含明文 API Key)"""
|
||||
id: str
|
||||
name: str
|
||||
apis: Dict[str, dict] # apiKey 字段会被移除或脱敏
|
||||
|
||||
|
||||
def encrypt_api_key(api_key: str) -> str:
|
||||
"""加密 API Key"""
|
||||
if not api_key:
|
||||
return ""
|
||||
encrypted = fernet.encrypt(api_key.encode())
|
||||
return base64.urlsafe_b64encode(encrypted).decode()
|
||||
|
||||
|
||||
def decrypt_api_key(encrypted_key: str) -> str:
|
||||
"""解密 API Key(仅在后端内部使用)"""
|
||||
if not encrypted_key:
|
||||
return ""
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(encrypted_key.encode())
|
||||
decrypted = fernet.decrypt(decoded)
|
||||
return decrypted.decode()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def mask_api_key(api_key: str) -> str:
|
||||
"""脱敏 API Key(返回给前端)"""
|
||||
if not api_key or len(api_key) < 8:
|
||||
return "****"
|
||||
return api_key[:4] + "****" + api_key[-4:]
|
||||
|
||||
|
||||
def load_profile(profile_id: str) -> Optional[dict]:
|
||||
"""加载配置文件"""
|
||||
config_file = CONFIG_DIR / f"{profile_id}.json"
|
||||
if not config_file.exists():
|
||||
return None
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_profile(profile_id: str, profile_data: dict):
|
||||
"""保存配置文件"""
|
||||
config_file = CONFIG_DIR / f"{profile_id}.json"
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(profile_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def list_profiles() -> List[dict]:
|
||||
"""列出所有配置文件"""
|
||||
profiles = []
|
||||
for config_file in CONFIG_DIR.glob("*.json"):
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
profile = json.load(f)
|
||||
profiles.append({
|
||||
"id": profile.get("id", config_file.stem),
|
||||
"name": profile.get("name", config_file.stem),
|
||||
"createdAt": profile.get("createdAt", "")
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return profiles
|
||||
|
||||
|
||||
@router.get("/profiles", response_model=List[dict])
|
||||
def get_all_profiles():
|
||||
"""获取所有配置文件列表"""
|
||||
return list_profiles()
|
||||
|
||||
|
||||
@router.get("/profiles/{profile_id}", response_model=ProfileResponse)
|
||||
def get_profile(profile_id: str):
|
||||
"""获取单个配置文件(API Key 已脱敏)"""
|
||||
profile = load_profile(profile_id)
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="配置文件不存在")
|
||||
|
||||
# 脱敏所有 API Key
|
||||
masked_apis = {}
|
||||
for category, api_config in profile.get("apis", {}).items():
|
||||
masked_config = api_config.copy()
|
||||
if "apiKey" in masked_config and masked_config["apiKey"]:
|
||||
masked_config["apiKey"] = mask_api_key(masked_config["apiKey"])
|
||||
masked_apis[category] = masked_config
|
||||
|
||||
return {
|
||||
"id": profile.get("id", profile_id),
|
||||
"name": profile.get("name", profile_id),
|
||||
"apis": masked_apis
|
||||
}
|
||||
|
||||
|
||||
@router.post("/profiles", response_model=ProfileResponse)
|
||||
def create_or_update_profile(request: ProfileSaveRequest):
|
||||
"""创建或更新配置文件(增量更新)"""
|
||||
# 加载现有配置
|
||||
existing_profile = load_profile(request.profileId)
|
||||
|
||||
if existing_profile:
|
||||
# 更新现有配置:只更新提供的 API 配置
|
||||
for category, api_config in request.apis.items():
|
||||
api_config_dict = api_config.dict(exclude_none=True)
|
||||
|
||||
# 处理 API Key 加密
|
||||
if api_config.apiKey and api_config.apiKey != "****":
|
||||
# 如果是新的明文 key,加密它
|
||||
api_config_dict["apiKey"] = encrypt_api_key(api_config.apiKey)
|
||||
elif api_config.apiKey == "****":
|
||||
# 如果是脱敏的 key,保留原有的加密 key
|
||||
if category in existing_profile.get("apis", {}):
|
||||
api_config_dict["apiKey"] = existing_profile["apis"][category].get("apiKey", "")
|
||||
else:
|
||||
api_config_dict.pop("apiKey", None)
|
||||
|
||||
# 更新配置
|
||||
if "apis" not in existing_profile:
|
||||
existing_profile["apis"] = {}
|
||||
existing_profile["apis"][category] = api_config_dict
|
||||
|
||||
profile_data = existing_profile
|
||||
else:
|
||||
# 新建配置文件
|
||||
from datetime import datetime
|
||||
profile_data = {
|
||||
"id": request.profileId,
|
||||
"name": request.name or request.profileId,
|
||||
"createdAt": datetime.now().isoformat(),
|
||||
"apis": {}
|
||||
}
|
||||
|
||||
# 添加所有 API 配置
|
||||
for category, api_config in request.apis.items():
|
||||
api_config_dict = api_config.dict(exclude_none=True)
|
||||
if api_config_dict.get("apiKey"):
|
||||
api_config_dict["apiKey"] = encrypt_api_key(api_config_dict["apiKey"])
|
||||
profile_data["apis"][category] = api_config_dict
|
||||
|
||||
# 保存配置文件
|
||||
save_profile(request.profileId, profile_data)
|
||||
|
||||
# 返回脱敏后的数据
|
||||
masked_apis = {}
|
||||
for category, api_config in profile_data.get("apis", {}).items():
|
||||
masked_config = api_config.copy()
|
||||
if "apiKey" in masked_config and masked_config["apiKey"]:
|
||||
masked_config["apiKey"] = mask_api_key(masked_config["apiKey"])
|
||||
masked_apis[category] = masked_config
|
||||
|
||||
return {
|
||||
"id": profile_data.get("id", request.profileId),
|
||||
"name": profile_data.get("name", request.profileId),
|
||||
"apis": masked_apis
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/profiles/{profile_id}")
|
||||
def delete_profile(profile_id: str):
|
||||
"""删除配置文件"""
|
||||
config_file = CONFIG_DIR / f"{profile_id}.json"
|
||||
if not config_file.exists():
|
||||
raise HTTPException(status_code=404, detail="配置文件不存在")
|
||||
|
||||
config_file.unlink()
|
||||
return {"message": "配置文件已删除"}
|
||||
|
||||
|
||||
@router.post("/test-connection")
|
||||
def test_connection(api_config: ApiConfigItem):
|
||||
"""测试 API 连接并获取模型列表"""
|
||||
try:
|
||||
# 检测提供商类型
|
||||
provider = LLMModelService.detect_provider(api_config.apiUrl)
|
||||
|
||||
# 获取模型列表
|
||||
models = LLMModelService.get_models_by_provider(
|
||||
provider=provider,
|
||||
api_key=api_config.apiKey or "",
|
||||
api_url=api_config.apiUrl
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"models": models,
|
||||
"provider": provider,
|
||||
"message": f"成功获取 {len(models)} 个模型"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"获取模型列表失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== ComfyUI Workflow Management ====================
|
||||
|
||||
@router.get("/comfyui/workflows", response_model=List[Dict[str, Any]])
|
||||
def get_comfyui_workflows():
|
||||
"""获取所有可用的 ComfyUI 工作流列表"""
|
||||
return workflow_manager.list_workflows()
|
||||
|
||||
|
||||
@router.post("/comfyui/workflows/upload")
|
||||
async def upload_comfyui_workflow(file: UploadFile = File(...)):
|
||||
"""上传 ComfyUI 工作流 JSON 文件"""
|
||||
return await workflow_manager.upload_workflow(file)
|
||||
|
||||
|
||||
@router.delete("/comfyui/workflows/{filename}")
|
||||
def delete_comfyui_workflow(filename: str):
|
||||
"""删除 ComfyUI 工作流文件"""
|
||||
return workflow_manager.delete_workflow(filename)
|
||||
|
||||
|
||||
@router.get("/comfyui/workflows/{filename}")
|
||||
def get_comfyui_workflow(filename: str):
|
||||
"""获取指定工作流的详细内容"""
|
||||
return workflow_manager.load_workflow(filename)
|
||||
|
||||
|
||||
# ==================== Connection Testing ====================
|
||||
|
||||
@router.post("/test-comfyui-connection")
|
||||
def test_comfyui_connection(request: dict):
|
||||
"""测试 ComfyUI 连接"""
|
||||
import requests as req
|
||||
|
||||
api_url = request.get("apiUrl", "http://comfyui:8188")
|
||||
|
||||
try:
|
||||
# 测试基本连通性
|
||||
response = req.get(f"{api_url}/system_stats", timeout=5)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"HTTP {response.status_code}"
|
||||
}
|
||||
|
||||
stats = response.json()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "连接成功",
|
||||
"stats": {
|
||||
"vram_total": stats.get("vram_total", 0),
|
||||
"vram_free": stats.get("vram_free", 0),
|
||||
"torch_version": stats.get("torch_version", ""),
|
||||
"device": stats.get("device", "")
|
||||
}
|
||||
}
|
||||
|
||||
except req.exceptions.ConnectionError:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "无法连接到 ComfyUI,请检查地址和端口"
|
||||
}
|
||||
except req.exceptions.Timeout:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "连接超时,请检查 ComfyUI 是否正常运行"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"错误: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/test-cloud-connection")
|
||||
def test_cloud_connection(request: dict):
|
||||
"""测试云端 API 连接"""
|
||||
import openai
|
||||
|
||||
provider = request.get("provider", "dall-e")
|
||||
api_key = request.get("apiKey", "")
|
||||
model = request.get("model", "dall-e-3")
|
||||
|
||||
if not api_key:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "API Key 不能为空"
|
||||
}
|
||||
|
||||
try:
|
||||
if provider == "dall-e":
|
||||
# 测试 DALL-E
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
|
||||
# 尝试获取模型列表(轻量级测试)
|
||||
models = client.models.list()
|
||||
|
||||
# 检查指定的模型是否存在
|
||||
model_exists = any(m.id == model for m in models.data)
|
||||
|
||||
if model_exists:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"连接成功,模型 {model} 可用"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"模型 {model} 不可用"
|
||||
}
|
||||
|
||||
elif provider == "stability":
|
||||
# 测试 Stability AI
|
||||
import requests as req
|
||||
|
||||
response = req.get(
|
||||
"https://api.stability.ai/v1/engines/list",
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "连接成功"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"HTTP {response.status_code}: {response.text}"
|
||||
}
|
||||
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"不支持的提供商: {provider}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"连接失败: {str(e)}"
|
||||
}
|
||||
Reference in New Issue
Block a user