Compare commits

...

2 Commits

Author SHA1 Message Date
朱潮
51f988e535 update sql表名 2026-01-30 00:03:26 +08:00
朱潮
f88aca74f2 add table prefix 2026-01-29 15:59:42 +08:00
3 changed files with 256 additions and 63 deletions

View File

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

View File

@ -164,36 +164,60 @@ async def list_files(path: str = "", recursive: bool = False):
async def upload_file(file: UploadFile = File(...), path: str = Form("")):
"""
上传文件
Args:
file: 上传的文件
path: 目标路径相对于支持目录
path: 目标路径相对于支持目录如果不存在会自动创建
"""
try:
target_path = resolve_path(path) if path else resolve_path("projects")
if not target_path.exists() or not target_path.is_dir():
# 如果没有指定路径,默认使用 projects 目录
if not path:
target_path = resolve_path("projects")
else:
# 验证路径格式是否合法(必须以支持的目录开头)
path_parts = Path(path).parts
if not path_parts or path_parts[0] not in SUPPORTED_DIRECTORIES:
raise HTTPException(
status_code=400,
detail=f"路径必须以以下目录之一开头: {', '.join(SUPPORTED_DIRECTORIES)}"
)
# 直接构建路径,不检查是否存在(稍后会创建)
target_path = PROJECTS_DIR / path
# 如果目标路径已存在且是文件,则使用其父目录
if target_path.exists() and target_path.is_file():
target_path = target_path.parent
# 创建目标目录(包括所有不存在的父目录)
target_path.mkdir(parents=True, exist_ok=True)
file_path = target_path / file.filename
# 如果文件已存在,检查是否覆盖
if file_path.exists():
# 可以添加版本控制或重命名逻辑
pass
with open(file_path, "wb") as buffer:
content = await file.read()
buffer.write(content)
# 计算返回的相对路径
try:
relative_path = file_path.relative_to(PROJECTS_DIR)
except ValueError:
relative_path = file_path
return {
"success": True,
"message": "文件上传成功",
"filename": file.filename,
"path": str(Path(path) / file.filename),
"path": str(relative_path),
"size": len(content)
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"文件上传失败: {str(e)}")

View File

@ -465,7 +465,7 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]:
async with conn.cursor() as cursor:
# 首先根据 bot_user_id 查找 bot 的 UUID
await cursor.execute(
"SELECT id, name FROM bots WHERE bot_id = %s",
"SELECT id, name FROM agent_bots WHERE bot_id = %s",
(bot_user_id,)
)
bot_row = await cursor.fetchone()
@ -484,7 +484,7 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]:
SELECT model_id,
language, robot_type, dataset_ids, system_prompt, user_identifier,
enable_memori, tool_response, skills
FROM bot_settings WHERE bot_id = %s
FROM agent_bot_settings WHERE bot_id = %s
""",
(bot_uuid,)
)
@ -521,7 +521,7 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]:
await cursor.execute(
"""
SELECT model, server, api_key
FROM models WHERE id = %s
FROM agent_models WHERE id = %s
""",
(model_id,)
)
@ -570,7 +570,7 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]:
await cursor.execute(
"""
SELECT name, type, config, enabled
FROM mcp_servers WHERE bot_id = %s AND enabled = true
FROM agent_mcp_servers WHERE bot_id = %s AND enabled = true
""",
(bot_uuid,)
)