Files

1 line
88 KiB
JSON

{"repo": "ashely-looki/mycut", "files": {"backend/api/v1/settings.py": {"85230562d821e7ec17081193fdd10b57de0df5ce": {"t": 1785746906.393152, "n": 25590, "h": "e81d50cd664f9472", "c": "\"\"\"\n设置管理API端点\n为Desktop客户端提供设置管理功能\n\"\"\"\n\nimport os\nimport json\nimport shutil\nimport logging\nfrom pathlib import Path\nfrom typing import Dict, Any, Optional, List\nfrom fastapi import APIRouter, HTTPException, UploadFile, File, Depends\nfrom pydantic import BaseModel, Field, validator\n\nlogger = logging.getLogger(__name__)\n\nfrom backend.core.desktop_config import get_desktop_config, is_desktop_mode, DesktopConfig, save_desktop_config\nfrom backend.services.config_sync_service import config_sync_service\nfrom pathlib import Path\n\nrouter = APIRouter(prefix=\"/settings\", tags=[\"settings\"])\n\n\nclass BasicSettings(BaseModel):\n \"\"\"基础设置\"\"\"\n app_name: str = Field(default=\"AutoClip Desktop\", description=\"应用名称\")\n app_version: str = Field(default=\"1.0.0\", description=\"应用版本\")\n debug_mode: bool = Field(default=False, description=\"调试模式\")\n auto_start: bool = Field(default=True, description=\"自动启动\")\n\n\nclass ServiceSettings(BaseModel):\n \"\"\"服务设置\"\"\"\n host: str = Field(default=\"127.0.0.1\", description=\"服务主机\")\n port: int = Field(default=8000, description=\"服务端口\")\n max_memory_usage: int = Field(default=2048, description=\"最大内存使用(MB)\")\n \n @validator('port')\n def validate_port(cls, v):\n if not 1024 <= v <= 65535:\n raise ValueError('端口号必须在1024-65535之间')\n return v\n \n @validator('max_memory_usage')\n def validate_memory(cls, v):\n if not 512 <= v <= 8192:\n raise ValueError('内存使用限制必须在512-8192MB之间')\n return v\n\n\nclass ApiKeys(BaseModel):\n \"\"\"API密钥设置\"\"\"\n dashscope: str = Field(default=\"\", description=\"通义千问API密钥\")\n openai: str = Field(default=\"\", description=\"OpenAI API密钥\")\n gemini: str = Field(default=\"\", description=\"Gemini API密钥\")\n siliconflow: str = Field(default=\"\", description=\"SiliconFlow API密钥\")\n jimeng_access: str = Field(default=\"\", description=\"即梦AI访问密钥\")\n jimeng_secret: str = Field(default=\"\", description=\"即梦AI秘密密钥\")\n\n\nclass ApiSettings(BaseModel):\n \"\"\"API设置\"\"\"\n api_keys: ApiKeys = Field(default_factory=ApiKeys, description=\"API密钥\")\n api_model: str = Field(default=\"qwen-plus\", description=\"默认模型\")\n api_max_tokens: int = Field(default=4096, description=\"最大Token数\")\n api_timeout: int = Field(default=30, description=\"API超时时间(秒)\")\n \n @validator('api_timeout')\n def validate_timeout(cls, v):\n if not 5 <= v <= 300:\n raise ValueError('API超时时间必须在5-300秒之间')\n return v\n\n\nclass ProcessingSettings(BaseModel):\n \"\"\"处理设置\"\"\"\n processing_chunk_size: int = Field(default=5000, description=\"处理块大小\")\n processing_min_score: float = Field(default=0.7, description=\"最小评分阈值\")\n processing_max_clips: int = Field(default=5, description=\"合集最大切片数\")\n processing_max_retries: int = Field(default=3, description=\"最大重试次数\")\n \n @validator('processing_chunk_size')\n def validate_chunk_size(cls, v):\n if not 1000 <= v <= 10000:\n raise ValueError('处理块大小必须在1000-10000之间')\n return v\n \n @validator('processing_min_score')\n def validate_min_score(cls, v):\n if not 0.1 <= v <= 1.0:\n raise ValueError('最小评分阈值必须在0.1-1.0之间')\n return v\n\n\nclass LogSettings(BaseModel):\n \"\"\"日志设置\"\"\"\n log_level: str = Field(default=\"INFO\", description=\"日志级别\")\n log_retention_days: int = Field(default=7, description=\"日志保留天数\")\n \n @validator('log_level')\n def validate_log_level(cls, v):\n if v not in ['DEBUG', 'INFO', 'WARNING', 'ERROR']:\n raise ValueError('日志级别必须是DEBUG、INFO、WARNING或ERROR')\n return v\n \n @validator('log_retention_days')\n def validate_retention_days(cls, v):\n if not 1 <= v <= 30:\n raise ValueError('日志保留天数必须在1-30天之间')\n return v\n\n\nclass PathSettings(BaseModel):\n \"\"\"路径设置\"\"\"\n data_directory: str = Field(description=\"数据目录\")\n cache_directory: str = Field(description=\"缓存目录\")\n temp_directory: str = Field(description=\"临时目录\")\n\n\nclass UpdateDataDirRequest(BaseModel):\n \"\"\"更新数据目录请求\"\"\"\n new_data_directory: str = Field(description=\"新的数据目录路径\")\n migrate: bool = Field(default=True, description=\"是否迁移旧目录数据\")\n\n\nclass DesktopSettings(BaseModel):\n \"\"\"完整的Desktop设置\"\"\"\n basic: BasicSettings = Field(default_factory=BasicSettings)\n service: ServiceSettings = Field(default_factory=ServiceSettings)\n api: ApiSettings = Field(default_factory=ApiSettings)\n processing: ProcessingSettings = Field(default_factory=ProcessingSettings)\n logs: LogSettings = Field(default_factory=LogSettings)\n paths: Optional[PathSettings] = Field(default=None, description=\"路径设置\")\n\n\ndef check_desktop_mode(relaxed: bool = False):\n \"\"\"检查是否在Desktop模式\n relaxed=True 时放宽限制,允许内测安装包/开发环境调用(返回告警但不阻断)。\n \"\"\"\n if not is_desktop_mode():\n if relaxed:\n return False\n raise HTTPException(status_code=400, detail=\"此端点仅在Desktop模式下可用\")\n\n\n@router.get(\"/desktop-mode\")\nasync def check_desktop_mode_endpoint():\n \"\"\"检查是否在桌面模式 - 供前端调用\"\"\"\n return {\n \"is_desktop_mode\": is_desktop_mode(),\n \"environment\": {\n \"AUTOCLIP_DESKTOP_MODE\": os.getenv(\"AUTOCLIP_DESKTOP_MODE\"),\n \"AUTOCLIP_MODE\": os.getenv(\"AUTOCLIP_MODE\"),\n \"TAURI_PLATFORM\": os.getenv(\"TAURI_PLATFORM\"),\n }\n }\n\n\n@router.get(\"/\", response_model=DesktopSettings)\nasync def get_settings():\n \"\"\"获取所有设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 尝试从保存的设置文件中读取\n settings_file = config.paths.data_dir / \"settings.json\"\n print(f\"设置文件路径: {settings_file}\")\n print(f\"设置文件存在: {settings_file.exists()}\")\n \n if settings_file.exists():\n try:\n with open(settings_file, 'r', encoding='utf-8') as f:\n saved_settings = json.load(f)\n \n print(f\"从文件读取的设置: {saved_settings.get('basic', {}).get('app_name', 'unknown')}\")\n \n # 验证并返回保存的设置\n settings = DesktopSettings(**saved_settings)\n return settings\n except Exception as e:\n # 如果读取失败,回退到默认配置\n print(f\"读取设置文件失败: {e}\")\n pass\n \n # 构建路径设置\n paths = PathSettings(\n data_directory=str(config.paths.data_dir),\n cache_directory=str(config.paths.cache_dir),\n temp_directory=str(config.paths.temp_dir)\n )\n \n # 构建完整设置\n settings = DesktopSettings(\n basic=BasicSettings(\n app_name=config.app_name,\n app_version=config.app_version,\n debug_mode=config.debug_mode,\n auto_start=True # 默认值\n ),\n service=ServiceSettings(\n host=config.host,\n port=config.port,\n max_memory_usage=config.max_memory_usage\n ),\n api=ApiSettings(\n api_keys=ApiKeys(\n dashscope=config.dashscope_api_key,\n openai=config.openai_api_key,\n gemini=config.gemini_api_key,\n siliconflow=config.siliconflow_api_key,\n jimeng_access=\"\", # 默认值\n jimeng_secret=\"\" # 默认值\n ),\n api_model=config.default_model,\n api_max_tokens=config.max_tokens,\n api_timeout=config.timeout\n ),\n processing=ProcessingSettings(\n processing_chunk_size=config.chunk_size,\n processing_min_score=config.min_score_threshold,\n processing_max_clips=config.max_clips_per_collection,\n processing_max_retries=config.max_retries\n ),\n logs=LogSettings(\n log_level=config.log_level,\n log_retention_days=config.log_retention_days\n ),\n paths=paths\n )\n \n return settings\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取设置失败: {str(e)}\")\n\n\n@router.post(\"/paths/data-directory\")\nasync def update_data_directory(\n new_path: str,\n migrate_data: bool = True\n):\n \"\"\"更新数据目录\"\"\"\n check_desktop_mode()\n \n try:\n from backend.core.desktop_config import set_data_dir\n \n result = set_data_dir(new_path, migrate_data)\n \n if result[\"success\"]:\n return {\n \"message\": result[\"message\"],\n \"new_path\": result[\"new_path\"],\n \"migrated_files\": result.get(\"migrated_files\", []),\n \"failed_files\": result.get(\"failed_files\", [])\n }\n else:\n raise HTTPException(status_code=400, detail=result[\"error\"])\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"更新数据目录失败: {str(e)}\")\n\n@router.get(\"/paths/data-directory\")\nasync def get_data_directory_info():\n \"\"\"获取数据目录信息\"\"\"\n check_desktop_mode()\n \n try:\n from backend.core.desktop_config import get_data_dir_info\n \n return get_data_dir_info()\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取数据目录信息失败: {str(e)}\")\n\n@router.delete(\"/\")\nasync def clear_settings(\n config: DesktopConfig = Depends(get_desktop_config)\n):\n \"\"\"清除所有设置\"\"\"\n check_desktop_mode()\n \n try:\n # 重置配置为默认值\n config.dashscope_api_key = \"\"\n config.openai_api_key = \"\"\n config.gemini_api_key = \"\"\n config.siliconflow_api_key = \"\"\n config.jimeng_access_key = \"\"\n config.jimeng_secret_key = \"\"\n \n # 保存配置\n save_desktop_config(config)\n \n return {\"message\": \"设置已清除\", \"success\": True}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"清除设置失败: {str(e)}\")\n\nclass TestApiRequest(BaseModel):\n provider: str\n api_key: str\n\n@router.post(\"/test-api\")\nasync def test_api_connection(request: TestApiRequest):\n \"\"\"测试API连接\"\"\"\n check_desktop_mode()\n \n try:\n # 首先进行基本的API Key格式验证\n if not request.api_key or len(request.api_key.strip()) < 10:\n return {\n \"success\": False,\n \"error\": \"API Key为空或过短,请检查输入\",\n \"provider\": request.provider\n }\n \n # 根据提供商进行格式验证\n if request.provider in [\"dashscope\", \"openai\", \"deepseek\"]:\n if not request.api_key.startswith(\"sk-\"):\n return {\n \"success\": False,\n \"error\": f\"{request.provider} API Key格式可能不正确,通常以'sk-'开头\",\n \"provider\": request.provider\n }\n\n # 根据提供商测试API连接\n if request.provider == \"dashscope\":\n from backend.core.llm_providers import DashScopeProvider\n provider_instance = DashScopeProvider(api_key=request.api_key)\n elif request.provider == \"openai\":\n from backend.core.llm_providers import OpenAIProvider\n provider_instance = OpenAIProvider(api_key=request.api_key)\n elif request.provider == \"gemini\":\n from backend.core.llm_providers import GeminiProvider\n provider_instance = GeminiProvider(api_key=request.api_key)\n elif request.provider == \"siliconflow\":\n from backend.core.llm_providers import SiliconFlowProvider\n provider_instance = SiliconFlowProvider(api_key=request.api_key)\n elif request.provider == \"deepseek\":\n from backend.core.llm_providers import DeepSeekProvider\n provider_instance = DeepSeekProvider(api_key=request.api_key)\n else:\n raise HTTPException(status_code=400, detail=\"不支持的API提供商\")\n \n # 测试连接\n test_result = provider_instance.test_connection()\n \n if test_result:\n return {\n \"success\": True,\n \"message\": \"API连接测试成功\",\n \"provider\": request.provider\n }\n else:\n # 提供更详细的错误信息\n error_msg = f\"API连接测试失败\"\n if request.provider == \"dashscope\":\n error_msg += \"。请检查API Key是否正确,DashScope API Key通常以'sk-'开头\"\n elif request.provider == \"openai\":\n error_msg += \"。请检查API Key是否正确,OpenAI API Key通常以'sk-'开头\"\n elif request.provider == \"gemini\":\n error_msg += \"。请检查API Key是否正确\"\n elif request.provider == \"siliconflow\":\n error_msg += \"。请检查API Key是否正确\"\n \n return {\n \"success\": False,\n \"error\": error_msg,\n \"provider\": request.provider\n }\n \n except Exception as e:\n logger.error(f\"API连接测试异常: {str(e)}\")\n return {\n \"success\": False,\n \"error\": f\"API连接测试失败: {str(e)}\",\n \"provider\": request.provider\n }\n\n@router.put(\"/\", response_model=Dict[str, Any])\nasync def update_settings(settings: DesktopSettings):\n \"\"\"更新设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 更新配置\n config.debug_mode = settings.basic.debug_mode\n config.host = settings.service.host\n config.port = settings.service.port\n config.max_memory_usage = settings.service.max_memory_usage\n \n # 更新API设置\n config.dashscope_api_key = settings.api.api_keys.dashscope\n config.openai_api_key = settings.api.api_keys.openai\n config.gemini_api_key = settings.api.api_keys.gemini\n config.siliconflow_api_key = settings.api.api_keys.siliconflow\n config.default_model = settings.api.api_model\n config.max_tokens = settings.api.api_max_tokens\n config.timeout = settings.api.api_timeout\n \n # 更新处理设置\n config.chunk_size = settings.processing.processing_chunk_size\n config.min_score_threshold = settings.processing.processing_min_score\n config.max_clips_per_collection = settings.processing.processing_max_clips\n config.max_retries = settings.processing.processing_max_retries\n \n # 更新日志设置\n config.log_level = settings.logs.log_level\n \n # 保存设置到文件\n settings_file = config.paths.data_dir / \"settings.json\"\n with open(settings_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n # 重要:保存主配置文件,确保API key等关键配置被持久化\n from backend.core.desktop_config import save_desktop_config\n if not save_desktop_config(config):\n raise HTTPException(status_code=500, detail=\"保存主配置文件失败\")\n \n return {\"message\": \"设置更新成功\", \"settings_file\": str(settings_file)}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"更新设置失败: {str(e)}\")\n\n\n@router.post(\"/reset\")\nasync def reset_settings():\n \"\"\"重置设置为默认值\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 删除设置文件\n settings_file = config.paths.data_dir / \"settings.json\"\n if settings_file.exists():\n settings_file.unlink()\n \n # 重新加载默认配置\n config._settings = None\n config._paths = None\n \n return {\"message\": \"设置已重置为默认值\"}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"重置设置失败: {str(e)}\")\n\n\n@router.post(\"/paths/data-directory\", response_model=Dict[str, Any])\nasync def update_data_directory(payload: UpdateDataDirRequest):\n \"\"\"更新数据目录(可选迁移数据)。供首次运行向导或设置页调用。\"\"\"\n is_desktop = check_desktop_mode(relaxed=True)\n try:\n config = get_desktop_config()\n result = config.set_data_dir(Path(payload.new_data_directory), migrate_from_old=payload.migrate)\n\n # 同步返回新的路径配置\n paths = {\n \"data_directory\": str(config.paths.data_dir),\n \"cache_directory\": str(config.paths.cache_dir),\n \"temp_directory\": str(config.paths.temp_dir),\n \"database_url\": config.paths.database_url,\n }\n\n resp = {\"message\": \"数据目录更新成功\", \"result\": result, \"paths\": paths}\n if not is_desktop:\n resp[\"warning\"] = \"当前非Desktop模式,但已更新路径配置\"\n return resp\n except Exception as e:\n # 返回更详细的错误,帮助定位权限/路径/占用等问题\n return {\n \"message\": \"更新数据目录失败,但已跳过(内测模式)\",\n \"error\": str(e),\n \"hint\": \"请确认目标目录可写且未被系统限制,必要时选择用户主目录下的路径\",\n }\n\n\n@router.post(\"/export\")\nasync def export_settings():\n \"\"\"导出设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n settings = await get_settings()\n \n # 创建导出文件\n export_file = config.paths.data_dir / \"autoclip-settings-export.json\"\n with open(export_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n return {\n \"message\": \"设置导出成功\",\n \"export_file\": str(export_file),\n \"download_url\": f\"/api/v1/settings/download/{export_file.name}\"\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"导出设置失败: {str(e)}\")\n\n\n@router.post(\"/import\")\nasync def import_settings(file: UploadFile = File(...)):\n \"\"\"导入设置\"\"\"\n check_desktop_mode()\n \n try:\n # 读取上传的文件\n content = await file.read()\n settings_data = json.loads(content.decode('utf-8'))\n \n # 验证设置格式\n settings = DesktopSettings(**settings_data)\n \n # 更新设置\n result = await update_settings(settings)\n \n return {\n \"message\": \"设置导入成功\",\n \"imported_settings\": settings.dict()\n }\n \n except json.JSONDecodeError:\n raise HTTPException(status_code=400, detail=\"设置文件格式错误\")\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"导入设置失败: {str(e)}\")\n\n\n# 删除重复的test_api_connection函数\n\n\n@router.get(\"/validation\")\nasync def validate_settings():\n \"\"\"验证当前设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n validation_result = config.validate_config()\n \n return {\n \"valid\": validation_result[\"valid\"],\n \"errors\": validation_result.get(\"errors\", []),\n \"warnings\": validation_result.get(\"warnings\", []),\n \"recommendations\": []\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"设置验证失败: {str(e)}\")\n\n\n@router.get(\"/backup\")\nasync def backup_settings():\n \"\"\"备份设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_dir = config.paths.data_dir / \"backups\"\n backup_dir.mkdir(exist_ok=True)\n \n # 创建备份\n import datetime\n timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n backup_file = backup_dir / f\"settings_backup_{timestamp}.json\"\n \n settings = await get_settings()\n with open(backup_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n return {\n \"message\": \"设置备份成功\",\n \"backup_file\": str(backup_file),\n \"backup_time\": timestamp\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"备份设置失败: {str(e)}\")\n\n\n@router.get(\"/backups\")\nasync def list_backups():\n \"\"\"列出所有备份\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_dir = config.paths.data_dir / \"backups\"\n \n if not backup_dir.exists():\n return {\"backups\": []}\n \n backups = []\n for backup_file in backup_dir.glob(\"settings_backup_*.json\"):\n stat = backup_file.stat()\n backups.append({\n \"filename\": backup_file.name,\n \"path\": str(backup_file),\n \"size\": stat.st_size,\n \"created_time\": stat.st_ctime,\n \"modified_time\": stat.st_mtime\n })\n \n # 按创建时间排序\n backups.sort(key=lambda x: x[\"created_time\"], reverse=True)\n \n return {\"backups\": backups}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取备份列表失败: {str(e)}\")\n\n\n@router.get(\"/available-models\")\nasync def get_available_models():\n \"\"\"获取可用的模型列表\"\"\"\n check_desktop_mode()\n \n try:\n # 返回按供应商分类的模型列表\n models = {\n \"dashscope\": [\n {\"name\": \"qwen-plus\", \"display_name\": \"通义千问增强版\", \"max_tokens\": 8192, \"description\": \"适合复杂推理和创作任务\"},\n {\"name\": \"qwen-turbo\", \"display_name\": \"通义千问标准版\", \"max_tokens\": 8192, \"description\": \"平衡性能和成本\"},\n {\"name\": \"qwen-max\", \"display_name\": \"通义千问旗舰版\", \"max_tokens\": 8192, \"description\": \"最强性能,适合复杂任务\"},\n {\"name\": \"qwen-long\", \"display_name\": \"通义千问长文本版\", \"max_tokens\": 100000, \"description\": \"支持超长文本处理\"}\n ],\n \"openai\": [\n {\"name\": \"gpt-4o\", \"display_name\": \"GPT-4 Omni\", \"max_tokens\": 128000, \"description\": \"最新多模态模型\"},\n {\"name\": \"gpt-4o-mini\", \"display_name\": \"GPT-4 Omni Mini\", \"max_tokens\": 128000, \"description\": \"轻量级多模态模型\"},\n {\"name\": \"gpt-4-turbo\", \"display_name\": \"GPT-4 Turbo\", \"max_tokens\": 128000, \"description\": \"高性能版本\"},\n {\"name\": \"gpt-4\", \"display_name\": \"GPT-4\", \"max_tokens\": 8192, \"description\": \"经典版本\"},\n {\"name\": \"gpt-3.5-turbo\", \"display_name\": \"GPT-3.5 Turbo\", \"max_tokens\": 16384, \"description\": \"经济实用版本\"}\n ],\n \"gemini\": [\n {\"name\": \"gemini-1.5-pro\", \"display_name\": \"Gemini 1.5 Pro\", \"max_tokens\": 2000000, \"description\": \"最新专业版\"},\n {\"name\": \"gemini-1.5-flash\", \"display_name\": \"Gemini 1.5 Flash\", \"max_tokens\": 1000000, \"description\": \"快速响应版本\"},\n {\"name\": \"gemini-pro\", \"display_name\": \"Gemini Pro\", \"max_tokens\": 30720, \"description\": \"经典专业版\"}\n ],\n \"siliconflow\": [\n {\"name\": \"deepseek-chat\", \"display_name\": \"DeepSeek Chat\", \"max_tokens\": 32768, \"description\": \"深度求索对话模型\"},\n {\"name\": \"deepseek-coder\", \"display_name\": \"DeepSeek Coder\", \"max_tokens\": 16384, \"description\": \"代码生成专用模型\"},\n {\"name\": \"qwen-plus\", \"display_name\": \"通义千问增强版\", \"max_tokens\": 8192, \"description\": \"通过硅基流动访问\"},\n {\"name\": \"qwen-turbo\", \"display_name\": \"通义千问标准版\", \"max_tokens\": 8192, \"description\": \"通过硅基流动访问\"}\n ],\n }\n \n return {\"models\": models}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取模型列表失败: {str(e)}\")\n\n\n@router.get(\"/current-provider\")\nasync def get_current_provider():\n \"\"\"获取当前提供商信息\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 根据当前配置返回提供商信息\n provider_info = {\n \"provider\": \"dashscope\", # 默认提供商\n \"model\": config.default_model or \"qwen-plus\",\n \"available\": True,\n \"display_name\": \"通义千问\",\n \"description\": \"阿里云通义千问服务\"\n }\n \n return provider_info\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取当前提供商信息失败: {str(e)}\")\n\n\n@router.post(\"/restore/{backup_filename}\")\nasync def restore_backup(backup_filename: str):\n \"\"\"从备份恢复设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_file = config.paths.data_dir / \"backups\" / backup_filename\n \n if not backup_file.exists():\n raise HTTPException(status_code=404, detail=\"备份文件不存在\")\n \n # 读取备份文件\n with open(backup_file, 'r', encoding='utf-8') as f:\n settings_data = json.load(f)\n \n # 验证并恢复设置\n settings = DesktopSettings(**settings_data)\n result = await update_settings(settings)\n \n return {\n \"message\": \"设置恢复成功\",\n \"restored_from\": backup_filename,\n \"restored_settings\": settings.dict()\n }\n \n except HTTPException:\n raise\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"恢复设置失败: {str(e)}\")\n\n\n@router.post(\"/sync-config\")\nasync def sync_config():\n \"\"\"手动同步客户端配置到后端\"\"\"\n check_desktop_mode()\n \n try:\n if config_sync_service.sync_from_client():\n return {\n \"status\": \"success\",\n \"message\": \"配置同步成功\"\n }\n else:\n return {\n \"status\": \"error\",\n \"message\": \"配置同步失败\"\n }\n except Exception as e:\n return {\n \"status\": \"error\",\n \"message\": f\"配置同步失败: {str(e)}\"\n }\n\n\n@router.get(\"/config-status\")\nasync def get_config_status():\n \"\"\"获取配置同步状态\"\"\"\n check_desktop_mode()\n \n try:\n client_time = config_sync_service.get_client_config_timestamp()\n backup_time = config_sync_service.get_backup_config_timestamp()\n sync_needed = config_sync_service.is_sync_needed()\n \n return {\n \"client_config_exists\": client_time is not None,\n \"backup_config_exists\": backup_time is not None,\n \"client_config_time\": client_time,\n \"backup_config_time\": backup_time,\n \"sync_needed\": sync_needed,\n \"client_config_path\": str(config_sync_service.client_config_path),\n \"backup_config_path\": str(config_sync_service.backup_config_path)\n }\n except Exception as e:\n return {\n \"status\": \"error\",\n \"message\": f\"获取配置状态失败: {str(e)}\"\n }"}, "0c106f0df62e6ad3d7f56e9be86068633afd141c": {"t": 1785768787.0607538, "n": 25590, "h": "e81d50cd664f9472", "c": "\"\"\"\n设置管理API端点\n为Desktop客户端提供设置管理功能\n\"\"\"\n\nimport os\nimport json\nimport shutil\nimport logging\nfrom pathlib import Path\nfrom typing import Dict, Any, Optional, List\nfrom fastapi import APIRouter, HTTPException, UploadFile, File, Depends\nfrom pydantic import BaseModel, Field, validator\n\nlogger = logging.getLogger(__name__)\n\nfrom backend.core.desktop_config import get_desktop_config, is_desktop_mode, DesktopConfig, save_desktop_config\nfrom backend.services.config_sync_service import config_sync_service\nfrom pathlib import Path\n\nrouter = APIRouter(prefix=\"/settings\", tags=[\"settings\"])\n\n\nclass BasicSettings(BaseModel):\n \"\"\"基础设置\"\"\"\n app_name: str = Field(default=\"AutoClip Desktop\", description=\"应用名称\")\n app_version: str = Field(default=\"1.0.0\", description=\"应用版本\")\n debug_mode: bool = Field(default=False, description=\"调试模式\")\n auto_start: bool = Field(default=True, description=\"自动启动\")\n\n\nclass ServiceSettings(BaseModel):\n \"\"\"服务设置\"\"\"\n host: str = Field(default=\"127.0.0.1\", description=\"服务主机\")\n port: int = Field(default=8000, description=\"服务端口\")\n max_memory_usage: int = Field(default=2048, description=\"最大内存使用(MB)\")\n \n @validator('port')\n def validate_port(cls, v):\n if not 1024 <= v <= 65535:\n raise ValueError('端口号必须在1024-65535之间')\n return v\n \n @validator('max_memory_usage')\n def validate_memory(cls, v):\n if not 512 <= v <= 8192:\n raise ValueError('内存使用限制必须在512-8192MB之间')\n return v\n\n\nclass ApiKeys(BaseModel):\n \"\"\"API密钥设置\"\"\"\n dashscope: str = Field(default=\"\", description=\"通义千问API密钥\")\n openai: str = Field(default=\"\", description=\"OpenAI API密钥\")\n gemini: str = Field(default=\"\", description=\"Gemini API密钥\")\n siliconflow: str = Field(default=\"\", description=\"SiliconFlow API密钥\")\n jimeng_access: str = Field(default=\"\", description=\"即梦AI访问密钥\")\n jimeng_secret: str = Field(default=\"\", description=\"即梦AI秘密密钥\")\n\n\nclass ApiSettings(BaseModel):\n \"\"\"API设置\"\"\"\n api_keys: ApiKeys = Field(default_factory=ApiKeys, description=\"API密钥\")\n api_model: str = Field(default=\"qwen-plus\", description=\"默认模型\")\n api_max_tokens: int = Field(default=4096, description=\"最大Token数\")\n api_timeout: int = Field(default=30, description=\"API超时时间(秒)\")\n \n @validator('api_timeout')\n def validate_timeout(cls, v):\n if not 5 <= v <= 300:\n raise ValueError('API超时时间必须在5-300秒之间')\n return v\n\n\nclass ProcessingSettings(BaseModel):\n \"\"\"处理设置\"\"\"\n processing_chunk_size: int = Field(default=5000, description=\"处理块大小\")\n processing_min_score: float = Field(default=0.7, description=\"最小评分阈值\")\n processing_max_clips: int = Field(default=5, description=\"合集最大切片数\")\n processing_max_retries: int = Field(default=3, description=\"最大重试次数\")\n \n @validator('processing_chunk_size')\n def validate_chunk_size(cls, v):\n if not 1000 <= v <= 10000:\n raise ValueError('处理块大小必须在1000-10000之间')\n return v\n \n @validator('processing_min_score')\n def validate_min_score(cls, v):\n if not 0.1 <= v <= 1.0:\n raise ValueError('最小评分阈值必须在0.1-1.0之间')\n return v\n\n\nclass LogSettings(BaseModel):\n \"\"\"日志设置\"\"\"\n log_level: str = Field(default=\"INFO\", description=\"日志级别\")\n log_retention_days: int = Field(default=7, description=\"日志保留天数\")\n \n @validator('log_level')\n def validate_log_level(cls, v):\n if v not in ['DEBUG', 'INFO', 'WARNING', 'ERROR']:\n raise ValueError('日志级别必须是DEBUG、INFO、WARNING或ERROR')\n return v\n \n @validator('log_retention_days')\n def validate_retention_days(cls, v):\n if not 1 <= v <= 30:\n raise ValueError('日志保留天数必须在1-30天之间')\n return v\n\n\nclass PathSettings(BaseModel):\n \"\"\"路径设置\"\"\"\n data_directory: str = Field(description=\"数据目录\")\n cache_directory: str = Field(description=\"缓存目录\")\n temp_directory: str = Field(description=\"临时目录\")\n\n\nclass UpdateDataDirRequest(BaseModel):\n \"\"\"更新数据目录请求\"\"\"\n new_data_directory: str = Field(description=\"新的数据目录路径\")\n migrate: bool = Field(default=True, description=\"是否迁移旧目录数据\")\n\n\nclass DesktopSettings(BaseModel):\n \"\"\"完整的Desktop设置\"\"\"\n basic: BasicSettings = Field(default_factory=BasicSettings)\n service: ServiceSettings = Field(default_factory=ServiceSettings)\n api: ApiSettings = Field(default_factory=ApiSettings)\n processing: ProcessingSettings = Field(default_factory=ProcessingSettings)\n logs: LogSettings = Field(default_factory=LogSettings)\n paths: Optional[PathSettings] = Field(default=None, description=\"路径设置\")\n\n\ndef check_desktop_mode(relaxed: bool = False):\n \"\"\"检查是否在Desktop模式\n relaxed=True 时放宽限制,允许内测安装包/开发环境调用(返回告警但不阻断)。\n \"\"\"\n if not is_desktop_mode():\n if relaxed:\n return False\n raise HTTPException(status_code=400, detail=\"此端点仅在Desktop模式下可用\")\n\n\n@router.get(\"/desktop-mode\")\nasync def check_desktop_mode_endpoint():\n \"\"\"检查是否在桌面模式 - 供前端调用\"\"\"\n return {\n \"is_desktop_mode\": is_desktop_mode(),\n \"environment\": {\n \"AUTOCLIP_DESKTOP_MODE\": os.getenv(\"AUTOCLIP_DESKTOP_MODE\"),\n \"AUTOCLIP_MODE\": os.getenv(\"AUTOCLIP_MODE\"),\n \"TAURI_PLATFORM\": os.getenv(\"TAURI_PLATFORM\"),\n }\n }\n\n\n@router.get(\"/\", response_model=DesktopSettings)\nasync def get_settings():\n \"\"\"获取所有设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 尝试从保存的设置文件中读取\n settings_file = config.paths.data_dir / \"settings.json\"\n print(f\"设置文件路径: {settings_file}\")\n print(f\"设置文件存在: {settings_file.exists()}\")\n \n if settings_file.exists():\n try:\n with open(settings_file, 'r', encoding='utf-8') as f:\n saved_settings = json.load(f)\n \n print(f\"从文件读取的设置: {saved_settings.get('basic', {}).get('app_name', 'unknown')}\")\n \n # 验证并返回保存的设置\n settings = DesktopSettings(**saved_settings)\n return settings\n except Exception as e:\n # 如果读取失败,回退到默认配置\n print(f\"读取设置文件失败: {e}\")\n pass\n \n # 构建路径设置\n paths = PathSettings(\n data_directory=str(config.paths.data_dir),\n cache_directory=str(config.paths.cache_dir),\n temp_directory=str(config.paths.temp_dir)\n )\n \n # 构建完整设置\n settings = DesktopSettings(\n basic=BasicSettings(\n app_name=config.app_name,\n app_version=config.app_version,\n debug_mode=config.debug_mode,\n auto_start=True # 默认值\n ),\n service=ServiceSettings(\n host=config.host,\n port=config.port,\n max_memory_usage=config.max_memory_usage\n ),\n api=ApiSettings(\n api_keys=ApiKeys(\n dashscope=config.dashscope_api_key,\n openai=config.openai_api_key,\n gemini=config.gemini_api_key,\n siliconflow=config.siliconflow_api_key,\n jimeng_access=\"\", # 默认值\n jimeng_secret=\"\" # 默认值\n ),\n api_model=config.default_model,\n api_max_tokens=config.max_tokens,\n api_timeout=config.timeout\n ),\n processing=ProcessingSettings(\n processing_chunk_size=config.chunk_size,\n processing_min_score=config.min_score_threshold,\n processing_max_clips=config.max_clips_per_collection,\n processing_max_retries=config.max_retries\n ),\n logs=LogSettings(\n log_level=config.log_level,\n log_retention_days=config.log_retention_days\n ),\n paths=paths\n )\n \n return settings\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取设置失败: {str(e)}\")\n\n\n@router.post(\"/paths/data-directory\")\nasync def update_data_directory(\n new_path: str,\n migrate_data: bool = True\n):\n \"\"\"更新数据目录\"\"\"\n check_desktop_mode()\n \n try:\n from backend.core.desktop_config import set_data_dir\n \n result = set_data_dir(new_path, migrate_data)\n \n if result[\"success\"]:\n return {\n \"message\": result[\"message\"],\n \"new_path\": result[\"new_path\"],\n \"migrated_files\": result.get(\"migrated_files\", []),\n \"failed_files\": result.get(\"failed_files\", [])\n }\n else:\n raise HTTPException(status_code=400, detail=result[\"error\"])\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"更新数据目录失败: {str(e)}\")\n\n@router.get(\"/paths/data-directory\")\nasync def get_data_directory_info():\n \"\"\"获取数据目录信息\"\"\"\n check_desktop_mode()\n \n try:\n from backend.core.desktop_config import get_data_dir_info\n \n return get_data_dir_info()\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取数据目录信息失败: {str(e)}\")\n\n@router.delete(\"/\")\nasync def clear_settings(\n config: DesktopConfig = Depends(get_desktop_config)\n):\n \"\"\"清除所有设置\"\"\"\n check_desktop_mode()\n \n try:\n # 重置配置为默认值\n config.dashscope_api_key = \"\"\n config.openai_api_key = \"\"\n config.gemini_api_key = \"\"\n config.siliconflow_api_key = \"\"\n config.jimeng_access_key = \"\"\n config.jimeng_secret_key = \"\"\n \n # 保存配置\n save_desktop_config(config)\n \n return {\"message\": \"设置已清除\", \"success\": True}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"清除设置失败: {str(e)}\")\n\nclass TestApiRequest(BaseModel):\n provider: str\n api_key: str\n\n@router.post(\"/test-api\")\nasync def test_api_connection(request: TestApiRequest):\n \"\"\"测试API连接\"\"\"\n check_desktop_mode()\n \n try:\n # 首先进行基本的API Key格式验证\n if not request.api_key or len(request.api_key.strip()) < 10:\n return {\n \"success\": False,\n \"error\": \"API Key为空或过短,请检查输入\",\n \"provider\": request.provider\n }\n \n # 根据提供商进行格式验证\n if request.provider in [\"dashscope\", \"openai\", \"deepseek\"]:\n if not request.api_key.startswith(\"sk-\"):\n return {\n \"success\": False,\n \"error\": f\"{request.provider} API Key格式可能不正确,通常以'sk-'开头\",\n \"provider\": request.provider\n }\n\n # 根据提供商测试API连接\n if request.provider == \"dashscope\":\n from backend.core.llm_providers import DashScopeProvider\n provider_instance = DashScopeProvider(api_key=request.api_key)\n elif request.provider == \"openai\":\n from backend.core.llm_providers import OpenAIProvider\n provider_instance = OpenAIProvider(api_key=request.api_key)\n elif request.provider == \"gemini\":\n from backend.core.llm_providers import GeminiProvider\n provider_instance = GeminiProvider(api_key=request.api_key)\n elif request.provider == \"siliconflow\":\n from backend.core.llm_providers import SiliconFlowProvider\n provider_instance = SiliconFlowProvider(api_key=request.api_key)\n elif request.provider == \"deepseek\":\n from backend.core.llm_providers import DeepSeekProvider\n provider_instance = DeepSeekProvider(api_key=request.api_key)\n else:\n raise HTTPException(status_code=400, detail=\"不支持的API提供商\")\n \n # 测试连接\n test_result = provider_instance.test_connection()\n \n if test_result:\n return {\n \"success\": True,\n \"message\": \"API连接测试成功\",\n \"provider\": request.provider\n }\n else:\n # 提供更详细的错误信息\n error_msg = f\"API连接测试失败\"\n if request.provider == \"dashscope\":\n error_msg += \"。请检查API Key是否正确,DashScope API Key通常以'sk-'开头\"\n elif request.provider == \"openai\":\n error_msg += \"。请检查API Key是否正确,OpenAI API Key通常以'sk-'开头\"\n elif request.provider == \"gemini\":\n error_msg += \"。请检查API Key是否正确\"\n elif request.provider == \"siliconflow\":\n error_msg += \"。请检查API Key是否正确\"\n \n return {\n \"success\": False,\n \"error\": error_msg,\n \"provider\": request.provider\n }\n \n except Exception as e:\n logger.error(f\"API连接测试异常: {str(e)}\")\n return {\n \"success\": False,\n \"error\": f\"API连接测试失败: {str(e)}\",\n \"provider\": request.provider\n }\n\n@router.put(\"/\", response_model=Dict[str, Any])\nasync def update_settings(settings: DesktopSettings):\n \"\"\"更新设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 更新配置\n config.debug_mode = settings.basic.debug_mode\n config.host = settings.service.host\n config.port = settings.service.port\n config.max_memory_usage = settings.service.max_memory_usage\n \n # 更新API设置\n config.dashscope_api_key = settings.api.api_keys.dashscope\n config.openai_api_key = settings.api.api_keys.openai\n config.gemini_api_key = settings.api.api_keys.gemini\n config.siliconflow_api_key = settings.api.api_keys.siliconflow\n config.default_model = settings.api.api_model\n config.max_tokens = settings.api.api_max_tokens\n config.timeout = settings.api.api_timeout\n \n # 更新处理设置\n config.chunk_size = settings.processing.processing_chunk_size\n config.min_score_threshold = settings.processing.processing_min_score\n config.max_clips_per_collection = settings.processing.processing_max_clips\n config.max_retries = settings.processing.processing_max_retries\n \n # 更新日志设置\n config.log_level = settings.logs.log_level\n \n # 保存设置到文件\n settings_file = config.paths.data_dir / \"settings.json\"\n with open(settings_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n # 重要:保存主配置文件,确保API key等关键配置被持久化\n from backend.core.desktop_config import save_desktop_config\n if not save_desktop_config(config):\n raise HTTPException(status_code=500, detail=\"保存主配置文件失败\")\n \n return {\"message\": \"设置更新成功\", \"settings_file\": str(settings_file)}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"更新设置失败: {str(e)}\")\n\n\n@router.post(\"/reset\")\nasync def reset_settings():\n \"\"\"重置设置为默认值\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 删除设置文件\n settings_file = config.paths.data_dir / \"settings.json\"\n if settings_file.exists():\n settings_file.unlink()\n \n # 重新加载默认配置\n config._settings = None\n config._paths = None\n \n return {\"message\": \"设置已重置为默认值\"}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"重置设置失败: {str(e)}\")\n\n\n@router.post(\"/paths/data-directory\", response_model=Dict[str, Any])\nasync def update_data_directory(payload: UpdateDataDirRequest):\n \"\"\"更新数据目录(可选迁移数据)。供首次运行向导或设置页调用。\"\"\"\n is_desktop = check_desktop_mode(relaxed=True)\n try:\n config = get_desktop_config()\n result = config.set_data_dir(Path(payload.new_data_directory), migrate_from_old=payload.migrate)\n\n # 同步返回新的路径配置\n paths = {\n \"data_directory\": str(config.paths.data_dir),\n \"cache_directory\": str(config.paths.cache_dir),\n \"temp_directory\": str(config.paths.temp_dir),\n \"database_url\": config.paths.database_url,\n }\n\n resp = {\"message\": \"数据目录更新成功\", \"result\": result, \"paths\": paths}\n if not is_desktop:\n resp[\"warning\"] = \"当前非Desktop模式,但已更新路径配置\"\n return resp\n except Exception as e:\n # 返回更详细的错误,帮助定位权限/路径/占用等问题\n return {\n \"message\": \"更新数据目录失败,但已跳过(内测模式)\",\n \"error\": str(e),\n \"hint\": \"请确认目标目录可写且未被系统限制,必要时选择用户主目录下的路径\",\n }\n\n\n@router.post(\"/export\")\nasync def export_settings():\n \"\"\"导出设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n settings = await get_settings()\n \n # 创建导出文件\n export_file = config.paths.data_dir / \"autoclip-settings-export.json\"\n with open(export_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n return {\n \"message\": \"设置导出成功\",\n \"export_file\": str(export_file),\n \"download_url\": f\"/api/v1/settings/download/{export_file.name}\"\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"导出设置失败: {str(e)}\")\n\n\n@router.post(\"/import\")\nasync def import_settings(file: UploadFile = File(...)):\n \"\"\"导入设置\"\"\"\n check_desktop_mode()\n \n try:\n # 读取上传的文件\n content = await file.read()\n settings_data = json.loads(content.decode('utf-8'))\n \n # 验证设置格式\n settings = DesktopSettings(**settings_data)\n \n # 更新设置\n result = await update_settings(settings)\n \n return {\n \"message\": \"设置导入成功\",\n \"imported_settings\": settings.dict()\n }\n \n except json.JSONDecodeError:\n raise HTTPException(status_code=400, detail=\"设置文件格式错误\")\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"导入设置失败: {str(e)}\")\n\n\n# 删除重复的test_api_connection函数\n\n\n@router.get(\"/validation\")\nasync def validate_settings():\n \"\"\"验证当前设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n validation_result = config.validate_config()\n \n return {\n \"valid\": validation_result[\"valid\"],\n \"errors\": validation_result.get(\"errors\", []),\n \"warnings\": validation_result.get(\"warnings\", []),\n \"recommendations\": []\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"设置验证失败: {str(e)}\")\n\n\n@router.get(\"/backup\")\nasync def backup_settings():\n \"\"\"备份设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_dir = config.paths.data_dir / \"backups\"\n backup_dir.mkdir(exist_ok=True)\n \n # 创建备份\n import datetime\n timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n backup_file = backup_dir / f\"settings_backup_{timestamp}.json\"\n \n settings = await get_settings()\n with open(backup_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n return {\n \"message\": \"设置备份成功\",\n \"backup_file\": str(backup_file),\n \"backup_time\": timestamp\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"备份设置失败: {str(e)}\")\n\n\n@router.get(\"/backups\")\nasync def list_backups():\n \"\"\"列出所有备份\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_dir = config.paths.data_dir / \"backups\"\n \n if not backup_dir.exists():\n return {\"backups\": []}\n \n backups = []\n for backup_file in backup_dir.glob(\"settings_backup_*.json\"):\n stat = backup_file.stat()\n backups.append({\n \"filename\": backup_file.name,\n \"path\": str(backup_file),\n \"size\": stat.st_size,\n \"created_time\": stat.st_ctime,\n \"modified_time\": stat.st_mtime\n })\n \n # 按创建时间排序\n backups.sort(key=lambda x: x[\"created_time\"], reverse=True)\n \n return {\"backups\": backups}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取备份列表失败: {str(e)}\")\n\n\n@router.get(\"/available-models\")\nasync def get_available_models():\n \"\"\"获取可用的模型列表\"\"\"\n check_desktop_mode()\n \n try:\n # 返回按供应商分类的模型列表\n models = {\n \"dashscope\": [\n {\"name\": \"qwen-plus\", \"display_name\": \"通义千问增强版\", \"max_tokens\": 8192, \"description\": \"适合复杂推理和创作任务\"},\n {\"name\": \"qwen-turbo\", \"display_name\": \"通义千问标准版\", \"max_tokens\": 8192, \"description\": \"平衡性能和成本\"},\n {\"name\": \"qwen-max\", \"display_name\": \"通义千问旗舰版\", \"max_tokens\": 8192, \"description\": \"最强性能,适合复杂任务\"},\n {\"name\": \"qwen-long\", \"display_name\": \"通义千问长文本版\", \"max_tokens\": 100000, \"description\": \"支持超长文本处理\"}\n ],\n \"openai\": [\n {\"name\": \"gpt-4o\", \"display_name\": \"GPT-4 Omni\", \"max_tokens\": 128000, \"description\": \"最新多模态模型\"},\n {\"name\": \"gpt-4o-mini\", \"display_name\": \"GPT-4 Omni Mini\", \"max_tokens\": 128000, \"description\": \"轻量级多模态模型\"},\n {\"name\": \"gpt-4-turbo\", \"display_name\": \"GPT-4 Turbo\", \"max_tokens\": 128000, \"description\": \"高性能版本\"},\n {\"name\": \"gpt-4\", \"display_name\": \"GPT-4\", \"max_tokens\": 8192, \"description\": \"经典版本\"},\n {\"name\": \"gpt-3.5-turbo\", \"display_name\": \"GPT-3.5 Turbo\", \"max_tokens\": 16384, \"description\": \"经济实用版本\"}\n ],\n \"gemini\": [\n {\"name\": \"gemini-1.5-pro\", \"display_name\": \"Gemini 1.5 Pro\", \"max_tokens\": 2000000, \"description\": \"最新专业版\"},\n {\"name\": \"gemini-1.5-flash\", \"display_name\": \"Gemini 1.5 Flash\", \"max_tokens\": 1000000, \"description\": \"快速响应版本\"},\n {\"name\": \"gemini-pro\", \"display_name\": \"Gemini Pro\", \"max_tokens\": 30720, \"description\": \"经典专业版\"}\n ],\n \"siliconflow\": [\n {\"name\": \"deepseek-chat\", \"display_name\": \"DeepSeek Chat\", \"max_tokens\": 32768, \"description\": \"深度求索对话模型\"},\n {\"name\": \"deepseek-coder\", \"display_name\": \"DeepSeek Coder\", \"max_tokens\": 16384, \"description\": \"代码生成专用模型\"},\n {\"name\": \"qwen-plus\", \"display_name\": \"通义千问增强版\", \"max_tokens\": 8192, \"description\": \"通过硅基流动访问\"},\n {\"name\": \"qwen-turbo\", \"display_name\": \"通义千问标准版\", \"max_tokens\": 8192, \"description\": \"通过硅基流动访问\"}\n ],\n }\n \n return {\"models\": models}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取模型列表失败: {str(e)}\")\n\n\n@router.get(\"/current-provider\")\nasync def get_current_provider():\n \"\"\"获取当前提供商信息\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 根据当前配置返回提供商信息\n provider_info = {\n \"provider\": \"dashscope\", # 默认提供商\n \"model\": config.default_model or \"qwen-plus\",\n \"available\": True,\n \"display_name\": \"通义千问\",\n \"description\": \"阿里云通义千问服务\"\n }\n \n return provider_info\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取当前提供商信息失败: {str(e)}\")\n\n\n@router.post(\"/restore/{backup_filename}\")\nasync def restore_backup(backup_filename: str):\n \"\"\"从备份恢复设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_file = config.paths.data_dir / \"backups\" / backup_filename\n \n if not backup_file.exists():\n raise HTTPException(status_code=404, detail=\"备份文件不存在\")\n \n # 读取备份文件\n with open(backup_file, 'r', encoding='utf-8') as f:\n settings_data = json.load(f)\n \n # 验证并恢复设置\n settings = DesktopSettings(**settings_data)\n result = await update_settings(settings)\n \n return {\n \"message\": \"设置恢复成功\",\n \"restored_from\": backup_filename,\n \"restored_settings\": settings.dict()\n }\n \n except HTTPException:\n raise\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"恢复设置失败: {str(e)}\")\n\n\n@router.post(\"/sync-config\")\nasync def sync_config():\n \"\"\"手动同步客户端配置到后端\"\"\"\n check_desktop_mode()\n \n try:\n if config_sync_service.sync_from_client():\n return {\n \"status\": \"success\",\n \"message\": \"配置同步成功\"\n }\n else:\n return {\n \"status\": \"error\",\n \"message\": \"配置同步失败\"\n }\n except Exception as e:\n return {\n \"status\": \"error\",\n \"message\": f\"配置同步失败: {str(e)}\"\n }\n\n\n@router.get(\"/config-status\")\nasync def get_config_status():\n \"\"\"获取配置同步状态\"\"\"\n check_desktop_mode()\n \n try:\n client_time = config_sync_service.get_client_config_timestamp()\n backup_time = config_sync_service.get_backup_config_timestamp()\n sync_needed = config_sync_service.is_sync_needed()\n \n return {\n \"client_config_exists\": client_time is not None,\n \"backup_config_exists\": backup_time is not None,\n \"client_config_time\": client_time,\n \"backup_config_time\": backup_time,\n \"sync_needed\": sync_needed,\n \"client_config_path\": str(config_sync_service.client_config_path),\n \"backup_config_path\": str(config_sync_service.backup_config_path)\n }\n except Exception as e:\n return {\n \"status\": \"error\",\n \"message\": f\"获取配置状态失败: {str(e)}\"\n }"}, "11bad706b305f2af25423d9e3dba553994fa5bb0": {"t": 1785768789.9007058, "n": 25399, "h": "a407dbbbd6b5239f", "c": "\"\"\"\n设置管理API端点\n为Desktop客户端提供设置管理功能\n\"\"\"\n\nimport os\nimport json\nimport shutil\nimport logging\nfrom pathlib import Path\nfrom typing import Dict, Any, Optional, List\nfrom fastapi import APIRouter, HTTPException, UploadFile, File, Depends\nfrom pydantic import BaseModel, Field, validator\n\nlogger = logging.getLogger(__name__)\n\nfrom backend.core.desktop_config import get_desktop_config, is_desktop_mode, DesktopConfig, save_desktop_config\nfrom backend.services.config_sync_service import config_sync_service\nfrom pathlib import Path\n\nrouter = APIRouter(prefix=\"/settings\", tags=[\"settings\"])\n\n\nclass BasicSettings(BaseModel):\n \"\"\"基础设置\"\"\"\n app_name: str = Field(default=\"AutoClip Desktop\", description=\"应用名称\")\n app_version: str = Field(default=\"1.0.0\", description=\"应用版本\")\n debug_mode: bool = Field(default=False, description=\"调试模式\")\n auto_start: bool = Field(default=True, description=\"自动启动\")\n\n\nclass ServiceSettings(BaseModel):\n \"\"\"服务设置\"\"\"\n host: str = Field(default=\"127.0.0.1\", description=\"服务主机\")\n port: int = Field(default=8000, description=\"服务端口\")\n max_memory_usage: int = Field(default=2048, description=\"最大内存使用(MB)\")\n \n @validator('port')\n def validate_port(cls, v):\n if not 1024 <= v <= 65535:\n raise ValueError('端口号必须在1024-65535之间')\n return v\n \n @validator('max_memory_usage')\n def validate_memory(cls, v):\n if not 512 <= v <= 8192:\n raise ValueError('内存使用限制必须在512-8192MB之间')\n return v\n\n\nclass ApiKeys(BaseModel):\n \"\"\"API密钥设置\"\"\"\n dashscope: str = Field(default=\"\", description=\"通义千问API密钥\")\n openai: str = Field(default=\"\", description=\"OpenAI API密钥\")\n gemini: str = Field(default=\"\", description=\"Gemini API密钥\")\n siliconflow: str = Field(default=\"\", description=\"SiliconFlow API密钥\")\n jimeng_access: str = Field(default=\"\", description=\"即梦AI访问密钥\")\n jimeng_secret: str = Field(default=\"\", description=\"即梦AI秘密密钥\")\n\n\nclass ApiSettings(BaseModel):\n \"\"\"API设置\"\"\"\n api_keys: ApiKeys = Field(default_factory=ApiKeys, description=\"API密钥\")\n api_model: str = Field(default=\"qwen-plus\", description=\"默认模型\")\n api_max_tokens: int = Field(default=4096, description=\"最大Token数\")\n api_timeout: int = Field(default=30, description=\"API超时时间(秒)\")\n \n @validator('api_timeout')\n def validate_timeout(cls, v):\n if not 5 <= v <= 300:\n raise ValueError('API超时时间必须在5-300秒之间')\n return v\n\n\nclass ProcessingSettings(BaseModel):\n \"\"\"处理设置\"\"\"\n processing_chunk_size: int = Field(default=5000, description=\"处理块大小\")\n processing_min_score: float = Field(default=0.7, description=\"最小评分阈值\")\n processing_max_clips: int = Field(default=5, description=\"合集最大切片数\")\n processing_max_retries: int = Field(default=3, description=\"最大重试次数\")\n \n @validator('processing_chunk_size')\n def validate_chunk_size(cls, v):\n if not 1000 <= v <= 10000:\n raise ValueError('处理块大小必须在1000-10000之间')\n return v\n \n @validator('processing_min_score')\n def validate_min_score(cls, v):\n if not 0.1 <= v <= 1.0:\n raise ValueError('最小评分阈值必须在0.1-1.0之间')\n return v\n\n\nclass LogSettings(BaseModel):\n \"\"\"日志设置\"\"\"\n log_level: str = Field(default=\"INFO\", description=\"日志级别\")\n log_retention_days: int = Field(default=7, description=\"日志保留天数\")\n \n @validator('log_level')\n def validate_log_level(cls, v):\n if v not in ['DEBUG', 'INFO', 'WARNING', 'ERROR']:\n raise ValueError('日志级别必须是DEBUG、INFO、WARNING或ERROR')\n return v\n \n @validator('log_retention_days')\n def validate_retention_days(cls, v):\n if not 1 <= v <= 30:\n raise ValueError('日志保留天数必须在1-30天之间')\n return v\n\n\nclass PathSettings(BaseModel):\n \"\"\"路径设置\"\"\"\n data_directory: str = Field(description=\"数据目录\")\n cache_directory: str = Field(description=\"缓存目录\")\n temp_directory: str = Field(description=\"临时目录\")\n\n\nclass UpdateDataDirRequest(BaseModel):\n \"\"\"更新数据目录请求\"\"\"\n new_data_directory: str = Field(description=\"新的数据目录路径\")\n migrate: bool = Field(default=True, description=\"是否迁移旧目录数据\")\n\n\nclass DesktopSettings(BaseModel):\n \"\"\"完整的Desktop设置\"\"\"\n basic: BasicSettings = Field(default_factory=BasicSettings)\n service: ServiceSettings = Field(default_factory=ServiceSettings)\n api: ApiSettings = Field(default_factory=ApiSettings)\n processing: ProcessingSettings = Field(default_factory=ProcessingSettings)\n logs: LogSettings = Field(default_factory=LogSettings)\n paths: Optional[PathSettings] = Field(default=None, description=\"路径设置\")\n\n\ndef check_desktop_mode(relaxed: bool = False):\n \"\"\"检查是否在Desktop模式\n relaxed=True 时放宽限制,允许内测安装包/开发环境调用(返回告警但不阻断)。\n \"\"\"\n if not is_desktop_mode():\n if relaxed:\n return False\n raise HTTPException(status_code=400, detail=\"此端点仅在Desktop模式下可用\")\n\n\n@router.get(\"/desktop-mode\")\nasync def check_desktop_mode_endpoint():\n \"\"\"检查是否在桌面模式 - 供前端调用\"\"\"\n return {\n \"is_desktop_mode\": is_desktop_mode(),\n \"environment\": {\n \"AUTOCLIP_DESKTOP_MODE\": os.getenv(\"AUTOCLIP_DESKTOP_MODE\"),\n \"AUTOCLIP_MODE\": os.getenv(\"AUTOCLIP_MODE\"),\n \"TAURI_PLATFORM\": os.getenv(\"TAURI_PLATFORM\"),\n }\n }\n\n\n@router.get(\"/\", response_model=DesktopSettings)\nasync def get_settings():\n \"\"\"获取所有设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 尝试从保存的设置文件中读取\n settings_file = config.paths.data_dir / \"settings.json\"\n print(f\"设置文件路径: {settings_file}\")\n print(f\"设置文件存在: {settings_file.exists()}\")\n \n if settings_file.exists():\n try:\n with open(settings_file, 'r', encoding='utf-8') as f:\n saved_settings = json.load(f)\n \n print(f\"从文件读取的设置: {saved_settings.get('basic', {}).get('app_name', 'unknown')}\")\n \n # 验证并返回保存的设置\n settings = DesktopSettings(**saved_settings)\n return settings\n except Exception as e:\n # 如果读取失败,回退到默认配置\n print(f\"读取设置文件失败: {e}\")\n pass\n \n # 构建路径设置\n paths = PathSettings(\n data_directory=str(config.paths.data_dir),\n cache_directory=str(config.paths.cache_dir),\n temp_directory=str(config.paths.temp_dir)\n )\n \n # 构建完整设置\n settings = DesktopSettings(\n basic=BasicSettings(\n app_name=config.app_name,\n app_version=config.app_version,\n debug_mode=config.debug_mode,\n auto_start=True # 默认值\n ),\n service=ServiceSettings(\n host=config.host,\n port=config.port,\n max_memory_usage=config.max_memory_usage\n ),\n api=ApiSettings(\n api_keys=ApiKeys(\n dashscope=config.dashscope_api_key,\n openai=config.openai_api_key,\n gemini=config.gemini_api_key,\n siliconflow=config.siliconflow_api_key,\n jimeng_access=\"\", # 默认值\n jimeng_secret=\"\" # 默认值\n ),\n api_model=config.default_model,\n api_max_tokens=config.max_tokens,\n api_timeout=config.timeout\n ),\n processing=ProcessingSettings(\n processing_chunk_size=config.chunk_size,\n processing_min_score=config.min_score_threshold,\n processing_max_clips=config.max_clips_per_collection,\n processing_max_retries=config.max_retries\n ),\n logs=LogSettings(\n log_level=config.log_level,\n log_retention_days=config.log_retention_days\n ),\n paths=paths\n )\n \n return settings\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取设置失败: {str(e)}\")\n\n\n@router.post(\"/paths/data-directory\")\nasync def update_data_directory(\n new_path: str,\n migrate_data: bool = True\n):\n \"\"\"更新数据目录\"\"\"\n check_desktop_mode()\n \n try:\n from backend.core.desktop_config import set_data_dir\n \n result = set_data_dir(new_path, migrate_data)\n \n if result[\"success\"]:\n return {\n \"message\": result[\"message\"],\n \"new_path\": result[\"new_path\"],\n \"migrated_files\": result.get(\"migrated_files\", []),\n \"failed_files\": result.get(\"failed_files\", [])\n }\n else:\n raise HTTPException(status_code=400, detail=result[\"error\"])\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"更新数据目录失败: {str(e)}\")\n\n@router.get(\"/paths/data-directory\")\nasync def get_data_directory_info():\n \"\"\"获取数据目录信息\"\"\"\n check_desktop_mode()\n \n try:\n from backend.core.desktop_config import get_data_dir_info\n \n return get_data_dir_info()\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取数据目录信息失败: {str(e)}\")\n\n@router.delete(\"/\")\nasync def clear_settings(\n config: DesktopConfig = Depends(get_desktop_config)\n):\n \"\"\"清除所有设置\"\"\"\n check_desktop_mode()\n \n try:\n # 重置配置为默认值\n config.dashscope_api_key = \"\"\n config.openai_api_key = \"\"\n config.gemini_api_key = \"\"\n config.siliconflow_api_key = \"\"\n config.jimeng_access_key = \"\"\n config.jimeng_secret_key = \"\"\n \n # 保存配置\n save_desktop_config(config)\n \n return {\"message\": \"设置已清除\", \"success\": True}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"清除设置失败: {str(e)}\")\n\nclass TestApiRequest(BaseModel):\n provider: str\n api_key: str\n\n@router.post(\"/test-api\")\nasync def test_api_connection(request: TestApiRequest):\n \"\"\"测试API连接\"\"\"\n check_desktop_mode()\n \n try:\n # 首先进行基本的API Key格式验证\n if not request.api_key or len(request.api_key.strip()) < 10:\n return {\n \"success\": False,\n \"error\": \"API Key为空或过短,请检查输入\",\n \"provider\": request.provider\n }\n \n # 根据提供商进行格式验证\n if request.provider in [\"dashscope\", \"openai\"]:\n if not request.api_key.startswith(\"sk-\"):\n return {\n \"success\": False,\n \"error\": f\"{request.provider} API Key格式可能不正确,通常以'sk-'开头\",\n \"provider\": request.provider\n }\n \n # 根据提供商测试API连接\n if request.provider == \"dashscope\":\n from backend.core.llm_providers import DashScopeProvider\n provider_instance = DashScopeProvider(api_key=request.api_key)\n elif request.provider == \"openai\":\n from backend.core.llm_providers import OpenAIProvider\n provider_instance = OpenAIProvider(api_key=request.api_key)\n elif request.provider == \"gemini\":\n from backend.core.llm_providers import GeminiProvider\n provider_instance = GeminiProvider(api_key=request.api_key)\n elif request.provider == \"siliconflow\":\n from backend.core.llm_providers import SiliconFlowProvider\n provider_instance = SiliconFlowProvider(api_key=request.api_key)\n else:\n raise HTTPException(status_code=400, detail=\"不支持的API提供商\")\n \n # 测试连接\n test_result = provider_instance.test_connection()\n \n if test_result:\n return {\n \"success\": True,\n \"message\": \"API连接测试成功\",\n \"provider\": request.provider\n }\n else:\n # 提供更详细的错误信息\n error_msg = f\"API连接测试失败\"\n if request.provider == \"dashscope\":\n error_msg += \"。请检查API Key是否正确,DashScope API Key通常以'sk-'开头\"\n elif request.provider == \"openai\":\n error_msg += \"。请检查API Key是否正确,OpenAI API Key通常以'sk-'开头\"\n elif request.provider == \"gemini\":\n error_msg += \"。请检查API Key是否正确\"\n elif request.provider == \"siliconflow\":\n error_msg += \"。请检查API Key是否正确\"\n \n return {\n \"success\": False,\n \"error\": error_msg,\n \"provider\": request.provider\n }\n \n except Exception as e:\n logger.error(f\"API连接测试异常: {str(e)}\")\n return {\n \"success\": False,\n \"error\": f\"API连接测试失败: {str(e)}\",\n \"provider\": request.provider\n }\n\n@router.put(\"/\", response_model=Dict[str, Any])\nasync def update_settings(settings: DesktopSettings):\n \"\"\"更新设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 更新配置\n config.debug_mode = settings.basic.debug_mode\n config.host = settings.service.host\n config.port = settings.service.port\n config.max_memory_usage = settings.service.max_memory_usage\n \n # 更新API设置\n config.dashscope_api_key = settings.api.api_keys.dashscope\n config.openai_api_key = settings.api.api_keys.openai\n config.gemini_api_key = settings.api.api_keys.gemini\n config.siliconflow_api_key = settings.api.api_keys.siliconflow\n config.default_model = settings.api.api_model\n config.max_tokens = settings.api.api_max_tokens\n config.timeout = settings.api.api_timeout\n \n # 更新处理设置\n config.chunk_size = settings.processing.processing_chunk_size\n config.min_score_threshold = settings.processing.processing_min_score\n config.max_clips_per_collection = settings.processing.processing_max_clips\n config.max_retries = settings.processing.processing_max_retries\n \n # 更新日志设置\n config.log_level = settings.logs.log_level\n \n # 保存设置到文件\n settings_file = config.paths.data_dir / \"settings.json\"\n with open(settings_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n # 重要:保存主配置文件,确保API key等关键配置被持久化\n from backend.core.desktop_config import save_desktop_config\n if not save_desktop_config(config):\n raise HTTPException(status_code=500, detail=\"保存主配置文件失败\")\n \n return {\"message\": \"设置更新成功\", \"settings_file\": str(settings_file)}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"更新设置失败: {str(e)}\")\n\n\n@router.post(\"/reset\")\nasync def reset_settings():\n \"\"\"重置设置为默认值\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 删除设置文件\n settings_file = config.paths.data_dir / \"settings.json\"\n if settings_file.exists():\n settings_file.unlink()\n \n # 重新加载默认配置\n config._settings = None\n config._paths = None\n \n return {\"message\": \"设置已重置为默认值\"}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"重置设置失败: {str(e)}\")\n\n\n@router.post(\"/paths/data-directory\", response_model=Dict[str, Any])\nasync def update_data_directory(payload: UpdateDataDirRequest):\n \"\"\"更新数据目录(可选迁移数据)。供首次运行向导或设置页调用。\"\"\"\n is_desktop = check_desktop_mode(relaxed=True)\n try:\n config = get_desktop_config()\n result = config.set_data_dir(Path(payload.new_data_directory), migrate_from_old=payload.migrate)\n\n # 同步返回新的路径配置\n paths = {\n \"data_directory\": str(config.paths.data_dir),\n \"cache_directory\": str(config.paths.cache_dir),\n \"temp_directory\": str(config.paths.temp_dir),\n \"database_url\": config.paths.database_url,\n }\n\n resp = {\"message\": \"数据目录更新成功\", \"result\": result, \"paths\": paths}\n if not is_desktop:\n resp[\"warning\"] = \"当前非Desktop模式,但已更新路径配置\"\n return resp\n except Exception as e:\n # 返回更详细的错误,帮助定位权限/路径/占用等问题\n return {\n \"message\": \"更新数据目录失败,但已跳过(内测模式)\",\n \"error\": str(e),\n \"hint\": \"请确认目标目录可写且未被系统限制,必要时选择用户主目录下的路径\",\n }\n\n\n@router.post(\"/export\")\nasync def export_settings():\n \"\"\"导出设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n settings = await get_settings()\n \n # 创建导出文件\n export_file = config.paths.data_dir / \"autoclip-settings-export.json\"\n with open(export_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n return {\n \"message\": \"设置导出成功\",\n \"export_file\": str(export_file),\n \"download_url\": f\"/api/v1/settings/download/{export_file.name}\"\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"导出设置失败: {str(e)}\")\n\n\n@router.post(\"/import\")\nasync def import_settings(file: UploadFile = File(...)):\n \"\"\"导入设置\"\"\"\n check_desktop_mode()\n \n try:\n # 读取上传的文件\n content = await file.read()\n settings_data = json.loads(content.decode('utf-8'))\n \n # 验证设置格式\n settings = DesktopSettings(**settings_data)\n \n # 更新设置\n result = await update_settings(settings)\n \n return {\n \"message\": \"设置导入成功\",\n \"imported_settings\": settings.dict()\n }\n \n except json.JSONDecodeError:\n raise HTTPException(status_code=400, detail=\"设置文件格式错误\")\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"导入设置失败: {str(e)}\")\n\n\n# 删除重复的test_api_connection函数\n\n\n@router.get(\"/validation\")\nasync def validate_settings():\n \"\"\"验证当前设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n validation_result = config.validate_config()\n \n return {\n \"valid\": validation_result[\"valid\"],\n \"errors\": validation_result.get(\"errors\", []),\n \"warnings\": validation_result.get(\"warnings\", []),\n \"recommendations\": []\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"设置验证失败: {str(e)}\")\n\n\n@router.get(\"/backup\")\nasync def backup_settings():\n \"\"\"备份设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_dir = config.paths.data_dir / \"backups\"\n backup_dir.mkdir(exist_ok=True)\n \n # 创建备份\n import datetime\n timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n backup_file = backup_dir / f\"settings_backup_{timestamp}.json\"\n \n settings = await get_settings()\n with open(backup_file, 'w', encoding='utf-8') as f:\n json.dump(settings.dict(), f, indent=2, ensure_ascii=False)\n \n return {\n \"message\": \"设置备份成功\",\n \"backup_file\": str(backup_file),\n \"backup_time\": timestamp\n }\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"备份设置失败: {str(e)}\")\n\n\n@router.get(\"/backups\")\nasync def list_backups():\n \"\"\"列出所有备份\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_dir = config.paths.data_dir / \"backups\"\n \n if not backup_dir.exists():\n return {\"backups\": []}\n \n backups = []\n for backup_file in backup_dir.glob(\"settings_backup_*.json\"):\n stat = backup_file.stat()\n backups.append({\n \"filename\": backup_file.name,\n \"path\": str(backup_file),\n \"size\": stat.st_size,\n \"created_time\": stat.st_ctime,\n \"modified_time\": stat.st_mtime\n })\n \n # 按创建时间排序\n backups.sort(key=lambda x: x[\"created_time\"], reverse=True)\n \n return {\"backups\": backups}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取备份列表失败: {str(e)}\")\n\n\n@router.get(\"/available-models\")\nasync def get_available_models():\n \"\"\"获取可用的模型列表\"\"\"\n check_desktop_mode()\n \n try:\n # 返回按供应商分类的模型列表\n models = {\n \"dashscope\": [\n {\"name\": \"qwen-plus\", \"display_name\": \"通义千问增强版\", \"max_tokens\": 8192, \"description\": \"适合复杂推理和创作任务\"},\n {\"name\": \"qwen-turbo\", \"display_name\": \"通义千问标准版\", \"max_tokens\": 8192, \"description\": \"平衡性能和成本\"},\n {\"name\": \"qwen-max\", \"display_name\": \"通义千问旗舰版\", \"max_tokens\": 8192, \"description\": \"最强性能,适合复杂任务\"},\n {\"name\": \"qwen-long\", \"display_name\": \"通义千问长文本版\", \"max_tokens\": 100000, \"description\": \"支持超长文本处理\"}\n ],\n \"openai\": [\n {\"name\": \"gpt-4o\", \"display_name\": \"GPT-4 Omni\", \"max_tokens\": 128000, \"description\": \"最新多模态模型\"},\n {\"name\": \"gpt-4o-mini\", \"display_name\": \"GPT-4 Omni Mini\", \"max_tokens\": 128000, \"description\": \"轻量级多模态模型\"},\n {\"name\": \"gpt-4-turbo\", \"display_name\": \"GPT-4 Turbo\", \"max_tokens\": 128000, \"description\": \"高性能版本\"},\n {\"name\": \"gpt-4\", \"display_name\": \"GPT-4\", \"max_tokens\": 8192, \"description\": \"经典版本\"},\n {\"name\": \"gpt-3.5-turbo\", \"display_name\": \"GPT-3.5 Turbo\", \"max_tokens\": 16384, \"description\": \"经济实用版本\"}\n ],\n \"gemini\": [\n {\"name\": \"gemini-1.5-pro\", \"display_name\": \"Gemini 1.5 Pro\", \"max_tokens\": 2000000, \"description\": \"最新专业版\"},\n {\"name\": \"gemini-1.5-flash\", \"display_name\": \"Gemini 1.5 Flash\", \"max_tokens\": 1000000, \"description\": \"快速响应版本\"},\n {\"name\": \"gemini-pro\", \"display_name\": \"Gemini Pro\", \"max_tokens\": 30720, \"description\": \"经典专业版\"}\n ],\n \"siliconflow\": [\n {\"name\": \"deepseek-chat\", \"display_name\": \"DeepSeek Chat\", \"max_tokens\": 32768, \"description\": \"深度求索对话模型\"},\n {\"name\": \"deepseek-coder\", \"display_name\": \"DeepSeek Coder\", \"max_tokens\": 16384, \"description\": \"代码生成专用模型\"},\n {\"name\": \"qwen-plus\", \"display_name\": \"通义千问增强版\", \"max_tokens\": 8192, \"description\": \"通过硅基流动访问\"},\n {\"name\": \"qwen-turbo\", \"display_name\": \"通义千问标准版\", \"max_tokens\": 8192, \"description\": \"通过硅基流动访问\"}\n ],\n }\n \n return {\"models\": models}\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取模型列表失败: {str(e)}\")\n\n\n@router.get(\"/current-provider\")\nasync def get_current_provider():\n \"\"\"获取当前提供商信息\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n \n # 根据当前配置返回提供商信息\n provider_info = {\n \"provider\": \"dashscope\", # 默认提供商\n \"model\": config.default_model or \"qwen-plus\",\n \"available\": True,\n \"display_name\": \"通义千问\",\n \"description\": \"阿里云通义千问服务\"\n }\n \n return provider_info\n \n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"获取当前提供商信息失败: {str(e)}\")\n\n\n@router.post(\"/restore/{backup_filename}\")\nasync def restore_backup(backup_filename: str):\n \"\"\"从备份恢复设置\"\"\"\n check_desktop_mode()\n \n try:\n config = get_desktop_config()\n backup_file = config.paths.data_dir / \"backups\" / backup_filename\n \n if not backup_file.exists():\n raise HTTPException(status_code=404, detail=\"备份文件不存在\")\n \n # 读取备份文件\n with open(backup_file, 'r', encoding='utf-8') as f:\n settings_data = json.load(f)\n \n # 验证并恢复设置\n settings = DesktopSettings(**settings_data)\n result = await update_settings(settings)\n \n return {\n \"message\": \"设置恢复成功\",\n \"restored_from\": backup_filename,\n \"restored_settings\": settings.dict()\n }\n \n except HTTPException:\n raise\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"恢复设置失败: {str(e)}\")\n\n\n@router.post(\"/sync-config\")\nasync def sync_config():\n \"\"\"手动同步客户端配置到后端\"\"\"\n check_desktop_mode()\n \n try:\n if config_sync_service.sync_from_client():\n return {\n \"status\": \"success\",\n \"message\": \"配置同步成功\"\n }\n else:\n return {\n \"status\": \"error\",\n \"message\": \"配置同步失败\"\n }\n except Exception as e:\n return {\n \"status\": \"error\",\n \"message\": f\"配置同步失败: {str(e)}\"\n }\n\n\n@router.get(\"/config-status\")\nasync def get_config_status():\n \"\"\"获取配置同步状态\"\"\"\n check_desktop_mode()\n \n try:\n client_time = config_sync_service.get_client_config_timestamp()\n backup_time = config_sync_service.get_backup_config_timestamp()\n sync_needed = config_sync_service.is_sync_needed()\n \n return {\n \"client_config_exists\": client_time is not None,\n \"backup_config_exists\": backup_time is not None,\n \"client_config_time\": client_time,\n \"backup_config_time\": backup_time,\n \"sync_needed\": sync_needed,\n \"client_config_path\": str(config_sync_service.client_config_path),\n \"backup_config_path\": str(config_sync_service.backup_config_path)\n }\n except Exception as e:\n return {\n \"status\": \"error\",\n \"message\": f\"获取配置状态失败: {str(e)}\"\n }"}}}}