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>
This commit is contained in:
whm
2026-08-05 09:47:35 +08:00
parent 76cdcd760e
commit b04b180d30
59 changed files with 4762 additions and 308 deletions

View File

@@ -35,6 +35,22 @@ CREATE TABLE IF NOT EXISTS orders (
形态 B 下本机 SQLite 可能不可达:至少 **remote** 须能完成校验。
### 线上库 A DSN生产统一 Postgres
```text
postgres://user:pass@127.0.0.1:5432/app_online?sslmode=disable
```
联调临时可用 SQLite不宜多 writer
```text
file:E:/data/ajz_remote.db?_pragma=foreign_keys(1)
```
使用正斜杠;平台会规范化反斜杠并补 `busy_timeout`
单服务器一条通道即可;多服务器为**每台线上库各建一条通道**,客户端选用对应 `channel_id`
## 宇恒开通opt-in
```bash
@@ -55,6 +71,9 @@ YXD_SYNC_DBSYNC_TABLES=orders,order_items
推送落到通道 **remote**(线上 A幂等认客户端 version平台不连用户本机 SQLite。
**幂等**:同 version 再 push → `{ success:true, result:{ skipped:true, ... } }`;重复 HTTP 安全,线上不双行。
**可重试**remote IO 失败 → `503` + `retryable:true`(勿换 UUID
宇恒建议环境变量(由对方配置,不在智建仓改):
```bash

View File

@@ -1,6 +1,7 @@
# 跨库数据同步中间件
支持 **SQLite ↔ MySQL ↔ Postgres**。平台侧 worker 可轮询两端 outbox**形态 B本机 agent** 下由终端 agent 经平台 **push** 写线上 A平台**不直连用户本机 SQLite**
支持方言 **Postgres生产线上库 A 推荐)** / MySQL / SQLite。
**形态 B本机 agent**:终端 agent 经平台 **push** 写线上 A平台**不直连用户本机 SQLite**。
冲突策略默认 **自动 LWW源端覆盖**;落败写入**平台超级管理员**覆盖日志。公司管理员**无冲突台**。
@@ -13,20 +14,29 @@
| LWW 覆盖审计 | **仅平台超级管理员**;公司 top → 403 |
| 数据隔离 | 通道带 `tenant_id`;公司 A 看不到公司 B |
## 推荐场景(松离线 B→A
## 推荐场景(松离线 B→AA=Postgres
| 端 | 角色 |
|----|------|
| **B** | 本机正式库UUID 主键);开通且白名单表本地可见 |
| **A** | 线上agent 经平台 push 幂等写入 |
| **A** | 线上 **Postgres**(通道 `remote`agent 经平台 push 幂等写入 |
| **Agent** | 读本机 outbox → `POST /api/v1/agent/sync/channels/:id/push` |
推荐配置:
1. 通道:`local` 描述本机表名单,`remote` = A 的 DSN方向 **`local_to_remote`**;策略 **`lww_source`**。
2. 表白名单须 UUID TEXT PK + FK 闭包(保存时校验)。
1. 通道:`remote.driver=postgres` + DSN方向 **`local_to_remote`**;策略 **`lww_source`**。
2. 表白名单须 UUID TEXT/UUID PK + FK 闭包(保存时校验)。
3. 客户端显式 `local_dbsync`(见开通说明);未开通用户零感。
4. 怀疑漏数时点 **同步修复(对账)**(有最小间隔限流);双向通道自动对账约 **15 分钟** 一次
4. 怀疑漏数时点 **同步修复(对账)**(有最小间隔限流)。
### 单服务器 / 多服务器(同一套 API
| 部署 | 配置 |
|------|------|
| 单服务器 | 一条通道指向该 Postgres一个 `channel_id` |
| 多服务器 | **每库一条通道**(不同 remote DSN客户端按库选择 `channel_id` / Binding |
协议相同,均为「经智建 push」无需第二套接口。
如何保证**不漏、不多**
@@ -42,22 +52,104 @@
| 项 | 说明 |
|----|------|
| 方言 | `sqlite` / `mysql` / `postgres` |
| 方言 | 生产 A`postgres`;兼容 `mysql``sqlite` 仅联调临时 A不宜多 writer |
| 方向 | 本地→线上(推荐)/ 线上→本地 / 双向 |
| 冲突 | 默认 `lww_source``lww_target``queue` 仅调试(租户不可见) |
| Agent | `GET .../agent/sync/.../whitelist``POST .../push` |
| Binding | `GET/POST /api/v1/admin/sync/bindings` |
| 对账 | `POST .../reconcile`(默认最少间隔 300s |
| Agent | `GET .../agent/sync/.../whitelist``POST .../push`(管理员/智能体「数据同步」,或登录用户 + 本人 Binding |
| Binding | `GET/POST /api/v1/admin/sync/bindings`(同上鉴权;普通用户仅本人) |
| 对账 | `POST .../reconcile`(默认最少间隔 300s;仍需「数据同步」 |
| 审计 | `GET /api/v1/platform/dbsync/lww-overrides`(超管) |
持久化:`data/dbsync/channels.json``lww_overrides.json`(及遗留 `conflicts.json`)。
持久化:`data/dbsync/channels.json``bindings.json``lww_overrides.json`(及遗留 `conflicts.json`)。
## 用户自助同步(库级三态)
终端用户可在客户端对名下库选择仅本地 / 仅线上 / 同步;选 **同步** 后用**登录 JWT**(无需单独「数据同步」管理员权)调 agent API。
| 角色 | 范围 |
|------|------|
| 「数据同步」管理员 / 智能体 | 租户内通道push 不强制 Binding |
| 普通登录用户 | 仅本人 Binding`channel_id` + `online_db_id`push **须带** `online_db_id` |
### 表白名单策略Z4已冻结
| 项 | 行为 |
|----|------|
| **权限源** | 登录 JWT + 本人 Binding 的 `online_db_id`(不是通道 `tables` |
| **push** | 接受 Binding 库内**任意表**;表不存在时**自动建表**TEXT 列 + PK |
| **通道 tables** | 仅历史兼容 / whitelist 接口展示;**不拒收** |
管理员不必再为业务表维护白名单。宇恒默认 `YXD_SYNC_ENFORCE_WHITELIST=0`
### 下行 / 仅线上Z5
形态 B平台**不直连本机 B**,只读线上 A把行返回给本机 agent 写入 B建议 `WithApplying` 防回声)。
| mode | 用途 |
|------|------|
| `bootstrap` | 全量分页灌库(「仅线上」首启 / 本地空库) |
| `pks` | 只列 A 上主键,客户端 diff 后再 `rows` |
| `rows` | 按 `row_pks` 取行补齐 |
```http
POST /api/v1/agent/sync/channels/{id}/pull
POST /api/v1/agent/sync/channels/{id}/bootstrap # mode=bootstrap
```
```json
{
"mode": "bootstrap",
"table": "orders",
"after_pk": "",
"limit": 200,
"online_db_id": "用户自助必填"
}
```
循环直到 `result.has_more=false`。单页默认 200、上限 500。
## Agent push 幂等与重复投递
- **正确性**:同 `(table, row_pk, version)` 已落地 → `result.skipped=true` 且 HTTP 2xx线上 A **不双行**
- **允许**客户端重复 push例如多 agent / 双 drain去重是**客户端优化**(进程内 drain 锁),不是平台必做。
- 通道 stats`pushed_applied` / `pushed_skipped` 可观察重推噪声。
- remote 暂不可达HTTP **503** + `retryable: true`agent 可稍后重试,勿换 UUID
- **Postgres A** 适合正常并发;若联调仍用 SQLite 作 A请客户端**按库串行 drain**。
- 平台对 remote 连接做**池化复用 + 通道启动预热**,避免每次 push 冷开 SQLite 触发超时。
## 请求日志怎么查
| 来源 | 位置 / 接口 | 内容 |
|------|-------------|------|
| Platform HTTP | `.runtime/logs/platform.err.log`go-zero | 路径、耗时、状态码;超时曾见 `slowcall(3000ms)` |
| Platform dbsync | 同文件,关键字 `dbsync push` / `dbsync pull` | channel、table、pk、user、outcome、dur、`req=`(X-Request-Id) |
| Gateway | `.runtime/logs/gateway.out.log` | sync 路径与 ≥500`gateway POST /api/... status=... req=` |
| 审计 API | `GET /api/v1/audit/logs`(需「查看审计」) | `action``dbsync.push` / `dbsync.pull` / `*.error`detail JSON 带 `req_id` |
联调时用响应头 / 请求头 `X-Request-Id` 可在三处日志对齐同一笔请求。
## 线上 Postgres DSN 示例
```text
postgres://sync_user:pass@127.0.0.1:5432/app_online?sslmode=disable
```
联调临时 SQLite A不推荐生产
```text
file:E:/path/to/remote.db?_pragma=foreign_keys(1)
```
勿用反斜杠;平台会自动规范化并补 `busy_timeout`
## 控制台用法
1. **数据同步** → 新建通道(UUID 表白名单)→ 测试 → 保存 → 启动
1. **数据同步** → 新建通道(remote 选 **postgres**)→ 测试 → 保存 → 启动
2. 默认策略选 **源端覆盖**;方向首期用 **本地 → 线上**
3.**同步修复** 做主键对账(勿连续狂点,有限流)
4. LWW 明细在 **平台工作台**(超管),不在公司同步页
5. 多服务器:为每台库再新建一条通道,把对应 `channel_id` 发给客户端
开通与迁移:见 [数据同步-开通说明.md](./数据同步-开通说明.md)、[数据同步-迁移手册.md](./数据同步-迁移手册.md)。
@@ -76,13 +168,18 @@
| GET/POST | `/api/v1/admin/sync/bindings` |
| GET/POST | `/api/v1/admin/sync/conflicts*`**403**(已迁超管审计) |
### 本机 Agent
### 本机 Agent / 用户自助
鉴权Bearer = 「数据同步」**或** 已登录人类 JWT后者须先 `POST .../bindings` 且 push 带本人 `online_db_id`)。
| 方法 | 路径 |
|------|------|
| 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` |
| POST | `/api/v1/agent/sync/channels/{id}/pull` |
| POST | `/api/v1/agent/sync/channels/{id}/bootstrap` |
| GET/POST | `/api/v1/admin/sync/bindings`(用户自助亦可) |
### 平台超级管理员

View File

@@ -13,19 +13,70 @@
智建控制台「数据同步」配的是**通道 + 表白名单 + 线上 DSN**;是否走 `local_dbsync` 由客户端环境变量决定,平台**不会**替全员切默认。
## 线上库 A统一 Postgres
| 场景 | 推荐 |
|------|------|
| **生产 A** | **`postgres`**(经智建 agent push 写入;可与平台同机或独立实例) |
| 本机 B | 宇恒侧多为 SQLite通道 `local` 仅描述表白名单,形态 B 下平台可不连 B |
| 联调临时 A | 可用 sqlite 文件;**不宜**多 agent 并发写 |
Postgres DSN 示例:
```text
postgres://user:pass@127.0.0.1:5432/app_online?sslmode=disable
```
## 单服务器 / 多服务器(同一套 API
同步**一律经智建平台**agent → whitelist/push不因服务器数量改协议。
| 部署 | 怎么配 |
|------|--------|
| **单服务器** | 公司一条通道:`remote` 指向该 Postgres宇恒配置一个 `YXD_SYNC_CHANNEL_ID` |
| **多服务器** | **每台线上库一条通道**(不同 remote DSN宇恒按 `local_database_id` / Binding 选用对应 `channel_id`(或多 agent 实例各绑一通道) |
Binding`POST /api/v1/admin/sync/bindings` 登记 `local_database_id → online_db_id`,并带上 `channel_id`
管理员 Token 或**登录用户 JWT**均可登记;普通用户只能登记/查看本人 Binding。
## 模块与本机库(一份表 · Z8
| 原则 | 说明 |
|------|------|
| **本机 B 管理面** | 模块业务表应能在宇恒本机库如「AI建站智能体API」里增删改查/导入,与普通表同一体验 |
| **上云** | 开通 sync 后经 outbox → agent push → 智能体绑定的线上库 A |
| **智能体绑库** | 控制台「用户管理」为智能体填 `channel_id` / `online_db_id` / `database_name`;「数据同步」可按智能体筛通道 |
| **发布落库** | 智能体 Token 新建发布时,若已设 `database_name`,优先 `database_per_app` 写入该库 |
不要默认维持「模块只在平台 `/apps` schema、松离线又一套 SQLite」双轨对账时本机有、线上无 = 尚未 push不是串库。
## 用户自助(库级三态)
客户端对每个库可选:仅本地 / 仅线上 / 同步。选同步后:
1. 用户 JWT → `POST /api/v1/admin/sync/bindings`(带 `channel_id`
2. 同 JWT → `GET .../agent/sync/channels/{id}/whitelist`
3. 同 JWT → `POST .../push`Body 须含本人 `online_db_id`
通道表白名单**非空**时只同步命中表;**空名单**时平台接受整库表名UUID PK
「仅线上 / 下行拉齐」:`POST .../agent/sync/channels/{id}/pull``mode=bootstrap|pks|rows`)或 `.../bootstrap`;平台返回 A 上行,客户端写入本机 B。
**空表也要两侧建齐**:本机空表 → `POST .../schema/ensure`;线上空表 → `POST .../schema`(或 pull 的 `columns`)在本机 `CREATE IF NOT EXISTS`。仅靠 outbox 行 push **不会**带上空表。
控制台「查看线上表」可 **删表**`POST .../admin/sync/channels/{id}/drop-table`):只删当前查看侧,**不同步** `DROP` 到另一侧;本机仍有同名表时下次 push/ensure 可能再建回来。
## 开通步骤(增值)
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`
1. 「数据同步」建通道:`remote.driver=postgres` + 线上 DSN表白名单须 **UUID TEXT/UUID PK** + FK 闭包。
2. 方向 **本地 → 线上**,策略 **源端覆盖lww_source**
3. 客户端显式 `YXD_SYNC_MODE=local_dbsync`,配置 `YXD_ONLINE_API_BASE``YXD_SYNC_CHANNEL_ID`token`同步表约定.md`)。
4. 装本机 sync agent 后上云;未装:**本地可保存**,文案「需 agent 才上云」。
5. 多库时登记 Binding,避免串库
## 谁能看什么
| 角色 | 可见 |
|------|------|
| 公司管理员 | 通道配置、对账(同步修复)、统计;**无**冲突台 / LWW 覆盖明细 |
| 普通登录用户 | 不可配通道;可自助 Binding + 用登录 JWT push 本人库 |
| 平台超级管理员 | LWW 覆盖审计(平台工作台) |
| 未开通终端用户 | **零同步文案**,无强制状态条 |