feat: ship loose-offline dbsync (validate, agent push, LWW audit)

Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 17:54:14 +08:00
parent 632057c857
commit 76cdcd760e
39 changed files with 3302 additions and 199 deletions

View File

@@ -6,7 +6,11 @@
|------|------|
| [智能体-生成发布-能力说明.md](./智能体-生成发布-能力说明.md) | **需求 / 能力边界**(做什么、不做什么、工作流、验收) |
| [智能体-生成发布-API.md](./智能体-生成发布-API.md) | **接口契约**(路径、请求/响应、宿主回执字段) |
| [数据同步-中间件.md](./数据同步-中间件.md) | 跨库实时同步SQLite/MySQL/Postgres |
| [数据同步-中间件.md](./数据同步-中间件.md) | 跨库同步能力与 APILWW / agent push |
| [数据同步-开通说明.md](./数据同步-开通说明.md) | 管理员开通默认无感、opt-in |
| [数据同步-迁移手册.md](./数据同步-迁移手册.md) | O7旧客从双写迁入禁止静默 |
| [同步表约定.md](./同步表约定.md) | UUID 同步表模板与 agent 接口约定 |
| [发版说明-数据同步.md](./发版说明-数据同步.md) | 默认无感 / 增值开通发版摘录 |
> 业务用语称「**模块**」。HTTP 路径仍为 `/api/v1/apps/...`。智能体默认无需配置模块白名单即可自建发布。

View File

