# sync_state_machine 接入手册

## 1. 目标

本项目现在可以作为可编辑安装库使用：

- 源码开发：`pip install -e .`
- 代码引用：`import sync_state_machine`
- 命令行运行：`ecm-sync-run --config_path=...`

推荐接入方式：

- `sync_state_machine` 负责同步引擎、状态机、pipeline、datasource 抽象。
- 业务系统负责提供自己的 datasource / handler 适配层，不要反向让 `sync_state_machine` 依赖业务系统。

---

## 2. 安装方式

### 2.1 本仓库本地安装

在仓库根目录执行：

```bash
pip install -e .
```

安装后可直接使用：

```python
import sync_state_machine
print(sync_state_machine.__version__)
```

也可以直接走命令：

```bash
ecm-sync-run --config_path=run_profiles/preset/jsonl_to_api.default.yaml
```

---

## 3. 对外可直接引用的入口

常用入口：

- `sync_state_machine.build_config`
- `sync_state_machine.build_default_config`
- `sync_state_machine.create_pipeline_from_config`
- `sync_state_machine.build_config_from_file`
- `sync_state_machine.PipelineRunConfig`
- `sync_state_machine.JsonlDataSourceConfig`
- `sync_state_machine.ApiDataSourceConfig`
- `sync_state_machine.DomainRegistry`

配置加载分两步：

1. 先构造或读取 `PipelineRunConfig`
2. 再交给 `create_pipeline_from_config()` 创建 pipeline

内置 domain 注册会在导入包时自动完成。

---

## 4. 最小接入示例

### 4.1 直接运行现有 pipeline

```python
from pathlib import Path

from sync_state_machine import build_config, load_overrides_from_file, run_pipeline_from_config


async def main() -> None:
    project_root = Path("/path/to/ecm_sync_system")
    profile_path = project_root / "run_profiles" / "preset" / "jsonl_to_api.default.yaml"

    overrides = load_overrides_from_file(profile_path, project_root)
    config = build_config(project_root=project_root, overrides=overrides)
    await run_pipeline_from_config(config, title="demo", print_summary=True)
```

适合场景：

- 仍沿用当前仓库的 domain 注册
- 仍沿用当前仓库的 JSONL / API handler
- 外部系统只想“调用引擎”

---

## 5. 数据源怎么引用

当前引擎的 datasource 分两层：

1. `DataSource`
   - 管理加载、批量执行、轮询、collection 注入、handler 注册。
2. `Handler`
   - 只处理某一个 `node_type` 的 I/O 逻辑。

因此接入时，不是“直接把业务 ORM 塞进 pipeline”，而是：

- 先确定你的数据源类型
- 再决定复用已有 datasource，还是只新增 handler

### 5.1 已有两类 datasource

#### JSONL

可直接复用：

- `sync_state_machine.datasource.JsonlDataSource`
- `sync_state_machine.datasource.BaseJsonlHandler`

适合：

- 本地 fixture
- 离线回放
- 调试数据集

#### API

可直接复用：

- `sync_state_machine.datasource.ApiDataSource`
- `sync_state_machine.datasource.ApiClient`
- `sync_state_machine.datasource.BaseApiHandler`

适合：

- HTTP 接口推送
- 异步 push + poll
- 远端系统同步

---

## 6. 怎么适配数据源

### 6.1 最推荐：复用已有 datasource，只写 handler

这是最小成本方案。

比如你的本地数据来自 backend 的数据库，但你不想改引擎核心，可以：

- 保持 `ApiDataSource` / `JsonlDataSource` 不变
- 新增你自己的 handler
- 在 handler 里把 backend 数据模型转换成节点 schema

如果是 HTTP 源：继承 `BaseApiHandler`。

如果是 JSONL 源：继承 `BaseJsonlHandler`。

#### API handler 示例

```python
from pydantic import BaseModel

from sync_state_machine.datasource.api.handler import BaseApiHandler


class DemoSchema(BaseModel):
    id: str
    name: str


class DemoApiHandler(BaseApiHandler[DemoSchema]):
    _node_type = "demo"
    _node_class = DemoSyncNode
    _schema = DemoSchema

    async def load(self):
        payload = await self.api_client.get("/demo/list")
        return [self._create_node(item) for item in payload["data"]]

    async def create_all(self, nodes):
        ...

    async def update_all(self, nodes):
        ...

    async def delete_all(self, nodes):
        ...

    async def poll_tasks(self, task_ids):
        ...
```

#### JSONL handler 示例

```python
from sync_state_machine.datasource.jsonl.handler import BaseJsonlHandler


class DemoJsonlHandler(BaseJsonlHandler):
    def __init__(self, datasource):
        super().__init__(
            datasource=datasource,
            node_type="demo",
            node_class=DemoSyncNode,
            schema=DemoSchema,
        )
```

