feat: 集成数据库配置管理并添加错误日志查看器

主要变更:

1.  **数据库集成**:
    *   引入 MySQL 数据库支持,使用 SQLAlchemy 和 `databases` 库持久化存储应用程序设置。
    *   添加了 `app/database` 目录,包含数据库连接、模型和初始化逻辑。
    *   更新 `requirements.txt` 添加数据库相关依赖 (`pymysql`, `sqlalchemy`, `aiomysql`, `databases`, `python-dotenv`)。

2.  **配置管理重构**:
    *   重构 `ConfigService` (`app/service/config/config_service.py`),使其从数据库加载和保存设置,并支持从 `.env` 文件同步初始配置到数据库。
    *   修改 `Settings` 模型 (`app/config/config.py`) 以包含数据库连接信息,并添加了从数据库加载/同步配置的逻辑。
    *   配置相关的路由 (`app/router/config_routes.py`) 更新为异步,并调用新的 `ConfigService` 方法。
    *   `KeyManager` (`app/service/key/key_manager.py`) 现在可以在配置更新后重置和重新初始化。

3.  **错误日志查看器**:
    *   新增 `/logs` 页面 (`app/templates/error_logs.html`) 用于展示应用程序错误日志。
    *   添加了相应的路由 (`app/router/log_routes.py`)、静态资源 (`app/static/css/error_logs.css`, `app/static/js/error_logs.js`) 和日志记录器 (`app/log/logger.py`)。
    *   在配置页面和密钥管理页面的导航栏中添加了指向日志页面的链接。

4.  **异步操作**:
    *   将配置服务和相关路由转换为异步 (`async def`) 以支持异步数据库操作。

5.  **其他**:
    *   更新了应用程序初始化逻辑 (`app/core/application.py`, `app/core/initialization.py`) 以包含数据库连接的建立和关闭。
