from __future__ import annotations import logging from enum import Enum from typing import Any, Dict, List, Mapping, Type from pydantic import BaseModel, ConfigDict, Field def resolve_domain_option_config( domain_option_model: Type[BaseModel] | None, domain_option: Mapping[str, Any] | None, ) -> Dict[str, Any]: if domain_option_model is None: return dict(domain_option or {}) return domain_option_model.model_validate(domain_option or {}).model_dump() class OrphanAction(str, Enum): CREATE_REMOTE = "create_remote" CREATE_LOCAL = "create_local" DELETE_LOCAL = "delete_local" DELETE_REMOTE = "delete_remote" NONE = "none" class UpdateDirection(str, Enum): PUSH = "push" PULL = "pull" NONE = "none" class PreBindDataIdPair(BaseModel): model_config = ConfigDict(validate_assignment=True, extra="forbid") local_data_id: str remote_data_id: str class StrategyConfig(BaseModel): model_config = ConfigDict(validate_assignment=True, extra="forbid") auto_bind: bool = True auto_bind_fields: List[str] = Field(default_factory=list) pre_bind_data_id: List[PreBindDataIdPair] = Field(default_factory=list) depend_fields: Dict[str, str] = Field(default_factory=dict) local_orphan_action: OrphanAction = OrphanAction.NONE remote_orphan_action: OrphanAction = OrphanAction.NONE update_direction: UpdateDirection = UpdateDirection.PUSH # post-check 一致性比较时忽略的顶层字段。 # 只影响最终一致性评估,不影响 create/update 的差异检测。 post_check_ignore_fields: List[str] = Field(default_factory=list) # post-check 一致性比较时,忽略列表型字段内各元素的特定子键。 # 格式: {字段名: [子键, ...]},如 {"plan_node": ["is_audit"]}。 # 用于排除后端硬写死、永远与本地不一致的字段,避免误报。 post_check_ignore_list_item_fields: Dict[str, List[str]] = Field(default_factory=dict) # schema diff / validate 时忽略的顶层字段。 compare_ignore_fields: List[str] = Field(default_factory=list) # 当 auto-bind 命中“多对一”候选时,允许稳定选择一条进行绑定,剩余候选按 orphan/create 流程继续处理。 # 默认关闭,避免在一般场景下引入隐式匹配。 allow_many_to_one_auto_bind: bool = False # 是否跳过该类型的 create/update 阶段。 skip_sync: bool = False # 是否跳过该类型的 post-check reload 与一致性校验。 skip_post_check: bool = False # 领域策略运行参数,由各 strategy 的 domain_option_model 负责解释与校验。 domain_option: Dict[str, Any] = Field(default_factory=dict) def log_logic_warnings(self, *, node_type: str, logger: logging.Logger) -> None: direction = self.update_direction local_action = self.local_orphan_action remote_action = self.remote_orphan_action if direction == UpdateDirection.PUSH: if local_action == OrphanAction.CREATE_LOCAL: logger.warning( f"[{node_type}] Config inconsistency: " f"update_direction=PUSH but local_orphan_action=CREATE_LOCAL (本地创建)。" f"应使用 CREATE_REMOTE (推送到远程)" ) if remote_action == OrphanAction.CREATE_LOCAL: logger.warning( f"[{node_type}] Config inconsistency: " f"update_direction=PUSH but remote_orphan_action=CREATE_LOCAL (本地创建)。" f"PUSH 模式应设为 NONE 或 DELETE_REMOTE" ) elif direction == UpdateDirection.PULL: if local_action == OrphanAction.CREATE_REMOTE: logger.warning( f"[{node_type}] Config inconsistency: " f"update_direction=PULL but local_orphan_action=CREATE_REMOTE (推送到远程)。" f"PULL 模式应设为 NONE 或 DELETE_LOCAL" ) if remote_action in {OrphanAction.CREATE_REMOTE, OrphanAction.DELETE_LOCAL}: logger.warning( f"[{node_type}] Config inconsistency: " f"update_direction=PULL but remote_orphan_action={remote_action}。" f"PULL 模式建议使用 CREATE_LOCAL 或 NONE" ) elif direction == UpdateDirection.NONE: if local_action != OrphanAction.NONE: logger.warning( f"[{node_type}] Config inconsistency: " f"update_direction=NONE but local_orphan_action={local_action}。" f"应设为 NONE" ) if remote_action != OrphanAction.NONE: logger.warning( f"[{node_type}] Config inconsistency: " f"update_direction=NONE but remote_orphan_action={remote_action}。" f"应设为 NONE" ) class StrategyRuntimeConfig(BaseModel): """Pipeline 层策略运行配置(按 node_type)。""" model_config = ConfigDict(validate_assignment=True, extra="forbid") node_type: str config: StrategyConfig = Field(default_factory=StrategyConfig) class ConfigPresets: @staticmethod def first_sync() -> StrategyConfig: return StrategyConfig( local_orphan_action=OrphanAction.CREATE_REMOTE, remote_orphan_action=OrphanAction.CREATE_LOCAL, update_direction=UpdateDirection.PUSH, ) @staticmethod def daily_sync() -> StrategyConfig: return ConfigPresets.push_only() @staticmethod def repair_sync() -> StrategyConfig: return StrategyConfig( local_orphan_action=OrphanAction.NONE, remote_orphan_action=OrphanAction.NONE, update_direction=UpdateDirection.NONE, ) @staticmethod def pull_only() -> StrategyConfig: return StrategyConfig( local_orphan_action=OrphanAction.NONE, remote_orphan_action=OrphanAction.CREATE_LOCAL, update_direction=UpdateDirection.PULL, ) @staticmethod def push_only() -> StrategyConfig: return StrategyConfig( local_orphan_action=OrphanAction.CREATE_REMOTE, remote_orphan_action=OrphanAction.NONE, update_direction=UpdateDirection.PUSH, ) @staticmethod def get_preset(name: str) -> StrategyConfig: presets = { "first_sync": ConfigPresets.first_sync, "daily_sync": ConfigPresets.daily_sync, "repair_sync": ConfigPresets.repair_sync, "pull_only": ConfigPresets.pull_only, "push_only": ConfigPresets.push_only, } if name not in presets: raise ValueError(f"Unknown preset: {name}. Available: {list(presets.keys())}") return presets[name]()