# 宇恒 × 智建 · 松离线数据同步使用文档

> 面向：**宇恒一号客户端**对接同学  
> 平台侧仓库：智建（ai建站）  
> 依据：`松离线-dbsync方案-最终版.md`（冻结）、`联调后修改意见-宇恒松离线.md`（Z12/Z13 产品路径）  
> 本地测试基址示例：`http://127.0.0.1:8180`；生产示例：`https://aisite.yuxindazhineng.com`  
> **生产·宇信达联调登录**：手机号 `13531041944`（公司侧专用；勿用超管号 `13531041945` / 演示号 `13800000001`）

---

## 1. 你要做什么（一句话）

未开通用户**零改动**。仅当显式 `YXD_SYNC_MODE=local_dbsync`（或库级选「同步」）且走白名单/Binding 时：业务写进本机正式库 B（UUID）→ 写 outbox → **本机 agent** 调智建 push API → 落到线上库 A。agent 停了也**不能挡保存**。

```text
业务保存 ──► 本机 B（最终 UUID，立刻可见）
              │
              ▼
           outbox（积压可接受）
              │
              ▼ 本机 agent（可选装）
         智建 POST .../push  ──► 线上 A
```

**生产开通（推荐，Z12/Z13）**：不要引导用户手抄 `channel_id`。  
后台启用智能体 / 发绑定码 / 同号确认 → 换票带 `sync_bound` + 落点 → 本机库选「同步」即可用。  
手建通道 + `YXD_SYNC_CHANNEL_ID` 仅作**运维高级 / 联调过渡**。

---

## 2. 硬约束（违反即不合入）

| # | 要求 |
|---|------|
| H1 | 未开通：保存 / 自增表 / 插件与现网 **行为 diff = 0** |
| H2 | **禁止**自动升为 `local_dbsync`（仅显式配置或用户库级选「同步」） |
| H3 | agent 停运 / 推送失败 → **保存仍成功**；心跳只驱动 UI |
| H4 | 默认建表仍自增；仅「同步表」用 UUID TEXT PK |
| H5 | 旧 `online_primary` 客户禁止静默关双写 |
| H6 | 未开通用户：**零同步文案**（无强制状态条、无「已自动合并」） |

开通判定（写路径）：

```text
mode == local_dbsync  AND  （表白名单缓存命中 或 Binding 整库策略）
→ 走松离线旁路
否则 → 原写路径
```

同表互斥：某表不可同时 HTTP 双写 + dbsync 白名单。

---

## 3. 模式三分（环境变量）

| `YXD_SYNC_MODE` | 含义 | 说明 |
|-----------------|------|------|
| `local_only` | 仅本地 | 默认之一 |
| `online_primary` | HTTP 双写 / 离线 pending | **旧客保留**；仅配了 `YXD_ONLINE_API_BASE` 且未写 MODE 时可兼容升为此模式 |
| `local_dbsync` | 松离线 + 白名单 + agent | **必须显式写出**，永不因「配了线上地址」自动升 |

建议本地测试：

```bash
# 默认无感：不要设 local_dbsync，或显式：
YXD_SYNC_MODE=local_only
```

开通增值（**过渡 / 运维**仍可用 env；生产优先换票落点）：

```bash
YXD_SYNC_MODE=local_dbsync
YXD_SYNC_AGENT=1
YXD_ONLINE_API_BASE=http://127.0.0.1:8180
# 过渡：可手填；生产应优先用换票/agents/me 的 channel_id
YXD_SYNC_CHANNEL_ID=<可选过渡缓存>
YXD_SYNC_ACCESS_TOKEN=<Bearer：管理员/智能体「数据同步」，或登录用户 JWT（自助须先 Binding）>
# 可选
YXD_SYNC_WHITELIST_TTL_SEC=600
YXD_SYNC_AGENT_INTERVAL_SEC=5
YXD_SYNC_WHITELIST_CACHE=cache/db_sync/whitelist.json
# 同号绑定联调（生产宇信达）
YXD_SYNC_LOGIN_PHONE=13531041944
```

调试兜底表白名单（通道拉取失败前）：

```bash
YXD_SYNC_WHITELIST=orders,order_items
# 或
YXD_SYNC_DBSYNC_TABLES=orders,order_items
```

---

## 3.1 生产绑定与落点（Z12 / Z13 · 必读）

### 产品约定

