diff --git a/backend/core/conf.py b/backend/core/conf.py index a9e5cdba..e04e0ee2 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -5,9 +5,10 @@ from re import Pattern from typing import Any, Literal from pydantic import model_validator -from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict from backend.core.path_conf import ENV_EXAMPLE_FILE_PATH, ENV_FILE_PATH +from backend.plugin.settings_source import PluginSettingsSource class Settings(BaseSettings): @@ -16,10 +17,22 @@ class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=ENV_FILE_PATH, env_file_encoding='utf-8', - extra='ignore', + extra='allow', case_sensitive=True, ) + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + """自定义配置源优先级""" + return env_settings, dotenv_settings, PluginSettingsSource(settings_cls) + # .env 当前环境 ENVIRONMENT: Literal['dev', 'prod'] diff --git a/backend/plugin/settings_source.py b/backend/plugin/settings_source.py new file mode 100644 index 00000000..4e4405c9 --- /dev/null +++ b/backend/plugin/settings_source.py @@ -0,0 +1,39 @@ +import os + +from typing import Any + +import rtoml + +from pydantic.fields import FieldInfo +from pydantic_settings import PydanticBaseSettingsSource + +from backend.core.path_conf import PLUGIN_DIR + + +class PluginSettingsSource(PydanticBaseSettingsSource): + """从所有插件的 plugin.toml 加载配置的自定义配置源""" + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """获取单个字段的值""" + # 不在这里实现,使用 __call__ 批量加载 + return None, field_name, False + + def __call__(self) -> dict[str, Any]: + """加载所有插件配置""" + merged_settings: dict[str, Any] = {} + + for item in os.listdir(PLUGIN_DIR): + item_path = PLUGIN_DIR / item + if not os.path.isdir(item_path): + continue + if '__init__.py' not in os.listdir(item_path): + continue + + toml_path = item_path / 'plugin.toml' + if toml_path.exists(): + with open(toml_path, encoding='utf-8') as f: + config = rtoml.load(f) + plugin_settings = config.get('settings', {}) + merged_settings.update(plugin_settings) + + return merged_settings