Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
20 KiB
宇恒 × 智建 · 松离线数据同步使用文档
面向:宇恒一号客户端对接同学
平台侧仓库:智建(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 停了也不能挡保存。
业务保存 ──► 本机 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 | 未开通用户:零同步文案(无强制状态条、无「已自动合并」) |
开通判定(写路径):
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 | 必须显式写出,永不因「配了线上地址」自动升 |
建议本地测试:
# 默认无感:不要设 local_dbsync,或显式:
YXD_SYNC_MODE=local_only
开通增值(过渡 / 运维仍可用 env;生产优先换票落点):
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
调试兜底表白名单(通道拉取失败前):
YXD_SYNC_WHITELIST=orders,order_items
# 或
YXD_SYNC_DBSYNC_TABLES=orders,order_items
3.1 生产绑定与落点(Z12 / Z13 · 必读)
产品约定
- 一个登录账号 / 一个智能体 ↔ 本公司同步落点;个人库默认隔离,禁止 A 数据进 B 库。
- 禁止把「手抄通道 ID / 先去控制台新建通道」当作普通用户开通主路径。
- 公司默认同步通道由智建在启用智能体时自动创建(
is_system_default);控制台「数据同步」留给运维改 DSN。 - 库选「同步」→ 自动 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)
GET /api/v1/agents/me
Authorization: Bearer <智能体 Token>
返回 channel_id / online_db_id / database_name / sync_bound / status(无需「管理智能体」权限)。
绑定码(Z13a/b)
管理员(需「数据同步」):
POST /api/v1/admin/bind-codes
Authorization: Bearer <管理员 JWT>
Content-Type: application/json
{ "max_uses": 1, "expires_hours": 168, "note": "开通单" }
GET /api/v1/admin/bind-codes
DELETE /api/v1/admin/bind-codes/{code}
终端兑换(公开):
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。
POST /api/v1/auth/bind/phone-lookup
Content-Type: application/json
{ "phone": "13531041944" }
响应要点:exists、tenant_name、masked_name、need_confirm、message(不执行绑定)。
POST /api/v1/auth/bind/phone-confirm
Content-Type: application/json
{
"phone": "13531041944",
"host_key": "<本机 host_key>",
"confirm": true,
"local_database_id": "可选,本机库 ID"
}
| 场景 | 行为 |
|---|---|
| 同号命中公司成员 | 必须询问「已有账号是否绑定」;确认才 phone-confirm |
| 用户取消 | 不绑定;可改走绑定码 / 换号 |
| 无此成员 | 提示用绑定码;勿用超管号 13531041945 测 |
| 生产联调样例 | 智建与宇恒均为 13531041944 |
未绑定时客户端流程(Z13d · 宇恒待接)
换票 / agents/me → sync_bound=false
├─ 有手机号 → phone-lookup
│ ├─ need_confirm → 【弹窗】确认?→ phone-confirm / 取消
│ └─ 未命中 → 绑定码或换号表单
└─ 无手机号 → 绑定码表单
绑定成功 → 库选「同步」→ Binding + drain(零通道配置)
4. 智建侧前置(公司管理员)
生产推荐(Z12/Z13)
- 公司级配置默认同步 DSN(
DBSync.DefaultRemoteDSN,生产 Postgres)。 - 启用智能体(勿要求用户先「新建通道」)→ 平台自动建默认同步通道并写回智能体落点。
- (可选)生成绑定码发给终端;或引导用户用公司成员手机号做同号确认(须弹窗)。
- 运维需要时再在「数据同步」改 remote DSN / 查看通道 ID(可复制)。
运维高级 / 联调过渡(手建通道)
- 登录智建控制台 → 数据同步 → 新建通道。
- 生产:
remote.driver=postgres,DSN 例:postgres://user:pass@host:5432/db?sslmode=disable(须可达)。 - 表白名单可空(整库 Binding 策略);若填表须 UUID TEXT/UUID PK + 外键闭包。
- 方向推荐 本地 → 线上;冲突策略推荐 源端覆盖(lww_source)。
- 保存通过校验后记下 通道 ID(过渡写入
YXD_SYNC_CHANNEL_ID)。 - 二选一鉴权:
- 管理路径:发带「数据同步」的智能体 Token / 管理员 JWT;或
- 用户自助:终端用登录用户 JWT;先登记 Binding,push 带本人
online_db_id。
单服务器:一条默认同步通道即可(自动或手建)。
多服务器:每台线上 Postgres 各建一条通道;按本机库选择对应 channel_id(建议登记 Binding)。
Binding(多库 / 用户自助时建议登记;登录 JWT 即可,不必管理员权):
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「本地库名 ↔ 线上库名」。
GET /api/v1/admin/sync/bindings?local_database_id=...
5. 宇恒侧实现清单
5.1 写网关旁路(M1,可无 agent)
对 insert / update / delete / 写 SQL:
- 读模式与白名单。
- 未命中:原路径,响应形状与现网一致(可多字段,不可少成功语义)。
- 命中:
- insert:缺 id 或非法 id → 自动补小写带连字符 UUID;
- 写入本机正式库 B;
- append outbox(失败只打日志,不挡保存);
sync_status:pending/local_only_table等仅开通用户可见。
文案:需 agent 才上云(不得暗示已上云)。
5.2 Outbox
建议路径(等价即可):
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。默认可不启。
循环建议:
- (TTL)拉白名单 → 写本地缓存。
list_pending→ 逐条 push → 成功mark_done/ 失败mark_error并 break。- 写心跳文件(供
/sync/status展示agent.running/last_beat)。
拉白名单
GET /api/v1/agent/sync/channels/{channel_id}/whitelist
Authorization: Bearer <token>
响应要点:tables、pk_columns、conflict_policy。
推单条
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。
下行(仅线上 / 灌库 / 补齐)
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(见下「表结构同步」)。
成功响应示例:
{
"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 时,空表不会出现在线上(无变更事件)。同步周期应额外:
- 本机 → 线上:对本机每张业务表(含 0 行)调用 ensure:
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);已存在则跳过。
- 线上 → 本机:先拉结构,本机缺表则建空表,再 bootstrap 行:
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(或你们现有等价路由)对开通用户返回:
modelocal_dbsync.whitelist_tables/outbox_pendingagent.enabled_flag/running/last_errornote:未开通勿强塞同步 UI
6. UUID 约定
- 标准形式:小写 + 连字符
8-4-4-4-12 - 入库前
normalize;无连字符 32 hex 可规范化 - 默认建表模板不要改成 UUID;只给同步表白名单表用
SQLite 同步表示例:
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY NOT NULL,
title TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
7. 从旧双写迁入(O7,摘要)
- 该表先停
online_primary双写。 - 观察无残留。
- 再进白名单并切
local_dbsync。 - 装 agent,看 outbox 清空。
- 禁止静默全员迁移;旧客默认不变。
完整步骤见智建仓:docs/数据同步-迁移手册.md。
8. 联调检查表
未开通回归(必过)
- 不设
local_dbsync:插入自增表与现网一致 - 无同步强制文案、无强制装 agent
- 保存不因同步模块报错失败
开通 + 无 agent
- 白名单表本地立刻可见(UUID)
- outbox 增长;响应提示需 agent 才上云
- 非白名单表仍走原路径
开通 + agent
- 拉白名单成功并缓存
- push 后线上 A 出现同一 UUID(不双行)
- 重复 push 同 version 幂等
- agent 停:仍可本地保存;恢复后按序追上
- delete 保序,不换新 UUID
- 空表:
schema/ensure后线上出现空表(Z10c)
生产绑定(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/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) |
| 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. 本地联调最小步骤
生产路径(推荐)
- 智建起栈;公司配置
DefaultRemoteDSN(或联调用空 DSN → sqlite 文件)。 - 启用智能体 → 自动绑默认同步通道;或管理员
POST .../admin/bind-codes发码。 - 宇恒:
host_key换票 /agents/me看sync_bound;未绑则 lookup→确认 或 redeem。 - 本机库选「同步」→ Binding + agent drain;不必手填
YXD_SYNC_CHANNEL_ID。 - 同号联调用
13531041944,禁止超管号。
过渡 / 手建通道(仍可用)
- 智建本机起栈(
.env已是本地:AIJZ_PUBLIC_BASE_URL=http://127.0.0.1:8180)。 - 控制台建通道:
remote优先本机 Postgres;拷贝通道 ID。 - 拿 token(管理员登录或智能体 client_credentials)。
- 宇恒设:
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=...
- 对白名单表插一行 → 看本机 B + outbox → agent 推上 A → 比对 UUID。
- 多服务器演练:再建第二条通道指向另一 Postgres,换
CHANNEL_ID验证不串库。 - 空表:sync 周期调
schema/ensure+schema(Z10c),否则空表不会出现在线上。
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 弹窗与选同步即用由宇恒在己方仓库实现,勿改智建仓业务代码。