1. **一个登录账号 / 一个智能体 ↔ 本公司同步落点**；个人库默认隔离，禁止 A 数据进 B 库。  
2. **禁止**把「手抄通道 ID / 先去控制台新建通道」当作普通用户开通主路径。  
3. 公司默认同步通道由智建在**启用智能体**时自动创建（`is_system_default`）；控制台「数据同步」留给运维改 DSN。  
4. 库选「同步」→ 自动 Binding + drain（宇恒 Z13e）；全程零手填 DSN/通道。

### 换票带回绑定（Z12a）

`POST /api/v1/auth/token`（client_credentials）或登录响应可含：

| 字段 | 说明 |
|------|------|
| `channel_id` | 已绑则非空 |
| `online_db_id` | 已绑则非空 |
| `database_name` | 可读落库名 |
| `sync_bound` | `true` = 已绑落点；`false` = 须走绑定流程 |

通道解析顺序建议：`env 过渡缓存` → 换票/`agents/me` → Binding → 租户仅 1 条通道自动选用。

### 智能体自查（Z12b）

```http
GET /api/v1/agents/me
Authorization: Bearer <智能体 Token>
```

返回 `channel_id` / `online_db_id` / `database_name` / `sync_bound` / `status`（无需「管理智能体」权限）。

### 绑定码（Z13a/b）

管理员（需「数据同步」）：

```http
POST /api/v1/admin/bind-codes
Authorization: Bearer <管理员 JWT>
Content-Type: application/json

{ "max_uses": 1, "expires_hours": 168, "note": "开通单" }
```

```http
GET /api/v1/admin/bind-codes
DELETE /api/v1/admin/bind-codes/{code}
```

终端兑换（公开）：

```http
POST /api/v1/auth/bind-code/redeem
Content-Type: application/json

{ "code": "A1B2C3D4", "host_key": "<本机稳定宿主机 key>", "name": "可选显示名" }
```

成功 → 智能体挂到该公司 + 默认同步落点，`sync_bound=true`。

### 同号探测 / 确认（Z13c / Z13c-1）

**禁止静默绑定**。有手机号时必须先 lookup，命中后**弹窗确认**再 confirm。

**推荐（仅宇恒 · 安全凭票免登录）**：用户已登录宇恒时，由**宇恒后端**用共享 Secret 签发短时凭票，客户端调：

```http
POST /api/v1/auth/yuheng/ticket-exchange
Content-Type: application/json

{
  "ticket": "YHT1.<payload>.<hmac>",
  "host_key": "<本机稳定宿主机 key>",
  "local_database_id": "可选"
}
```

成功直接返回智能体 `access_token` + `sync_bound` / `channel_id` / `online_db_id`（**无需**智建账号密码，也**不要**再传 `attested_same_phone`）。

凭票 payload（JSON，HMAC-SHA256，前缀 `YHT1`）字段：

| 字段 | 说明 |
|------|------|
| `iss` | 固定 `yuheng` |
| `aud` | 固定 `aijianzhan` |
| `phone` | 宇恒当前已绑手机（须已是智建公司成员） |
| `host_key` | 与请求一致 |
| `exp` | Unix 秒；建议 60–120s，智建拒 >now+10m |
| `jti` | 一次性随机串（防重放） |
| `scope` | `sync_bind` |
| `name` / `local_database_id` | 可选 |

配置（智建 `platform.yaml` → `Agent.YuhengTicket`）：`Enabled` + 与宇恒共享的 `Secret`。生产必换 Secret；**其它客户端不得使用**。

启动前可拉策略（公开，无需登录）：

```http
GET /api/v1/auth/bind/policy
```

| 字段 | 说明 |
|------|------|
| `trial_mode` | `true` = 试运行（不强制绑定短信） |
| `require_for_bind` | 是否强制短信（与 `SMS.RequireForBind` + Provider≠off 一致） |
| `yuheng_ticket_enabled` | 是否启用凭票换票 |
| `message` | 给人看的提示文案 |

---

仍可用手机号 API（无凭票或试运行）：

```http
POST /api/v1/auth/bind/phone-lookup
Content-Type: application/json

{ "phone": "13531041944" }
```

响应要点：`exists`、`tenant_name`、`masked_name`、`need_confirm`、`sms_required_unless_attested`、`message`（**不**执行绑定）。

```http
POST /api/v1/auth/bind/phone-confirm
Content-Type: application/json

{
  "phone": "13531041944",
  "host_key": "<本机 host_key>",
  "confirm": true,
  "local_database_id": "可选，本机库 ID",
  "attested_same_phone": true,
  "sms_code": ""
}
```

