qwen_agent/routes/bot_manager.py
2026-01-29 15:59:42 +08:00

1420 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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")