然后把它注册到 `DomainRegistry`。

---

### 6.2 如果 backend 是数据库：建议写 handler / adapter，不要让引擎直接依赖 ORM

推荐结构：

```text
backend/
  app/
    integrations/
      ecm_sync/
        datasource/
          user_db_handler.py
          contract_db_handler.py
        mapper/
          user_mapper.py
          contract_mapper.py
        services/
          sync_runner.py
```

职责建议：

- `mapper/`
  - backend model -> sync schema
  - sync schema -> backend push payload
- `datasource/handler`
  - 调 repository / service 获取数据
  - 转成 `SyncNode`
- `services/sync_runner.py`
  - 组装配置并调用 pipeline

不要这样做：

- 不要在 `sync_state_machine` 包里 import backend 的 model/repository
- 不要让 `DomainRegistry` 注册逻辑散落在 backend 业务层之外
- 不要在 datasource 之外做额外的 project 过滤；项目范围应通过 `handler_configs.project.data_id_filter` 下沉到具体 handler 解释

---

### 6.3 什么时候需要新增 DataSource 类

只有当你的 I/O 模型与当前两种 datasource 都不匹配时，才新增 `BaseDataSource` 子类。

例如：

- MQ / Kafka 批量消费型源
- 直接数据库快照源
- 特殊 RPC 源

此时建议：

1. 继承 `BaseDataSource`
2. 保持接口与现有 datasource 一致：
   - `register_handler()`
   - `set_collection()`
   - `load_all()`
   - `sync_all()`
3. 把“节点类型差异”仍下沉到 handler，不要写死在 datasource 里

---

## 7. Domain 怎么注册

引擎通过 `DomainRegistry` 查找：

- `schema`
- `node_class`
- `strategy_class`
- `jsonl_handler_class`
- `api_handler_class`
- 以及任意通过 `register_handler()` 追加的 datasource handler

示例：

```python
from sync_state_machine.common.registry import DomainRegistry

DomainRegistry.register(
    node_type="demo",
    schema=DemoSchema,
    node_class=DemoSyncNode,
    strategy_class=DemoStrategy,
    jsonl_handler_class=DemoJsonlHandler,
    api_handler_class=DemoApiHandler,
)

DomainRegistry.register_handler("demo", "custom_remote", DemoCustomHandler)
```

如果是 backend 接入，建议在启动入口集中注册，而不是分散在 controller / service 里。

---

## 8. backend 里的推荐接法

建议 backend 只新增一层 integration：

```python
from pathlib import Path

from sync_state_machine import build_config, create_pipeline_from_config


async def run_demo_sync() -> None:
    project_root = Path("/path/to/ecm_sync_system")
    overrides = {
        "node_types": ["project", "contract"],
        "local_datasource": {
            "type": "jsonl",
            "jsonl_dir": str(project_root / "tests" / "fixtures" / "demo"),
        },
        "remote_datasource": {
            "type": "api",
            "api_base_url": "https://example.com",
            "api_uid": "xxx",
            "api_secret": "xxx",
          "handler_configs": {
            "project": {"data_id_filter": ["demo-project"]}
          },
        },
    }

    config = build_config(project_root=project_root, overrides=overrides)
    pipeline = await create_pipeline_from_config(config)
    await pipeline.run()
```

      如果 backend 自己提供本地数据库数据，不要把 backend model 直接灌进 pipeline；先经过 handler / mapper 转成 schema 再进入引擎。

---

## 9. 接入时的边界约束

建议保持下面的依赖方向：

- `backend integration -> sync_state_machine`
- `sync_state_machine -X-> backend`

这样做的好处：

- 引擎能独立测试
- backend 改 repository 不会直接污染引擎
- 后续可以单独发布库

---

## 10. 当前阶段建议

如果项目还在高频迭代，推荐顺序：

1. 先 `pip install -e .`
2. 先在 backend 做 integration 目录，不急着发私有包
3. 先接 1~2 个 node_type，跑通完整链路
4. 稳定后再考虑拆成正式发布包

---

## 11. 相关文件

- 包入口：[sync_state_machine/__init__.py](../sync_state_machine/__init__.py)
- CLI / 运行入口：[sync_state_machine/cli.py](../sync_state_machine/cli.py)
- 配置工厂：[sync_state_machine/config](../sync_state_machine/config)
- datasource 基础层：[sync_state_machine/datasource](../sync_state_machine/datasource)
- pipeline 工厂：[sync_state_machine/pipeline/factory.py](../sync_state_machine/pipeline/factory.py)
- domain 注册：[sync_state_machine/common/registry.py](../sync_state_machine/common/registry.py)
- backend 集成入口：[backend/app/thirdparty/ecm_sync/services/sync_runner.py](../backend/app/thirdparty/ecm_sync/services/sync_runner.py)