import copy from typing import TypeVar, Generic, Type, Dict, List, Optional, Any from pydantic import BaseModel import logging from pathlib import Path from ..common.collection import DataCollection from ..common.binding import BindingManager from ..common.sync_node import SyncNode from .config import StrategyConfig, OrphanAction, UpdateDirection, ConfigPresets from .strategy_ops import ( get_phase1_reset_defaults, run_bind, run_create, run_delete, run_phase2_cleanup, run_update, ) from ..engine import ( StateMachineConfig, StateMachineRuntime, ) from ..validation import SchemaDiffValidator from .strategy_ops.compare_ops import normalized_data_for_compare T = TypeVar("T", bound=BaseModel) logger = logging.getLogger(__name__) class BaseSyncStrategy(Generic[T]): """ 同步策略基类。 纯逻辑层,不负责 IO 执行。 子类可以通过覆盖 default_config 来设置默认配置: ```python class ProjectStrategy(DefaultSyncStrategy): default_config = StrategyConfig( auto_bind_fields=["name", "code"], local_orphan_action=OrphanAction.CREATE_REMOTE ) schema = ProjectResponse ``` 跳过同步控制: skip_sync: bool - 如果为 True,则跳过该类型的 bind/create/update/sync 阶段 数据仍会被加载,但不会执行任何同步操作 适用于只需要加载数据供其他类型引用的场景(如 supplier) skip_post_check: bool - 如果为 True,则跳过该类型最终 post-check 的 reload 与一致性校验 """ # 类变量:默认配置(子类可覆盖) default_config: StrategyConfig = StrategyConfig() # 类变量:schema 类型(子类必须设置) schema: Type[T] = None # type: ignore # 类变量:是否默认跳过同步(子类可覆盖) default_skip_sync: bool = False # 类变量:是否默认跳过 post-check(子类可覆盖) default_skip_post_check: bool = False def __init__( self, node_type: str, local_collection: DataCollection, remote_collection: DataCollection, binding_manager: BindingManager, ): if self.schema is None: raise ValueError(f"{self.__class__.__name__} must define schema as a class variable") self.node_type = node_type self.local_collection = local_collection self.remote_collection = remote_collection self.binding_manager = binding_manager # 复制类级别的默认配置到实例 self.config = self.default_config.model_copy(deep=True) # 是否跳过同步(可运行时修改) self.skip_sync = self.default_skip_sync # 是否跳过 post-check(可运行时修改) self.skip_post_check = self.default_skip_post_check # 统一由 Pipeline 注入 runtime;未注入时在执行阶段显式报错 self.sm_runtime: Optional[StateMachineRuntime] = None self._schema_diff_validator: Optional[SchemaDiffValidator] = None # 配置逻辑校验 self._validate_config() def ensure_runtime(self) -> StateMachineRuntime: runtime = self.sm_runtime if runtime is None: cfg_path = Path(__file__).resolve().parents[1] / "config" / "node_state_machine.yaml" runtime = StateMachineRuntime(StateMachineConfig.load(cfg_path)) self.sm_runtime = runtime self.local_collection.set_state_machine_runtime(runtime) self.remote_collection.set_state_machine_runtime(runtime) return runtime def _validate_config(self) -> None: """ 校验配置的逻辑一致性。 规则: - PUSH 模式:local_orphan_action 应为 CREATE_REMOTE,remote_orphan_action 应为 NONE - PULL 模式:local_orphan_action 应为 NONE,remote_orphan_action 应为 CREATE_LOCAL - NONE 模式:两者都应为 NONE """ direction = self.config.update_direction local_action = self.config.local_orphan_action remote_action = self.config.remote_orphan_action # PUSH 模式检查 if direction == UpdateDirection.PUSH: if local_action == OrphanAction.CREATE_LOCAL: logger.warning( f"[{self.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"[{self.node_type}] Config inconsistency: " f"update_direction=PUSH but remote_orphan_action=CREATE_LOCAL (本地创建)。" f"PUSH 模式应设为 NONE 或 DELETE_REMOTE" ) # PULL 模式检查 elif direction == UpdateDirection.PULL: if local_action == OrphanAction.CREATE_REMOTE: logger.warning( f"[{self.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"[{self.node_type}] Config inconsistency: " f"update_direction=PULL but remote_orphan_action={remote_action}。" f"PULL 模式建议使用 CREATE_LOCAL 或 NONE" ) # NONE 模式检查 elif direction == UpdateDirection.NONE: if local_action != OrphanAction.NONE: logger.warning( f"[{self.node_type}] Config inconsistency: " f"update_direction=NONE but local_orphan_action={local_action}。" f"应设为 NONE" ) if remote_action != OrphanAction.NONE: logger.warning( f"[{self.node_type}] Config inconsistency: " f"update_direction=NONE but remote_orphan_action={remote_action}。" f"应设为 NONE" ) def set_config(self, config: StrategyConfig): """完全替换配置对象""" self.config = config self._schema_diff_validator = None self._validate_config() def update_config(self, **kwargs): """ 临时修改部分配置项。 Example: strategy.update_config( local_orphan_action=OrphanAction.NONE, update_direction=UpdateDirection.PULL ) """ valid_fields = type(self.config).model_fields for key, value in kwargs.items(): if key in valid_fields: setattr(self.config, key, value) else: logger.warning(f"[{self.node_type}] Unknown config key: {key}") self._schema_diff_validator = None # 更新配置后也要校验 self._validate_config() def apply_preset(self, preset_name: str): """ 应用配置预设。 Args: preset_name: 预设名称,可选:first_sync, daily_sync, repair_sync, pull_only, push_only Example: strategy.apply_preset("first_sync") """ preset_config = ConfigPresets.get_preset(preset_name) self.config = preset_config self._schema_diff_validator = None self._validate_config() logger.info(f"[{self.node_type}] Applied preset: {preset_name}") def get_schema_diff_validator(self) -> SchemaDiffValidator: if self._schema_diff_validator is None: self._schema_diff_validator = SchemaDiffValidator( schema_name=self.schema.__name__, ignore_fields=self.config.compare_ignore_fields, ignore_list_item_fields=self.config.post_check_ignore_list_item_fields, ) return self._schema_diff_validator def reset_schema_diff_validator(self) -> None: self.get_schema_diff_validator().reset() def emit_schema_diff_report(self) -> None: validator = self.get_schema_diff_validator() if not validator.has_records(): return for line in validator.format_summary_lines(): logger.info(f"[{self.node_type}] validate {line}") def preprocess_compare_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]: return payload def normalize_compare_payload( self, data: Optional[Dict[str, Any]], data_id_map: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: normalized = normalized_data_for_compare( data, data_id_map, ignore_fields=set(self.config.compare_ignore_fields), ignore_list_item_fields=self.config.post_check_ignore_list_item_fields, ) return self.preprocess_compare_payload(copy.deepcopy(normalized)) async def bind(self, **kwargs): """进行绑定逻辑判定,设置 node.binding_status 和 node.action""" raise NotImplementedError async def create(self) -> List[SyncNode]: """准备 CREATE 操作""" raise NotImplementedError async def update(self) -> List[SyncNode]: """准备 UPDATE 操作""" raise NotImplementedError def _needs_update( self, source_node: SyncNode, target_node: SyncNode, resolved_data: Optional[Dict[str, Any]] = None, data_id_map: Optional[Dict[str, str]] = None, ) -> bool: from .strategy_ops.update_ops import default_needs_update return default_needs_update(self, source_node, target_node, resolved_data, data_id_map) @classmethod def get_phase1_reset_defaults(cls) -> Dict[str, Any]: return get_phase1_reset_defaults() @classmethod async def run_reset( cls, *, node_types: List[str], local_collection: DataCollection, remote_collection: DataCollection, binding_manager: BindingManager, runtime: StateMachineRuntime, ) -> int: """ 执行加载后的重置清理(僵尸 CREATE 节点清理)。 说明: - `DataCollection.load_from_persistence()` 仅恢复持久化状态; - 这里触发 E01:create_zombie -> S15(删除),其余 -> S00。 """ return await run_phase2_cleanup( node_types=node_types, local_collection=local_collection, remote_collection=remote_collection, binding_manager=binding_manager, runtime=runtime, ) @classmethod async def run_phase2_reset( cls, *, node_types: List[str], local_collection: DataCollection, remote_collection: DataCollection, binding_manager: BindingManager, runtime: StateMachineRuntime, ) -> int: """兼容旧命名,等价于 `run_reset`。""" return await cls.run_reset( node_types=node_types, local_collection=local_collection, remote_collection=remote_collection, binding_manager=binding_manager, runtime=runtime, ) class DefaultSyncStrategy(BaseSyncStrategy[T]): """ 基于 STATE_DEFINITIONS.md 实现的标准化同步策略。 采用四阶段管线:核心状态 -> 依赖检查 -> 自动匹配 -> 动作映射。 """ async def bind(self, **kwargs): self.ensure_runtime() await run_bind(self, **kwargs) def _get_id_field_hints(self) -> Dict[str, str]: """ 获取 ID 字段的 node_type 提示。 默认实现:从 depend_fields 中提取。 子类可以重写此方法提供更精确的映射。 Returns: 字段名到 node_type 的映射,例如 {"project_id": "project"} """ return dict(self.config.depend_fields) if self.config.depend_fields else {} async def create(self) -> List[SyncNode]: self.ensure_runtime() return await run_create(self) async def update(self) -> List[SyncNode]: self.ensure_runtime() return await run_update(self) async def delete(self) -> List[SyncNode]: self.ensure_runtime() return await run_delete(self)