Files
xiaozhi-esp32-server/main/xiaozhi-server/config/config_loader.py
T
2025-12-29 00:09:26 +08:00

606 lines
21 KiB
Python

import os
import yaml
from collections.abc import Mapping
from typing import Any, Dict, Optional, Type, TypeVar, Union, get_type_hints, get_origin, get_args
from dataclasses import dataclass, field, fields, MISSING
import inspect
from config.manage_api_client import init_service, get_server_config, get_agent_models
T = TypeVar('T')
class ConfigDict(dict):
"""增强的配置字典,支持点号访问和嵌套获取"""
def __init__(self, data: Dict[str, Any] = None):
super().__init__()
if data:
for key, value in data.items():
if isinstance(value, dict):
self[key] = ConfigDict(value)
else:
self[key] = value
def __getattr__(self, key: str) -> Any:
"""支持点号访问"""
try:
return self[key]
except KeyError:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'")
def __setattr__(self, key: str, value: Any) -> None:
"""支持点号设置"""
self[key] = value
def __getitem__(self, key: str) -> Any:
"""重写[]访问,支持嵌套路径,找不到抛出KeyError"""
if '.' in key:
keys = key.split('.')
current = self
for k in keys:
if not isinstance(current, (dict, ConfigDict)):
raise KeyError(f"Cannot access '{k}' on non-dict object")
current = super(ConfigDict, current).__getitem__(k)
return current
return super().__getitem__(key)
def get(self, key: str, default: Any = None) -> Any:
"""重写get方法,支持嵌套路径"""
try:
return self[key]
except KeyError:
return default
def __setitem__(self, key: str, value: Any) -> None:
"""重写[]设置,支持嵌套路径"""
if '.' in key:
keys = key.split('.')
current = self
for k in keys[:-1]:
if k not in current:
current[k] = ConfigDict()
elif not isinstance(current[k], (dict, ConfigDict)):
current[k] = ConfigDict()
current = current[k]
if isinstance(value, dict) and not isinstance(value, ConfigDict):
value = ConfigDict(value)
super(ConfigDict, current).__setitem__(keys[-1], value)
else:
if isinstance(value, dict) and not isinstance(value, ConfigDict):
value = ConfigDict(value)
super().__setitem__(key, value)
class ConfigField:
"""配置字段,模仿dataclass的field功能"""
def __init__(self, default=MISSING, default_factory=MISSING, prefix: str = None):
self.default = default
self.default_factory = default_factory
self.prefix = prefix
if default is not MISSING and default_factory is not MISSING:
raise ValueError("Cannot specify both default and default_factory")
def config_field(default=MISSING, default_factory=MISSING, prefix: str = None):
"""创建配置字段"""
return ConfigField(default, default_factory, prefix)
def _is_config_class(cls: Type) -> bool:
"""检查类是否是ConfigurationProperties装饰的配置类"""
return hasattr(cls, '_config_prefix') and hasattr(cls, '_inject_config')
def _create_nested_config_instance(config_class: Type, config: ConfigDict, config_path: str):
"""创建嵌套配置类实例"""
try:
# 获取嵌套配置数据 - 直接传递整个config,让嵌套类自己处理前缀
# 因为嵌套类有自己的prefix,它会从config中正确提取数据
return config_class(config)
except Exception as e:
# 如果创建失败,返回None或抛出更详细的错误
raise ValueError(f"Failed to create nested config instance for {config_class.__name__} at path '{config_path}': {e}")
def ConfigurationProperties(prefix: str = "", auto_inject: bool = True):
"""
配置属性装饰器,模仿Spring Boot的@ConfigurationProperties
Args:
prefix: 配置前缀,如 'server.database'
auto_inject: 是否自动注入配置
"""
def decorator(cls: Type[T]) -> Type[T]:
if not inspect.isclass(cls):
raise TypeError("ConfigurationProperties can only be applied to classes")
# 保存原始的__init__方法
original_init = cls.__init__ if hasattr(cls, '__init__') else None
# 获取类的类型注解
type_hints = get_type_hints(cls)
def new_init(self, config: ConfigDict = None, **kwargs):
# 如果有原始的__init__,先调用它
if original_init and original_init is not object.__init__:
try:
original_init(self)
except TypeError:
# 如果原始__init__不接受参数,忽略
pass
if config is None:
# 如果没有传入config,尝试从全局获取
config = getattr(self.__class__, '_global_config', None)
if config is None:
return
# 注入配置
self._inject_config(config, prefix, **kwargs)
def _inject_config(self, config: ConfigDict, config_prefix: str = "", **overrides):
"""注入配置到实例属性"""
# 处理类属性
for attr_name in dir(self.__class__):
if attr_name.startswith('_'):
continue
attr_value = getattr(self.__class__, attr_name)
if isinstance(attr_value, ConfigField):
# 确定配置路径
field_prefix = attr_value.prefix or config_prefix
config_path = f"{field_prefix}.{attr_name}" if field_prefix else attr_name
# 从overrides或config获取值
if attr_name in overrides:
value = overrides[attr_name]
else:
# 检查是否有类型注解,如果是嵌套配置类则特殊处理
attr_type = type_hints.get(attr_name)
if attr_type and inspect.isclass(attr_type) and _is_config_class(attr_type):
# 嵌套配置类,创建实例
try:
value = _create_nested_config_instance(attr_type, config, config_path)
except ValueError as e:
# 如果创建失败,使用默认值
print(f"Warning: {e}")
if attr_value.default_factory is not MISSING:
value = attr_value.default_factory()
else:
value = attr_value.default
else:
# 普通类型,使用默认值逻辑
if attr_value.default_factory is not MISSING:
default_val = attr_value.default_factory()
else:
default_val = attr_value.default
value = config.get(config_path, default_val)
setattr(self, attr_name, value)
# 处理类型注解的属性
for attr_name, attr_type in type_hints.items():
if hasattr(self, attr_name):
continue # 已经通过ConfigField处理过了
config_path = f"{config_prefix}.{attr_name}" if config_prefix else attr_name
if attr_name in overrides:
value = overrides[attr_name]
else:
# 检查是否是嵌套的ConfigurationProperties类
if inspect.isclass(attr_type) and _is_config_class(attr_type):
# 创建嵌套配置类实例
try:
value = _create_nested_config_instance(attr_type, config, config_path)
except ValueError as e:
# 如果创建失败,使用None或默认值
print(f"Warning: {e}")
value = None
else:
# 普通类型,直接从配置获取
value = config.get(config_path)
if value is not None:
setattr(self, attr_name, value)
# 添加方法到类
cls.__init__ = new_init
cls._inject_config = _inject_config
cls._config_prefix = prefix
# 添加类方法用于设置全局配置
@classmethod
def set_global_config(cls, config: ConfigDict):
cls._global_config = config
cls.set_global_config = set_global_config
return cls
return decorator
def get_project_dir():
"""获取项目根目录"""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/"
def read_config(config_path):
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
def load_config():
"""加载配置文件"""
from core.utils.cache.manager import cache_manager, CacheType
# 检查缓存
cached_config = cache_manager.get(CacheType.CONFIG, "main_config")
if cached_config is not None:
# 确保返回的是ConfigDict类型
if not isinstance(cached_config, ConfigDict):
cached_config = ConfigDict(cached_config)
return cached_config
default_config_path = get_project_dir() + "config.yaml"
custom_config_path = get_project_dir() + "data/.config.yaml"
# 加载默认配置
default_config = read_config(default_config_path)
custom_config = read_config(custom_config_path)
if custom_config.get("manager-api", {}).get("url"):
import asyncio
try:
loop = asyncio.get_running_loop()
# 如果已经在事件循环中,使用异步版本
config = asyncio.run_coroutine_threadsafe(
get_config_from_api_async(custom_config), loop
).result()
except RuntimeError:
# 如果不在事件循环中(启动时),创建新的事件循环
config = asyncio.run(get_config_from_api_async(custom_config))
else:
# 合并配置
config = merge_configs(default_config, custom_config)
# 转换为ConfigDict
config = ConfigDict(config)
# 初始化目录
ensure_directories(config)
# 缓存配置
cache_manager.set(CacheType.CONFIG, "main_config", config)
return config
async def get_config_from_api_async(config):
"""从Java API获取配置(异步版本)"""
# 初始化API客户端
init_service(config)
# 获取服务器配置
config_data = await get_server_config()
if config_data is None:
raise Exception("Failed to fetch server config from API")
config_data["read_config_from_api"] = True
config_data["manager-api"] = {
"url": config["manager-api"].get("url", ""),
"secret": config["manager-api"].get("secret", ""),
}
auth_enabled = config_data.get("server", {}).get("auth", {}).get("enabled", False)
# server的配置以本地为准
if config.get("server"):
config_data["server"] = {
"ip": config["server"].get("ip", ""),
"port": config["server"].get("port", ""),
"http_port": config["server"].get("http_port", ""),
"vision_explain": config["server"].get("vision_explain", ""),
"auth_key": config["server"].get("auth_key", ""),
}
config_data["server"]["auth"] = {"enabled": auth_enabled}
# 如果服务器没有prompt_template,则从本地配置读取
if not config_data.get("prompt_template"):
config_data["prompt_template"] = config.get("prompt_template")
return ConfigDict(config_data)
async def get_private_config_from_api(config, device_id, client_id):
"""从Java API获取私有配置"""
return await get_agent_models(device_id, client_id, config["selected_module"])
def ensure_directories(config):
"""确保所有配置路径存在"""
dirs_to_create = set()
project_dir = get_project_dir() # 获取项目根目录
# 日志文件目录
log_dir = config.get("log", {}).get("log_dir", "tmp")
dirs_to_create.add(os.path.join(project_dir, log_dir))
# ASR/TTS模块输出目录
for module in ["ASR", "TTS"]:
if config.get(module) is None:
continue
for provider in config.get(module, {}).values():
output_dir = provider.get("output_dir", "")
if output_dir:
dirs_to_create.add(output_dir)
# 根据selected_module创建模型目录
selected_modules = config.get("selected_module", {})
for module_type in ["ASR", "LLM", "TTS"]:
selected_provider = selected_modules.get(module_type)
if not selected_provider:
continue
if config.get(module) is None:
continue
if config.get(selected_provider) is None:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get("output_dir")
if output_dir:
full_model_dir = os.path.join(project_dir, output_dir)
dirs_to_create.add(full_model_dir)
# 统一创建目录(保留原data目录创建)
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
except PermissionError:
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
def merge_configs(default_config, custom_config):
"""
递归合并配置,custom_config优先级更高
Args:
default_config: 默认配置
custom_config: 用户自定义配置
Returns:
合并后的配置
"""
if not isinstance(default_config, Mapping) or not isinstance(
custom_config, Mapping
):
return custom_config
merged = dict(default_config)
for key, value in custom_config.items():
if (
key in merged
and isinstance(merged[key], Mapping)
and isinstance(value, Mapping)
):
merged[key] = merge_configs(merged[key], value)
else:
merged[key] = value
return merged
# 导出主要的类和函数
__all__ = [
'ConfigDict',
'ConfigField',
'config_field',
'ConfigurationProperties',
'load_config',
'get_project_dir',
'merge_configs'
]
# 配置类定义
@ConfigurationProperties(prefix="server.database")
class DatabaseConfig:
"""数据库配置类"""
host: str = config_field(default="localhost")
port: int = config_field(default=3306)
username: str = config_field(default="root")
password: str = config_field(default="")
database: str = config_field(default="xiaozhi")
@ConfigurationProperties(prefix="server.redis")
class RedisConfig:
"""Redis配置类"""
host: str = config_field(default="localhost")
port: int = config_field(default=6379)
password: str = config_field(default="")
db: int = config_field(default=0)
@ConfigurationProperties(prefix="mqtt_server")
class MQTTServerConfig:
"""MQTT服务器配置类"""
enabled: bool = config_field(default=False)
host: str = config_field(default="0.0.0.0")
port: int = config_field(default=1883)
udp_port: int = config_field(default=1883)
public_ip: str = config_field(default="localhost")
max_connections: int = config_field(default=1000)
heartbeat_interval: int = config_field(default=30)
max_payload_size: int = config_field(default=8192)
@ConfigurationProperties(prefix="server")
class ServerConfig:
"""服务器配置类"""
ip: str = config_field(default="0.0.0.0")
port: int = config_field(default=8080)
http_port: int = config_field(default=8081)
auth_key: str = config_field(default="")
vision_explain: str = config_field(default="")
# 嵌套配置类
database: DatabaseConfig = config_field(default_factory=lambda: DatabaseConfig())
redis: RedisConfig = config_field(default_factory=lambda: RedisConfig())
mqtt_server: MQTTServerConfig = config_field(default_factory=lambda: MQTTServerConfig())
@ConfigurationProperties(prefix="asr.whisper")
class WhisperConfig:
"""Whisper ASR配置类"""
model: str = config_field(default="base")
language: str = config_field(default="zh")
device: str = config_field(default="cpu")
@ConfigurationProperties(prefix="asr")
class ASRConfig:
"""ASR配置类"""
provider: str = config_field(default="whisper")
# 嵌套配置
whisper: WhisperConfig = config_field(default_factory=lambda: WhisperConfig())
@ConfigurationProperties(prefix="selected_module")
class SelectedModuleConfig:
"""选中模块配置类"""
ASR: str = config_field(default="")
TTS: str = config_field(default="")
LLM: str = config_field(default="")
VLLM: str = config_field(default="")
VAD: str = config_field(default="")
Memory: str = config_field(default="")
Intent: str = config_field(default="")
@ConfigurationProperties(prefix="log")
class LogConfig:
"""日志配置类"""
log_dir: str = config_field(default="tmp")
level: str = config_field(default="INFO")
@ConfigurationProperties(prefix="protocols")
class ProtocolConfig:
"""协议配置类"""
enabled_protocols: list = config_field(default_factory=lambda: ["websocket"])
websocket_enabled: bool = config_field(default=True)
mqtt_enabled: bool = config_field(default=False)
@ConfigurationProperties(prefix="")
class MainConfig:
"""主配置类,包含常用的顶级配置"""
read_config_from_api: bool = config_field(default=False)
exit_commands: list = config_field(default_factory=list)
close_connection_no_voice_time: int = config_field(default=120)
xiaozhi: str = config_field(default="")
prompt: str = config_field(default="")
delete_audio: bool = config_field(default=True)
# 协议配置
protocols: ProtocolConfig = config_field(default_factory=lambda: ProtocolConfig())
@ConfigurationProperties(prefix="voiceprint")
class VoiceprintConfig:
"""声纹配置类"""
enabled: bool = config_field(default=False)
model_path: str = config_field(default="")
threshold: float = config_field(default=0.5)
# 全局配置实例
_global_config_dict: ConfigDict = None
_server_config: ServerConfig = None
_selected_module_config: SelectedModuleConfig = None
_log_config: LogConfig = None
_main_config: MainConfig = None
_voiceprint_config: VoiceprintConfig = None
_mqtt_server_config: MQTTServerConfig = None
_protocol_config: ProtocolConfig = None
def get_config_instance(config_class: Type[T]) -> T:
"""获取配置类实例的工厂方法"""
global _global_config_dict
if _global_config_dict is None:
_global_config_dict = load_config()
return config_class(_global_config_dict)
def get_server_config() -> ServerConfig:
"""获取服务器配置实例"""
global _server_config
if _server_config is None:
_server_config = get_config_instance(ServerConfig)
return _server_config
def get_selected_module_config() -> SelectedModuleConfig:
"""获取选中模块配置实例"""
global _selected_module_config
if _selected_module_config is None:
_selected_module_config = get_config_instance(SelectedModuleConfig)
return _selected_module_config
def get_log_config() -> LogConfig:
"""获取日志配置实例"""
global _log_config
if _log_config is None:
_log_config = get_config_instance(LogConfig)
return _log_config
def get_main_config() -> MainConfig:
"""获取主配置实例"""
global _main_config
if _main_config is None:
_main_config = get_config_instance(MainConfig)
return _main_config
def get_voiceprint_config() -> VoiceprintConfig:
"""获取声纹配置实例"""
global _voiceprint_config
if _voiceprint_config is None:
_voiceprint_config = get_config_instance(VoiceprintConfig)
return _voiceprint_config
def get_mqtt_server_config() -> MQTTServerConfig:
"""获取MQTT服务器配置实例"""
global _mqtt_server_config
if _mqtt_server_config is None:
_mqtt_server_config = get_config_instance(MQTTServerConfig)
return _mqtt_server_config
def get_protocol_config() -> ProtocolConfig:
"""获取协议配置实例"""
global _protocol_config
if _protocol_config is None:
_protocol_config = get_config_instance(ProtocolConfig)
return _protocol_config
def refresh_config():
"""刷新所有配置实例"""
global _global_config_dict, _server_config, _selected_module_config, _log_config, _main_config, _voiceprint_config, _mqtt_server_config, _protocol_config
_global_config_dict = None
_server_config = None
_selected_module_config = None
_log_config = None
_main_config = None
_voiceprint_config = None
_mqtt_server_config = None
_protocol_config = None