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

@@ -0,0 +1,433 @@
# 联调后修改意见 · 宇恒松离线(形态 B
> 初稿2026-08-01
> 修订2026-08-03对照智建已合入改动 + 宇恒压测结果回写)
> 再修订2026-08-03 下午(用户自助 JWT / Binding / 库级策略复测,见 §5.3
> 再修订2026-08-04§5.6 Z9 模块导入默认能力 · 方案 A
> 再修订2026-08-04 晚§5.5 Z8f 本机镜像已通§5.5.2 Z8g SyncPage
> 再修订2026-08-04§5.7 **Z10 空表双侧建齐**§0/§1/§5.4 现状回写)
> 再修订2026-08-05§5.6 **Z9e/Z9f 存量 import 必须扫库**`coerce_fields` 复现)
> 来源:宇恒客户端 `yuhengyihao_client` ↔ 本机智建 `127.0.0.1:8180`gateway/ `:8888`platform
> 依据:`松离线-dbsync方案-最终版.md`、`宇恒-松离线数据同步使用文档.md`
---
## 0. 当前结论
| 级别 | 状态 | 说明 |
|------|------|------|
| **硬改(阻塞宇恒上线)** | **无**(松离线主路径) | Z1Z7、**Z9**、**Z10a/b智建 API** 已落实;主路径可上线 |
| **原建议项 2.12.5** | **智建侧基本已落实** | 见 §2 对照表;接口请保持兼容 |
| **用户自助 Z1Z7** | **已落实** | Z2 宇恒复测通过Z7 Binding 可读名已合入(需宇恒 ensure 时带 `database_name`/`display_name` |
| **Z8 模块↔本机库+智能体库** | **大部分落实** | Z8a/b/c/e 智建;**Z8f 宇恒本机 `模块·名·资源` 已可管+可 sync****Z8d 路径已通****Z8g** 展示增强仍待智建P1非硬阻塞见 §5.5.2 |
| **Z9 模块导入默认能力(方案 A** | **新建已落实;存量未闭环** | Z9ad 发布时兜底;**Z9e/Z9f 必须做**:旧模块未再发布仍会 `operation import not allowed`2026-08-05 再复现),见 §5.6 |
| **Z10 空表双侧建齐** | **智建 API 已落实;宇恒待接** | `schema/ensure` + `schema` + pull `columns`**Z10c 待宇恒** sync 周期调用,见 §5.7 |
| **Z11 蓝图↔库列一致** | **待智建** | 增字段未迁 Postgres → 导入 42703见 §5.8 |
| **仍建议关注** | 性能/运维 | SQLite remote **高并发 push** 易锁agent 宜单库串行 drain属宇恒用法非平台硬改 |
宇恒对照脚本:
- `yxd/app_fastapi/smoke_zhijian_e2e.py`
- `yxd/app_fastapi/smoke_offline_concurrent.py`
- `yxd/app_fastapi/smoke_zhijian_gateway_burst.py`(连续 push / stats
- `yxd/app_fastapi/smoke_stress_sync.py`(高并发 + gateway 压测)
复测摘要改后gateway 串行 60/60、`eof502=0`;同 version → `skipped``pushed_applied` / `pushed_skipped` 有值;并发 push 加重试可压满(锁冲突靠重试消化)。
---
## 1. 已验证可用(请保持兼容)
请**勿破坏**下列契约(宇恒 agent 已按此对接):
1. `GET /api/v1/agent/sync/channels/{id}/whitelist`
- 返回含 `tables``pk_columns``conflict_policy`
2. `POST /api/v1/agent/sync/channels/{id}/push`
- Body`table, op, row_pk, row/rows, version, client_outbox_id`(用户自助另加 `online_db_id`
- `op``insert|update|update_by_id|delete`(平台侧 insert/update 归一为 upsert 可接受)
- 成功:`{ success: true, result: { ok, applied, skipped, conflict, applied_version, message } }`
- **同 `(table,row_pk,version)` 已落地 → `skipped=true` 且 HTTP 2xx**
3. `POST .../pull``POST .../bootstrap`
- 下行灌库;响应含 `columns`(空表也返回列,便于本机建表)
4. `POST .../schema``POST .../schema/ensure`**Z10**
- 拉线上表结构 / 本机空表结构推到线上(`CREATE IF NOT EXISTS`,无需 outbox 行)
5. 鉴权:管理员 JWT含「数据同步」、智能体 Token或**登录用户 JWT**(须本人 Binding + `online_db_id`)均可调 agent API
6. 错误体优先带可读 `message`;忙/上游失败宜带 `retryable: true`502/503
演示账号联调可用:`phone=13800000001` / `demo123`(超管快捷号见控制台;另见 seed `13531041945`)。
---
## 2. 原建议项落实对照2026-08-03
| 编号 | 原建议 | 智建现状 | 是否还要改 |
|------|--------|----------|------------|
| **2.1 P1** | gateway 偶发 502 EOF超时与可重试 JSON | `ProxyTimeoutSec` 已生效sync 路径加长超时502 体含 `retryable`SQLite DSN 规范化 + `busy_timeout`remote IO → 503+retryable见方案 changelog 2026-08-01 | **否(已落实)**。若生产仍偶发 EOF按运维排障不升格为接口变更 |
| **2.2 P2** | 文档写明多 agent / version 幂等 | `docs/数据同步-中间件.md` 已写「允许重复 push / 客户端 drain 锁优化」 | **否** |
| **2.3 P3** | `pushed_applied` / `pushed_skipped` | `ChannelStats` 已有burst 联调可见 | **否**。OpenAPI 已标注 applied/skipped示例可再补一句即可非必须 |
| **2.4 P4** | Windows `file:` DSN 示例 | 开通/同步表约定已补正斜杠示例;平台规范化反斜杠 | **否** |
| **2.5 P5** | 503/502 + retryable | 已按表落实 | **否** |
### 2.6 【新增·知会】高并发 push 与 SQLite remote
**现象**(宇恒 `smoke_stress_sync`):对同一通道 remote本机 SQLite**并行** push 时,大量 `database is locked` / 失败,串行则稳定;加重试后可最终成功,但 p95 延迟明显升高。
**边界**
- 正确性仍靠 **version 幂等**,不要求平台做 push 租约。
- **推荐用法**:宇恒 agent **按库串行 drain**(勿多线程齐推同一 SQLite A
- 若线上 A 为 MySQL/Postgres并发能力通常好于 SQLite文档可一句带过「SQLite remote 不适合高并发多 writer」。
**智建是否必须改****否**。可选更低优remote 打开时统一更长 `busy_timeout`、或文档强调 SQLite 并发限制(已有 busy_timeout 则足够)。
---
## 3. 明确不需要智建改的部分
| 项 | 负责方 | 说明 |
|----|--------|------|
| 本机 drain 互斥锁 / 串行 drain | 宇恒 | 减重复 HTTP 与 SQLite 锁风暴 |
| 写网关 / outbox / UUID 补齐 | 宇恒 | 未开通零感;仅白名单表同步 |
| 「agent 停仍可保存」 | 宇恒 | H3 |
| 终端冲突处理台 | 双方已否决 | 冲突只在平台超管侧 |
| 改宇恒仓库代码 | 禁止由智建代改 | 分工已冻结 |
---
## 4. 联调回归清单(改后复测)
| 项 | 宇恒侧结果 |
|----|------------|
| whitelist 与通道一致 | ✅ |
| push insert → A 同 UUID | ✅ |
| push update / delete | ✅ |
| 同 version → skippedA 不增行 | ✅ |
| gateway 连续 push ≥20 无 EOF | ✅burst 25、stress 串行 60 |
| stats `pushed_applied` / `pushed_skipped` | ✅ |
| 并发 pushSQLite | ⚠️ 需重试;建议客户端串行 |
未在每次脚本覆盖:无「数据同步」权限 → 403旧 version + `lww_source` 全矩阵——以平台单测/文档为准即可。
---
## 5. 宇恒侧说明(知会)
- 测试已收紧mock 对齐 skipped、双 drain 断言 applied==N、update/delete 保序、压测脚本带锁重试。
- **开通仍是可选**`YXD_SYNC_MODE=local_dbsync`;用户自助以 Binding 为准(**Z4**:不按通道表白名单拒收)。
- **空表**sync 周期须调 **Z10** `schema/ensure`(见 §5.7),否则本机空表不会出现在线上。
- 原「若采纳 2.1 则稳定性提升」:**已验证提升**;本意见可归档为「历史建议 + 落实对照」,无需再开一轮同名改造。
---
## 5.1 【新增·2026-08-03】用户自助库级三态宇恒已开做
产品目标:用户登录智能体后,用**自己的全部权限**管理名下数据库/网页;可对每个库选择:
| 产品态 | 含义 | 宇恒引擎映射 |
|--------|------|--------------|
| **仅本地** `local_only` | 只写本机 | `local_only` |
| **仅线上** `online_only` | 以线上为主 | `online_primary` |
| **同步** `sync` | 本机落库 + outbox 上云 | `local_dbsync` |
宇恒已落地(本机):
- `POST/GET /database/sync/mode``GET /database/sync/policies`
- 策略键:`(tenant_id, user_id, local_database_id, database_name)`
- 写网关按命名库策略覆盖全局 `YXD_SYNC_MODE`(默认仍不静默升 sync
- 数据库区卡片「同步设置」可视化三态;鉴权优先**登录用户 JWT**`app.client.access_token`
- 选「同步」后可拉起本机 agentpush 带头里的用户 Bearer
---
## 5.2 【请智建配合改 / 兼容】用户态同步
> 不破坏现有管理员通道 + agent Token 联调路径;以下为**用户自助开通**所需平台能力。
| 编号 | 优先级 | 诉求 | 说明 | 智建现状2026-08-04 |
|------|--------|------|------|------------------------|
| **Z1** | **P0** | **登录用户 JWT 可调 agent sync API** | whitelist / push / batch 允许已登录普通用户(本人资源范围)。 | **已落实** |
| **Z2** | **P0** | **权限范围 = 用户自己的库** | 带 `online_db_id` 时一律校验本人 Binding异库 → 403含有「数据同步」的人类。 | **已落实** |
| **Z3** | **P1** | **Binding 支持用户自助登记** | `POST/GET .../bindings` 认用户 JWT强制本人 `user_id`。 | **已落实** |
| **Z4** | **P1** | **用户选 sync 时白名单策略** | **冻结②**不按通道表白名单拒收Binding 库内任意表可 push可自动建表。 | **已落实** |
| **Z5** | **P2** | **仅线上 / 下行补齐** | A→B / pull / bootstrap。 | **已落实** |
| **Z6** | **P2** | **控制台文案** | Binding 即权限;管理员不负责维护表白名单。 | **已落实** |
| **Z7** | **P1** | **SyncPage / Binding 展示「可读库名 + 映射」** | 通道「线上」列优先可读名;副文案 online_db_id / 落库路径;统计标注为 push 次数Binding 列表「本地库名 ↔ 线上库名」。 | **已落实2026-08-04**Binding 增 `database_name`/`display_name`SyncPage 主标题优先本地 `database_name`,↑ 标注「推送次数 / 非表数」。宇恒 ensure 须带可读名,否则仍会回退到通道名或 driver |
| **Z8** | **P0** | **模块数据:本机库可管理 + 按智能体上云** | 见 §5.5:本机 B 管理模块表push 到智能体绑定的 A废弃默认双轨 | **大部分落实**Z8a/b/c/d/e/f**Z8g 展示增强待智建**P1 |
| **Z10** | **P0** | **空表两侧建齐** | 见 §5.7:仅靠 outbox 不会建空表 | **智建 API 已落实****Z10c 待宇恒**接 ensure/schema |
### 建议验收(智建改后)
1. 演示账号登录拿用户 JWT**无**单独「数据同步」管理员权)→ `GET .../channels/{id}/whitelist` → 2xx。
2. 同 JWT → `POST .../push` 写入本人 Binding 库 → A 出现同 UUID。
3. 同 JWT 推他人 `online_db_id` → 403。
4. 原管理员 / 智能体 Token 路径回归仍通过(不破坏联调结论)。
### 宇恒 ↔ 智建分工(本项)
| 方 | 负责 |
|----|------|
| 宇恒 | 库级三态 UI/API、写路径分流、outbox、用登录态调 push**默认不按通道表白名单过滤****Z10c** 调 schema/ensure |
| 智建 | 用户 JWT 鉴权、Binding 校验、任意表 push**SyncPage/Binding 按可读名展示Z7****Z10a/b** 空表 API |
---
## 5.3 【复测·2026-08-03 下午】智建改后 × 宇恒库级策略
> 环境:`127.0.0.1:8180` + `:8888`;演示账号 `13800000001` / `demo123`
> 脚本:`yuhengyihao_client/yxd/app_fastapi/smoke_zhijian_e2e.py`、`smoke_user_jwt_policy.py`
> 说明:直连智建验收(当时本机 8080 未起push 遇 gateway **502 EOF / 503** 时按 `retryable` 重试后成功。
### 对照表
| 编号 | 项 | 结果 | 备注 |
|------|----|------|------|
| — | 登录拿用户 JWT | ✅ | `token_len=225``/api/v1/auth/me` 可读 |
| — | 经典 e2eB→outbox→push→A 同 UUID | ✅ **PASSED** | 首次 push 常 502/503约 24 次重试后成功;二次 insert 同行数≥2 |
| **Z1** | 用户 JWT 拉 whitelist | ✅ | HTTP 200`tables=['orders']` |
| **Z1** | 用户 JWT 直推本人库 | ✅ | 带 `online_db_id`A 可见同 UUID重试后 200或同 version → `skipped` |
| **Z3** | 用户 JWT 自助 Binding | ✅ | `POST .../admin/sync/bindings` → 200返回 `user_id/local/online/channel_id` |
| — | 库级策略 `sync` → 写网关 `local_dbsync` | ✅ | 全局未强制 `YXD_SYNC_MODE=local_dbsync` 时,命名库策略仍进 outbox |
| — | 登录态 agent drain 上云 | ✅ | drain 重试后 `pushed=1`A 行 title=`policy-sync` |
| **Z2** | 推他人 `online_db_id` → 403 | ✅ **宇恒复测通过2026-08-04** | `403 无权访问该 online_db_id非本人 Binding``smoke_user_jwt_policy.py` PASSED |
| **Z4Z6** | 白名单 / 下行 / 文案 | ✅ | Z4 冻结②已落实(任意表+自动建表Z5 pullZ6 SyncPage |
### 现象与建议(运维)
1. **Gateway 偶发 502 EOF / 503 Request Timeout**,体带 `retryable: true`;正确性仍靠 version 幂等(重试后常见 `skipped=already applied`)。
2. **推荐**:宇恒 agent / 联调脚本对 push **串行 + 按 retryable 重试**(已在上述 smoke 加重试与 error→pending 恢复)。
3. **Z2****2026-08-04 宇恒复测通过**——异库返回 `403 无权访问该 online_db_id非本人 Binding`
4. 前端「数据库区 → 同步设置」需本机服务起来后人工点验;本次后端契约已通。
### 再测摘要2026-08-03 17:53
| 脚本 | 结果 |
|------|------|
| `smoke_zhijian_e2e.py` | ✅ PASSED首推即成功无 502 |
| `smoke_user_jwt_policy.py` | ✅ 主路径 PASSED**当时 Z2 WARN** 异库仍 applied |
### 再测摘要2026-08-04 · 智建 Z2/Z4/Z6 重启后 · 宇恒确认)
| 脚本 | 结果 |
|------|------|
| `smoke_user_jwt_policy.py` | ✅ **PASSED****Z2 OK**(异库 403直推 + 库级 sync drain 上云均通过 |
### 结论
- **用户自助主路径Z1 + Z2 + Z3 + 宇恒库级 sync已打通**。
- **Z22026-08-04**:异库 `online_db_id`**403**(智建已修,宇恒已确认)。
- **Z4 已冻结并落地**push 不按通道表白名单拒收Binding + 用户 JWT 即权限;可自动建表。
### 宇恒澄清2026-08-04 · 对照截图)
| 现象 | 说明 | 责任方 |
|------|------|--------|
| SyncPage `↑37` | **不是表数**,是通道 `pushed_ok`push 成功次数);与线上 `orders` 行数巧合接近 | 智建 Z7 已改列名为「推送次数」并加副文案 |
| 显示 `ai_site_….db` 而非「AI建站智能体API」 | SyncPage 曾用落库 DSN 当主展示应对齐本地库名id/路径作副文案 | **智建 Z7**(主标题优先 Binding.`database_name`);宇恒 ensure 带 `database_name`/`display_name` |
| Binding `local_*``online_*`「名字对不上」 | **id 本就可不同**(映射);烟雾联调造的占位 id。真实库 `6a3df5…` 已补可读名「AI建站智能体API」默认隐藏 smoke Binding | 智建已清烟雾数据 + UI 过滤;宇恒正式 ensure 带可读名 |
| 本地多表 vs 线上曾只有 `orders` | **当时** remote 仅 `orders``accounts` / `填土高度` 尚未 push**不是**映射错库 | 后续 `填土高度` 已 push 对齐;空表仍需 **Z10 ensure**(见 §5.7 |
| 弹窗 4 表「0 条记录 / 0 字段」 | 卡片统计不准;库内仍有数据 | **宇恒前端**(智建 SyncPage 无此卡片);可用数据预览或打开 remote db |
| **要看线上表内容验同步** | SyncPage 原先只显示通道名/推送次数 | **智建已加**:「查看线上表」→ 表名/行数/字段 + 行预览;**可「删表」**(仅当前侧,需确认;不同步 DDL |
| **图1「线上表」≠ 图2 宇恒「数据表」** | 见 §5.4:一边是线上 A一边是本机 B | **不是串库**;未 push / 未 ensure 的表不会出现在线上 |
| **模块数据按智能体进库** | 模块与 dbsync 原无关联 | **§5.5 Z8 大部分已通**;剩余 **Z8g** 展示与 **Z10c** 空表 |
同步链路本身可用;展示层勿把「通道文件名 + 推送次数」当成「本地库名 + 表行数」。
### 5.4 图1 vs 图2表名「对不上」说明2026-08-04 · 现状已回写)
| | 图1 智建「查看线上表」 | 图2 宇恒「数据库管理 · 数据表」 |
|--|------------------------|--------------------------------|
| **看的是哪边** | 通道 **线上 A**(例:`ai_site_1785779755.db` | 本机正式库 **B**「AI建站智能体API」 |
| **联调当时(早)** | 仅 `orders`(约 37 行)+ `_ajz_*` | `accounts``orders``填土高度…` 等 |
| **回写后(已 push** | `orders`1+ **`填土高度(6标一工区)`96**outbox 全 `done` | 同行数对齐;`accounts` 仍 0 行 |
| **仍可能不一致** | 空表(如 `accounts`)未 ensure → 线上无此表 | 本机有空表;须走 **Z10** `schema/ensure`,单靠 outbox 不会建空表 |
| **如何验同步** | 「查看线上表」预览行;或对照 `_ajz_sync_meta` | 宇恒「数据预览」看本机真实行 |
**结论**:表名集合不同 = **本机有、线上尚未 push/ensure**,不是 Binding/通道指错库。有行的表走 drain push**空表必须走 Z10 ensure**。
`_ajz_sync_*` 是中间件系统表(版本/outbox**不是业务模块表**控制台「查看线上表」默认隐藏Z8e
### 5.5 【进展 Z8】模块数据 ↔ 本机库 + 智能体库
> **产品原则2026-08-04 确认)**
> 1. **模块数据也要能在本机库 B 里管理**(宇恒数据库区:建表/增删改查/导入),与普通业务表同一套体验,不是只能在聊天「模块操作」或线上 Postgres 里改。
> 2. 本机改完后,经松离线 **push → 该智能体绑定的线上库 A**;换机/仅线上可用 **pull/bootstrap** 灌回本机。
> 3. **哪个智能体的模块 → 进哪个智能体的库**(通道/`online_db_id` 绑定)。
> **进展**Z8f 本机镜像已通;智建绑库/筛通道已通;剩余主要是 **Z8g SyncPage 展示** 与空表 **Z10c**。
**目标数据流**
```text
宇恒本机库 B含模块表可本地管理
│ sync / agent drain push
智能体绑定的线上库 A与通道 remote 一致)
│ 智能体 / 控制台「查看线上表」核对
(可选)模块页 CRUD 读同一 A或本机 B 为唯一写入口 + 同步
```
| 编号 | 优先级 | 诉求 | 说明 | 现状 |
|------|--------|------|------|------|
| **Z8f** | **P0** | **模块表可在本机库管理** | 模块实体表出现在宇恒「AI建站智能体API」类本机库中数据预览/录入/导入与业务表一致;开通 sync 后进 outbox | **宇恒已通2026-08-04**`local_module_db` 镜像;表名 **`模块·{模块名}·{资源}`**无下划线列含「所属模块」「模块slug」写网关 → outbox → push |
| **Z8a** | **P0** | **统一「一份表」叙事** | 废弃「模块一套库、松离线又一套」双轨并存为默认;默认:**本机 B 为模块数据管理面**A 为同步副本(或同库) | **已补开通说明**`docs/数据同步-开通说明.md` |
| **Z8b** | **P0** | **智能体选库 / 选通道** | 该智能体绑定 `channel_id` + `online_db_id`;宇恒可选「同步到哪个智能体库」 | **智建已落实**:用户管理可绑通道/线上库/落库名SyncPage 按智能体筛通道。宇恒开通 sync 时会 `PUT` 通道 `agent_id`/`app_slug`(读 `YXD_SYNC_*` |
| **Z8c** | **P1** | **线上与本机对齐** | 智能体读模块数据时优先本机 B离线或已同步的 A发布蓝图在本机建表UUID PK再 sync避免只在平台 schema 建一份 | **部分**:智能体新建发布若设 `database_name``database_per_app` 落该库;本机建表靠宇恒 Z8f |
| **Z8d** | **P1** | **本地上传 / 编辑 → 上云** | 本机模块表变更经 Binding + push 进该智能体 A「查看线上表」可核对 | **路径已通**:本机改 `模块·…` 表 → drain → 「查看线上表」应见同表;若仍缺行 = 未 push / 未 drain |
| **Z8e** | **P2** | **查看线上表默认藏 `_ajz_*`** | 减少「表名不对」误解 | **智建已做默认隐藏**2026-08-04 |
| **Z8g** | **P1** | **本机可见 = 线上可见(展示闭环)** | SyncPage「智能体/模块」勿长期「未绑模块」;「查看线上表」列出本机已有业务表(含 `模块·…`);模块列优先展示名 | **待智建**,见 §5.5.2 |
**分工建议**
| 方 | 负责 |
|----|------|
| 智建 | Z8a 文案Z8b 智能体↔通道/库绑定;**Z8g SyncPage 模块展示名**Z10a/b API开通说明 |
| 宇恒 | **本机库管理模块表Z8f 已通)**;开通/全量推送时挂通道 `agent_id`/`app_slug`**Z10c** sync 周期调 schema/ensure |
**验收Z8 完成后)**
1. ✅ 在宇恒本机库能看到并编辑模块相关表Z8f
2. ✅ 本机改模块行 → sync 后智建「查看线上表」出现同表同行(有数据路径;空表见 Z10
3. ✅ 智能体 X 只落到绑定库管理员可按智能体筛通道验数Z8b
4. ✅ 文档写明:本机有、线上无 = 尚未 push/ensure不是串库。
5. ⬜ SyncPage「智能体/模块」显示智能体名 + 模块**展示名**Z8g目前多显示 slug /「未绑模块」)。
### 5.5.1 【联调补丁·2026-08-04】编辑发布撞 page id
**现象**:编辑模块 `mode=add_pages` 时报
`page id already exists: record_list`(模型复用了已有页 id实为要改 API `import`)。
**智建已改**`platform/internal/blueprint/merge.go` — 同 page id **合并 actions**(不再 400同 path resource **并集 operations**;仅有 API/按钮更新也视为可发布(`UpdatedResources`/`UpdatedPages`)。需**重启 platform** 后生效。
**与 Z9**Z9 默认带 import 后,此类「仅为开导入而编辑」会减少;本补丁仍保留给「改已有页能力」用。
### 5.5.2 【待智建 Z8g】本机可见 ↔ 线上可见(展示增强 · 非硬阻塞)
> **联调现象(对照两图)**
> - 宇恒「数据表」已有:`orders`、`填土高度(6标一工区)`、**`模块·测·Sheet1`**(及业务表)。
> - 智建 SyncPage 通道行若未写 `agent_id`/`app_slug` 会显示 **「未绑模块」**;有 slug 时目前多直接显示 slug未解析模块**展示名**。
> - 期望:本机库卡片能看见的业务表,同步后「查看线上表」也应能看见(行数允许短暂滞后;**空表走 Z10**)。
> - **有数据表对齐**靠 push**空表对齐**靠 Z10不单靠 Z8g。
| 编号 | 优先级 | 诉求 | 说明 | 负责 |
|------|--------|------|------|------|
| **Z8g-1** | **P1** | **SyncPage 模块列可读** | 「智能体/模块」:有 `app_slug` 时解析已发布模块**展示名**如「测」slug 作副文案;仅缺绑定才显示「未绑模块」。`agent_id` 有值但 agents 列表未加载时仍显示 `#id`,勿整列空白 | **智建** `web/src/SyncPage.tsx` |
| **Z8g-2** | **P1** | **通道绑定可被宇恒写入** | 保持 `PUT /api/v1/admin/sync/channels/{id}` 可写 `agent_id``app_slug`;列表 API 原样返回。宇恒已在 sync/mode、full-push 调 `ensure_channel_module_link`(环境 `YXD_SYNC_AGENT_ID` / `YXD_SYNC_APP_SLUG` / `YXD_SYNC_MODULE_NAME` | **智建保持契约**;宇恒已接 |
| **Z8g-3** | **P1** | **查看线上表 = 本机业务表子集** | inspect 列出 remote 全部业务表(含中文/含 `模块·`);继续默认藏 `_ajz_*`。缺表文案明确:「本机有、这里没有 = 尚未 push」 | **智建**(展示已基本具备;验收与文案强化) |
| **Z8g-4** | **P2** | **白名单列勿误导** | Z4 已允许任意表 push若通道 `tables` 仍只配 `["orders"]`UI 勿暗示「只能同步 orders」。可标「策略表参考」或「整库同步中」 | **智建** SyncPage/配置抽屉 |
**验收**
1. 通道已挂 `agent_id=1``app_slug=mismatch_test2` → SyncPage 不再显示「未绑模块」,模块列可见「测」或至少 slug。
2. 本机 `模块·测·Sheet1` 全量 push 后,「查看线上表」出现同名表且行数对齐(幂等 upsert
3. 本机 `填土高度(6标一工区)` / `orders` 与线上一致。
---
### 5.6 【已落实 Z9】模块导入默认能力方案 A · 2026-08-04
> **联调现象**
> 宇恒绑定账号已具备 `row.import`(导入数据),智能体调用
> `POST /api/v1/apps/{slug}/{resource}/import` 仍返回
> `HTTP 400: operation import not allowed`。
> 根因(已修):平台在角色权限之外,还校验蓝图 `apis.resources[].operations` 是否含 `import`
> 旧生成默认只有 `list/get/create/update/delete`。现已默认带 import且 publish 兜底补全Z9
> 编辑补丁时模型仍可能只改部分表;**再发布**或依赖 Z9d/`EnsureDefaultImportExport` 即可。
> 另:同 page id 编辑曾 400见 §5.5.1merge 已改)。
**产品结论:采用方案 A**
绑定宇恒账号解决「谁可以调导入接口」;方案 A 解决「业务表是否声明允许导入」,避免「有权限仍 400」。
不采用「仅有 `row.import` 就跳过 `hasOp`」(方案 B作主路径以免列表页无导入入口、前后端能力不一致。
| 编号 | 优先级 | 诉求 | 说明 | 现状 |
|------|--------|------|------|------|
| **Z9a** | **P0** | **生成默认带 import** | 业务 `apis.resources[].operations` 默认含 `list,get,create,update,delete,**import**,**export**`(除非需求明确禁止) | **已落实**`generation_rules` + `prompt-contract.md` |
| **Z9b** | **P0** | **列表 actions 对齐** | list 页 `actions` 默认含 **`import`**(及可选 `export` | **已落实**:发布兜底补 list actions |
| **Z9c** | **P0** | **改 prompt / 文档** | `prompt-contract.md`:由「提到导入再加」改为「默认加;明确不要再去掉」 | **已落实** |
| **Z9d** | **P0** | **publish/merge 兜底** | 发布或编辑合并时:业务 resource 缺 `import` 则自动补上(可配置,默认开) | **已落实**`EnsureDefaultImportExport`;但**只在 publish 路径触发** |
| **Z9e** | **P0升格** | **存量模块一次性扫库补齐** | **禁止**依赖用户「碰巧再发布一次」。平台启动或管理接口:遍历租户已发布蓝图,对可写业务 resource 缺 `import/export` 的执行与 Z9d 相同补齐并落库(可干跑+确认)。目标:**任意旧模块** `POST .../import` 不再因缺 ops 400 | **待智建**2026-08-05 `coerce_fields/items` 仍只有 list/create/… 无 import联调第三次踩同一坑 |
| **Z9f** | **P1** | **读路径也兜底(双保险)** | `GetBlueprint` / `ResolveResource` / `ImportRows` 入口:若可写 resource 缺 import**内存补齐后再校验**或返回明确引导「请点修复导入能力」。避免「UI 能点导入、API 仍 400」 | **待智建**;宇恒侧已在导入前自动 replace 补 ops不能替代平台扫存量 |
**存量复现(必须消灭)**
```text
POST /api/v1/apps/coerce_fields/items/import
→ HTTP 400: operation import not allowed
蓝图 ops 实为list, create, get, update, delete ← 无 import
Z9d 已合入后新建模块正常;未再发布的旧模块仍坏)
```
**智建交付标准Z9e 验收)**
1. 不手工编辑、不触发「编辑模块」的情况下,对租户内**全部**已发布可写业务 resource 执行扫库后:`hasOp("import")==true`
2. 回归:任取 3 个「合入 Z9 之前发布」的旧 slug直接 `POST .../import` **不再**出现 `operation import not allowed`
3. 控制台可选:「数据同步 / 模块管理 → 一键开启全部业务表导入」按钮,调用同一扫库逻辑。
4. 文档写明Z9d≠存量修复上线 Z9 后**必须跑一次 Z9e**,否则用户会反复以为「绑定/权限坏了」。
**分工**
| 方 | 负责 |
|----|------|
| **智建** | **Z9e 扫存量P0**、Z9f 读路径兜底;保持 Z9ad错误文案区分「蓝图未开 import」vs「账号无 row.import」 |
| 宇恒 / 智能体 | 导入前尽力自动补齐(已做);表单选对 slug/resource**不能**代替平台扫全租户 |
**验收(含存量)**
1. 新建模块需求**不提**「导入」→ 发布后 `POST .../import` 不再因 `operation import not allowed` 失败。
2. **旧模块**Z9 合入前发布、之后未再发布)在跑完 Z9e 后同样不再 400。
3. 用户明确写「禁止导入」的表可不含 `import``disable_default_import` 或表级标记)。
4. 400 文案可读:指出是蓝图 operations 缺 import而不是笼统「操作失败」。
### 5.7 【智建已落实 Z10】空表也要两侧建齐2026-08-04
> **现象**
> 本机有空表(如 `accounts` 0 行)时,线上「查看线上表」没有该表;
> 仅靠 outbox 行 push 时,**没有行就没有建表事件**。同理,线上空表下行时本机若缺表,旧 pull 只给空 `items`、不给列,本机无法建表。
| 编号 | 优先级 | 诉求 | 现状 |
|------|--------|------|------|
| **Z10a** | **P0** | 本机空表 → 线上建空表 | **智建已落实** `POST /api/v1/agent/sync/channels/{id}/schema/ensure` |
| **Z10b** | **P0** | 线上空表 → 本机建空表 | **智建已落实** `POST .../schema``pull`/`bootstrap` 响应带 `columns` |
| **Z10c** | **P0** | 宇恒 sync 周期调用上述 API | **待宇恒**drain **前**对本机业务表(含 0 行ensurebootstrap **前** schema + 本地 `CREATE IF NOT EXISTS` |
**契约摘要**
```http
POST .../schema/ensure
{ "online_db_id": "...", "tables": [{ "table": "accounts", "pk_column": "id", "columns": ["id","username",...] }] }
POST .../schema
{ "online_db_id": "..." }
result.tables[].name / columns / row_count
```
列一律按 TEXT + 指定 PK 建空表;已存在幂等跳过。详例见 `宇恒-松离线数据同步使用文档.md`「表结构同步」。
**验收**
1. 本机空表 ensure 后,智建「查看线上表」可见同名 **0 行**表。
2. 换机 / 仅线上schema → 本机建空表 → bootstrap 灌行。
3. 有数据的表仍走原 pushensure **不替代** outbox 行同步。
### 5.8 【待智建 Z11】蓝图增字段须迁 Postgres2026-08-04
> **联调现象**
> 模块 `record` 蓝图后来带了 `title/content/status`,但 Postgres 表仍只有 `col_0/col_1/col_2`。
> 导入 Excel 若含「标题/内容/状态」→ `pq: 关系 "record" 的 "title" 字段不存在 (42703)`,整批 skipped。
> 反过来蓝图把这些标成非空、Excel 又没有时,会先报 `missing required field: title`。
| 编号 | 优先级 | 诉求 | 负责 |
|------|--------|------|------|
| **Z11a** | **P1** | 编辑/发布增字段后 **自动 ALTER** 业务表补列(或发布失败并提示未迁库) | **智建** |
| **Z11b** | **P2** | 导入时Excel 列映射到蓝图有、库无的字段 → 明确报「请先迁库」,勿只堆 28 条 pq 错误 | **智建** |
**宇恒临时绕过**:导入 Excel 只保留库里已有列对应表头(填土:`序号/断面里程/填土高度` → col_0/1/2
---
## 6. 联系与附件
- 方案:`松离线-dbsync方案-最终版.md`(含 2026-08-01 联调建议落地记录)
- 宇恒使用说明:`宇恒-松离线数据同步使用文档.md`(含 Z10 schema/ensure
- 开通说明:`docs/数据同步-开通说明.md`
- 本意见如与冻结方案冲突,**以冻结方案为准**§5.15.7 为产品增量与复测记录,不推翻 H1H6 默认无感约束。