| 字段 | 说明 |
|------|------|
| `attested_same_phone` | **试运行**可传；**正式且已启凭票**时禁用，须改用 ticket-exchange |
| `sms_code` | **试运行**可不传；正式异号必填 |

> **试运行**（当前）：`SMS.RequireForBind=false` 或 `Provider=off` → 绑定不校验短信，仍须 `confirm=true`。正式：`RequireForBind: true` 且 Provider 非 off。

| 场景 | 行为 |
|------|------|
| 同号命中公司成员 | **必须询问**「已有账号是否绑定」；确认才 `phone-confirm` / 或凭票换票 |
| 用户取消 | 不绑定；可改走绑定码 / 换号 |
| 无此成员 | 提示用绑定码；**勿**用超管号 `13531041945` 测 |
| 生产联调样例 | 智建与宇恒均为 **`13531041944`** → 凭票或弹确认 |

### 未绑定时客户端流程（Z13d · 宇恒已接 2026-08-05）

> 宇恒落地：宿主 `ensure_sync_bound_interactive`；本机 `/database/sync/bind/*`；选「同步」走 `POST /database/sync/mode`（Z13e）。生产仍须智建部署含 `ticket-exchange` / `bind/policy` 的 gateway，并对齐凭票 Secret。

```text
换票 / agents/me → sync_bound=false
  ├─ 有手机号 → phone-lookup
  │     ├─ need_confirm → 【弹窗】确认？→ phone-confirm / 取消
  │     └─ 未命中 → 绑定码或换号表单
  └─ 无手机号 → 绑定码表单
绑定成功 → 库选「同步」→ Binding + drain（零通道配置）
```

---

## 4. 智建侧前置（公司管理员）

### 生产推荐（Z12/Z13）

1. 公司级配置默认同步 DSN（`DBSync.DefaultRemoteDSN`，生产 Postgres）。  
2. **启用智能体**（勿要求用户先「新建通道」）→ 平台自动建默认同步通道并写回智能体落点。  
3. （可选）生成**绑定码**发给终端；或引导用户用公司成员手机号做同号确认（须弹窗）。  
4. 运维需要时再在「数据同步」改 remote DSN / 查看通道 ID（可复制）。

### 运维高级 / 联调过渡（手建通道）

1. 登录智建控制台 → **数据同步** → 新建通道。  
2. **生产**：`remote.driver=postgres`，DSN 例：`postgres://user:pass@host:5432/db?sslmode=disable`（须可达）。  
3. 表白名单可空（整库 Binding 策略）；若填表须 **UUID TEXT/UUID PK** + **外键闭包**。  
4. 方向推荐 **本地 → 线上**；冲突策略推荐 **源端覆盖（lww_source）**。  
5. 保存通过校验后记下 **通道 ID**（过渡写入 `YXD_SYNC_CHANNEL_ID`）。  
6. **二选一鉴权**：  
   - 管理路径：发带「数据同步」的智能体 Token / 管理员 JWT；或  
   - **用户自助**：终端用登录用户 JWT；先登记 Binding，push 带本人 `online_db_id`。  

**单服务器**：一条默认同步通道即可（自动或手建）。  
**多服务器**：每台线上 Postgres **各建一条通道**；按本机库选择对应 `channel_id`（建议登记 Binding）。

Binding（多库 / 用户自助时建议登记；登录 JWT 即可，不必管理员权）：

```http
POST /api/v1/admin/sync/bindings
Authorization: Bearer <token>
Content-Type: application/json

{
  "local_database_id": "用户本机库 ID",
  "online_db_id": "线上库 ID",
  "channel_id": "<通道ID>",
  "database_name": "可选，本机库可读名（控制台优先展示）",
  "display_name": "可选，线上库可读名（控制台优先展示）",
  "note": "可选"
}
```

不传可读名时控制台仍显示 id；宇恒有命名库名时建议一并写入，便于 SyncPage「本地库名 ↔ 线上库名」。

```http
GET /api/v1/admin/sync/bindings?local_database_id=...
```

---

## 5. 宇恒侧实现清单

### 5.1 写网关旁路（M1，可无 agent）

对 `insert / update / delete / 写 SQL`：

1. 读模式与白名单。  
2. **未命中**：原路径，响应形状与现网一致（可多字段，不可少成功语义）。  
3. **命中**：  
   - insert：缺 id 或非法 id → **自动补**小写带连字符 UUID；  
   - 写入本机正式库 B；  
   - append outbox（失败只打日志，**不挡保存**）；  
   - `sync_status`：`pending` / `local_only_table` 等仅开通用户可见。  

