diff --git a/server/db_manager.py b/server/db_manager.py index 7b835d51..9a37a363 100644 --- a/server/db_manager.py +++ b/server/db_manager.py @@ -27,7 +27,7 @@ class DBManager: # 首先创建基本表结构 self.create_tables() - + # 然后检查并执行数据库迁移 self.run_migrations() @@ -41,7 +41,7 @@ class DBManager: # 确保所有表都会被创建 Base.metadata.create_all(self.engine) logger.info("Database tables created/checked") - + def run_migrations(self): """运行数据库迁移""" try: diff --git a/server/models/user_model.py b/server/models/user_model.py index 97ea1f9e..cbecc0d3 100644 --- a/server/models/user_model.py +++ b/server/models/user_model.py @@ -16,7 +16,7 @@ class User(Base): role = Column(String, nullable=False, default="user") # 角色: superadmin, admin, user created_at = Column(DateTime, default=func.now()) last_login = Column(DateTime, nullable=True) - + # 登录失败限制相关字段 login_failed_count = Column(Integer, nullable=False, default=0) # 登录失败次数 last_failed_login = Column(DateTime, nullable=True) # 最后一次登录失败时间 @@ -39,44 +39,46 @@ class User(Base): if include_password: result["password_hash"] = self.password_hash return result - + def is_login_locked(self): """检查用户是否处于登录锁定状态""" if self.login_locked_until is None: return False from datetime import datetime + return datetime.now() < self.login_locked_until - + def get_remaining_lock_time(self): """获取剩余锁定时间(秒)""" if not self.is_login_locked(): return 0 from datetime import datetime + return int((self.login_locked_until - datetime.now()).total_seconds()) - + def calculate_lock_duration(self): """根据失败次数计算锁定时长(秒)""" if self.login_failed_count < 10: return 0 - + # 从第10次失败开始,等待时间从1秒开始,每次翻倍 wait_seconds = 2 ** (self.login_failed_count - 10) - + # 最大锁定时间:365天 max_seconds = 365 * 24 * 60 * 60 return min(wait_seconds, max_seconds) - + def increment_failed_login(self): """增加登录失败次数并设置锁定时间""" from datetime import datetime, timedelta - + self.login_failed_count += 1 self.last_failed_login = datetime.now() - + lock_duration = self.calculate_lock_duration() if lock_duration > 0: self.login_locked_until = datetime.now() + timedelta(seconds=lock_duration) - + def reset_failed_login(self): """重置登录失败相关字段""" self.login_failed_count = 0 diff --git a/server/routers/auth_router.py b/server/routers/auth_router.py index 34389661..3b8bab83 100644 --- a/server/routers/auth_router.py +++ b/server/routers/auth_router.py @@ -72,17 +72,14 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( detail="用户名或密码错误", headers={"WWW-Authenticate": "Bearer"}, ) - + # 检查用户是否处于登录锁定状态 if user.is_login_locked(): remaining_time = user.get_remaining_lock_time() raise HTTPException( status_code=status.HTTP_423_LOCKED, detail=f"登录被锁定,请等待 {remaining_time} 秒后再试", - headers={ - "WWW-Authenticate": "Bearer", - "X-Lock-Remaining": str(remaining_time) - }, + headers={"WWW-Authenticate": "Bearer", "X-Lock-Remaining": str(remaining_time)}, ) # 验证密码 @@ -90,20 +87,17 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( # 密码错误,增加失败次数 user.increment_failed_login() db.commit() - + # 记录失败操作 log_operation(db, user.id if user else None, "登录失败", f"密码错误,失败次数: {user.login_failed_count}") - + # 检查是否需要锁定 if user.is_login_locked(): remaining_time = user.get_remaining_lock_time() raise HTTPException( status_code=status.HTTP_423_LOCKED, detail=f"由于多次登录失败,账户已被锁定 {remaining_time} 秒", - headers={ - "WWW-Authenticate": "Bearer", - "X-Lock-Remaining": str(remaining_time) - }, + headers={"WWW-Authenticate": "Bearer", "X-Lock-Remaining": str(remaining_time)}, ) else: raise HTTPException( diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index eb17a680..ae5a9c2c 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -15,7 +15,8 @@ from server.models.thread_model import Thread from server.models.user_model import User from server.routers.auth_router import get_admin_user from server.utils.auth_middleware import get_db, get_required_user -from src import config, executor +from src import executor +from src import config as conf from src.agents import agent_manager from src.agents.common.tools import gen_tool_info, get_buildin_tools from src.models import select_model @@ -32,7 +33,7 @@ chat = APIRouter(prefix="/chat", tags=["chat"]) async def get_default_agent(current_user: User = Depends(get_required_user)): """获取默认智能体ID(需要登录)""" try: - default_agent_id = config.default_agent_id + default_agent_id = conf.default_agent_id # 如果没有设置默认智能体,尝试获取第一个可用的智能体 if not default_agent_id: agents = await agent_manager.get_agents_info() @@ -61,9 +62,9 @@ async def set_default_agent(request_data: dict = Body(...), current_user=Depends raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在") # 设置默认智能体ID - config.default_agent_id = agent_id + conf.default_agent_id = agent_id # 保存配置 - config.save() + conf.save() return {"success": True, "default_agent_id": agent_id} except HTTPException as he: @@ -187,9 +188,9 @@ async def get_chat_models(model_provider: str, current_user: User = Depends(get_ @chat.post("/models/update") async def update_chat_models(model_provider: str, model_names: list[str], current_user=Depends(get_admin_user)): """更新指定模型提供商的模型列表 (仅管理员)""" - config.model_names[model_provider]["models"] = model_names - config._save_models_to_file() - return {"models": config.model_names[model_provider]["models"]} + conf.model_names[model_provider]["models"] = model_names + conf._save_models_to_file() + return {"models": conf.model_names[model_provider]["models"]} @chat.get("/tools") diff --git a/server/utils/migrate.py b/server/utils/migrate.py index 9a1ea624..a1dd4211 100644 --- a/server/utils/migrate.py +++ b/server/utils/migrate.py @@ -1,39 +1,39 @@ """ 数据库迁移系统 """ + import os import shutil import sqlite3 from datetime import datetime from pathlib import Path -from typing import Dict, List, Tuple from src.utils import logger class DatabaseMigrator: """数据库迁移器""" - + def __init__(self, db_path: str): self.db_path = db_path self.backup_dir = os.path.join(os.path.dirname(db_path), "backups") self.migration_version_key = "migration_version" - + def ensure_backup_dir(self): """确保备份目录存在""" Path(self.backup_dir).mkdir(parents=True, exist_ok=True) - + def backup_database(self) -> str: """备份数据库文件""" if not os.path.exists(self.db_path): logger.info("数据库文件不存在,无需备份") return "" - + self.ensure_backup_dir() timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_filename = f"server_backup_{timestamp}.db" backup_path = os.path.join(self.backup_dir, backup_filename) - + try: shutil.copy2(self.db_path, backup_path) logger.info(f"数据库已备份到: {backup_path}") @@ -41,26 +41,25 @@ class DatabaseMigrator: except Exception as e: logger.error(f"数据库备份失败: {e}") raise - + def get_current_version(self) -> int: """获取当前数据库版本""" if not os.path.exists(self.db_path): return 0 - + try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - + # 检查版本表是否存在 cursor.execute(""" - SELECT name FROM sqlite_master - WHERE type='table' AND name='migration_versions' - """) - + SELECT name FROM sqlite_master + WHERE type='table' AND name='migration_versions' """) + if not cursor.fetchone(): # 版本表不存在,检查是否为旧版本数据库 cursor.execute(""" - SELECT name FROM sqlite_master + SELECT name FROM sqlite_master WHERE type='table' AND name='users' """) if cursor.fetchone(): @@ -69,25 +68,25 @@ class DatabaseMigrator: else: # 全新数据库 return 0 - + # 获取当前版本 cursor.execute("SELECT version FROM migration_versions ORDER BY version DESC LIMIT 1") result = cursor.fetchone() return result[0] if result else 0 - + except Exception as e: logger.error(f"获取数据库版本失败: {e}") return 0 finally: - if 'conn' in locals(): + if "conn" in locals(): conn.close() - + def set_version(self, version: int): """设置数据库版本""" try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - + # 创建版本表 cursor.execute(""" CREATE TABLE IF NOT EXISTS migration_versions ( @@ -97,93 +96,98 @@ class DatabaseMigrator: description TEXT ) """) - + # 插入版本记录 - cursor.execute(""" - INSERT INTO migration_versions (version, description) + cursor.execute( + """ + INSERT INTO migration_versions (version, description) VALUES (?, ?) - """, (version, f"Migration to version {version}")) - + """, + (version, f"Migration to version {version}"), + ) + conn.commit() logger.info(f"数据库版本设置为: {version}") - + except Exception as e: logger.error(f"设置数据库版本失败: {e}") raise finally: - if 'conn' in locals(): + if "conn" in locals(): conn.close() - - def execute_migration(self, version: int, description: str, sql_commands: List[str]): + + def execute_migration(self, version: int, description: str, sql_commands: list[str]): """执行迁移""" logger.info(f"执行迁移 v{version}: {description}") - + try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - + # 执行迁移SQL命令 for sql in sql_commands: if sql.strip(): # 跳过空命令 logger.info(f"执行SQL: {sql}") cursor.execute(sql) - + conn.commit() logger.info(f"迁移 v{version} 执行成功") - + except Exception as e: logger.error(f"迁移 v{version} 执行失败: {e}") raise finally: - if 'conn' in locals(): + if "conn" in locals(): conn.close() - + def check_column_exists(self, table_name: str, column_name: str) -> bool: """检查列是否存在""" try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - + cursor.execute(f"PRAGMA table_info({table_name})") columns = [column[1] for column in cursor.fetchall()] return column_name in columns - + except Exception: return False finally: - if 'conn' in locals(): + if "conn" in locals(): conn.close() - + def run_migrations(self): """运行所有待执行的迁移""" current_version = self.get_current_version() latest_version = self.get_latest_migration_version() - + # 如果数据库已存在但没有版本表,创建版本表并设置为最新版本 if current_version == 0 and latest_version > 0 and os.path.exists(self.db_path): # 检查users表是否已有新字段,如果有,说明是通过SQLAlchemy创建的 - if (self.check_column_exists('users', 'login_failed_count') and - self.check_column_exists('users', 'last_failed_login') and - self.check_column_exists('users', 'login_locked_until')): + if ( + self.check_column_exists("users", "login_failed_count") + and self.check_column_exists("users", "last_failed_login") + and self.check_column_exists("users", "login_locked_until") + ): # 字段已存在,直接设置为最新版本 logger.info(f"检测到现有数据库已包含最新字段,设置版本为 v{latest_version}") self.set_version(latest_version) return - + if current_version >= latest_version: logger.info(f"数据库已是最新版本 v{current_version}") return - + logger.info(f"开始数据库迁移: v{current_version} -> v{latest_version}") - + # 备份数据库 backup_path = self.backup_database() - + try: # 执行迁移 migrations = self.get_migrations() has_executed_migrations = False - + for version, description, sql_commands in migrations: if version > current_version: if sql_commands: # 只有当有SQL命令时才执行迁移 @@ -191,15 +195,15 @@ class DatabaseMigrator: has_executed_migrations = True else: logger.info(f"迁移 v{version}: {description} - 无需执行,字段已存在") - + # 无论是否有SQL命令,都设置版本 self.set_version(version) - + if has_executed_migrations: logger.info("数据库迁移完成") else: logger.info("数据库结构已是最新,仅更新版本记录") - + except Exception as e: logger.error(f"数据库迁移失败: {e}") if backup_path and os.path.exists(backup_path): @@ -210,39 +214,39 @@ class DatabaseMigrator: except Exception as restore_error: logger.error(f"数据库恢复失败: {restore_error}") raise - + def get_latest_migration_version(self) -> int: """获取最新迁移版本号""" # 这里返回硬编码的最新版本号,不依赖迁移定义 # 因为迁移定义可能为空(字段已存在) return 1 # 当前最新版本是 v1 - - def get_migrations(self) -> List[Tuple[int, str, List[str]]]: + + def get_migrations(self) -> list[tuple[int, str, list[str]]]: """获取所有迁移定义 返回格式: [(version, description, [sql_commands])] """ migrations = [] - + # 迁移 v1: 为 users 表添加登录失败限制字段 # 使用条件检查来避免重复添加字段 v1_commands = [] - + # 检查并添加 login_failed_count 字段 - if not self.check_column_exists('users', 'login_failed_count'): + if not self.check_column_exists("users", "login_failed_count"): v1_commands.append("ALTER TABLE users ADD COLUMN login_failed_count INTEGER NOT NULL DEFAULT 0") - + # 检查并添加 last_failed_login 字段 - if not self.check_column_exists('users', 'last_failed_login'): + if not self.check_column_exists("users", "last_failed_login"): v1_commands.append("ALTER TABLE users ADD COLUMN last_failed_login DATETIME") - + # 检查并添加 login_locked_until 字段 - if not self.check_column_exists('users', 'login_locked_until'): + if not self.check_column_exists("users", "login_locked_until"): v1_commands.append("ALTER TABLE users ADD COLUMN login_locked_until DATETIME") - + # 如果有命令需要执行,才添加迁移 if v1_commands: migrations.append((1, "为用户表添加登录失败限制字段", v1_commands)) - + # 未来的迁移可以在这里添加 # migrations.append(( # 2, @@ -252,17 +256,17 @@ class DatabaseMigrator: # "ALTER TABLE existing_table ADD COLUMN new_field ..." # ] # )) - + return migrations def check_and_migrate(db_path: str): """检查并执行数据库迁移""" migrator = DatabaseMigrator(db_path) - + try: migrator.run_migrations() return True except Exception as e: logger.error(f"数据库迁移过程中发生错误: {e}") - return False \ No newline at end of file + return False diff --git a/src/__init__.py b/src/__init__.py index 7a4c4f73..4062beb4 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,3 @@ -import os - from dotenv import load_dotenv load_dotenv("src/.env", override=True) diff --git a/src/config/__init__.py b/src/config/__init__.py index af0710c3..740b7954 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -1,3 +1,3 @@ from .app import config -__all__ = ["config"] \ No newline at end of file +__all__ = ["config"] diff --git a/src/config/app.py b/src/config/app.py index 08110083..3d601948 100644 --- a/src/config/app.py +++ b/src/config/app.py @@ -228,4 +228,4 @@ class Config(SimpleConfig): return value -config = Config() \ No newline at end of file +config = Config() diff --git a/src/knowledge/__init__.py b/src/knowledge/__init__.py index 19d9a15a..576e28b3 100644 --- a/src/knowledge/__init__.py +++ b/src/knowledge/__init__.py @@ -21,4 +21,3 @@ knowledge_base = KnowledgeBaseManager(work_dir) graph_base = GraphDatabase() __all__ = ["GraphDatabase", "knowledge_base", "graph_base"] - diff --git a/src/static/agents_meta.yaml b/src/static/agents_meta.yaml index 2d03a4be..e69de29b 100644 --- a/src/static/agents_meta.yaml +++ b/src/static/agents_meta.yaml @@ -1,9 +0,0 @@ -ChatbotAgent: - name: 食品安全知识助手 - icon: /agents-icon/chatbot.png - examples: - - 冰箱里不同食材应该怎么存放? - - 有机食品和普通食品有什么区别? - - 外卖食品安全需要注意什么? - - 如何判断食品是否已经变质? - - 食品添加剂对健康有害吗? \ No newline at end of file