@@ -0,0 +1,35 @@
# 发版说明 · 数据同步(智建平台)
## 对默认客户
- **无感**:未显式开通时,不改变现有保存 / 自增表行为。
- `local_dbsync` 为**增值开通**(客户端显式配置);平台**不会**全员切默认。
## 本版本智建已交付
| 能力 | 说明 |
|------|------|
| 通道校验 | UUID TEXT PK + FK 闭包;拒绝自增整数作同步键 |
| Agent API | 拉白名单、push / push batch → 线上 A |
| Binding | `local_database_id ↔ online_db_id` 登记查询 |
| LWW | 默认源端覆盖;超管审计 + 单行回滚;公司 conflicts → 403 |
| 同步修复 | 对账限流(默认 300s |
## 管理员文档
- [数据同步-开通说明.md](./数据同步-开通说明.md)
- [数据同步-迁移手册.md](./数据同步-迁移手册.md)(旧客迁入,禁止静默)
- [同步表约定.md](./同步表约定.md)
- [数据同步-中间件.md](./数据同步-中间件.md)
## 配置platform
```yaml
DBSync:
Enabled: true
DataDir: ./data/dbsync
LwwAuditTTLDays: 90
ReconcileMinSec: 300
```
客户端 agent / 写网关由对接方自行落地;接口以本文档与 OpenAPI `/api/v1/meta/openapi.yaml` 为准。

76
docs/同步表约定.md Normal file
View File

@@ -0,0 +1,76 @@
# 同步表约定M0
> 依据:`松离线-dbsync方案-最终版.md`
> 默认建表模板**仍为自增**;仅「同步表」使用下列模板。
## 同步表模板UUID 主键)
SQLite 示例:
```sql
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY NOT NULL, -- UUID小写带连字符
-- ... 业务列 ...
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```
Postgres 示例:
```sql
CREATE TABLE IF NOT EXISTS orders (
id UUID PRIMARY KEY, -- 或 TEXT
-- ... 业务列 ...
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```
## 入通道校验(平台已启用)
保存 / 启用同步通道时:
1. 白名单非空
2. 可达端(通常为 **remote 线上库**)上,各表主键列须为 **TEXT/VARCHAR/UUID** 类(拒绝 INTEGER/SERIAL
3. **外键闭包**:白名单内表若引用名单外表(或反之一侧在名单),保存失败
形态 B 下本机 SQLite 可能不可达:至少 **remote** 须能完成校验。
## 宇恒开通opt-in
```bash
YXD_SYNC_MODE=local_dbsync
YXD_SYNC_DBSYNC_TABLES=orders,order_items
# 或写入 cache/db_sync/whitelist.json: {"tables":["orders","order_items"]}
```
未设置 `local_dbsync` 时行为与现网一致。
## M2 本机 agentB→A— 智建已提供的接口
宇恒侧自行实现 agent智建只提供下列 API需「数据同步」权限
- `GET /api/v1/agent/sync/channels/:id/whitelist`
- `POST /api/v1/agent/sync/channels/:id/push` — body: `{table,op,row_pk,row,version,client_outbox_id}`
- `POST /api/v1/agent/sync/channels/:id/push/batch`
推送落到通道 **remote**(线上 A幂等认客户端 version平台不连用户本机 SQLite。
宇恒建议环境变量(由对方配置,不在智建仓改):
```bash
YXD_SYNC_MODE=local_dbsync
YXD_SYNC_AGENT=1
YXD_ONLINE_API_BASE=https://aisite.example.com
YXD_SYNC_CHANNEL_ID=<通道ID>
YXD_SYNC_ACCESS_TOKEN=<含「数据同步」权限的 JWT 或 agent token>
```
## M3 超管 LWW 审计(智建)
- 存储:`data/dbsync/lww_overrides.json`(与租户 conflicts 分离)
- API`GET /api/v1/platform/dbsync/lww-overrides`**仅平台超级管理员**
- 公司侧:`/api/v1/admin/sync/conflicts*`**403**
- 双入口写入worker `drain` + agent `push` 在 LWW 覆盖/保留时记审计
- TTL默认 90 天(`DBSync.LwwAuditTTLDays`
- 对账限流:手动默认 300 秒(`DBSync.ReconcileMinSec`
- 超管回滚:`POST /api/v1/platform/dbsync/lww-overrides/:id/rollback`(仅 `applied_source`;按 `loser_payload` 写回线上 A

View File

@@ -1,69 +1,69 @@
# 跨库数据同步中间件
支持 **SQLite ↔ MySQL ↔ Postgres**,不要求两端同一种数据库。变更经 **outbox 队列** 近实时投递;冲突进 **冲突队列**
支持 **SQLite ↔ MySQL ↔ Postgres**。平台侧 worker 可轮询两端 outbox**形态 B本机 agent** 下由终端 agent 经平台 **push** 写线上 A平台**不直连用户本机 SQLite**
冲突策略默认 **自动 LWW源端覆盖**;落败写入**平台超级管理员**覆盖日志。公司管理员**无冲突台**。
## 权限与隔离
| 项 | 说明 |
|----|------|
| 谁可配 | 公司**顶级权限(管理员)**权限名「数据同步」 |
| 谁不可 | 编辑 / 只读、智能体账号(即使有「发布模块」 |
| 数据隔离 | 通道与冲突带 `tenant_id`;公司 A 看不到公司 B 的通道/DSN |
| 多服务器 | 同一公司可建多条通道,分别填 B、C 等库的 DSN |
| 谁可配通道 | 公司**顶级权限(管理员)**,「数据同步」 |
| Agent push / 拉白名单 | JWT 含「数据同步」(人类管理员或智能体凭证 |
| LWW 覆盖审计 | **仅平台超级管理员**;公司 top → 403 |
| 数据隔离 | 通道带 `tenant_id`;公司 A 看不到公司 B |
## 典型场景A / B / C
## 推荐场景(松离线 B→A
| 端 | 角色 |
|----|------|
| **A** | 线上库(用户增删改) |
| **B** | 本地库(本机业务 + 接收 C |
| **C** | 额外数据源Excel / API / 导入),只写入 **B** |
| **B** | 本机正式库UUID 主键);开通且白名单表本地可见 |
| **A** | 线上库agent 经平台 push 幂等写入 |
| **Agent** | 读本机 outbox → `POST /api/v1/agent/sync/channels/:id/push` |
推荐配置:
1. 建一条通道:`local` = B`remote` = A**方向 `bidirectional`**,冲突策略 `queue`(或 LWW
2. C 的数据用 **ingest API**(或业务直接写 B写入本地触发器进 outbox再推到 A。
3. A 上用户改的数据经 outbox 拉回 B。
4. 怀疑漏数时点 **对账**,或等双向通道约每分钟自动对账
1. 通道:`local` 描述本机表名单`remote` = A 的 DSN方向 **`local_to_remote`**;策略 **`lww_source`**。
2. 表白名单须 UUID TEXT PK + FK 闭包(保存时校验)。
3. 客户端显式 `local_dbsync`(见开通说明);未开通用户零感。
4. 怀疑漏数时点 **同步修复(对账**(有最小间隔限流);双向通道自动对账约 **15 分钟** 一次
如何保证**不漏、不多**
| 手段 | 防什么 |
|------|--------|
| 触发器 `_ajz_sync_outbox` | 漏(本地/线上变更必入队) |
| 应用远端时 `WithApplying`(触发器不写 outbox | 多(A↔B 回声 |
| 目标 meta 版本相等则跳过 | 多(重复投递) |
| 目标版本更新 → 冲突队列 / LWW | 并发改同一行 |
| 主键对账 reconcile | 漏(存量差、触发器未装前的行 |
| C→B upsert 同主键 | 多(重复灌入) |
| 触发器 / 本机 outbox → agent push | 漏 |
| `WithApplying` / 远端应用不回写 outbox | 多(回声) |
| 目标 meta 同 version 跳过 | 多(重复投递) |
| LWW + 超管覆盖日志 | 并发同 PK |
| 主键对账 reconcile | 漏(存量差) |
```
C ──ingest/写库──► B (local) ◄──bidirectional outbox──► A (remote)
```
## 能力
## 能力一览
| 项 | 说明 |
|----|------|
| 方言 | `sqlite` / `mysql` / `postgres` |
| 实时性 | 表触发器写 `_ajz_sync_outbox`worker 默认每 500ms 拉取 |
| 方向 | 本地→线上 / 线上→本地 / **双向**A↔B 场景用这个 |
| 冲突 | `queue`(入队)/ `lww_source` / `lww_target` |
| 对账 | `POST .../reconcile`;双向运行中约每分钟自动一次 |
| 外部源 | `POST .../ingest`C → B再同步到 A |
| 配置 | 控制台「数据同步」页;可改线上 DSN |
| 方向 | 本地→线上(推荐)/ 线上→本地 / 双向 |
| 冲突 | 默认 `lww_source``lww_target``queue` 仅调试(租户不可见 |
| Agent | `GET .../agent/sync/.../whitelist``POST .../push` |
| Binding | `GET/POST /api/v1/admin/sync/bindings` |
| 对账 | `POST .../reconcile`(默认最少间隔 300s |
| 审计 | `GET /api/v1/platform/dbsync/lww-overrides`(超管) |
配置与冲突持久化:`data/dbsync/channels.json``conflicts.json`Docker`.runtime/dbsync`)。
持久化:`data/dbsync/channels.json``lww_overrides.json`(及遗留 `conflicts.json`)。
## 控制台用法
1. 登录 → **数据同步****新建通道**
2. 本地 B`sqlite` + `file:./data/local.db`,表名逗号分隔
3. 线上 A`mysql` + `user:pass@tcp(host:3306)/db?parseTime=true`
4. 方向选 **双向****测试连接****保存****启动**
5. 需要补漏时点 **对账**C 数据走业务写 B 或调用 ingest API
1. **数据同步** → 新建通道UUID 表白名单)→ 测试 → 保存 → 启动
2. 默认策略选 **源端覆盖**;方向首期用 **本地 → 线上**
3. **同步修复** 做主键对账(勿连续狂点,有限流)
4. LWW 明细在 **平台工作台**(超管),不在公司同步页
## API需公司顶级权限「数据同步」/ 管理员)
开通与迁移:见 [数据同步-开通说明.md](./数据同步-开通说明.md)、[数据同步-迁移手册.md](./数据同步-迁移手册.md)。
## API
### 公司管理员(「数据同步」)
| 方法 | 路径 |
|------|------|
@@ -72,29 +72,41 @@ C ──ingest/写库──► B (local) ◄──bidirectional outbox──►
| POST | `/api/v1/admin/sync/test` |
| POST | `/api/v1/admin/sync/channels/{id}/prepare\|start\|stop` |
| POST | `/api/v1/admin/sync/channels/{id}/reconcile` |
| POST | `/api/v1/admin/sync/channels/{id}/ingest` |
| GET | `/api/v1/admin/sync/conflicts` |
| POST | `/api/v1/admin/sync/conflicts/{id}/resolve` |
| POST | `/api/v1/admin/sync/channels/{id}/ingest`(写通道 local偏形态 A |
| GET/POST | `/api/v1/admin/sync/bindings` |
| GET/POST | `/api/v1/admin/sync/conflicts*`**403**(已迁超管审计) |
### ingest 示例
### 本机 Agent
| 方法 | 路径 |
|------|------|
| GET | `/api/v1/agent/sync/channels/{id}/whitelist` |
| POST | `/api/v1/agent/sync/channels/{id}/push` |
| POST | `/api/v1/agent/sync/channels/{id}/push/batch` |
### 平台超级管理员
| 方法 | 路径 |
|------|------|
| GET | `/api/v1/platform/dbsync/lww-overrides` |
| POST | `/api/v1/platform/dbsync/lww-overrides/{id}/rollback` |
### push 示例
```json
POST /api/v1/admin/sync/channels/{id}/ingest
POST /api/v1/agent/sync/channels/{id}/push
{
"table": "article",
"source": "excel",
"rows": [
{ "id": "c-001", "title": "来自 C" }
]
"table": "orders",
"op": "insert",
"row_pk": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"row": { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "title": "x" },
"version": 1710000000000000000,
"client_outbox_id": "..."
}
```
按主键 upsert 写入本地 B触发器入 outboxworker 再推到线上 A。
## 相关
## 注意
- 两端业务表结构需兼容(同名列);主键默认 `id`,可用 `pk_columns` 覆盖。
- MySQL 需账号有建触发器权限。
- 密钥在 DSN 中;列表页会打码显示。
- 「实时」为亚秒级轮询 + 触发器,非 MySQL binlog CDC同机延迟通常 &lt;1s。
- 对账按**主键集合**补缺行,不做逐字段内容 diff同 PK 内容冲突仍靠版本 / 冲突队列。
- [同步表约定.md](./同步表约定.md)
- [数据同步-开通说明.md](./数据同步-开通说明.md)
- [数据同步-迁移手册.md](./数据同步-迁移手册.md)

View File

@@ -0,0 +1,36 @@
# 数据同步 · 开通说明(管理员)
> 依据:`松离线-dbsync方案-最终版.md`
> **默认客户无感**:未显式开通时,终端保存与自增表行为与现网一致。
## 三分模式(客户端配置,智建通道为表白名单源)
| 模式 | 含义 | 谁改 |
|------|------|------|
| `local_only` | 仅本地,无同步 | 默认之一 |
| `online_primary` | HTTP 双写 / 离线 pending旧路径 | 已配线上 API 且未写 MODE 时兼容升 |
| `local_dbsync` | 松离线 + 表白名单 + 本机 agent | **仅显式配置**,禁止自动升 |
智建控制台「数据同步」配的是**通道 + 表白名单 + 线上 DSN**;是否走 `local_dbsync` 由客户端环境变量决定,平台**不会**替全员切默认。
## 开通步骤(增值)
1. 公司管理员在「数据同步」建通道:`local`(本机 B 描述)+ `remote`(线上 A DSN表白名单须 **UUID TEXT PK** + FK 闭包。
2. 默认方向 **本地 → 线上**,冲突策略 **源端覆盖lww_source**
3. 客户端显式设 `YXD_SYNC_MODE=local_dbsync`,并配置通道 ID / token`同步表约定.md`)。
4. 装本机 sync agent 后变更才会上云;未装 agent**本地可保存**,文案须为「需 agent 才上云」。
5. Binding可选登记 `local_database_id → online_db_id`,见 API `/api/v1/admin/sync/bindings`
## 谁能看什么
| 角色 | 可见 |
|------|------|
| 公司管理员 | 通道配置、对账(同步修复)、统计;**无**冲突台 / LWW 覆盖明细 |
| 平台超级管理员 | LWW 覆盖审计(平台工作台) |
| 未开通终端用户 | **零同步文案**,无强制状态条 |
## 相关文档
- [同步表约定.md](./同步表约定.md)
- [数据同步-迁移手册.md](./数据同步-迁移手册.md)(旧客从 HTTP 双写迁入)
- [数据同步-中间件.md](./数据同步-中间件.md)

View File

@@ -0,0 +1,40 @@
# 数据同步 · 迁移手册O7
> 从旧路径 `online_primary`HTTP 双写)迁到 `local_dbsync`(松离线 + agent
> **禁止静默迁移、禁止全员一刀切关双写。**
## 硬规则
1. **旧客默认不变**:未评估、未签字前不得改默认模式。
2. **同表互斥**:某表不得「一边 HTTP 双写、一边进 dbsync 白名单」。
3. **顺序不可颠倒**:先停该表双写 → 再进白名单 / 切 `local_dbsync`
4. **agent 非安装强依赖**:迁移观察期允许只落本地 + 积压;须告知「需 agent 才上云」。
## 推荐流程(单表 / 单库)
| 步骤 | 动作 | 验收 |
|------|------|------|
| 1 | 智建通道准备目标表白名单、UUID PK 校验通过、remote DSN 可达 | 保存通道成功 |
| 2 | 客户端仍 `online_primary`:对该表**停止**双写(或从双写表白名单移除) | 该表仅写本地或仅走约定路径 |
| 3 | 观察 ≥1 个业务周期:无双写残留、无重复行 | 抽查线上/本地主键 |
| 4 | 显式设 `YXD_SYNC_MODE=local_dbsync`,写入表白名单缓存/通道拉名单 | `/sync/status` 显示 local_dbsync |
| 5 | 登记 Binding可选`local_database_id → online_db_id` | GET bindings 命中 |
| 6 | 装本机 agent观察 outbox 清空、同 UUID 上云 | agent push 成功;超管可查 LWW若有覆盖 |
| 7 | 确认稳定后再扩大白名单;**勿**对全员默认切模式 | 旧客未改默认 |
## 回滚
1. 客户端改回 `online_primary``local_only`
2. 智建侧可停通道 / 缩表白名单(勿删线上数据)。
3. 未推完的 outbox 由对方客户端自行处理;平台不强制清。
## 新客评估M4
仅对**新客**或**书面确认的迁移客**评估是否默认 `local_dbsync`
M4 前:**禁止**全员切默认。
## 相关
- [数据同步-开通说明.md](./数据同步-开通说明.md)
- [同步表约定.md](./同步表约定.md)
- 冻结方案:`松离线-dbsync方案-最终版.md` §8 / O7