feat: 添加启动绑定地址解析功能,支持从配置文件和环境变量迁移
This commit is contained in:
@@ -12,7 +12,7 @@ import tomlkit
|
||||
from .config_base import AttributeData, ConfigBase, Field
|
||||
from .config_utils import compare_versions, output_config_changes, recursive_parse_item_to_table
|
||||
from .file_watcher import FileChange, FileWatcher
|
||||
from .legacy_migration import try_migrate_legacy_bot_config_dict
|
||||
from .legacy_migration import migrate_legacy_bind_env_to_bot_config_dict, try_migrate_legacy_bot_config_dict
|
||||
from .model_configs import APIProvider, ModelInfo, ModelTaskConfig
|
||||
from .official_configs import (
|
||||
BotConfig,
|
||||
@@ -55,7 +55,7 @@ CONFIG_DIR: Path = PROJECT_ROOT / "config"
|
||||
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
|
||||
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
|
||||
MMC_VERSION: str = "1.0.0"
|
||||
CONFIG_VERSION: str = "8.3.0"
|
||||
CONFIG_VERSION: str = "8.3.1"
|
||||
MODEL_CONFIG_VERSION: str = "1.13.1"
|
||||
|
||||
logger = get_logger("config")
|
||||
@@ -472,6 +472,11 @@ def load_config_from_file(
|
||||
old_ver: str = inner_version
|
||||
config_data.remove("inner") # 移除 inner 部分,避免干扰后续处理
|
||||
config_data = config_data.unwrap() # 转换为普通字典,方便后续处理
|
||||
if config_path.name == "bot_config.toml" and config_class.__name__ == "Config":
|
||||
env_migration = migrate_legacy_bind_env_to_bot_config_dict(config_data)
|
||||
if env_migration.migrated:
|
||||
logger.warning(f"检测到旧版环境变量绑定配置,已迁移到主配置: {env_migration.reason}")
|
||||
config_data = env_migration.data
|
||||
# 保留一份“干净”的原始数据副本,避免第一次 from_dict 过程中对 dict 的就地修改
|
||||
original_data: dict[str, Any] = copy.deepcopy(config_data)
|
||||
try:
|
||||
|
||||
@@ -14,6 +14,8 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import os
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("legacy_migration")
|
||||
@@ -38,6 +40,41 @@ def _as_list(x: Any) -> Optional[list[Any]]:
|
||||
return x if isinstance(x, list) else None
|
||||
|
||||
|
||||
def _parse_host_env(value: Any) -> Optional[str]:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
normalized_value = value.strip()
|
||||
return normalized_value or None
|
||||
|
||||
|
||||
def _parse_port_env(value: Any) -> Optional[int]:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
|
||||
try:
|
||||
normalized_value = int(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
if normalized_value <= 0 or normalized_value > 65535:
|
||||
return None
|
||||
return normalized_value
|
||||
|
||||
|
||||
def _migrate_env_value(section: dict[str, Any], key: str, parsed_env_value: Any, default_value: Any) -> bool:
|
||||
if parsed_env_value is None:
|
||||
return False
|
||||
|
||||
current_value = section.get(key)
|
||||
if current_value == parsed_env_value:
|
||||
return False
|
||||
if key in section and current_value != default_value:
|
||||
return False
|
||||
|
||||
section[key] = parsed_env_value
|
||||
return True
|
||||
|
||||
|
||||
def _parse_triplet_target(s: str) -> Optional[dict[str, str]]:
|
||||
"""
|
||||
解析 "platform:id:type" -> {platform,item_id,rule_type}
|
||||
@@ -236,6 +273,43 @@ def _migrate_extra_prompt_list(exp: dict[str, Any], key: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def migrate_legacy_bind_env_to_bot_config_dict(data: dict[str, Any]) -> MigrationResult:
|
||||
"""将旧版环境变量中的绑定地址迁移到主配置结构。"""
|
||||
|
||||
migrated_any = False
|
||||
reasons: list[str] = []
|
||||
|
||||
main_host_env = _parse_host_env(os.getenv("HOST"))
|
||||
main_port_env = _parse_port_env(os.getenv("PORT"))
|
||||
maim_message = _as_dict(data.get("maim_message"))
|
||||
if maim_message is None and (main_host_env is not None or main_port_env is not None):
|
||||
maim_message = {}
|
||||
data["maim_message"] = maim_message
|
||||
|
||||
if maim_message is not None and _migrate_env_value(maim_message, "ws_server_host", main_host_env, "127.0.0.1"):
|
||||
migrated_any = True
|
||||
reasons.append("HOST->maim_message.ws_server_host")
|
||||
if maim_message is not None and _migrate_env_value(maim_message, "ws_server_port", main_port_env, 8080):
|
||||
migrated_any = True
|
||||
reasons.append("PORT->maim_message.ws_server_port")
|
||||
|
||||
webui_host_env = _parse_host_env(os.getenv("WEBUI_HOST"))
|
||||
webui_port_env = _parse_port_env(os.getenv("WEBUI_PORT"))
|
||||
webui = _as_dict(data.get("webui"))
|
||||
if webui is None and (webui_host_env is not None or webui_port_env is not None):
|
||||
webui = {}
|
||||
data["webui"] = webui
|
||||
|
||||
if webui is not None and _migrate_env_value(webui, "host", webui_host_env, "127.0.0.1"):
|
||||
migrated_any = True
|
||||
reasons.append("WEBUI_HOST->webui.host")
|
||||
if webui is not None and _migrate_env_value(webui, "port", webui_port_env, 8001):
|
||||
migrated_any = True
|
||||
reasons.append("WEBUI_PORT->webui.port")
|
||||
|
||||
return MigrationResult(data=data, migrated=migrated_any, reason=",".join(reasons))
|
||||
|
||||
|
||||
def try_migrate_legacy_bot_config_dict(data: dict[str, Any]) -> MigrationResult:
|
||||
"""
|
||||
尝试对“总配置 bot_config.toml”的 dict(已 unwrap)进行旧格式修复。
|
||||
|
||||
@@ -1414,6 +1414,24 @@ class WebUIConfig(ConfigBase):
|
||||
)
|
||||
"""是否启用WebUI"""
|
||||
|
||||
host: str = Field(
|
||||
default="127.0.0.1",
|
||||
json_schema_extra={
|
||||
"x-widget": "input",
|
||||
"x-icon": "globe",
|
||||
},
|
||||
)
|
||||
"""WebUI 绑定主机地址"""
|
||||
|
||||
port: int = Field(
|
||||
default=8001,
|
||||
json_schema_extra={
|
||||
"x-widget": "input",
|
||||
"x-icon": "hash",
|
||||
},
|
||||
)
|
||||
"""WebUI 绑定端口"""
|
||||
|
||||
mode: Literal["development", "production"] = Field(
|
||||
default="production",
|
||||
json_schema_extra={
|
||||
|
||||
135
src/config/startup_bindings.py
Normal file
135
src/config/startup_bindings.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
import sys
|
||||
|
||||
import tomlkit
|
||||
|
||||
|
||||
PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute().resolve()
|
||||
CONFIG_DIR: Path = PROJECT_ROOT / "config"
|
||||
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BindAddress:
|
||||
"""启动阶段使用的绑定地址。"""
|
||||
|
||||
host: str
|
||||
port: int
|
||||
|
||||
|
||||
_DEFAULT_MAIN_BIND_ADDRESS = BindAddress(host="127.0.0.1", port=8080)
|
||||
_DEFAULT_WEBUI_BIND_ADDRESS = BindAddress(host="127.0.0.1", port=8001)
|
||||
|
||||
|
||||
def _as_mapping(value: Any) -> Optional[Mapping[str, Any]]:
|
||||
return value if isinstance(value, Mapping) else None
|
||||
|
||||
|
||||
def _normalize_host(value: Any, default_host: str) -> str:
|
||||
if not isinstance(value, str):
|
||||
return default_host
|
||||
|
||||
normalized_host = value.strip()
|
||||
return normalized_host or default_host
|
||||
|
||||
|
||||
def _normalize_port(value: Any, default_port: int) -> int:
|
||||
if isinstance(value, bool):
|
||||
return default_port
|
||||
|
||||
try:
|
||||
normalized_port = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default_port
|
||||
|
||||
if normalized_port <= 0 or normalized_port > 65535:
|
||||
return default_port
|
||||
return normalized_port
|
||||
|
||||
|
||||
def _load_bootstrap_config_dict(config_path: Path = BOT_CONFIG_PATH) -> Dict[str, Any]:
|
||||
"""读取启动阶段需要的最小配置,不依赖完整 ConfigManager。"""
|
||||
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as file_obj:
|
||||
config_data = tomlkit.load(file_obj).unwrap()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
if not isinstance(config_data, dict):
|
||||
return {}
|
||||
return config_data
|
||||
|
||||
|
||||
def _resolve_bind_address_from_section(
|
||||
section: Mapping[str, Any],
|
||||
host_key: str,
|
||||
port_key: str,
|
||||
default_address: BindAddress,
|
||||
) -> BindAddress:
|
||||
return BindAddress(
|
||||
host=_normalize_host(section.get(host_key), default_address.host),
|
||||
port=_normalize_port(section.get(port_key), default_address.port),
|
||||
)
|
||||
|
||||
|
||||
def _get_loaded_global_config() -> Optional[Any]:
|
||||
config_module = sys.modules.get("src.config.config")
|
||||
if config_module is None:
|
||||
return None
|
||||
return getattr(config_module, "global_config", None)
|
||||
|
||||
|
||||
def get_startup_main_bind_address(config_path: Path = BOT_CONFIG_PATH) -> BindAddress:
|
||||
"""读取主程序消息服务绑定地址。"""
|
||||
|
||||
config_data = _load_bootstrap_config_dict(config_path)
|
||||
maim_message_config = _as_mapping(config_data.get("maim_message")) or {}
|
||||
return _resolve_bind_address_from_section(
|
||||
maim_message_config,
|
||||
host_key="ws_server_host",
|
||||
port_key="ws_server_port",
|
||||
default_address=_DEFAULT_MAIN_BIND_ADDRESS,
|
||||
)
|
||||
|
||||
|
||||
def get_startup_webui_bind_address(config_path: Path = BOT_CONFIG_PATH) -> BindAddress:
|
||||
"""读取 WebUI 绑定地址。"""
|
||||
|
||||
config_data = _load_bootstrap_config_dict(config_path)
|
||||
webui_config = _as_mapping(config_data.get("webui")) or {}
|
||||
return _resolve_bind_address_from_section(
|
||||
webui_config,
|
||||
host_key="host",
|
||||
port_key="port",
|
||||
default_address=_DEFAULT_WEBUI_BIND_ADDRESS,
|
||||
)
|
||||
|
||||
|
||||
def resolve_main_bind_address(config_path: Path = BOT_CONFIG_PATH) -> BindAddress:
|
||||
"""优先读取已初始化的主配置,否则回退到启动阶段配置读取。"""
|
||||
|
||||
global_config = _get_loaded_global_config()
|
||||
if global_config is not None:
|
||||
return BindAddress(
|
||||
host=global_config.maim_message.ws_server_host,
|
||||
port=global_config.maim_message.ws_server_port,
|
||||
)
|
||||
return get_startup_main_bind_address(config_path)
|
||||
|
||||
|
||||
def resolve_webui_bind_address(config_path: Path = BOT_CONFIG_PATH) -> BindAddress:
|
||||
"""优先读取已初始化的主配置,否则回退到启动阶段配置读取。"""
|
||||
|
||||
global_config = _get_loaded_global_config()
|
||||
if global_config is not None:
|
||||
return BindAddress(
|
||||
host=global_config.webui.host,
|
||||
port=global_config.webui.port,
|
||||
)
|
||||
return get_startup_webui_bind_address(config_path)
|
||||
Reference in New Issue
Block a user