Files
ai_site/宇恒-松离线数据同步使用文档.md
whm b04b180d30 feat: harden loose-offline sync for user JWT, schema, and console ops
Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 09:47:35 +08:00

431 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 宇恒 × 智建 · 松离线数据同步使用文档
> 面向:**宇恒一号客户端**对接同学
> 平台侧仓库智建ai建站
> 依据:`松离线-dbsync方案-最终版.md`(冻结)
> 本地测试基址示例:`http://127.0.0.1:8180`;生产示例:`https://aisite.yuxindazhineng.com`
---
## 1. 你要做什么(一句话)
未开通用户**零改动**。仅当显式 `YXD_SYNC_MODE=local_dbsync` 且表在白名单时:业务写进本机正式库 BUUID→ 写 outbox → **本机 agent** 调智建 push API → 落到线上库 A。agent 停了也**不能挡保存**。
```text
业务保存 ──► 本机 B最终 UUID立刻可见
outbox积压可接受
▼ 本机 agent可选装
智建 POST .../push ──► 线上 A
```
---
## 2. 硬约束(违反即不合入)
| # | 要求 |
|---|------|
| H1 | 未开通:保存 / 自增表 / 插件与现网 **行为 diff = 0** |
| H2 | **禁止**自动升为 `local_dbsync`(仅显式配置) |
| H3 | agent 停运 / 推送失败 → **保存仍成功**;心跳只驱动 UI |
| H4 | 默认建表仍自增;仅「同步表」用 UUID TEXT PK |
| H5 | 旧 `online_primary` 客户禁止静默关双写 |
| H6 | 未开通用户:**零同步文案**(无强制状态条、无「已自动合并」) |
开通判定(写路径):
```text
mode == local_dbsync AND 表白名单缓存命中该表
→ 走松离线旁路
否则 → 原写路径
```
同表互斥:某表不可同时 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
```
开通增值:
```bash
YXD_SYNC_MODE=local_dbsync
YXD_SYNC_AGENT=1
YXD_ONLINE_API_BASE=http://127.0.0.1:8180
YXD_SYNC_CHANNEL_ID=<智建控制台通道 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
```
调试兜底表白名单(通道拉取失败前):
```bash
YXD_SYNC_WHITELIST=orders,order_items
# 或
YXD_SYNC_DBSYNC_TABLES=orders,order_items
```
---
## 4. 智建侧前置(公司管理员)
1. 登录智建控制台 → **数据同步** → 新建通道。
2. **生产**`remote.driver=postgres`DSN 例:`postgres://user:pass@host:5432/db?sslmode=disable`(须可达)。
3. 表白名单须 **UUID TEXT/UUID PK** + **外键闭包**
4. 方向推荐 **本地 → 线上**;冲突策略推荐 **源端覆盖lww_source**
5. 保存通过校验后记下 **通道 ID**
6. **二选一鉴权**
- 管理路径:发带「数据同步」的智能体 Token / 管理员 JWT
- **用户自助**:终端用登录用户 JWT先登记 Bindingpush 带本人 `online_db_id`
**单服务器**:一条通道 + 一个 `YXD_SYNC_CHANNEL_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 本机 AgentM2
启用:`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
### 冲突
- [ ] 终端**无**冲突处理台
- [ ] 公司管理员打 conflicts API → 403
- [ ] 超管可在平台工作台看 LWW 日志 / 回滚
---
## 9. 智建 API 速查
基址:`{YXD_ONLINE_API_BASE}`,鉴权:`Authorization: Bearer ...`
- 管理路径:需「数据同步」
- **用户自助**:登录用户 JWT + 本人 Bindingpush 须带 `online_db_id`
- LWW 仅超管
| 方法 | 路径 | 谁用 |
|------|------|------|
| 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` | agent / 用户 JWT |
| POST | `/api/v1/agent/sync/channels/{id}/schema` | agent / 用户 JWT拉线上表结构含空表 |
| POST | `/api/v1/agent/sync/channels/{id}/schema/ensure` | agent / 用户 JWT本机空表建到线上 |
| POST | `/api/v1/agent/sync/channels/{id}/pull` | agent / 用户 JWT下行 |
| POST | `/api/v1/agent/sync/channels/{id}/bootstrap` | 全量灌库别名 |
| 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. 智建本机起栈(`.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` 验证不串库。
---
## 11. 相关文档(智建仓)
| 文档 | 内容 |
|------|------|
| `松离线-dbsync方案-最终版.md` | 双方冻结方案 |
| `docs/同步表约定.md` | UUID / 接口约定 |
| `docs/数据同步-开通说明.md` | 管理员开通 |
| `docs/数据同步-迁移手册.md` | 旧客迁移 |
| `docs/数据同步-中间件.md` | 平台能力与 API |
| `docs/发版说明-数据同步.md` | 默认无感发版说明 |
---
**分工提醒**智建只提供平台通道、校验、push/whitelist/Binding/LWW**写网关、outbox、agent、未开通回归**由宇恒在己方仓库实现,勿改智建仓业务代码。