@ -4,7 +4,9 @@ Bot Manager API 路由
"""
import logging
import uuid
from datetime import datetime
import hashlib
import secrets
from datetime import datetime , timedelta
from typing import Optional , List
from fastapi import APIRouter , HTTPException , Header
from pydantic import BaseModel
@ -16,9 +18,47 @@ 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 :
"""
验证请求认证
@ -39,6 +79,26 @@ def verify_auth(authorization: Optional[str]) -> None:
# ============== 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 ) :
""" 创建模型请求 """
@ -77,7 +137,6 @@ class ModelResponse(BaseModel):
class BotCreate ( BaseModel ) :
""" 创建 Bot 请求 """
name : str
bot_id : str
class BotUpdate ( BaseModel ) :
@ -196,9 +255,23 @@ async def init_bot_manager_tables():
# 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 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 ,
@ -211,11 +284,11 @@ async def init_bot_manager_tables():
)
""" ,
# models 索引
" CREATE INDEX IF NOT EXISTS idx_ models_is_default ON models(is_default)" ,
" CREATE INDEX IF NOT EXISTS idx_ agent_ models_is_default ON agent_ models(is_default)" ,
# bots 表
"""
CREATE TABLE IF NOT EXISTS 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 ,
@ -224,13 +297,13 @@ async def init_bot_manager_tables():
)
""" ,
# bots 索引
" CREATE INDEX IF NOT EXISTS idx_ bots_bot_id ON bots(bot_id)" ,
" CREATE INDEX IF NOT EXISTS idx_ agent_ bots_bot_id ON agent_ bots(bot_id)" ,
# bot_settings 表
"""
CREATE TABLE IF NOT EXISTS bot_settings (
bot_id UUID PRIMARY KEY REFERENCES bots( id ) ON DELETE CASCADE ,
model_id UUID REFERENCES models( id ) ON DELETE SET NULL ,
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 ,
@ -245,9 +318,9 @@ async def init_bot_manager_tables():
# mcp_servers 表
"""
CREATE TABLE IF NOT EXISTS mcp_servers (
CREATE TABLE IF NOT EXISTS agent_ mcp_servers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid ( ) ,
bot_id UUID REFERENCES bots( id ) ON DELETE CASCADE ,
bot_id UUID REFERENCES agent_ bots( id ) ON DELETE CASCADE ,
name VARCHAR ( 255 ) NOT NULL ,
type VARCHAR ( 50 ) NOT NULL ,
config JSONB NOT NULL ,
@ -257,22 +330,22 @@ async def init_bot_manager_tables():
)
""" ,
# mcp_servers 索引
" CREATE INDEX IF NOT EXISTS idx_ mcp_servers_bot_id ON mcp_servers(bot_id)" ,
" CREATE INDEX IF NOT EXISTS idx_ mcp_servers_enabled ON mcp_servers(enabled)" ,
" 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 chat_sessions (
CREATE TABLE IF NOT EXISTS agent_ chat_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid ( ) ,
bot_id UUID REFERENCES bots( id ) ON DELETE CASCADE ,
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_ chat_sessions_bot_id ON chat_sessions(bot_id)" ,
" CREATE INDEX IF NOT EXISTS idx_ chat_sessions_created ON chat_sessions(created_at DESC)" ,
" 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 :
@ -321,7 +394,7 @@ async def get_models(authorization: Optional[str] = Header(None)):
async with conn . cursor ( ) as cursor :
await cursor . execute ( """
SELECT id , name , provider , model , server , api_key , is_default , created_at , updated_at
FROM models
FROM agent_ models
ORDER BY is_default DESC , created_at DESC
""" )
rows = await cursor . fetchall ( )
@ -362,13 +435,13 @@ async def create_model(request: ModelCreate, authorization: Optional[str] = Head
if request . is_default :
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " UPDATE models SET is_default = FALSE WHERE is_default = TRUE" )
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 models ( name , provider , model , server , api_key , is_default )
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
""" , (
@ -449,13 +522,13 @@ async def update_model(
if request . is_default is True :
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " UPDATE models SET is_default = FALSE WHERE is_default = TRUE" )
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 models
UPDATE agent_ models
SET { ' , ' . join ( update_fields ) }
WHERE id = % s
RETURNING id , name , provider , model , server , api_key , is_default , created_at , updated_at
@ -498,7 +571,7 @@ async def delete_model(model_id: str, authorization: Optional[str] = Header(None
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " DELETE FROM models WHERE id = %s RETURNING id " , ( model_id , ) )
await cursor . execute ( " DELETE FROM agent_ models WHERE id = %s RETURNING id " , ( model_id , ) )
row = await cursor . fetchone ( )
if not row :
@ -528,17 +601,17 @@ async def set_default_model(model_id: str, authorization: Optional[str] = Header
# 首先检查模型是否存在
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " SELECT id FROM models WHERE id = %s " , ( model_id , ) )
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 models SET is_default = FALSE WHERE is_default = TRUE" )
await cursor . execute ( " UPDATE agent_ models SET is_default = FALSE WHERE is_default = TRUE" )
# 设置新的默认模型
await cursor . execute ( " UPDATE models SET is_default = TRUE WHERE id = %s " , ( model_id , ) )
await cursor . execute ( " UPDATE agent_ models SET is_default = TRUE WHERE id = %s " , ( model_id , ) )
await conn . commit ( )
@ -566,7 +639,7 @@ async def get_bots(authorization: Optional[str] = Header(None)):
async with conn . cursor ( ) as cursor :
await cursor . execute ( """
SELECT id , name , bot_id , created_at , updated_at
FROM bots
FROM agent_ bots
ORDER BY created_at DESC
""" )
rows = await cursor . fetchall ( )
@ -599,19 +672,22 @@ async def create_bot(request: BotCreate, authorization: Optional[str] = Header(N
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 bots ( name , bot_id )
INSERT INTO agent_ bots ( name , bot_id )
VALUES ( % s , % s )
RETURNING id , created_at , updated_at
""" , (request.name, request. bot_id))
""" , (request.name, bot_id))
row = await cursor . fetchone ( )
# 创建对应的设置记录
await cursor . execute ( """
INSERT INTO bot_settings ( bot_id , language )
INSERT INTO agent_ bot_settings ( bot_id , language )
VALUES ( % s , ' zh ' )
""" , (str(row[0]),))
@ -620,7 +696,7 @@ async def create_bot(request: BotCreate, authorization: Optional[str] = Header(N
return BotResponse (
id = str ( row [ 0 ] ) ,
name = request . name ,
bot_id = request. bot_id,
bot_id = bot_id,
created_at = datetime_to_str ( row [ 1 ] ) ,
updated_at = datetime_to_str ( row [ 2 ] )
)
@ -671,7 +747,7 @@ async def update_bot(
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( f """
UPDATE bots
UPDATE agent_ bots
SET { ' , ' . join ( update_fields ) }
WHERE id = % s
RETURNING id , name , bot_id , created_at , updated_at
@ -710,7 +786,7 @@ async def delete_bot(bot_uuid: str, authorization: Optional[str] = Header(None))
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " DELETE FROM bots WHERE id = %s RETURNING id " , ( bot_uuid , ) )
await cursor . execute ( " DELETE FROM agent_ bots WHERE id = %s RETURNING id " , ( bot_uuid , ) )
row = await cursor . fetchone ( )
if not row :
@ -745,7 +821,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
SELECT bot_id , model_id ,
language , robot_type , dataset_ids , system_prompt , user_identifier ,
enable_memori , tool_response , skills , updated_at
FROM bot_settings
FROM agent_ bot_settings
WHERE bot_id = % s
""" , (bot_uuid,))
row = await cursor . fetchone ( )
@ -759,7 +835,7 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header(
if model_id :
await cursor . execute ( """
SELECT id , name , provider , model , server , api_key
FROM models WHERE id = % s
FROM agent_ models WHERE id = % s
""" , (model_id,))
model_row = await cursor . fetchone ( )
if model_row :
@ -821,7 +897,7 @@ async def update_bot_settings(
if model_id_value :
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " SELECT id FROM models WHERE id = %s " , ( model_id_value , ) )
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 或占位符
@ -864,7 +940,7 @@ async def update_bot_settings(
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
# 检查设置是否存在
await cursor . execute ( " SELECT bot_id FROM bot_settings WHERE bot_id = %s " , ( bot_uuid , ) )
await cursor . execute ( " SELECT bot_id FROM agent_ bot_settings WHERE bot_id = %s " , ( bot_uuid , ) )
if not await cursor . fetchone ( ) :
# 不存在则创建
# 需要分别处理带占位符和不带占位符的字段
@ -935,13 +1011,13 @@ async def update_bot_settings(
# 构建 SQL: 混合使用占位符和 NULL
values_clause = " , " . join ( insert_placeholders )
sql = f " INSERT INTO bot_settings ({ ' , ' . join ( insert_fields ) } ) VALUES ( { values_clause } ) "
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 bot_settings
UPDATE agent_ bot_settings
SET { ' , ' . join ( update_fields ) }
WHERE bot_id = % s
""" , values)
@ -973,7 +1049,7 @@ async def get_bot_sessions(bot_uuid: str, authorization: Optional[str] = Header(
async with conn . cursor ( ) as cursor :
await cursor . execute ( """
SELECT id , bot_id , title , created_at , updated_at
FROM chat_sessions
FROM agent_ chat_sessions
WHERE bot_id = % s
ORDER BY updated_at DESC
""" , (bot_uuid,))
@ -1015,13 +1091,13 @@ async def create_session(
# 验证 Bot 是否存在
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " SELECT id FROM bots WHERE id = %s " , ( bot_uuid , ) )
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 chat_sessions ( bot_id , title )
INSERT INTO agent_ chat_sessions ( bot_id , title )
VALUES ( % s , % s )
RETURNING id , created_at , updated_at
""" , (bot_uuid, request.title))
@ -1056,7 +1132,7 @@ async def delete_session(session_id: str, authorization: Optional[str] = Header(
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " DELETE FROM chat_sessions WHERE id = %s RETURNING id " , ( session_id , ) )
await cursor . execute ( " DELETE FROM agent_ chat_sessions WHERE id = %s RETURNING id " , ( session_id , ) )
row = await cursor . fetchone ( )
if not row :
@ -1089,7 +1165,7 @@ async def get_mcp_servers(bot_uuid: str, authorization: Optional[str] = Header(N
async with conn . cursor ( ) as cursor :
await cursor . execute ( """
SELECT id , bot_id , name , type , config , enabled , created_at , updated_at
FROM mcp_servers
FROM agent_ mcp_servers
WHERE bot_id = % s
ORDER BY created_at DESC
""" , (bot_uuid,))
@ -1134,12 +1210,12 @@ async def update_mcp_servers(
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
# 删除旧的 MCP 配置
await cursor . execute ( " DELETE FROM mcp_servers WHERE bot_id = %s " , ( bot_uuid , ) )
await cursor . execute ( " DELETE FROM agent_ mcp_servers WHERE bot_id = %s " , ( bot_uuid , ) )
# 插入新的 MCP 配置
for server in servers :
await cursor . execute ( """
INSERT INTO mcp_servers ( bot_id , name , type , config , enabled )
INSERT INTO agent_ mcp_servers ( bot_id , name , type , config , enabled )
VALUES ( % s , % s , % s , % s , % s )
""" , (
bot_uuid ,
@ -1181,13 +1257,13 @@ async def add_mcp_server(
# 验证 Bot 是否存在
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute ( " SELECT id FROM bots WHERE id = %s " , ( bot_uuid , ) )
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 mcp_servers ( bot_id , name , type , config , enabled )
INSERT INTO agent_ mcp_servers ( bot_id , name , type , config , enabled )
VALUES ( % s , % s , % s , % s , % s )
RETURNING id , created_at , updated_at
""" , (
@ -1237,7 +1313,7 @@ async def delete_mcp_server(
async with pool . connection ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute (
" DELETE FROM mcp_servers WHERE id = %s AND bot_id = %s RETURNING id " ,
" DELETE FROM agent_ mcp_servers WHERE id = %s AND bot_id = %s RETURNING id " ,
( mcp_id , bot_uuid )
)
row = await cursor . fetchone ( )
@ -1248,3 +1324,96 @@ async def delete_mcp_server(
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 " )