文案：`需 agent 才上云`（不得暗示已上云）。

### 5.2 Outbox

建议路径（等价即可）：

```text
cache/db_sync/outbox/<local_database_id>/pending.jsonl
```

每条建议字段：

| 字段 | 说明 |
|------|------|
| `id` | outbox 记录 UUID |
| `seq` | 单调版本（如 `time.time_ns()`），作 push 的 `version` |
| `op` | `insert` / `update` / `update_by_id` / `delete` |
| `table_name` | 表名 |
| `row_pk` | 行主键 UUID |
| `payload` | 含 `data` / `update_data` / `where_*` |
| `status` | `pending` |

规则：按 `seq` **保序**消费；失败标记 error 并**停该库后续**，恢复后重试；**禁止**失败后换新 UUID。

### 5.3 本机 Agent（M2）

启用：`YXD_SYNC_AGENT=1` 且 `mode=local_dbsync`。默认可不启。

循环建议：

1. （TTL）拉白名单 → 写本地缓存。  
2. `list_pending` → 逐条 push → 成功 `mark_done` / 失败 `mark_error` 并 break。  
3. 写心跳文件（供 `/sync/status` 展示 `agent.running` / `last_beat`）。  

**拉白名单**

```http
GET /api/v1/agent/sync/channels/{channel_id}/whitelist
Authorization: Bearer <token>
```

响应要点：`tables`、`pk_columns`、`conflict_policy`。

**推单条**

```http
POST /api/v1/agent/sync/channels/{channel_id}/push
Authorization: Bearer <token>
Content-Type: application/json

{
  "table": "orders",
  "op": "insert",
  "row_pk": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "row": {
    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "title": "demo"
  },
  "version": 1710000000000000000,
  "client_outbox_id": "outbox-record-uuid",
  "online_db_id": "用户自助必填（本人 Binding）；管理员/智能体可选"
}
```

| `op` | 平台行为 |
|------|----------|
| `insert` / `update` / `update_by_id` | upsert 到 remote A |
| `delete` | 按 pk 删 A |

幂等：同 `version` 已落地 → 跳过成功（`skipped=true`）。  
LWW：目标更新 → 按通道策略；源端覆盖会记**超管**审计（公司管理员不可见）。  
重复 push（双 drain）正确性由 version 保证；客户端可用 drain 锁减流量，非平台必做。  
remote 暂不可达：`503` + `retryable:true`。

**下行（仅线上 / 灌库 / 补齐）**

```http
POST /api/v1/agent/sync/channels/{channel_id}/bootstrap
Authorization: Bearer <token>
Content-Type: application/json

{
  "table": "orders",
  "after_pk": "",
  "limit": 200,
  "online_db_id": "用户自助必填"
}
```

或 `POST .../pull`，`mode` 取：

| mode | 作用 |
|------|------|
| `bootstrap` | 分页全量（循环至 `has_more=false`） |
| `pks` | 只取 A 主键，本机 diff |
| `rows` | 按 `row_pks` 取行 |

客户端把 `result.items` upsert 进本机 B 时务必 **WithApplying**（或等价），避免回声进 outbox。  
空表：`result.columns` 仍会返回；本机应用其 `CREATE TABLE IF NOT EXISTS`（见下「表结构同步」）。

成功响应示例：

```json
{
  "success": true,
  "result": {
    "ok": true,
    "applied": false,
    "skipped": true,
    "conflict": false,
    "applied_version": 1710000000000000000,
    "message": "already applied (same version)",
    "client_outbox_id": "outbox-record-uuid"
  }
}
```

批量：`POST .../push/batch`，body `{ "items": [ ... ] }`，最多 100，保序遇错即停。

也可设完整 URL：`YXD_SYNC_PUSH_URL=...`（覆盖默认拼装）。

**表结构同步（空表也要建）**

仅靠 outbox 行 push 时，**空表不会出现在线上**（无变更事件）。同步周期应额外：

1. **本机 → 线上**：对本机每张业务表（含 0 行）调用 ensure：

```http
POST /api/v1/agent/sync/channels/{channel_id}/schema/ensure
Authorization: Bearer <token>
Content-Type: application/json

{
  "online_db_id": "用户自助必填",
  "tables": [
    {
      "table": "accounts",
      "pk_column": "id",
      "columns": ["id", "username", "password", "email", "nickname", "status", "created_at", "updated_at"]
    }
  ]
}
```

