from fastapi import APIRouter, HTTPException, UploadFile, File from pydantic import BaseModel, Field from typing import Dict, Optional, List, Any import json import sys from pathlib import Path from core.config import settings from services.comfyui_workflow_manager import workflow_manager from services.llm_model_service import LLMModelService router = APIRouter(prefix="/api-config", tags=["API Configuration"]) # 配置文件路径 CONFIG_DIR = Path(settings.DATA_PATH) / "apiconfig" CONFIG_DIR.mkdir(parents=True, exist_ok=True) # 调试信息:打印配置目录路径 print(f"[API Config] DATA_PATH: {settings.DATA_PATH}", file=sys.stderr) print(f"[API Config] CONFIG_DIR: {CONFIG_DIR}", file=sys.stderr) print(f"[API Config] CONFIG_DIR exists: {CONFIG_DIR.exists()}", file=sys.stderr) if CONFIG_DIR.exists(): config_files = list(CONFIG_DIR.glob("*.json")) print(f"[API Config] Found {len(config_files)} config files", file=sys.stderr) for f in config_files: print(f" - {f.name}", file=sys.stderr) 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 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 字段,不返回给前端 safe_apis = {} for category, api_config in profile.get("apis", {}).items(): safe_config = api_config.copy() safe_config.pop("apiKey", None) safe_apis[category] = safe_config return { "id": profile.get("id", profile_id), "name": profile.get("name", profile_id), "apis": safe_apis } @router.post("/profiles", response_model=ProfileResponse) def create_or_update_profile(request: ProfileSaveRequest): """创建或更新配置文件(增量更新,明文存储 API Key)""" # 加载现有配置 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) # 如果前端传入了空的 apiKey,保留原有的 key if api_config.apiKey == "" and category in existing_profile.get("apis", {}): existing_key = existing_profile["apis"][category].get("apiKey", "") if existing_key: api_config_dict["apiKey"] = existing_key # 更新配置 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) profile_data["apis"][category] = api_config_dict # 保存配置文件 save_profile(request.profileId, profile_data) # 返回不包含 API Key 的数据 safe_apis = {} for category, api_config in profile_data.get("apis", {}).items(): safe_config = api_config.copy() safe_config.pop("apiKey", None) safe_apis[category] = safe_config return { "id": profile_data.get("id", request.profileId), "name": profile_data.get("name", request.profileId), "apis": safe_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: api_key_to_use = api_config.apiKey or "" # 如果 API Key 为空,尝试从已保存的配置中获取 if not api_key_to_use and api_config.category: # 遍历所有配置文件,找到包含该 category 的配置 for config_file in CONFIG_DIR.glob("*.json"): try: with open(config_file, 'r', encoding='utf-8') as f: profile = json.load(f) # 检查是否包含该 category if api_config.category in profile.get("apis", {}): api_key_to_use = profile["apis"][api_config.category].get("apiKey", "") if api_key_to_use: break except Exception: continue # 检测提供商类型 provider = LLMModelService.detect_provider(api_config.apiUrl) # 获取模型列表 models = LLMModelService.get_models_by_provider( provider=provider, api_key=api_key_to_use, 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)}" }