diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index e21ecb44..7704c788 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -4,7 +4,6 @@ from datetime import datetime from typing import Union, Any import tailer -from dotenv import set_key from fastapi import APIRouter, HTTPException, Depends, Response from fastapi.responses import StreamingResponse @@ -23,6 +22,7 @@ from app.helper.rule import RuleHelper from app.helper.sites import SitesHelper from app.monitor import Monitor from app.scheduler import Scheduler +from app.schemas.types import SystemConfigKey from app.utils.http import RequestUtils from app.utils.system import SystemUtils from version import APP_VERSION @@ -112,19 +112,28 @@ def set_env_setting(env: dict, """ 更新系统环境变量(仅管理员) """ - for k, v in env.items(): - if k == "undefined": - continue - if hasattr(settings, k): - if v == "None": - v = None - setattr(settings, k, v) - if v is None: - v = '' - else: - v = str(v) - set_key(SystemUtils.get_env_path(), k, v) - return schemas.Response(success=True) + result = settings.update_settings(env=env) + # 统计成功和失败的结果 + success_updates = {k: v for k, v in result.items() if v[0]} + failed_updates = {k: v for k, v in result.items() if not v[0]} + + if failed_updates: + return schemas.Response( + success=False, + message="部分配置项更新失败", + data={ + "success_updates": success_updates, + "failed_updates": failed_updates + } + ) + + return schemas.Response( + success=True, + message="所有配置项更新成功", + data={ + "success_updates": success_updates + } + ) @router.get("/progress/{process_type}", summary="实时进度") @@ -173,17 +182,13 @@ def set_setting(key: str, value: Union[list, dict, bool, int, str] = None, 更新系统设置(仅管理员) """ if hasattr(settings, key): - if value == "None": - value = None - setattr(settings, key, value) - if value is None: - value = '' - else: - value = str(value) - set_key(SystemUtils.get_env_path(), key, value) - else: + success, message = settings.update_setting(key=key, value=value) + return schemas.Response(success=success, message=message) + elif key in {item.value for item in SystemConfigKey}: SystemConfigOper().set(key, value) - return schemas.Response(success=True) + return schemas.Response(success=True) + else: + return schemas.Response(success=False, message=f"配置项 '{key}' 不存在") @router.get("/message", summary="实时消息") diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index 94bf52f5..57c3e73b 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -1118,9 +1118,9 @@ class SubscribeChain(ChainBase): """ default_subscribe_key = None if mtype == MediaType.TV: - default_subscribe_key = "DefaultTvSubscribeConfig" + default_subscribe_key = SystemConfigKey.DefaultTvSubscribeConfig.value if mtype == MediaType.MOVIE: - default_subscribe_key = "DefaultMovieSubscribeConfig" + default_subscribe_key = SystemConfigKey.DefaultMovieSubscribeConfig.value # 默认订阅规则 if hasattr(settings, default_subscribe_key): diff --git a/app/core/config.py b/app/core/config.py index 9c70740d..337213e4 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,9 +1,10 @@ import os +import re import secrets import sys import threading from pathlib import Path -from typing import Optional, List, Any, Type, Tuple +from typing import Optional, List, Any, Type, Tuple, Dict from dotenv import set_key from pydantic import BaseSettings, validator, BaseModel @@ -217,24 +218,26 @@ class Settings(BaseSettings, ConfigModel): SystemUtils.copy(self.INNER_CONFIG_PATH / "app.env", app_env_path) @validator("API_TOKEN", pre=True, always=True) - def validate_api_token(cls, v): - if not v: + def validate_api_token(cls, value: Any, field): + if not value or len(value) < 16: new_token = secrets.token_urlsafe(16) - logger.info(f"'API_TOKEN' 未设置,已随机生成新的 API_TOKEN:{new_token}") - set_key(str(SystemUtils.get_env_path()), "API_TOKEN", new_token) + if not value: + logger.info(f"'API_TOKEN' 未设置,已随机生成新的 API_TOKEN:{new_token}") + else: + logger.warning(f"'API_TOKEN' 长度不足 16 个字符,存在安全隐患,已生成新的更复杂的 API_TOKEN:{new_token}") + cls.update_env_config(field, original_value=value or "", converted_value=new_token) return new_token - elif len(v) < 16: - logger.warning("'API_TOKEN' 长度不足 16 个字符,存在安全隐患,建议尽快更换为更复杂的密钥!") - return v + return value @staticmethod - def generic_type_converter(value: Any, expected_type: Type, default: Any, field_name: str) -> Tuple[Any, bool]: + def generic_type_converter(value: Any, original_value: Any, expected_type: Type, default: Any, field_name: str, + raise_exception: bool = False) -> Tuple[Any, bool]: + """ + 通用类型转换函数,根据预期类型转换值。如果转换失败,返回默认值 """ - 通用类型转换函数,根据预期类型转换值。如果转换失败,返回默认值 """ if value is None: return default, False - original_value = value if isinstance(value, str): value = value.strip() @@ -266,6 +269,10 @@ class Settings(BaseSettings, ConfigModel): converted = float(value) return converted, value != original_value elif expected_type is str: + # 清理 value 中所有空白字符的字段 + fields_not_keep_spaces = {"AUTO_DOWNLOAD_USER", "REPO_GITHUB_TOKEN", "PLUGIN_MARKET"} + if field_name in fields_not_keep_spaces: + value = re.sub(r"\s+", "", value) return value, value != original_value # # 后续考虑支持 list 类型的处理 # elif expected_type is list: @@ -277,25 +284,71 @@ class Settings(BaseSettings, ConfigModel): # 可根据需要添加更多类型处理 else: return value, False - except (ValueError, TypeError): + except (ValueError, TypeError) as e: + if raise_exception: + raise ValueError(f"配置项 '{field_name}' 的值 '{value}' 无法转换成正确的类型") from e + logger.error( + f"配置项 '{field_name}' 的值 '{value}' 无法转换成正确的类型,使用默认值 '{default}',错误信息: {e}") return default, True @validator('*', pre=True, always=True) def generic_type_validator(cls, value: Any, field): """ - 通用校验器 + 通用校验器,尝试将配置值转换为期望的类型 """ - converted_value, needs_update = cls.generic_type_converter(value, field.type_, field.default, field.name) + converted_value, needs_update = cls.generic_type_converter(value, value, field.type_, field.default, + field.name) if needs_update: - logger.error(f"字段 '{field.name}' 的值 '{value}' 无效,已使用 '{converted_value}' 进行替换") - if field.name in os.environ: - logger.warning(f"字段 '{field.name}' 已存在于环境变量中,请手动修改") - else: - set_key(SystemUtils.get_env_path(), field.name, - str(converted_value) if converted_value is not None else "") - logger.info(f"字段 '{field.name}' 已由应用修改并写入到 app.env 中") + cls.update_env_config(field, value, converted_value) return converted_value + @staticmethod + def update_env_config(field: Any, original_value: Any, converted_value: Any) -> Tuple[bool, str]: + """ + 更新 env 配置 + """ + is_converted = original_value is not None and original_value != converted_value + if is_converted: + logger.warning(f"配置项 '{field.name}' 的值 '{original_value}' 无效,已替换为 '{converted_value}'") + + if field.name in os.environ: + if is_converted: + message = f"配置项 '{field.name}' 已在环境变量中设置,请手动更新以保持一致性" + logger.warning(message) + return False, message + return True, "" + else: + set_key(SystemUtils.get_env_path(), field.name, str(converted_value) if converted_value is not None else "") + if is_converted: + logger.info(f"配置项 '{field.name}' 已自动修正并写入到 'app.env' 文件") + return True, "" + + def update_setting(self, key: str, value: Any) -> Tuple[bool, str]: + """ + 更新单个配置项 + """ + if not hasattr(self, key): + return False, f"配置项 '{key}' 不存在" + + try: + field = self.__fields__[key] + converted_value, _ = self.generic_type_converter(value, getattr(self, key), field.type_, + field.default, key) + # 如果没有抛出异常,则统一使用 converted_value 进行更新 + setattr(self, key, converted_value) + return self.update_env_config(field, value, converted_value) + except Exception as e: + return False, str(e) + + def update_settings(self, env: Dict[str, Any]) -> Dict[str, Tuple[bool, str]]: + """ + 更新多个配置项 + """ + results = {} + for k, v in env.items(): + results[k] = self.update_setting(k, v) + return results + @property def VERSION_FLAG(self) -> str: """ diff --git a/app/schemas/types.py b/app/schemas/types.py index ebea3ad5..0a1a644e 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -113,6 +113,10 @@ class SystemConfigKey(Enum): UserInstalledPlugins = "UserInstalledPlugins" # 插件安装统计 PluginInstallReport = "PluginInstallReport" + # 默认电影订阅规则 + DefaultMovieSubscribeConfig = "DefaultMovieSubscribeConfig" + # 默认电视剧订阅规则 + DefaultTvSubscribeConfig = "DefaultTvSubscribeConfig" # 处理进度Key字典