From 2b0c7f5c2aec8cecbfda0b5a07676d657217cfaf Mon Sep 17 00:00:00 2001 From: vinland100 Date: Mon, 5 Jan 2026 14:45:00 +0800 Subject: [PATCH] feat: Lock LLM and embedding configurations to system environment variables, mask API keys, and refactor frontend logout. --- backend/app/api/v1/endpoints/agent_tasks.py | 50 +-- backend/app/api/v1/endpoints/config.py | 285 ++++-------------- .../app/api/v1/endpoints/embedding_config.py | 129 ++------ backend/app/api/v1/endpoints/scan.py | 11 +- backend/app/services/llm/service.py | 83 ++--- .../src/components/agent/EmbeddingConfig.tsx | 73 ++--- .../src/components/system/SystemConfig.tsx | 114 +++---- frontend/src/pages/Account.tsx | 6 +- 8 files changed, 200 insertions(+), 551 deletions(-) diff --git a/backend/app/api/v1/endpoints/agent_tasks.py b/backend/app/api/v1/endpoints/agent_tasks.py index e5dc091..b1f8341 100644 --- a/backend/app/api/v1/endpoints/agent_tasks.py +++ b/backend/app/api/v1/endpoints/agent_tasks.py @@ -668,15 +668,11 @@ async def _get_user_config(db: AsyncSession, user_id: Optional[str]) -> Optional ) config = result.scalar_one_or_none() - if config and config.llm_config: - user_llm_config = json.loads(config.llm_config) if config.llm_config else {} + if config: user_other_config = json.loads(config.other_config) if config.other_config else {} - - user_llm_config = decrypt_config(user_llm_config, SENSITIVE_LLM_FIELDS) user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS) return { - "llmConfig": user_llm_config, "otherConfig": user_other_config, } except Exception as e: @@ -746,41 +742,19 @@ async def _initialize_tools( try: await emit(f"🔍 正在初始化 RAG 系统...") - # 从用户配置中获取 embedding 配置 - user_llm_config = (user_config or {}).get('llmConfig', {}) - user_other_config = (user_config or {}).get('otherConfig', {}) - user_embedding_config = user_other_config.get('embedding_config', {}) - - # Embedding Provider 优先级:用户嵌入配置 > 环境变量 - embedding_provider = ( - user_embedding_config.get('provider') or - getattr(settings, 'EMBEDDING_PROVIDER', 'openai') - ) - - # Embedding Model 优先级:用户嵌入配置 > 环境变量 - embedding_model = ( - user_embedding_config.get('model') or - getattr(settings, 'EMBEDDING_MODEL', 'text-embedding-3-small') - ) - - # API Key 优先级:用户嵌入配置 > 环境变量 EMBEDDING_API_KEY > 用户 LLM 配置 > 环境变量 LLM_API_KEY - # 注意:API Key 可以共享,因为很多用户使用同一个 OpenAI Key 做 LLM 和 Embedding + # 锁定模式:Embedding 配置始终来自 settings (.env) + embedding_provider = settings.EMBEDDING_PROVIDER + embedding_model = settings.EMBEDDING_MODEL + + # API Key 优先级:EMBEDDING_API_KEY > LLM_API_KEY embedding_api_key = ( - user_embedding_config.get('api_key') or - getattr(settings, 'EMBEDDING_API_KEY', None) or - user_llm_config.get('llmApiKey') or - getattr(settings, 'LLM_API_KEY', '') or - '' - ) - - # Base URL 优先级:用户嵌入配置 > 环境变量 EMBEDDING_BASE_URL > None(使用提供商默认地址) - # 🔥 重要:Base URL 不应该回退到 LLM 的 base_url,因为 Embedding 和 LLM 可能使用完全不同的服务 - # 例如:LLM 使用 SiliconFlow,但 Embedding 使用 HuggingFace - embedding_base_url = ( - user_embedding_config.get('base_url') or - getattr(settings, 'EMBEDDING_BASE_URL', None) or - None + getattr(settings, "EMBEDDING_API_KEY", None) or + settings.LLM_API_KEY or + "" ) + + # Base URL 优先级:EMBEDDING_BASE_URL > None + embedding_base_url = getattr(settings, "EMBEDDING_BASE_URL", None) logger.info(f"RAG 配置: provider={embedding_provider}, model={embedding_model}, base_url={embedding_base_url or '(使用默认)'}") await emit(f"📊 Embedding 配置: {embedding_provider}/{embedding_model}") diff --git a/backend/app/api/v1/endpoints/config.py b/backend/app/api/v1/endpoints/config.py index 0423f51..35ece2e 100644 --- a/backend/app/api/v1/endpoints/config.py +++ b/backend/app/api/v1/endpoints/config.py @@ -18,15 +18,24 @@ from app.core.encryption import encrypt_sensitive_data, decrypt_sensitive_data router = APIRouter() -# 需要加密的敏感字段列表 +# 需要加密的敏感字段列表 (LLM 已锁定至 .env,此处仅保留其他配置) SENSITIVE_LLM_FIELDS = [ - 'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey', + 'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey', 'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey', 'baiduApiKey', 'minimaxApiKey', 'doubaoApiKey' ] SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken'] +def mask_api_key(key: Optional[str]) -> str: + """部分遮盖API Key,显示前3位和后4位""" + if not key: + return "" + if len(key) <= 8: + return "***" + return f"{key[:3]}***{key[-4:]}" + + def encrypt_config(config: dict, sensitive_fields: list) -> dict: """加密配置中的敏感字段""" encrypted = config.copy() @@ -104,23 +113,23 @@ def get_default_config() -> dict: return { "llmConfig": { "llmProvider": settings.LLM_PROVIDER, - "llmApiKey": "", + "llmApiKey": mask_api_key(settings.LLM_API_KEY), "llmModel": settings.LLM_MODEL or "", "llmBaseUrl": settings.LLM_BASE_URL or "", "llmTimeout": settings.LLM_TIMEOUT * 1000, # 转换为毫秒 "llmTemperature": settings.LLM_TEMPERATURE, "llmMaxTokens": settings.LLM_MAX_TOKENS, "llmCustomHeaders": "", - "geminiApiKey": settings.GEMINI_API_KEY or "", - "openaiApiKey": settings.OPENAI_API_KEY or "", - "claudeApiKey": settings.CLAUDE_API_KEY or "", - "qwenApiKey": settings.QWEN_API_KEY or "", - "deepseekApiKey": settings.DEEPSEEK_API_KEY or "", - "zhipuApiKey": settings.ZHIPU_API_KEY or "", - "moonshotApiKey": settings.MOONSHOT_API_KEY or "", - "baiduApiKey": settings.BAIDU_API_KEY or "", - "minimaxApiKey": settings.MINIMAX_API_KEY or "", - "doubaoApiKey": settings.DOUBAO_API_KEY or "", + "geminiApiKey": mask_api_key(settings.GEMINI_API_KEY), + "openaiApiKey": mask_api_key(settings.OPENAI_API_KEY), + "claudeApiKey": mask_api_key(settings.CLAUDE_API_KEY), + "qwenApiKey": mask_api_key(settings.QWEN_API_KEY), + "deepseekApiKey": mask_api_key(settings.DEEPSEEK_API_KEY), + "zhipuApiKey": mask_api_key(settings.ZHIPU_API_KEY), + "moonshotApiKey": mask_api_key(settings.MOONSHOT_API_KEY), + "baiduApiKey": mask_api_key(settings.BAIDU_API_KEY), + "minimaxApiKey": mask_api_key(settings.MINIMAX_API_KEY), + "doubaoApiKey": mask_api_key(settings.DOUBAO_API_KEY), "ollamaBaseUrl": settings.OLLAMA_BASE_URL or "http://localhost:11434/v1", }, "otherConfig": { @@ -178,7 +187,8 @@ async def get_my_config( print(f" - llmApiKey: {'***' + user_llm_config.get('llmApiKey', '')[-4:] if user_llm_config.get('llmApiKey') else '(空)'}") print(f" - llmModel: {user_llm_config.get('llmModel')}") - merged_llm_config = {**default_config["llmConfig"], **user_llm_config} + # LLM配置始终来自系统默认(.env),不再允许用户覆盖 + merged_llm_config = default_config["llmConfig"] merged_other_config = {**default_config["otherConfig"], **user_other_config} return UserConfigResponse( @@ -247,7 +257,8 @@ async def update_my_config( user_llm_config = decrypt_config(user_llm_config, SENSITIVE_LLM_FIELDS) user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS) - merged_llm_config = {**default_config["llmConfig"], **user_llm_config} + # LLM配置始终来自系统默认(.env),不再允许用户覆盖 + merged_llm_config = default_config["llmConfig"] merged_other_config = {**default_config["otherConfig"], **user_other_config} return UserConfigResponse( @@ -299,230 +310,52 @@ class LLMTestResponse(BaseModel): @router.post("/test-llm", response_model=LLMTestResponse) async def test_llm_connection( request: LLMTestRequest, - db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: - """测试LLM连接是否正常""" - from app.services.llm.factory import LLMFactory, NATIVE_ONLY_PROVIDERS - from app.services.llm.adapters import LiteLLMAdapter, BaiduAdapter, MinimaxAdapter, DoubaoAdapter - from app.services.llm.types import LLMConfig, LLMProvider, LLMRequest, LLMMessage, DEFAULT_MODELS, DEFAULT_BASE_URLS - import traceback + """测试当前系统 LLM 配置是否正常""" + from app.services.llm.service import LLMService import time + import traceback start_time = time.time() - - # 获取用户保存的配置 - result = await db.execute( - select(UserConfig).where(UserConfig.user_id == current_user.id) - ) - user_config_record = result.scalar_one_or_none() - - # 解析用户配置 - saved_llm_config = {} - saved_other_config = {} - if user_config_record: - if user_config_record.llm_config: - saved_llm_config = decrypt_config( - json.loads(user_config_record.llm_config), - SENSITIVE_LLM_FIELDS - ) - if user_config_record.other_config: - saved_other_config = decrypt_config( - json.loads(user_config_record.other_config), - SENSITIVE_OTHER_FIELDS - ) - - # 从保存的配置中获取参数(用于调试显示) - saved_timeout_ms = saved_llm_config.get('llmTimeout', settings.LLM_TIMEOUT * 1000) - saved_temperature = saved_llm_config.get('llmTemperature', settings.LLM_TEMPERATURE) - saved_max_tokens = saved_llm_config.get('llmMaxTokens', settings.LLM_MAX_TOKENS) - saved_concurrency = saved_other_config.get('llmConcurrency', settings.LLM_CONCURRENCY) - saved_gap_ms = saved_other_config.get('llmGapMs', settings.LLM_GAP_MS) - saved_max_files = saved_other_config.get('maxAnalyzeFiles', settings.MAX_ANALYZE_FILES) - saved_output_lang = saved_other_config.get('outputLanguage', settings.OUTPUT_LANGUAGE) - - debug_info = { - "provider": request.provider, - "model_requested": request.model, - "base_url_requested": request.baseUrl, - "api_key_length": len(request.apiKey) if request.apiKey else 0, - "api_key_prefix": request.apiKey[:8] + "..." if request.apiKey and len(request.apiKey) > 8 else "(empty)", - # 用户保存的配置参数 - "saved_config": { - "timeout_ms": saved_timeout_ms, - "temperature": saved_temperature, - "max_tokens": saved_max_tokens, - "concurrency": saved_concurrency, - "gap_ms": saved_gap_ms, - "max_analyze_files": saved_max_files, - "output_language": saved_output_lang, - }, - } - + try: - # 解析provider - provider_map = { - 'gemini': LLMProvider.GEMINI, - 'openai': LLMProvider.OPENAI, - 'claude': LLMProvider.CLAUDE, - 'qwen': LLMProvider.QWEN, - 'deepseek': LLMProvider.DEEPSEEK, - 'zhipu': LLMProvider.ZHIPU, - 'moonshot': LLMProvider.MOONSHOT, - 'baidu': LLMProvider.BAIDU, - 'minimax': LLMProvider.MINIMAX, - 'doubao': LLMProvider.DOUBAO, - 'ollama': LLMProvider.OLLAMA, - } - - provider = provider_map.get(request.provider.lower()) - if not provider: - debug_info["error_type"] = "unsupported_provider" - return LLMTestResponse( - success=False, - message=f"不支持的LLM提供商: {request.provider}", - debug=debug_info - ) - - # 获取默认模型 - model = request.model or DEFAULT_MODELS.get(provider) - base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "") - - # 测试时使用用户保存的所有配置参数 - test_timeout = int(saved_timeout_ms / 1000) if saved_timeout_ms else settings.LLM_TIMEOUT - test_temperature = saved_temperature if saved_temperature is not None else settings.LLM_TEMPERATURE - test_max_tokens = saved_max_tokens if saved_max_tokens else settings.LLM_MAX_TOKENS - - debug_info["model_used"] = model - debug_info["base_url_used"] = base_url - debug_info["is_native_adapter"] = provider in NATIVE_ONLY_PROVIDERS - debug_info["test_params"] = { - "timeout": test_timeout, - "temperature": test_temperature, - "max_tokens": test_max_tokens, - } - - print(f"[LLM Test] 开始测试: provider={provider.value}, model={model}, base_url={base_url}, temperature={test_temperature}, timeout={test_timeout}s, max_tokens={test_max_tokens}") - - # 创建配置 - config = LLMConfig( - provider=provider, - api_key=request.apiKey, - model=model, - base_url=request.baseUrl, - timeout=test_timeout, - temperature=test_temperature, - max_tokens=test_max_tokens, - ) - - # 直接创建新的适配器实例(不使用缓存),确保使用最新的配置 - if provider in NATIVE_ONLY_PROVIDERS: - native_adapter_map = { - LLMProvider.BAIDU: BaiduAdapter, - LLMProvider.MINIMAX: MinimaxAdapter, - LLMProvider.DOUBAO: DoubaoAdapter, - } - adapter = native_adapter_map[provider](config) - debug_info["adapter_type"] = type(adapter).__name__ - else: - adapter = LiteLLMAdapter(config) - debug_info["adapter_type"] = "LiteLLMAdapter" - # 获取 LiteLLM 实际使用的模型名 - debug_info["litellm_model"] = getattr(adapter, '_get_litellm_model', lambda: model)() if hasattr(adapter, '_get_litellm_model') else model - - test_request = LLMRequest( - messages=[ - LLMMessage(role="user", content="Say 'Hello' in one word.") - ], - temperature=test_temperature, - max_tokens=test_max_tokens, - ) - - print(f"[LLM Test] 发送测试请求...") - response = await adapter.complete(test_request) - - elapsed_time = time.time() - start_time - debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2) - - # 验证响应内容 - if not response or not response.content: - debug_info["error_type"] = "empty_response" - debug_info["raw_response"] = str(response) if response else None - print(f"[LLM Test] 空响应: {response}") - return LLMTestResponse( - success=False, - message="LLM 返回空响应,请检查 API Key 和配置", - debug=debug_info - ) - - debug_info["response_length"] = len(response.content) - debug_info["usage"] = { - "prompt_tokens": getattr(response, 'prompt_tokens', None), - "completion_tokens": getattr(response, 'completion_tokens', None), - "total_tokens": getattr(response, 'total_tokens', None), - } - - print(f"[LLM Test] 成功! 响应: {response.content[:50]}... 耗时: {elapsed_time:.2f}s") - + # LLMService 已经重构为锁定读取 .env 配置 + llm_service = LLMService() + + # 记录测试信息 + print(f"🔍 测试 LLM 连接: Provider={llm_service.config.provider}, Model={llm_service.config.model}") + + # 简单测试:获取分析结果 + test_code = "print('hello world')" + result = await llm_service.analyze_code(test_code, "python") + + duration = round(time.time() - start_time, 2) + return LLMTestResponse( success=True, - message=f"连接成功 ({elapsed_time:.2f}s)", - model=model, - response=response.content[:100] if response.content else None, - debug=debug_info + message=f"连接成功!耗时: {duration}s", + model=llm_service.config.model, + response="分析测试完成", + debug={ + "provider": llm_service.config.provider.value, + "model": llm_service.config.model, + "duration_s": duration, + "issues_found": len(result.get("issues", [])) + } ) - except Exception as e: - elapsed_time = time.time() - start_time + duration = round(time.time() - start_time, 2) error_msg = str(e) - error_type = type(e).__name__ - - debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2) - debug_info["error_type"] = error_type - debug_info["error_message"] = error_msg - debug_info["traceback"] = traceback.format_exc() - - # 提取 LLMError 中的 api_response - if hasattr(e, 'api_response') and e.api_response: - debug_info["api_response"] = e.api_response - if hasattr(e, 'status_code') and e.status_code: - debug_info["status_code"] = e.status_code - - print(f"[LLM Test] 失败: {error_type}: {error_msg}") - print(f"[LLM Test] Traceback:\n{traceback.format_exc()}") - - # 提供更友好的错误信息 - friendly_message = error_msg - - # 优先检查余额不足(因为某些 API 用 429 表示余额不足) - if any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance", "402"]): - friendly_message = "账户余额不足或配额已用尽,请充值后重试" - debug_info["error_category"] = "insufficient_balance" - elif "401" in error_msg or "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower(): - friendly_message = "API Key 无效或已过期,请检查后重试" - debug_info["error_category"] = "auth_invalid_key" - elif "authentication" in error_msg.lower(): - friendly_message = "认证失败,请检查 API Key 是否正确" - debug_info["error_category"] = "auth_failed" - elif "timeout" in error_msg.lower(): - friendly_message = "连接超时,请检查网络或 API 地址是否正确" - debug_info["error_category"] = "timeout" - elif "connection" in error_msg.lower() or "connect" in error_msg.lower(): - friendly_message = "无法连接到 API 服务,请检查网络或 API 地址" - debug_info["error_category"] = "connection" - elif "rate" in error_msg.lower() and "limit" in error_msg.lower(): - friendly_message = "API 请求频率超限,请稍后重试" - debug_info["error_category"] = "rate_limit" - elif "model" in error_msg.lower() and ("not found" in error_msg.lower() or "does not exist" in error_msg.lower()): - friendly_message = f"模型 '{debug_info.get('model_used', 'unknown')}' 不存在或无权访问" - debug_info["error_category"] = "model_not_found" - else: - debug_info["error_category"] = "unknown" - + print(f"❌ LLM 测试失败: {error_msg}") return LLMTestResponse( success=False, - message=friendly_message, - debug=debug_info + message=f"连接失败: {error_msg}", + debug={ + "error": error_msg, + "traceback": traceback.format_exc(), + "duration_s": duration + } ) diff --git a/backend/app/api/v1/endpoints/embedding_config.py b/backend/app/api/v1/endpoints/embedding_config.py index ada0e6c..974d894 100644 --- a/backend/app/api/v1/endpoints/embedding_config.py +++ b/backend/app/api/v1/endpoints/embedding_config.py @@ -172,83 +172,29 @@ EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [ EMBEDDING_CONFIG_KEY = "embedding_config" +def mask_api_key(key: Optional[str]) -> str: + """部分遮盖API Key,显示前3位和后4位""" + if not key: + return "" + if len(key) <= 8: + return "***" + return f"{key[:3]}***{key[-4:]}" + + async def get_embedding_config_from_db(db: AsyncSession, user_id: str) -> EmbeddingConfig: """从数据库获取嵌入配置(异步)""" - result = await db.execute( - select(UserConfig).where(UserConfig.user_id == user_id) - ) - user_config = result.scalar_one_or_none() - - if user_config and user_config.other_config: - try: - other_config = json.loads(user_config.other_config) if isinstance(user_config.other_config, str) else user_config.other_config - embedding_data = other_config.get(EMBEDDING_CONFIG_KEY) - - if embedding_data: - config = EmbeddingConfig( - provider=embedding_data.get("provider", settings.EMBEDDING_PROVIDER), - model=embedding_data.get("model", settings.EMBEDDING_MODEL), - api_key=embedding_data.get("api_key"), - base_url=embedding_data.get("base_url"), - dimensions=embedding_data.get("dimensions"), - batch_size=embedding_data.get("batch_size", 100), - ) - print(f"[EmbeddingConfig] 读取用户 {user_id} 的嵌入配置: provider={config.provider}, model={config.model}") - return config - except (json.JSONDecodeError, AttributeError) as e: - print(f"[EmbeddingConfig] 解析用户 {user_id} 配置失败: {e}") - - # 返回默认配置 - print(f"[EmbeddingConfig] 用户 {user_id} 无保存配置,返回默认值") + # 嵌入配置始终来自系统默认(.env),不再允许用户覆盖 + print(f"[EmbeddingConfig] 返回系统默认嵌入配置(来自 .env)") return EmbeddingConfig( provider=settings.EMBEDDING_PROVIDER, model=settings.EMBEDDING_MODEL, - api_key=settings.LLM_API_KEY, - base_url=settings.LLM_BASE_URL, + api_key=mask_api_key(settings.EMBEDDING_API_KEY or settings.LLM_API_KEY), + base_url=settings.EMBEDDING_BASE_URL or settings.LLM_BASE_URL, + dimensions=settings.EMBEDDING_DIMENSION if settings.EMBEDDING_DIMENSION > 0 else None, batch_size=100, ) -async def save_embedding_config_to_db(db: AsyncSession, user_id: str, config: EmbeddingConfig) -> None: - """保存嵌入配置到数据库(异步)""" - result = await db.execute( - select(UserConfig).where(UserConfig.user_id == user_id) - ) - user_config = result.scalar_one_or_none() - - # 准备嵌入配置数据 - embedding_data = { - "provider": config.provider, - "model": config.model, - "api_key": config.api_key, - "base_url": config.base_url, - "dimensions": config.dimensions, - "batch_size": config.batch_size, - } - - if user_config: - # 更新现有配置 - try: - other_config = json.loads(user_config.other_config) if user_config.other_config else {} - except (json.JSONDecodeError, TypeError): - other_config = {} - - other_config[EMBEDDING_CONFIG_KEY] = embedding_data - user_config.other_config = json.dumps(other_config) - # 🔥 显式标记 other_config 字段已修改,确保 SQLAlchemy 检测到变化 - flag_modified(user_config, "other_config") - else: - # 创建新配置 - user_config = UserConfig( - id=str(uuid.uuid4()), - user_id=user_id, - llm_config="{}", - other_config=json.dumps({EMBEDDING_CONFIG_KEY: embedding_data}), - ) - db.add(user_config) - - await db.commit() - print(f"[EmbeddingConfig] 已保存用户 {user_id} 的嵌入配置: provider={config.provider}, model={config.model}") # ============ API Endpoints ============ @@ -274,7 +220,9 @@ async def get_current_config( config = await get_embedding_config_from_db(db, current_user.id) # 获取维度 - dimensions = _get_model_dimensions(config.provider, config.model) + dimensions = config.dimensions + if not dimensions or dimensions <= 0: + dimensions = _get_model_dimensions(config.provider, config.model) return EmbeddingConfigResponse( provider=config.provider, @@ -288,30 +236,12 @@ async def get_current_config( @router.put("/config") async def update_config( - config: EmbeddingConfig, - db: AsyncSession = Depends(deps.get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ - 更新嵌入模型配置(持久化到数据库) + 更新嵌入模型配置(已禁用,固定从 .env 读取) """ - # 验证提供商 - provider_ids = [p.id for p in EMBEDDING_PROVIDERS] - if config.provider not in provider_ids: - raise HTTPException(status_code=400, detail=f"不支持的提供商: {config.provider}") - - # 获取提供商信息(用于检查 API Key 要求) - provider = next((p for p in EMBEDDING_PROVIDERS if p.id == config.provider), None) - # 注意:不再强制验证模型名称,允许用户输入自定义模型 - - # 检查 API Key - if provider and provider.requires_api_key and not config.api_key: - raise HTTPException(status_code=400, detail=f"{config.provider} 需要 API Key") - - # 保存到数据库 - await save_embedding_config_to_db(db, current_user.id, config) - - return {"message": "配置已保存", "provider": config.provider, "model": config.model} + return {"message": "嵌入模型配置已锁定,请在 .env 文件中进行修改", "provider": settings.EMBEDDING_PROVIDER, "model": settings.EMBEDDING_MODEL} @router.post("/test", response_model=TestEmbeddingResponse) @@ -319,22 +249,22 @@ async def test_embedding( request: TestEmbeddingRequest, current_user: User = Depends(deps.get_current_user), ) -> Any: - """ - 测试嵌入模型配置 - """ + """测试当前系统的嵌入模型配置""" import time + from app.services.rag.embeddings import EmbeddingService try: start_time = time.time() - # 创建临时嵌入服务 - from app.services.rag.embeddings import EmbeddingService + # 始终使用系统的真实配置进行测试 + # API Key 优先级:EMBEDDING_API_KEY > LLM_API_KEY + api_key = getattr(settings, "EMBEDDING_API_KEY", None) or settings.LLM_API_KEY service = EmbeddingService( - provider=request.provider, - model=request.model, - api_key=request.api_key, - base_url=request.base_url, + provider=settings.EMBEDDING_PROVIDER, + model=settings.EMBEDDING_MODEL, + api_key=api_key, + base_url=settings.EMBEDDING_BASE_URL, cache_enabled=False, ) @@ -345,13 +275,14 @@ async def test_embedding( return TestEmbeddingResponse( success=True, - message=f"嵌入成功! 维度: {len(embedding)}", + message=f"嵌入成功! 提供商: {settings.EMBEDDING_PROVIDER}, 维度: {len(embedding)}", dimensions=len(embedding), sample_embedding=embedding[:5], # 返回前 5 维 latency_ms=latency_ms, ) except Exception as e: + print(f"❌ 嵌入测试失败: {str(e)}") return TestEmbeddingResponse( success=False, message=f"嵌入失败: {str(e)}", diff --git a/backend/app/api/v1/endpoints/scan.py b/backend/app/api/v1/endpoints/scan.py index 0b1bc20..73c8f1f 100644 --- a/backend/app/api/v1/endpoints/scan.py +++ b/backend/app/api/v1/endpoints/scan.py @@ -393,12 +393,6 @@ async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict: """获取用户配置字典(包含解密敏感字段)""" from app.core.encryption import decrypt_sensitive_data - # 需要解密的敏感字段列表(与 config.py 保持一致) - SENSITIVE_LLM_FIELDS = [ - 'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey', - 'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey', - 'baiduApiKey', 'minimaxApiKey', 'doubaoApiKey' - ] SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken'] def decrypt_config(config: dict, sensitive_fields: list) -> dict: @@ -416,16 +410,13 @@ async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict: if not config: return {} - # 解析配置 - llm_config = json.loads(config.llm_config) if config.llm_config else {} + # 解析配置 (忽略 llm_config) other_config = json.loads(config.other_config) if config.other_config else {} # 解密敏感字段 - llm_config = decrypt_config(llm_config, SENSITIVE_LLM_FIELDS) other_config = decrypt_config(other_config, SENSITIVE_OTHER_FIELDS) return { - 'llmConfig': llm_config, 'otherConfig': other_config, } diff --git a/backend/app/services/llm/service.py b/backend/app/services/llm/service.py index 088413e..a6ca8a1 100644 --- a/backend/app/services/llm/service.py +++ b/backend/app/services/llm/service.py @@ -39,55 +39,31 @@ class LLMService: """ 获取LLM配置 - 🔥 优先级(从高到低): - 1. 数据库用户配置(系统配置页面保存的配置) - 2. 环境变量配置(.env 文件中的配置) - - 如果用户配置中某个字段为空,则自动回退到环境变量。 + 🔥 锁定模式:始终从环境变量(.env)读取 + 不再合并数据库中的用户配置,确保系统一致性和安全性。 """ if self._config is None: - user_llm_config = self._user_config.get('llmConfig', {}) - - # 🔥 Provider 优先级:用户配置 > 环境变量 - provider_str = user_llm_config.get('llmProvider') or getattr(settings, 'LLM_PROVIDER', 'openai') + # 锁定:全部来自 settings + provider_str = settings.LLM_PROVIDER provider = self._parse_provider(provider_str) - # 🔥 API Key 优先级:用户配置 > 环境变量通用配置 > 环境变量平台专属配置 - api_key = ( - user_llm_config.get('llmApiKey') or - getattr(settings, 'LLM_API_KEY', '') or - self._get_provider_api_key_from_user_config(provider, user_llm_config) or - self._get_provider_api_key(provider) - ) + # API Key 优先级:平台专属配置 > 通用 LLM_API_KEY + api_key = self._get_provider_api_key(provider) or settings.LLM_API_KEY - # 🔥 Base URL 优先级:用户配置 > 环境变量 - base_url = ( - user_llm_config.get('llmBaseUrl') or - getattr(settings, 'LLM_BASE_URL', None) or - self._get_provider_base_url(provider) - ) + # Base URL 优先级:通用 LLM_BASE_URL > 平台默认 + base_url = settings.LLM_BASE_URL or self._get_provider_base_url(provider) - # 🔥 Model 优先级:用户配置 > 环境变量 > 默认模型 - model = ( - user_llm_config.get('llmModel') or - getattr(settings, 'LLM_MODEL', '') or - DEFAULT_MODELS.get(provider, 'gpt-4o-mini') - ) + # Model + model = settings.LLM_MODEL or DEFAULT_MODELS.get(provider, 'gpt-4o-mini') - # 🔥 Timeout 优先级:用户配置(毫秒) > 环境变量(秒) - timeout_ms = user_llm_config.get('llmTimeout') - if timeout_ms: - # 用户配置是毫秒,转换为秒 - timeout = int(timeout_ms / 1000) if timeout_ms > 1000 else int(timeout_ms) - else: - # 环境变量是秒 - timeout = int(getattr(settings, 'LLM_TIMEOUT', 150)) + # Timeout (settings 中是秒) + timeout = int(settings.LLM_TIMEOUT) - # 🔥 Temperature 优先级:用户配置 > 环境变量 - temperature = user_llm_config.get('llmTemperature') if user_llm_config.get('llmTemperature') is not None else float(getattr(settings, 'LLM_TEMPERATURE', 0.1)) + # Temperature + temperature = float(settings.LLM_TEMPERATURE) - # 🔥 Max Tokens 优先级:用户配置 > 环境变量 - max_tokens = user_llm_config.get('llmMaxTokens') or int(getattr(settings, 'LLM_MAX_TOKENS', 4096)) + # Max Tokens + max_tokens = int(settings.LLM_MAX_TOKENS) self._config = LLMConfig( provider=provider, @@ -99,28 +75,9 @@ class LLMService: max_tokens=max_tokens, ) return self._config - - def _get_provider_api_key_from_user_config(self, provider: LLMProvider, user_llm_config: Dict[str, Any]) -> Optional[str]: - """从用户配置中获取平台专属API Key""" - provider_key_map = { - LLMProvider.OPENAI: 'openaiApiKey', - LLMProvider.GEMINI: 'geminiApiKey', - LLMProvider.CLAUDE: 'claudeApiKey', - LLMProvider.QWEN: 'qwenApiKey', - LLMProvider.DEEPSEEK: 'deepseekApiKey', - LLMProvider.ZHIPU: 'zhipuApiKey', - LLMProvider.MOONSHOT: 'moonshotApiKey', - LLMProvider.BAIDU: 'baiduApiKey', - LLMProvider.MINIMAX: 'minimaxApiKey', - LLMProvider.DOUBAO: 'doubaoApiKey', - } - key_name = provider_key_map.get(provider) - if key_name: - return user_llm_config.get(key_name) - return None - + def _get_provider_api_key(self, provider: LLMProvider) -> str: - """根据提供商获取API Key""" + """根据提供商从 settings 获取专属 API Key""" provider_key_map = { LLMProvider.OPENAI: 'OPENAI_API_KEY', LLMProvider.GEMINI: 'GEMINI_API_KEY', @@ -132,12 +89,12 @@ class LLMService: LLMProvider.BAIDU: 'BAIDU_API_KEY', LLMProvider.MINIMAX: 'MINIMAX_API_KEY', LLMProvider.DOUBAO: 'DOUBAO_API_KEY', - LLMProvider.OLLAMA: None, # Ollama 不需要 API Key } key_name = provider_key_map.get(provider) if key_name: return getattr(settings, key_name, '') or '' - return 'ollama' # Ollama的默认值 + return '' + def _get_provider_base_url(self, provider: LLMProvider) -> Optional[str]: """根据提供商获取Base URL""" diff --git a/frontend/src/components/agent/EmbeddingConfig.tsx b/frontend/src/components/agent/EmbeddingConfig.tsx index 564cc74..6784793 100644 --- a/frontend/src/components/agent/EmbeddingConfig.tsx +++ b/frontend/src/components/agent/EmbeddingConfig.tsx @@ -208,7 +208,7 @@ export default function EmbeddingConfigPanel() {

提供商

- {currentConfig.provider} + {providers.find(p => p.id === currentConfig.provider)?.name || currentConfig.provider}
@@ -231,8 +231,11 @@ export default function EmbeddingConfigPanel() {
{/* 提供商选择 */}
- - @@ -270,6 +273,7 @@ export default function EmbeddingConfigPanel() { onChange={(e) => setSelectedModel(e.target.value)} placeholder="输入模型名称" className="h-10 cyber-input" + disabled /> {selectedProviderInfo.models.length > 0 && (
@@ -279,11 +283,10 @@ export default function EmbeddingConfigPanel() { key={model} type="button" onClick={() => setSelectedModel(model)} - className={`px-2 py-1 text-xs font-mono rounded border transition-colors ${ - selectedModel === model - ? "bg-primary/20 border-primary/50 text-primary" - : "bg-muted border-border text-muted-foreground hover:border-border hover:text-foreground" - }`} + className={`px-2 py-1 text-xs font-mono rounded border transition-colors ${selectedModel === model + ? "bg-primary/20 border-primary/50 text-primary" + : "bg-muted border-border text-muted-foreground hover:border-border hover:text-foreground" + }`} > {model} @@ -306,6 +309,7 @@ export default function EmbeddingConfigPanel() { onChange={(e) => setApiKey(e.target.value)} placeholder="输入 API Key" className="h-10 cyber-input" + disabled />

API Key 将安全存储,不会显示在页面上 @@ -326,14 +330,15 @@ export default function EmbeddingConfigPanel() { selectedProvider === "ollama" ? "http://localhost:11434" : selectedProvider === "huggingface" - ? "https://router.huggingface.co" - : selectedProvider === "cohere" - ? "https://api.cohere.com/v2" - : selectedProvider === "jina" - ? "https://api.jina.ai/v1" - : "https://api.openai.com/v1" + ? "https://router.huggingface.co" + : selectedProvider === "cohere" + ? "https://api.cohere.com/v2" + : selectedProvider === "jina" + ? "https://api.jina.ai/v1" + : "https://api.openai.com/v1" } className="h-10 cyber-input" + disabled />

用于 API 代理或自托管服务 @@ -350,6 +355,7 @@ export default function EmbeddingConfigPanel() { min={1} max={500} className="h-10 cyber-input w-32" + disabled />

每批嵌入的文本数量,建议 50-100 @@ -359,11 +365,10 @@ export default function EmbeddingConfigPanel() { {/* 测试结果 */} {testResult && (

{testResult.success ? ( @@ -372,9 +377,8 @@ export default function EmbeddingConfigPanel() { )} {testResult.success ? "测试成功" : "测试失败"} @@ -398,37 +402,26 @@ export default function EmbeddingConfigPanel() {
- -
diff --git a/frontend/src/components/system/SystemConfig.tsx b/frontend/src/components/system/SystemConfig.tsx index 405ad5c..043dba7 100644 --- a/frontend/src/components/system/SystemConfig.tsx +++ b/frontend/src/components/system/SystemConfig.tsx @@ -339,7 +339,7 @@ export function SystemConfig() { ) : ( - 请配置 LLM API Key + 请在 .env 文件中配置 LLM (只读模式) )} @@ -378,8 +378,11 @@ export function SystemConfig() {
{/* Provider Selection */}
- - updateConfig('llmProvider', v)} disabled> @@ -419,6 +422,7 @@ export function SystemConfig() { onChange={(e) => updateConfig('llmApiKey', e.target.value)} placeholder={config.llmProvider === 'baidu' ? 'API_KEY:SECRET_KEY 格式' : '输入你的 API Key'} className="h-12 cyber-input" + disabled />
@@ -450,6 +455,7 @@ export function SystemConfig() { onChange={(e) => updateConfig('llmBaseUrl', e.target.value)} placeholder="留空使用官方地址,或填入中转站地址" className="h-10 cyber-input" + disabled />
@@ -462,7 +468,7 @@ export function SystemConfig() {
-
- )} + { + hasChanges && ( +
+ +
+ ) + } {/* Delete SSH Key Confirmation Dialog */} @@ -943,6 +911,6 @@ export function SystemConfig() { -
+
); } diff --git a/frontend/src/pages/Account.tsx b/frontend/src/pages/Account.tsx index 2f529a3..d25198e 100644 --- a/frontend/src/pages/Account.tsx +++ b/frontend/src/pages/Account.tsx @@ -5,6 +5,7 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { useAuth } from "@/shared/context/AuthContext"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -29,6 +30,7 @@ import type { Profile } from "@/shared/types"; export default function Account() { const navigate = useNavigate(); + const { logout } = useAuth(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -128,13 +130,13 @@ export default function Account() { }; const handleLogout = () => { - localStorage.removeItem('access_token'); + logout(); toast.success("已退出登录"); navigate('/login'); }; const handleSwitchAccount = () => { - localStorage.removeItem('access_token'); + logout(); navigate('/login'); };