ForcePilot/backend/package/yuxi/channel/config/migration.py
Kris 4962d1f6f8 feat(channel/config): 新增完整的配置模块实现
新增了channel目录下的配置相关模块,包含默认配置、配置迁移、配置差异计算、文件加载器、重载计划、重载器以及配置校验功能,完善了配置管理体系
2026-05-21 10:24:03 +08:00

165 lines
4.6 KiB
Python

"""配置版本迁移 — schema_version + 迁移规则 + 自动执行 + 回滚"""
from __future__ import annotations
import copy
import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
CURRENT_SCHEMA_VERSION = 1
ConfigDict = dict[str, Any]
MigrationFn = Callable[[ConfigDict], ConfigDict]
@dataclass
class MigrationRule:
from_version: int
to_version: int
description: str
migrate: MigrationFn
_migration_registry: dict[int, MigrationRule] = {}
def register_migration(from_version: int, to_version: int, description: str):
"""装饰器:注册配置迁移规则"""
if to_version != from_version + 1:
raise ValueError(
f"迁移规则版本不连续: v{from_version} → v{to_version}, "
f"期望 to_version={from_version + 1}"
)
def decorator(fn: MigrationFn) -> MigrationFn:
existing = _migration_registry.get(from_version)
if existing is not None:
logger.warning(
"重复注册迁移规则 v%d → v%d: 旧规则 (%s) 将被覆盖为新规则 (%s)",
from_version,
to_version,
existing.description,
description,
)
_migration_registry[from_version] = MigrationRule(
from_version=from_version,
to_version=to_version,
description=description,
migrate=fn,
)
logger.info(
"注册配置迁移规则: v%d → v%d (%s)",
from_version,
to_version,
description,
)
return fn
return decorator
def get_migration_path(current_version: int) -> list[MigrationRule]:
if current_version >= CURRENT_SCHEMA_VERSION:
return []
path: list[MigrationRule] = []
version = current_version
while version < CURRENT_SCHEMA_VERSION:
rule = _migration_registry.get(version)
if rule is None:
logger.warning("缺少迁移规则: v%d → v%d", version, version + 1)
break
path.append(rule)
version = rule.to_version
return path
def migrate_config(
config: ConfigDict,
current_version: int | None = None,
*,
dry_run: bool = False,
) -> tuple[ConfigDict, int, list[MigrationRule]]:
"""执行配置迁移,返回 (新配置, 新版本号, 已应用的迁移规则列表)。
失败时自动恢复到原始配置(回滚)。
dry_run=True 时只返回迁移后的配置预览,不实际修改任何数据。
"""
if current_version is None:
version = config.get("schema_version", 0)
else:
version = current_version
path = get_migration_path(version)
if not path:
return config, version, []
original = copy.deepcopy(config)
working = copy.deepcopy(config)
applied: list[MigrationRule] = []
last_version = version
for rule in path:
try:
working = rule.migrate(working)
working["schema_version"] = rule.to_version
last_version = rule.to_version
applied.append(rule)
logger.info(
"配置迁移成功: v%d → v%d (%s)%s",
rule.from_version,
rule.to_version,
rule.description,
" [dry-run]" if dry_run else "",
)
except Exception:
logger.exception(
"配置迁移失败: v%d → v%d (%s), 回滚到 v%d",
rule.from_version,
rule.to_version,
rule.description,
version,
)
return original, version, applied
if dry_run:
logger.info(
"迁移 dry-run 完成: v%d → v%d, 共 %d",
version,
last_version,
len(applied),
)
return working, last_version, applied
return working, last_version, applied
def backup_config(config: ConfigDict, reason: str = "迁移前备份") -> ConfigDict:
logger.debug("备份配置: %s", reason)
return copy.deepcopy(config)
# ── 内置迁移规则 ────────────────────────────────────────
@register_migration(
from_version=0,
to_version=1,
description="初始化 schema_version 字段",
)
def migration_v0_to_v1(config: ConfigDict) -> ConfigDict:
result = dict(config)
if "approval" not in result:
result["approval"] = {
"enabled": False,
"approvers": [],
"auto_approve_internal": True,
}
return result