平台对每张表 `CREATE TABLE IF NOT EXISTS`（列一律 TEXT + 指定 PK）；已存在则跳过。

2. **线上 → 本机**：先拉结构，本机缺表则建空表，再 bootstrap 行：

```http
POST /api/v1/agent/sync/channels/{channel_id}/schema
Authorization: Bearer <token>
Content-Type: application/json

{ "online_db_id": "用户自助必填" }
```

响应 `result.tables[].columns` + `row_count`；对本地缺失表执行 `CREATE TABLE IF NOT EXISTS`（建议 TEXT + PK=`pk_column`）。  
`pull`/`bootstrap` 的 `result.columns` 同样可用于单表建空表。

### 5.4 状态接口（建议）

`GET /database/sync/status`（或你们现有等价路由）对开通用户返回：

- `mode`
- `local_dbsync.whitelist_tables` / `outbox_pending`
- `agent.enabled_flag` / `running` / `last_error`
- `note`：未开通勿强塞同步 UI

---

## 6. UUID 约定

- 标准形式：**小写 + 连字符** `8-4-4-4-12`  
- 入库前 `normalize`；无连字符 32 hex 可规范化  
- 默认建表模板**不要**改成 UUID；只给同步表白名单表用  

SQLite 同步表示例：

```sql
CREATE TABLE IF NOT EXISTS orders (
  id TEXT PRIMARY KEY NOT NULL,
  title TEXT,
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```

---

## 7. 从旧双写迁入（O7，摘要）

1. 该表先停 `online_primary` 双写。  
2. 观察无残留。  
3. 再进白名单并切 `local_dbsync`。  
4. 装 agent，看 outbox 清空。  
5. **禁止**静默全员迁移；旧客默认不变。  

完整步骤见智建仓：`docs/数据同步-迁移手册.md`。

---

## 8. 联调检查表

### 未开通回归（必过）

- [ ] 不设 `local_dbsync`：插入自增表与现网一致  
- [ ] 无同步强制文案、无强制装 agent  
- [ ] 保存不因同步模块报错失败  

### 开通 + 无 agent

- [ ] 白名单表本地立刻可见（UUID）  
- [ ] outbox 增长；响应提示需 agent 才上云  
- [ ] 非白名单表仍走原路径  

### 开通 + agent

- [ ] 拉白名单成功并缓存  
- [ ] push 后线上 A 出现**同一 UUID**（不双行）  
- [ ] 重复 push 同 version 幂等  
- [ ] agent 停：仍可本地保存；恢复后按序追上  
- [ ] delete 保序，不换新 UUID  
- [ ] 空表：`schema/ensure` 后线上出现空表（Z10c）  
- [ ] 缺列：ensure 后 push 含新列不再 503（Z10d）  
- [ ] 指纹：`schema/fingerprint` 返回 content_hash；智能体带挂载 `online_db_id` 不 403（Z14/Z14c）  

### 生产绑定（Z12/Z13）

- [ ] 启用智能体后换票 / `agents/me` 已有 `sync_bound=true`（无需手抄通道）  
- [ ] 绑定码 redeem 成功  
- [ ] 同号 `13531041944`：lookup → **弹窗** → confirm；取消不绑  
- [ ] 未用超管号 `13531041945` 做绑定联调  
- [ ] 库选「同步」后可 drain，零手填 `CHANNEL_ID`  

### 冲突

- [ ] 终端**无**冲突处理台  
- [ ] 公司管理员打 conflicts API → 403  
- [ ] 超管可在平台工作台看 LWW 日志 / 回滚  

---

## 9. 智建 API 速查

基址：`{YXD_ONLINE_API_BASE}`，鉴权：`Authorization: Bearer ...`  
- 管理路径：需「数据同步」  
- **用户自助**：登录用户 JWT + 本人 Binding；push 须带 `online_db_id`  
- LWW 仅超管  

