import copy import logging from typing import Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.user import User from app.services.auth_service import verify_password, get_password_hash from app.logging_utils import summarize_llm_config logger = logging.getLogger(__name__) async def get_user_settings(user_id: str, db: AsyncSession) -> dict: """获取用户完整设置""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return None return { "profile": user, "llm_config": user.llm_config or {}, "scheduler_config": user.scheduler_config or {} } async def update_user_profile( user_id: str, db: AsyncSession, full_name: Optional[str] = None, password: Optional[str] = None, current_password: Optional[str] = None ) -> User: """更新用户资料""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise ValueError("用户不存在") if password: if not current_password or not verify_password(current_password, user.hashed_password): raise ValueError("当前密码错误") user.hashed_password = get_password_hash(password) if full_name: user.full_name = full_name await db.commit() await db.refresh(user) return user async def update_llm_config(user_id: str, config: dict, db: AsyncSession) -> dict: """更新 LLM 配置""" logger.info("update_llm_config called", extra={"details": {"keys": list(config.keys())}}) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise ValueError("用户不存在") # 创建深拷贝,避免 SQLAlchemy 变更检测问题 current = copy.deepcopy(user.llm_config) or {} logger.info("llm_config before update", extra={"details": summarize_llm_config(current)}) # 合并配置 - 直接替换整个类型配置列表 for key, value in config.items(): if value is not None: if isinstance(value, list): # 列表直接替换 current[key] = value elif isinstance(value, dict): # 字典合并 if key in current and isinstance(current[key], dict): current[key] = {**current[key], **value} else: current[key] = value else: current[key] = value logger.info("llm_config after update", extra={"details": summarize_llm_config(current)}) user.llm_config = current await db.commit() await db.refresh(user) logger.info("user.llm_config after refresh", extra={"details": summarize_llm_config(user.llm_config)}) return current async def update_scheduler_config(user_id: str, config: dict, db: AsyncSession) -> dict: """更新定时任务配置""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise ValueError("用户不存在") current = user.scheduler_config or {} for key, value in config.items(): if value is not None: current[key] = value user.scheduler_config = current await db.commit() return current async def test_llm_connection( provider: str | None, model: str, base_url: str, api_key: str, ) -> dict: """测试 LLM 连接""" try: # base_url-first: provider 可省略 from app.services.llm_service import normalize_provider_name effective_provider = normalize_provider_name({ "provider": provider, "model": model, "base_url": base_url, }) # 根据不同 provider 创建临时 LLM 实例并测试 if effective_provider in {"openai", "custom", "minimax", "kimi", "qwen"}: from langchain_openai import ChatOpenAI llm = ChatOpenAI( api_key=api_key, model=model, base_url=base_url or None, timeout=30, ) elif effective_provider == "claude": from langchain_anthropic import ChatAnthropic llm = ChatAnthropic( api_key=api_key, model=model, timeout=30, ) elif effective_provider == "ollama": from langchain_ollama import ChatOllama llm = ChatOllama( base_url=base_url or "http://localhost:11434", model=model, timeout=30, ) elif effective_provider == "deepseek": from langchain_openai import ChatOpenAI llm = ChatOpenAI( api_key=api_key, model=model, base_url=base_url or "https://api.deepseek.com/v1", timeout=30, ) else: return {"success": False, "error": f"不支持的 endpoint/provider: {effective_provider}"} # 简单测试调用 from langchain_core.messages import HumanMessage response = await llm.ainvoke([HumanMessage(content="Hi")]) return {"success": True, "message": f"连接成功,模型响应: {response.content[:50]}..."} except Exception as e: return {"success": False, "error": str(e)}