feat: 修复代码审核报告问题
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""运行时配置管理:支持环境变量作为默认值,数据库覆盖"""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import settings
|
||||
from models import AppSetting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 可在 Web UI 中编辑的配置项清单
|
||||
EDITABLE_SETTINGS = {
|
||||
"RSSKEEPER_BASE_URL": {"description": "rssKeeper 服务地址", "sensitive": False},
|
||||
"OPENAI_API_KEY": {"description": "LLM API Key", "sensitive": True},
|
||||
"OPENAI_BASE_URL": {"description": "LLM API 基础地址", "sensitive": False},
|
||||
"OPENAI_MODEL": {"description": "LLM 模型名", "sensitive": False},
|
||||
"OPENAI_TIMEOUT": {"description": "LLM 调用超时(秒)", "sensitive": False},
|
||||
"OPENAI_MAX_RETRIES": {"description": "LLM 调用最大重试次数", "sensitive": False},
|
||||
"SUMMARIZE_INTERVAL_MINUTES": {"description": "摘要任务间隔(分钟)", "sensitive": False},
|
||||
"TAG_SCORE_INTERVAL_MINUTES": {"description": "分类/打分/去重任务间隔(分钟)", "sensitive": False},
|
||||
"DAILY_BRIEF_HOUR": {"description": "每日简报生成小时", "sensitive": False},
|
||||
"DAILY_BRIEF_MINUTE": {"description": "每日简报生成分钟", "sensitive": False},
|
||||
"TITLE_SIMILARITY_THRESHOLD": {"description": "标题相似度阈值", "sensitive": False},
|
||||
"CONTENT_SIMILARITY_THRESHOLD": {"description": "内容相似度阈值", "sensitive": False},
|
||||
"MAX_AI_SUMMARY_LENGTH": {"description": "AI 摘要最大长度", "sensitive": False},
|
||||
"MIN_ORIGINAL_SUMMARY_LENGTH": {"description": "原始摘要最小长度", "sensitive": False},
|
||||
"BRIEF_TOP_N_PER_CATEGORY": {"description": "简报每分类显示文章数", "sensitive": False},
|
||||
"LOG_LEVEL": {"description": "日志级别", "sensitive": False},
|
||||
"API_TOKEN": {"description": "API 鉴权 Token(为空时不启用鉴权)", "sensitive": True},
|
||||
"CORS_ALLOWED_ORIGINS": {"description": "CORS 允许来源(逗号分隔)", "sensitive": False},
|
||||
}
|
||||
|
||||
|
||||
def _get_env_default(key: str) -> str:
|
||||
"""从 Pydantic Settings 获取环境变量默认值"""
|
||||
value = getattr(settings, key, "")
|
||||
return str(value) if value is not None else ""
|
||||
|
||||
|
||||
def _mask_sensitive(value: str) -> str:
|
||||
"""对敏感值做部分脱敏"""
|
||||
if not value:
|
||||
return ""
|
||||
if len(value) <= 8:
|
||||
return "*" * len(value)
|
||||
return value[:4] + "..." + value[-4:]
|
||||
|
||||
|
||||
def init_default_settings(db: Session) -> None:
|
||||
"""若配置表为空,使用环境变量初始化默认配置"""
|
||||
existing_count = db.query(AppSetting).count()
|
||||
if existing_count > 0:
|
||||
return
|
||||
|
||||
for key, meta in EDITABLE_SETTINGS.items():
|
||||
default_value = _get_env_default(key)
|
||||
db.add(
|
||||
AppSetting(
|
||||
key=key,
|
||||
value=default_value,
|
||||
description=meta["description"],
|
||||
is_sensitive=meta["sensitive"],
|
||||
)
|
||||
)
|
||||
|
||||
db.commit()
|
||||
logger.info("已初始化默认配置项: %d 条", len(EDITABLE_SETTINGS))
|
||||
|
||||
|
||||
def get_setting(db: Session, key: str, default: Any = None) -> Any:
|
||||
"""从数据库读取配置,若不存在则返回环境变量默认值"""
|
||||
setting = db.query(AppSetting).filter(AppSetting.key == key).first()
|
||||
if setting:
|
||||
return setting.value
|
||||
return _get_env_default(key) if default is None else default
|
||||
|
||||
|
||||
def get_setting_value(key: str, default: Any = None) -> Any:
|
||||
"""不依赖 Session,直接创建临时会话读取"""
|
||||
from database import SessionLocal
|
||||
db = SessionLocal()
|
||||
try:
|
||||
return get_setting(db, key, default)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def set_setting(db: Session, key: str, value: str) -> bool:
|
||||
"""更新单个配置项"""
|
||||
if key not in EDITABLE_SETTINGS:
|
||||
return False
|
||||
|
||||
setting = db.query(AppSetting).filter(AppSetting.key == key).first()
|
||||
if setting:
|
||||
setting.value = str(value)
|
||||
setting.updated_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
meta = EDITABLE_SETTINGS[key]
|
||||
db.add(
|
||||
AppSetting(
|
||||
key=key,
|
||||
value=str(value),
|
||||
description=meta["description"],
|
||||
is_sensitive=meta["sensitive"],
|
||||
)
|
||||
)
|
||||
|
||||
db.commit()
|
||||
logger.info("配置已更新: %s", key)
|
||||
return True
|
||||
|
||||
|
||||
def list_settings(db: Session, mask_sensitive: bool = True) -> List[Dict[str, Any]]:
|
||||
"""列出所有可编辑配置"""
|
||||
db_settings = {s.key: s for s in db.query(AppSetting).all()}
|
||||
result = []
|
||||
|
||||
for key, meta in EDITABLE_SETTINGS.items():
|
||||
setting = db_settings.get(key)
|
||||
value = setting.value if setting else _get_env_default(key)
|
||||
is_sensitive = meta["sensitive"]
|
||||
|
||||
if is_sensitive and mask_sensitive:
|
||||
display_value = _mask_sensitive(value)
|
||||
is_masked = True
|
||||
else:
|
||||
display_value = value
|
||||
is_masked = False
|
||||
|
||||
result.append({
|
||||
"key": key,
|
||||
"value": display_value,
|
||||
"real_value": value if not mask_sensitive else None,
|
||||
"description": meta["description"],
|
||||
"is_sensitive": is_sensitive,
|
||||
"is_masked": is_masked,
|
||||
"updated_at": setting.updated_at.isoformat() if setting else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def reset_settings(db: Session) -> None:
|
||||
"""将所有配置重置为环境变量默认值"""
|
||||
for key in EDITABLE_SETTINGS:
|
||||
set_setting(db, key, _get_env_default(key))
|
||||
logger.info("配置已重置为环境变量默认值")
|
||||
|
||||
|
||||
def apply_db_settings_to_config(db: Session = None) -> None:
|
||||
"""将数据库中的配置覆盖到全局 settings 对象,重启后生效"""
|
||||
close_db = False
|
||||
if db is None:
|
||||
from database import SessionLocal
|
||||
db = SessionLocal()
|
||||
close_db = True
|
||||
try:
|
||||
for key in EDITABLE_SETTINGS:
|
||||
db_value = get_setting(db, key)
|
||||
if db_value is None or db_value == "":
|
||||
continue
|
||||
field_info = settings.model_fields.get(key)
|
||||
if field_info is None:
|
||||
continue
|
||||
target_type = field_info.annotation
|
||||
try:
|
||||
if target_type is int:
|
||||
converted = int(db_value)
|
||||
elif target_type is float:
|
||||
converted = float(db_value)
|
||||
elif target_type is bool:
|
||||
converted = db_value.lower() in ("true", "1", "yes")
|
||||
elif target_type is Path:
|
||||
converted = Path(db_value)
|
||||
else:
|
||||
converted = db_value
|
||||
setattr(settings, key, converted)
|
||||
logger.debug("已应用配置: %s=%s", key, converted)
|
||||
except Exception as exc:
|
||||
logger.error("应用配置 %s=%s 失败: %s", key, db_value, exc)
|
||||
raise ValueError(f"配置项 {key} 的值无效: {db_value}") from exc
|
||||
finally:
|
||||
if close_db:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user