| 方法 | 路径 | 谁用 |
|------|------|------|
| POST | `/api/v1/auth/token` | 换票；响应可含 `channel_id`/`online_db_id`/`sync_bound`（Z12a） |
| GET | `/api/v1/agents/me` | 智能体自查落点（Z12b） |
| POST | `/api/v1/auth/yuheng/ticket-exchange` | 宇恒凭票免登录换票（仅宇恒 HMAC） |
| GET | `/api/v1/auth/bind/policy` | 绑定策略：`require_for_bind` / `trial_mode` / `yuheng_ticket_enabled` |
| POST | `/api/v1/auth/bind-code/redeem` | 绑定码兑换（公开，Z13b） |
| POST | `/api/v1/auth/bind/phone-lookup` | 同号探测，不绑定（Z13c） |
| POST | `/api/v1/auth/bind/phone-confirm` | 同号确认后绑定（Z13c-1） |
| GET/POST/DELETE | `/api/v1/admin/bind-codes` | 管理员生成/列表/撤销绑定码（Z13a） |
| GET | `/api/v1/agent/sync/channels/{id}/whitelist` | agent / 用户 JWT |
| POST | `/api/v1/agent/sync/channels/{id}/push` | agent / 用户 JWT |
| POST | `/api/v1/agent/sync/channels/{id}/push/batch` | agent / 用户 JWT |
| POST | `/api/v1/agent/sync/channels/{id}/pull` | agent / 用户 JWT（下行） |
| POST | `/api/v1/agent/sync/channels/{id}/bootstrap` | 全量灌库 |
| POST | `/api/v1/agent/sync/channels/{id}/schema` | 拉线上表结构（含空表） |
| POST | `/api/v1/agent/sync/channels/{id}/schema/ensure` | 本机空表建到线上；已存在可补列（Z10/Z10d） |
| POST | `/api/v1/agent/sync/channels/{id}/schema/fingerprint` | 线上表指纹 `row_count`+`content_hash`（Z14；智能体可用挂载 online_db_id） |
| GET/POST | `/api/v1/admin/sync/bindings` | 管理员或用户自助 |
| GET | `/api/v1/platform/dbsync/lww-overrides` | 仅超管 |
| POST | `/api/v1/platform/dbsync/lww-overrides/{id}/rollback` | 仅超管 |

OpenAPI：`GET /api/v1/meta/openapi.yaml`

---

## 10. 本地联调最小步骤

### 生产路径（推荐）

1. 智建起栈；公司配置 `DefaultRemoteDSN`（或联调用空 DSN → sqlite 文件）。  
2. 启用智能体 → 自动绑默认同步通道；或管理员 `POST .../admin/bind-codes` 发码。  
3. 宇恒：`host_key` 换票 / `agents/me` 看 `sync_bound`；未绑则 lookup→确认 或 redeem。  
4. 本机库选「同步」→ Binding + agent drain；**不必**手填 `YXD_SYNC_CHANNEL_ID`。  
5. 同号联调用 **`13531041944`**，禁止超管号。

### 过渡 / 手建通道（仍可用）

1. 智建本机起栈（`.env` 已是本地：`AIJZ_PUBLIC_BASE_URL=http://127.0.0.1:8180`）。  
2. 控制台建通道：`remote` 优先本机 **Postgres**；拷贝通道 ID。  
3. 拿 token（管理员登录或智能体 client_credentials）。  
4. 宇恒设：

```bash
YXD_SYNC_MODE=local_dbsync
YXD_SYNC_AGENT=1
YXD_ONLINE_API_BASE=http://127.0.0.1:8180
YXD_SYNC_CHANNEL_ID=...
YXD_SYNC_ACCESS_TOKEN=...
```

5. 对白名单表插一行 → 看本机 B + outbox → agent 推上 A → 比对 UUID。  
6. 多服务器演练：再建第二条通道指向另一 Postgres，换 `CHANNEL_ID` 验证不串库。  
7. **空表**：sync 周期调 `schema/ensure` + `schema`（**Z10c 宇恒已接** `sync_schema.ensure_before_drain`），否则空表不会出现在线上。

---

## 11. 相关文档（智建仓）

| 文档 | 内容 |
|------|------|
| `松离线-dbsync方案-最终版.md` | 双方冻结方案 |
| `联调后修改意见-宇恒松离线.md` | 联调结论 + Z12/Z13 产品路径 |
| `docs/同步表约定.md` | UUID / 接口约定 |
| `docs/数据同步-开通说明.md` | 管理员开通 |
| `docs/数据同步-迁移手册.md` | 旧客迁移 |
| `docs/数据同步-中间件.md` | 平台能力与 API |
| `docs/发版说明-数据同步.md` | 默认无感发版说明 |

---

**分工提醒**：智建只提供平台通道、校验、push/whitelist/Binding/LWW/绑定 API；**写网关、outbox、agent、未开通回归、Z13 弹窗与选同步即用**由宇恒在己方仓库实现，勿改智建仓业务代码。
