""" Bot Manager API 路由 提供模型配置、Bot 管理、设置管理、MCP 服务器等功能的 API """ import logging import uuid import hashlib import secrets from datetime import datetime, timedelta from typing import Optional, List from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel from agent.db_pool_manager import get_db_pool_manager from utils.fastapi_utils import extract_api_key_from_auth logger = logging.getLogger('app') router = APIRouter() # ============== Admin 配置 ============== ADMIN_USERNAME = "admin" ADMIN_PASSWORD = "Admin123" # 生产环境应使用环境变量 TOKEN_EXPIRE_HOURS = 24 # ============== 认证函数 ============== async def verify_admin_auth(authorization: Optional[str]) -> tuple[bool, Optional[str]]: """ 验证管理员认证 Args: authorization: Authorization header 值 Returns: tuple[bool, Optional[str]]: (是否有效, 用户名) """ provided_token = extract_api_key_from_auth(authorization) if not provided_token: return False, None pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: # 检查 token 是否有效且未过期 await cursor.execute(""" SELECT username, expires_at FROM agent_admin_tokens WHERE token = %s AND expires_at > NOW() """, (provided_token,)) row = await cursor.fetchone() if not row: return False, None return True, row[0] def verify_auth(authorization: Optional[str]) -> None: """ 验证请求认证 Args: authorization: Authorization header 值 Raises: HTTPException: 认证失败时抛出 401 错误 """ provided_token = extract_api_key_from_auth(authorization) if not provided_token: raise HTTPException( status_code=401, detail="Authorization header is required" ) # ============== Pydantic Models ============== # --- Admin 登录相关 --- class AdminLoginRequest(BaseModel): """管理员登录请求""" username: str password: str class AdminLoginResponse(BaseModel): """管理员登录响应""" token: str username: str expires_at: str class AdminVerifyResponse(BaseModel): """管理员验证响应""" valid: bool username: Optional[str] = None # --- 模型相关 --- class ModelCreate(BaseModel): """创建模型请求""" name: str provider: str model: str server: Optional[str] = None api_key: Optional[str] = None is_default: bool = False class ModelUpdate(BaseModel): """更新模型请求""" name: Optional[str] = None provider: Optional[str] = None model: Optional[str] = None server: Optional[str] = None api_key: Optional[str] = None is_default: Optional[bool] = None class ModelResponse(BaseModel): """模型响应""" id: str name: str provider: str model: str server: Optional[str] api_key: Optional[str] # 掩码显示 is_default: bool created_at: str updated_at: str # --- Bot 相关 --- class BotCreate(BaseModel): """创建 Bot 请求""" name: str class BotUpdate(BaseModel): """更新 Bot 请求""" name: Optional[str] = None bot_id: Optional[str] = None class BotResponse(BaseModel): """Bot 响应""" id: str name: str bot_id: str created_at: str updated_at: str # --- Bot 设置相关 --- class BotSettingsUpdate(BaseModel): """更新 Bot 设置请求""" model_id: Optional[str] = None language: Optional[str] = None robot_type: Optional[str] = None dataset_ids: Optional[str] = None system_prompt: Optional[str] = None user_identifier: Optional[str] = None enable_memori: Optional[bool] = None tool_response: Optional[bool] = None skills: Optional[str] = None class ModelInfo(BaseModel): """模型信息""" id: str name: str provider: str model: str server: Optional[str] api_key: Optional[str] # 掩码显示 class BotSettingsResponse(BaseModel): """Bot 设置响应""" bot_id: str model_id: Optional[str] model: Optional[ModelInfo] # 关联的模型信息 language: str robot_type: Optional[str] dataset_ids: Optional[str] system_prompt: Optional[str] user_identifier: Optional[str] enable_memori: bool tool_response: bool skills: Optional[str] updated_at: str # --- 会话相关 --- class SessionCreate(BaseModel): """创建会话请求""" title: Optional[str] = None class SessionResponse(BaseModel): """会话响应""" id: str bot_id: str title: Optional[str] created_at: str updated_at: str # --- MCP 相关 --- class MCPServerCreate(BaseModel): """创建 MCP 服务器请求""" name: str type: str config: dict enabled: bool = True class MCPServerUpdate(BaseModel): """更新 MCP 服务器请求""" name: Optional[str] = None type: Optional[str] = None config: Optional[dict] = None enabled: Optional[bool] = None class MCPServerResponse(BaseModel): """MCP 服务器响应""" id: str bot_id: str name: str type: str config: dict enabled: bool created_at: str updated_at: str # --- 通用响应 --- class SuccessResponse(BaseModel): """通用成功响应""" success: bool message: str # ============== 数据库表初始化 ============== async def init_bot_manager_tables(): """ 初始化 Bot Manager 相关的所有数据库表 """ pool = get_db_pool_manager().pool # SQL 表创建语句 tables_sql = [ # admin_tokens 表(用于存储登录 token) """ CREATE TABLE IF NOT EXISTS agent_admin_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(255) NOT NULL, token VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) """, # admin_tokens 索引 "CREATE INDEX IF NOT EXISTS idx_agent_admin_tokens_token ON agent_admin_tokens(token)", "CREATE INDEX IF NOT EXISTS idx_agent_admin_tokens_expires ON agent_admin_tokens(expires_at)", # models 表 """ CREATE TABLE IF NOT EXISTS agent_models ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, provider VARCHAR(100) NOT NULL, model VARCHAR(255) NOT NULL, server VARCHAR(500), api_key VARCHAR(500), is_default BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) """, # models 索引 "CREATE INDEX IF NOT EXISTS idx_agent_models_is_default ON agent_models(is_default)", # bots 表 """ CREATE TABLE IF NOT EXISTS agent_bots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, bot_id VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) """, # bots 索引 "CREATE INDEX IF NOT EXISTS idx_agent_bots_bot_id ON agent_bots(bot_id)", # bot_settings 表 """ CREATE TABLE IF NOT EXISTS agent_bot_settings ( bot_id UUID PRIMARY KEY REFERENCES agent_bots(id) ON DELETE CASCADE, model_id UUID REFERENCES agent_models(id) ON DELETE SET NULL, language VARCHAR(10) DEFAULT 'zh', robot_type VARCHAR(50), dataset_ids TEXT, system_prompt TEXT, user_identifier VARCHAR(255), enable_memori BOOLEAN DEFAULT FALSE, tool_response BOOLEAN DEFAULT FALSE, skills TEXT, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) """, # mcp_servers 表 """ CREATE TABLE IF NOT EXISTS agent_mcp_servers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), bot_id UUID REFERENCES agent_bots(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, type VARCHAR(50) NOT NULL, config JSONB NOT NULL, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) """, # mcp_servers 索引 "CREATE INDEX IF NOT EXISTS idx_agent_mcp_servers_bot_id ON agent_mcp_servers(bot_id)", "CREATE INDEX IF NOT EXISTS idx_agent_mcp_servers_enabled ON agent_mcp_servers(enabled)", # chat_sessions 表 """ CREATE TABLE IF NOT EXISTS agent_chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), bot_id UUID REFERENCES agent_bots(id) ON DELETE CASCADE, title VARCHAR(500), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) """, # chat_sessions 索引 "CREATE INDEX IF NOT EXISTS idx_agent_chat_sessions_bot_id ON agent_chat_sessions(bot_id)", "CREATE INDEX IF NOT EXISTS idx_agent_chat_sessions_created ON agent_chat_sessions(created_at DESC)", ] async with pool.connection() as conn: async with conn.cursor() as cursor: for sql in tables_sql: await cursor.execute(sql) await conn.commit() logger.info("Bot Manager tables initialized successfully") # ============== 辅助函数 ============== def mask_api_key(api_key: Optional[str]) -> Optional[str]: """对 API Key 进行掩码处理""" if not api_key: return None if len(api_key) <= 8: return "****" return api_key[:4] + "****" + api_key[-4:] def datetime_to_str(dt: datetime) -> str: """将 datetime 转换为 ISO 格式字符串""" return dt.isoformat() if dt else "" # ============== 模型管理 API ============== @router.get("/api/v1/models", response_model=List[ModelResponse]) async def get_models(authorization: Optional[str] = Header(None)): """ 获取所有模型配置 Args: authorization: Bearer token Returns: List[ModelResponse]: 模型列表 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" SELECT id, name, provider, model, server, api_key, is_default, created_at, updated_at FROM agent_models ORDER BY is_default DESC, created_at DESC """) rows = await cursor.fetchall() return [ ModelResponse( id=str(row[0]), name=row[1], provider=row[2], model=row[3], server=row[4], api_key=mask_api_key(row[5]), is_default=row[6], created_at=datetime_to_str(row[7]), updated_at=datetime_to_str(row[8]) ) for row in rows ] @router.post("/api/v1/models", response_model=ModelResponse) async def create_model(request: ModelCreate, authorization: Optional[str] = Header(None)): """ 创建新模型 Args: request: 模型创建请求 authorization: Bearer token Returns: ModelResponse: 创建的模型信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 如果设置为默认,需要先取消其他默认模型 if request.is_default: async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("UPDATE agent_models SET is_default = FALSE WHERE is_default = TRUE") await conn.commit() async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" INSERT INTO agent_models (name, provider, model, server, api_key, is_default) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id, created_at, updated_at """, ( request.name, request.provider, request.model, request.server, request.api_key, request.is_default )) row = await cursor.fetchone() await conn.commit() return ModelResponse( id=str(row[0]), name=request.name, provider=request.provider, model=request.model, server=request.server, api_key=mask_api_key(request.api_key), is_default=request.is_default, created_at=datetime_to_str(row[1]), updated_at=datetime_to_str(row[2]) ) @router.put("/api/v1/models/{model_id}", response_model=ModelResponse) async def update_model( model_id: str, request: ModelUpdate, authorization: Optional[str] = Header(None) ): """ 更新模型 Args: model_id: 模型 ID request: 模型更新请求 authorization: Bearer token Returns: ModelResponse: 更新后的模型信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 构建更新字段 update_fields = [] values = [] if request.name is not None: update_fields.append("name = %s") values.append(request.name) if request.provider is not None: update_fields.append("provider = %s") values.append(request.provider) if request.model is not None: update_fields.append("model = %s") values.append(request.model) if request.server is not None: update_fields.append("server = %s") values.append(request.server) if request.api_key is not None: update_fields.append("api_key = %s") values.append(request.api_key) if request.is_default is not None: update_fields.append("is_default = %s") values.append(request.is_default) if not update_fields: raise HTTPException(status_code=400, detail="No fields to update") update_fields.append("updated_at = NOW()") values.append(model_id) # 如果设置为默认,需要先取消其他默认模型 if request.is_default is True: async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("UPDATE agent_models SET is_default = FALSE WHERE is_default = TRUE") await conn.commit() async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(f""" UPDATE agent_models SET {', '.join(update_fields)} WHERE id = %s RETURNING id, name, provider, model, server, api_key, is_default, created_at, updated_at """, values) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Model not found") await conn.commit() return ModelResponse( id=str(row[0]), name=row[1], provider=row[2], model=row[3], server=row[4], api_key=mask_api_key(row[5]), is_default=row[6], created_at=datetime_to_str(row[7]), updated_at=datetime_to_str(row[8]) ) @router.delete("/api/v1/models/{model_id}", response_model=SuccessResponse) async def delete_model(model_id: str, authorization: Optional[str] = Header(None)): """ 删除模型 Args: model_id: 模型 ID authorization: Bearer token Returns: SuccessResponse: 删除结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("DELETE FROM agent_models WHERE id = %s RETURNING id", (model_id,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Model not found") await conn.commit() return SuccessResponse(success=True, message="Model deleted successfully") @router.patch("/api/v1/models/{model_id}/default", response_model=SuccessResponse) async def set_default_model(model_id: str, authorization: Optional[str] = Header(None)): """ 设置默认模型 Args: model_id: 模型 ID authorization: Bearer token Returns: SuccessResponse: 设置结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 首先检查模型是否存在 async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("SELECT id FROM agent_models WHERE id = %s", (model_id,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Model not found") # 取消所有默认设置 await cursor.execute("UPDATE agent_models SET is_default = FALSE WHERE is_default = TRUE") # 设置新的默认模型 await cursor.execute("UPDATE agent_models SET is_default = TRUE WHERE id = %s", (model_id,)) await conn.commit() return SuccessResponse(success=True, message="Default model updated successfully") # ============== Bot 管理 API ============== @router.get("/api/v1/bots", response_model=List[BotResponse]) async def get_bots(authorization: Optional[str] = Header(None)): """ 获取所有 Bot Args: authorization: Bearer token Returns: List[BotResponse]: Bot 列表 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" SELECT id, name, bot_id, created_at, updated_at FROM agent_bots ORDER BY created_at DESC """) rows = await cursor.fetchall() return [ BotResponse( id=str(row[0]), name=row[1], bot_id=row[2], created_at=datetime_to_str(row[3]), updated_at=datetime_to_str(row[4]) ) for row in rows ] @router.post("/api/v1/bots", response_model=BotResponse) async def create_bot(request: BotCreate, authorization: Optional[str] = Header(None)): """ 创建新 Bot Args: request: Bot 创建请求 authorization: Bearer token Returns: BotResponse: 创建的 Bot 信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 自动生成 bot_id bot_id = str(uuid.uuid4()) try: async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" INSERT INTO agent_bots (name, bot_id) VALUES (%s, %s) RETURNING id, created_at, updated_at """, (request.name, bot_id)) row = await cursor.fetchone() # 创建对应的设置记录 await cursor.execute(""" INSERT INTO agent_bot_settings (bot_id, language) VALUES (%s, 'zh') """, (str(row[0]),)) await conn.commit() return BotResponse( id=str(row[0]), name=request.name, bot_id=bot_id, created_at=datetime_to_str(row[1]), updated_at=datetime_to_str(row[2]) ) except Exception as e: if "duplicate key" in str(e): raise HTTPException(status_code=400, detail="Bot ID already exists") raise @router.put("/api/v1/bots/{bot_uuid}", response_model=BotResponse) async def update_bot( bot_uuid: str, request: BotUpdate, authorization: Optional[str] = Header(None) ): """ 更新 Bot Args: bot_uuid: Bot 内部 UUID request: Bot 更新请求 authorization: Bearer token Returns: BotResponse: 更新后的 Bot 信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 构建更新字段 update_fields = [] values = [] if request.name is not None: update_fields.append("name = %s") values.append(request.name) if request.bot_id is not None: update_fields.append("bot_id = %s") values.append(request.bot_id) if not update_fields: raise HTTPException(status_code=400, detail="No fields to update") update_fields.append("updated_at = NOW()") values.append(bot_uuid) async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(f""" UPDATE agent_bots SET {', '.join(update_fields)} WHERE id = %s RETURNING id, name, bot_id, created_at, updated_at """, values) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Bot not found") await conn.commit() return BotResponse( id=str(row[0]), name=row[1], bot_id=row[2], created_at=datetime_to_str(row[3]), updated_at=datetime_to_str(row[4]) ) @router.delete("/api/v1/bots/{bot_uuid}", response_model=SuccessResponse) async def delete_bot(bot_uuid: str, authorization: Optional[str] = Header(None)): """ 删除 Bot(级联删除相关设置、会话、MCP 配置等) Args: bot_uuid: Bot 内部 UUID authorization: Bearer token Returns: SuccessResponse: 删除结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("DELETE FROM agent_bots WHERE id = %s RETURNING id", (bot_uuid,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Bot not found") await conn.commit() return SuccessResponse(success=True, message="Bot deleted successfully") # ============== Bot 设置 API ============== @router.get("/api/v1/bots/{bot_uuid}/settings", response_model=BotSettingsResponse) async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(None)): """ 获取 Bot 设置 Args: bot_uuid: Bot 内部 UUID authorization: Bearer token Returns: BotSettingsResponse: Bot 设置信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" SELECT bot_id, model_id, language, robot_type, dataset_ids, system_prompt, user_identifier, enable_memori, tool_response, skills, updated_at FROM agent_bot_settings WHERE bot_id = %s """, (bot_uuid,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Bot settings not found") # 获取关联的模型信息 model_info = None model_id = row[1] if model_id: await cursor.execute(""" SELECT id, name, provider, model, server, api_key FROM agent_models WHERE id = %s """, (model_id,)) model_row = await cursor.fetchone() if model_row: model_info = ModelInfo( id=str(model_row[0]), name=model_row[1], provider=model_row[2], model=model_row[3], server=model_row[4], api_key=mask_api_key(model_row[5]) ) return BotSettingsResponse( bot_id=str(row[0]), model_id=str(model_id) if model_id else None, model=model_info, language=row[2] or 'zh', robot_type=row[3], dataset_ids=row[4], system_prompt=row[5], user_identifier=row[6], enable_memori=row[7] or False, tool_response=row[8] or False, skills=row[9], updated_at=datetime_to_str(row[10]) ) @router.put("/api/v1/bots/{bot_uuid}/settings", response_model=SuccessResponse) async def update_bot_settings( bot_uuid: str, request: BotSettingsUpdate, authorization: Optional[str] = Header(None) ): """ 更新 Bot 设置 Args: bot_uuid: Bot 内部 UUID request: 设置更新请求 authorization: Bearer token Returns: SuccessResponse: 更新结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 构建更新字段 update_fields = [] values = [] # 处理 model_id:将空字符串转换为 None model_id_value = request.model_id.strip() if request.model_id else None if request.model_id is not None: # 验证 model_id 是否存在 if model_id_value: async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("SELECT id FROM agent_models WHERE id = %s", (model_id_value,)) if not await cursor.fetchone(): raise HTTPException(status_code=400, detail=f"Model with id '{request.model_id}' not found") # 使用 NULL 或占位符 if model_id_value: update_fields.append("model_id = %s") values.append(model_id_value) else: update_fields.append("model_id = NULL") if request.language is not None: update_fields.append("language = %s") values.append(request.language) if request.robot_type is not None: update_fields.append("robot_type = %s") values.append(request.robot_type) if request.dataset_ids is not None: update_fields.append("dataset_ids = %s") values.append(request.dataset_ids) if request.system_prompt is not None: update_fields.append("system_prompt = %s") values.append(request.system_prompt) if request.user_identifier is not None: update_fields.append("user_identifier = %s") values.append(request.user_identifier) if request.enable_memori is not None: update_fields.append("enable_memori = %s") values.append(request.enable_memori) if request.tool_response is not None: update_fields.append("tool_response = %s") values.append(request.tool_response) if request.skills is not None: update_fields.append("skills = %s") values.append(request.skills) if not update_fields: raise HTTPException(status_code=400, detail="No fields to update") update_fields.append("updated_at = NOW()") values.append(bot_uuid) async with pool.connection() as conn: async with conn.cursor() as cursor: # 检查设置是否存在 await cursor.execute("SELECT bot_id FROM agent_bot_settings WHERE bot_id = %s", (bot_uuid,)) if not await cursor.fetchone(): # 不存在则创建 # 需要分别处理带占位符和不带占位符的字段 insert_fields = [] insert_placeholders = [] insert_values = [] for field in update_fields[:-1]: # 去掉 updated_at if "= NULL" in field: # 不带占位符的 NULL 值 insert_fields.append(field.split(" = ")[0]) insert_placeholders.append("NULL") elif "= %s" in field: # 带占位符的字段 insert_fields.append(field.split(" = ")[0]) insert_placeholders.append("%s") # 从 values 中取值(需要追踪索引) # 这里简化处理:重新解析 # 重新构建:遍历原始请求字段 if model_id_value is not None: insert_fields.append("model_id") if model_id_value: insert_placeholders.append("%s") insert_values.append(model_id_value) else: insert_placeholders.append("NULL") if request.language is not None: insert_fields.append("language") insert_placeholders.append("%s") insert_values.append(request.language) else: insert_fields.append("language") insert_placeholders.append("'zh'") if request.robot_type is not None: insert_fields.append("robot_type") insert_placeholders.append("%s") insert_values.append(request.robot_type) if request.dataset_ids is not None: insert_fields.append("dataset_ids") insert_placeholders.append("%s") insert_values.append(request.dataset_ids) if request.system_prompt is not None: insert_fields.append("system_prompt") insert_placeholders.append("%s") insert_values.append(request.system_prompt) if request.user_identifier is not None: insert_fields.append("user_identifier") insert_placeholders.append("%s") insert_values.append(request.user_identifier) if request.enable_memori is not None: insert_fields.append("enable_memori") insert_placeholders.append("%s") insert_values.append(request.enable_memori) if request.tool_response is not None: insert_fields.append("tool_response") insert_placeholders.append("%s") insert_values.append(request.tool_response) if request.skills is not None: insert_fields.append("skills") insert_placeholders.append("%s") insert_values.append(request.skills) # 添加 bot_id insert_fields.append("bot_id") insert_placeholders.append("%s") insert_values.append(bot_uuid) # 构建 SQL:混合使用占位符和 NULL values_clause = ", ".join(insert_placeholders) sql = f"INSERT INTO agent_bot_settings ({', '.join(insert_fields)}) VALUES ({values_clause})" await cursor.execute(sql, insert_values) else: # 存在则更新 await cursor.execute(f""" UPDATE agent_bot_settings SET {', '.join(update_fields)} WHERE bot_id = %s """, values) await conn.commit() return SuccessResponse(success=True, message="Bot settings updated successfully") # ============== 会话管理 API ============== @router.get("/api/v1/bots/{bot_uuid}/sessions", response_model=List[SessionResponse]) async def get_bot_sessions(bot_uuid: str, authorization: Optional[str] = Header(None)): """ 获取 Bot 的会话列表 Args: bot_uuid: Bot 内部 UUID authorization: Bearer token Returns: List[SessionResponse]: 会话列表 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" SELECT id, bot_id, title, created_at, updated_at FROM agent_chat_sessions WHERE bot_id = %s ORDER BY updated_at DESC """, (bot_uuid,)) rows = await cursor.fetchall() return [ SessionResponse( id=str(row[0]), bot_id=str(row[1]), title=row[2], created_at=datetime_to_str(row[3]), updated_at=datetime_to_str(row[4]) ) for row in rows ] @router.post("/api/v1/bots/{bot_uuid}/sessions", response_model=SessionResponse) async def create_session( bot_uuid: str, request: SessionCreate, authorization: Optional[str] = Header(None) ): """ 创建新会话 Args: bot_uuid: Bot 内部 UUID request: 会话创建请求 authorization: Bearer token Returns: SessionResponse: 创建的会话信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 验证 Bot 是否存在 async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("SELECT id FROM agent_bots WHERE id = %s", (bot_uuid,)) if not await cursor.fetchone(): raise HTTPException(status_code=404, detail="Bot not found") # 创建会话 await cursor.execute(""" INSERT INTO agent_chat_sessions (bot_id, title) VALUES (%s, %s) RETURNING id, created_at, updated_at """, (bot_uuid, request.title)) row = await cursor.fetchone() await conn.commit() return SessionResponse( id=str(row[0]), bot_id=bot_uuid, title=request.title, created_at=datetime_to_str(row[1]), updated_at=datetime_to_str(row[2]) ) @router.delete("/api/v1/sessions/{session_id}", response_model=SuccessResponse) async def delete_session(session_id: str, authorization: Optional[str] = Header(None)): """ 删除会话 Args: session_id: 会话 ID authorization: Bearer token Returns: SuccessResponse: 删除结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("DELETE FROM agent_chat_sessions WHERE id = %s RETURNING id", (session_id,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Session not found") await conn.commit() return SuccessResponse(success=True, message="Session deleted successfully") # ============== MCP 服务器 API ============== @router.get("/api/v1/bots/{bot_uuid}/mcp", response_model=List[MCPServerResponse]) async def get_mcp_servers(bot_uuid: str, authorization: Optional[str] = Header(None)): """ 获取 Bot 的 MCP 服务器配置 Args: bot_uuid: Bot 内部 UUID authorization: Bearer token Returns: List[MCPServerResponse]: MCP 服务器列表 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" SELECT id, bot_id, name, type, config, enabled, created_at, updated_at FROM agent_mcp_servers WHERE bot_id = %s ORDER BY created_at DESC """, (bot_uuid,)) rows = await cursor.fetchall() return [ MCPServerResponse( id=str(row[0]), bot_id=str(row[1]), name=row[2], type=row[3], config=row[4], enabled=row[5], created_at=datetime_to_str(row[6]), updated_at=datetime_to_str(row[7]) ) for row in rows ] @router.put("/api/v1/bots/{bot_uuid}/mcp", response_model=SuccessResponse) async def update_mcp_servers( bot_uuid: str, servers: List[MCPServerCreate], authorization: Optional[str] = Header(None) ): """ 更新 Bot 的 MCP 服务器配置 Args: bot_uuid: Bot 内部 UUID servers: MCP 服务器列表 authorization: Bearer token Returns: SuccessResponse: 更新结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: # 删除旧的 MCP 配置 await cursor.execute("DELETE FROM agent_mcp_servers WHERE bot_id = %s", (bot_uuid,)) # 插入新的 MCP 配置 for server in servers: await cursor.execute(""" INSERT INTO agent_mcp_servers (bot_id, name, type, config, enabled) VALUES (%s, %s, %s, %s, %s) """, ( bot_uuid, server.name, server.type, server.config, server.enabled )) await conn.commit() return SuccessResponse( success=True, message=f"MCP servers updated successfully ({len(servers)} servers)" ) @router.post("/api/v1/bots/{bot_uuid}/mcp", response_model=MCPServerResponse) async def add_mcp_server( bot_uuid: str, request: MCPServerCreate, authorization: Optional[str] = Header(None) ): """ 添加单个 MCP 服务器 Args: bot_uuid: Bot 内部 UUID request: MCP 服务器创建请求 authorization: Bearer token Returns: MCPServerResponse: 创建的 MCP 服务器信息 """ verify_auth(authorization) pool = get_db_pool_manager().pool # 验证 Bot 是否存在 async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("SELECT id FROM agent_bots WHERE id = %s", (bot_uuid,)) if not await cursor.fetchone(): raise HTTPException(status_code=404, detail="Bot not found") # 创建 MCP 服务器 await cursor.execute(""" INSERT INTO agent_mcp_servers (bot_id, name, type, config, enabled) VALUES (%s, %s, %s, %s, %s) RETURNING id, created_at, updated_at """, ( bot_uuid, request.name, request.type, request.config, request.enabled )) row = await cursor.fetchone() await conn.commit() return MCPServerResponse( id=str(row[0]), bot_id=bot_uuid, name=request.name, type=request.type, config=request.config, enabled=request.enabled, created_at=datetime_to_str(row[1]), updated_at=datetime_to_str(row[2]) ) @router.delete("/api/v1/bots/{bot_uuid}/mcp/{mcp_id}", response_model=SuccessResponse) async def delete_mcp_server( bot_uuid: str, mcp_id: str, authorization: Optional[str] = Header(None) ): """ 删除 MCP 服务器 Args: bot_uuid: Bot 内部 UUID mcp_id: MCP 服务器 ID authorization: Bearer token Returns: SuccessResponse: 删除结果 """ verify_auth(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute( "DELETE FROM agent_mcp_servers WHERE id = %s AND bot_id = %s RETURNING id", (mcp_id, bot_uuid) ) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="MCP server not found") await conn.commit() return SuccessResponse(success=True, message="MCP server deleted successfully") # ============== Admin 登录 API ============== @router.post("/api/v1/admin/login", response_model=AdminLoginResponse) async def admin_login(request: AdminLoginRequest): """ 管理员登录 Args: request: 登录请求(用户名和密码) Returns: AdminLoginResponse: 登录成功返回 token """ # 硬编码验证账号密码 if request.username != ADMIN_USERNAME or request.password != ADMIN_PASSWORD: raise HTTPException( status_code=401, detail="用户名或密码错误" ) pool = get_db_pool_manager().pool # 生成 token token = secrets.token_urlsafe(32) expires_at = datetime.now() + timedelta(hours=TOKEN_EXPIRE_HOURS) async with pool.connection() as conn: async with conn.cursor() as cursor: # 清理该用户的旧 token await cursor.execute("DELETE FROM agent_admin_tokens WHERE username = %s", (request.username,)) # 保存新 token await cursor.execute(""" INSERT INTO agent_admin_tokens (username, token, expires_at) VALUES (%s, %s, %s) """, (request.username, token, expires_at)) await conn.commit() return AdminLoginResponse( token=token, username=request.username, expires_at=expires_at.isoformat() ) @router.post("/api/v1/admin/verify", response_model=AdminVerifyResponse) async def admin_verify(authorization: Optional[str] = Header(None)): """ 验证管理员 token 是否有效 Args: authorization: Bearer token Returns: AdminVerifyResponse: 验证结果 """ valid, username = await verify_admin_auth(authorization) return AdminVerifyResponse( valid=valid, username=username ) @router.post("/api/v1/admin/logout", response_model=SuccessResponse) async def admin_logout(authorization: Optional[str] = Header(None)): """ 管理员登出(删除 token) Args: authorization: Bearer token Returns: SuccessResponse: 登出结果 """ provided_token = extract_api_key_from_auth(authorization) if not provided_token: raise HTTPException( status_code=401, detail="Authorization header is required" ) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute("DELETE FROM agent_admin_tokens WHERE token = %s", (provided_token,)) await conn.commit() return SuccessResponse(success=True, message="Logged out successfully")