This commit is contained in:
snaily
2025-04-09 15:04:29 +08:00
parent a7dc05a359
commit 169488851f
21 changed files with 1510 additions and 86 deletions
+145 -72
View File
@@ -1,57 +1,118 @@
"""
配置服务模块
"""
import os
from typing import Any, Dict
import datetime
import json
from dotenv import load_dotenv, set_key
import os
from typing import Any, Dict, List
from app.config.config import settings, Settings
from dotenv import load_dotenv
from sqlalchemy import insert, update
from app.config.config import settings, reload_settings
from app.database.connection import database
from app.database.models import Settings
from app.database.services import get_all_settings
from app.service.key.key_manager import get_key_manager_instance, reset_key_manager_instance
from app.log.logger import get_config_routes_logger
logger = get_config_routes_logger()
class ConfigService:
"""配置服务类,用于管理应用程序配置"""
@staticmethod
def get_config() -> Dict[str, Any]:
"""
获取当前配置
Returns:
Dict[str, Any]: 配置字典
"""
config_dict = {}
# 获取Settings类的所有字段
for field_name in settings.model_fields:
value = getattr(settings, field_name)
config_dict[field_name] = value
return config_dict
async def get_config() -> Dict[str, Any]:
return settings.model_dump()
@staticmethod
def update_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
"""
更新配置
Args:
config_data (Dict[str, Any]): 新的配置数据
Returns:
Dict[str, Any]: 更新后的配置字典
"""
# 更新settings对象
async def update_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
for key, value in config_data.items():
if hasattr(settings, key):
setattr(settings, key, value)
logger.info(f"Updated setting in memory: {key}")
# 更新.env文件
ConfigService._update_env_file(config_data)
return ConfigService.get_config()
# 获取现有设置
existing_settings_raw: List[Dict[str, Any]] = await get_all_settings()
existing_settings_map: Dict[str, Dict[str, Any]] = {s['key']: s for s in existing_settings_raw}
existing_keys = set(existing_settings_map.keys())
settings_to_update: List[Dict[str, Any]] = []
settings_to_insert: List[Dict[str, Any]] = []
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))
# 准备要更新或插入的数据
for key, value in config_data.items():
# 处理不同类型的值
if isinstance(value, list):
db_value = json.dumps(value)
elif isinstance(value, bool):
db_value = str(value).lower()
else:
db_value = str(value)
# 仅当值发生变化时才更新
if key in existing_keys and existing_settings_map[key]['value'] == db_value:
continue
description = f"{key}配置项"
data = {
'key': key,
'value': db_value,
'description': description,
'updated_at': now
}
if key in existing_keys:
# Preserve original description if not explicitly provided
data['description'] = existing_settings_map[key].get('description', description)
settings_to_update.append(data)
else:
data['created_at'] = now
settings_to_insert.append(data)
# 在事务中执行批量插入和更新
if settings_to_insert or settings_to_update:
try:
async with database.transaction():
if settings_to_insert:
query_insert = insert(Settings).values(settings_to_insert)
await database.execute(query=query_insert)
logger.info(f"Bulk inserted {len(settings_to_insert)} settings.")
if settings_to_update:
for setting_data in settings_to_update:
query_update = (
update(Settings)
.where(Settings.key == setting_data['key'])
.values(
value=setting_data['value'],
description=setting_data['description'],
updated_at=setting_data['updated_at']
)
)
await database.execute(query=query_update)
logger.info(f"Updated {len(settings_to_update)} settings.")
except Exception as e:
logger.error(f"Failed to bulk update/insert settings: {str(e)}")
raise # Re-raise the exception after logging
# 重置并重新初始化 KeyManager
try:
await reset_key_manager_instance()
await get_key_manager_instance(settings.API_KEYS)
logger.info("KeyManager instance re-initialized with updated settings.")
except Exception as e:
logger.error(f"Failed to re-initialize KeyManager: {str(e)}")
# Decide if this error should prevent returning the updated config
# For now, we log the error and continue
return await ConfigService.get_config()
@staticmethod
def reset_config() -> Dict[str, Any]:
async def reset_config() -> Dict[str, Any]:
"""
重置配置到默认值
@@ -60,45 +121,57 @@ class ConfigService:
"""
# 重新加载.env文件
load_dotenv(override=True)
# 重新创建settings对象
global settings
settings = Settings()
return ConfigService.get_config()
# 重新加载配置对象以反映最新的环境变量
reload_settings()
logger.info("Settings object reloaded from environment variables.")
# 同步数据库中的配置到settings对象
await ConfigService._sync_db_config()
return await ConfigService.get_config()
@staticmethod
def _update_env_file(config_data: Dict[str, Any]) -> None:
async def _sync_db_config() -> None:
"""
更新.env文件
Args:
config_data (Dict[str, Any]): 配置数据
.env文件中的配置项同步到数据库
"""
env_path = ".env"
# 确保.env文件存在
if not os.path.exists(env_path):
# 如果不存在,复制.env.example
if os.path.exists(".env.example"):
with open(".env.example", "r", encoding="utf-8") as example_file:
with open(env_path, "w", encoding="utf-8") as env_file:
env_file.write(example_file.read())
else:
# 创建空文件
open(env_path, "w", encoding="utf-8").close()
# 更新.env文件中的配置
for key, value in config_data.items():
# 处理不同类型的值
if isinstance(value, list):
# 将列表转换为JSON字符串
env_value = json.dumps(value)
elif isinstance(value, bool):
# 布尔值转换为小写字符串
env_value = str(value).lower()
else:
env_value = str(value)
try:
# 获取.env文件中的所有配置项
env_values = dotenv_values(".env")
await ConfigService.update_config(env_values)
# 更新.env文件
set_key(env_path, key, env_value)
logger.info("Synced configuration to database")
except Exception as e:
logger.error(f"Failed to sync configuration to database: {str(e)}")
# 添加dotenv_values函数
def dotenv_values(dotenv_path: str) -> Dict[str, str]:
"""
从.env文件中读取配置项
Args:
dotenv_path: .env文件路径
Returns:
Dict[str, str]: 配置项字典
"""
if not os.path.exists(dotenv_path):
return {}
result = {}
with open(dotenv_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
# 去除引号
if value and value[0] == value[-1] and value[0] in ["'", '"']:
value = value[1:-1]
result[key] = value
return result
+8
View File
@@ -107,4 +107,12 @@ async def get_key_manager_instance(api_keys: list = None) -> KeyManager:
if api_keys is None:
raise ValueError("API keys are required to initialize the KeyManager")
_singleton_instance = KeyManager(api_keys)
logger.info("KeyManager instance created.")
return _singleton_instance
async def reset_key_manager_instance():
"""重置 KeyManager 单例实例"""
global _singleton_instance
async with _singleton_lock:
if _singleton_instance:
_singleton_instance = None
logger.info("KeyManager instance reset.")