mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-06 16:16:37 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user