feat: 添加启动绑定地址解析功能,支持从配置文件和环境变量迁移

This commit is contained in:
DrSmoothl
2026-04-04 20:13:04 +08:00
parent d87e6ec0bb
commit 2fb911a8d5
11 changed files with 437 additions and 41 deletions

View File

@@ -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:

View File

@@ -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进行旧格式修复。

View File

@@ -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={

View 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)