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, ) 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) """ # 类变量:默认配置(子类可覆盖) default_config: StrategyConfig = StrategyConfig() # 类变量:schema 类型(子类必须设置) schema: Type[T] = None # type: ignore # 类变量:是否默认跳过同步(子类可覆盖) default_skip_sync: 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 # 统一由 Pipeline 注入 runtime;未注入时在执行阶段显式报错 self.sm_runtime: Optional[StateMachineRuntime] = 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._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._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._validate_config() logger.info(f"[{self.node_type}] Applied preset: {preset_name}") 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)