# 联调后修改意见 · 宇恒松离线（形态 B）

> 初稿：2026-08-01 · 修订至 **2026-08-06 20:05**（Z26 已有 CHANNEL 禁止再弹绑定表）（Z25 自动引导勿 force 清绑 + host_key 回落）  
> 焦点：**§0.2**（按负责方）；**改代码前须先写入本意见**（见 §0.0）  
> 来源：宇恒 `yuhengyihao_client` ↔ 智建（生产 `aisite.yuxindazhineng.com`）  
> 依据：`松离线-dbsync方案-最终版.md`、`宇恒-松离线数据同步使用文档.md`

---

## 0. 当前结论

| 级别 | 状态 | 说明 |
|------|------|------|
| **硬改（阻塞接口）** | **无** | 契约不变；剩生产 pull + 配号/Secret |
| **生产栈** | **已恢复** | `SMS.Provider: "off"` 须引号；health `platform/ai=true` |
| **Z1–Z9 / Z11–Z13** | **已落实** | 绑定/凭票/模块导入等；见 §5 |
| **Z10 + Z10d** | **代码齐；生产 pull** | 空表 ensure；已存在表 **ADD COLUMN**（§5.7） |
| **Z12 默认同步** | **已落实（含 Z12h）** | 启用即通道；误删 → Ensure+重挂（§5.9） |
| **数据恢复** | **智建已落实** | SyncPage「数据恢复」：同步成功后打线上快照，可恢复上次/上一代（§5.13） |
| **Z14 + Z14c** | **代码齐；生产 pull** | fingerprint；智能体挂载 `online_db_id`（§5.11） |
| **Z14d 双向补齐** | **宇恒已接** | 误删任一侧 → 心跳/选同步按主键并集 pull↔push（§5.11） |
| **Z15 建站配置/绑定优先** | **宇恒已改完；智建无需** | 新机缺 `config.json` 自动生成；无权限先绑定再鉴权（§5.12） |
| **Z16 禁硬编码联调手机** | **宇恒已改完；智建无需** | 代码/默认值不得内置 `13531041944`；凭票手机仅来自 env 或用户绑定（§5.14） |
| **Z17 鉴权失败可换绑** | **宇恒已改完；智建无需** | 鉴权/换票失败门闸提供「绑定或更换绑定」：清本机同步落点后重走绑定表单（§5.15） |
| **Z18 模块操作换绑按钮** | **宇恒已改完；智建无需** | 「模块操作」表单底部独立**按钮**「换绑」（非下拉选项）；点后强制清落点并弹绑定表单（§5.16） |
| **Z19 禁默认登记先绑定** | **宇恒已改完；智建无需** | 无绑定不得静默 agent/register；登记失败引导绑定；修 sendFrom 漏 components（§5.17） |
| **Z20 失败即弹绑表单** | **宇恒已改完；智建无需** | 启动失败不只发文字，直接 force_rebind 弹绑定/换绑表单（§5.18） |
| **Z21 绑定接口空体安全解析** | **宇恒已改完；智建无需** | `sync_bind_flow` 空响应/`Expecting value` 不再打断 prepare；配合 Z19/Z20（§5.19） |
| **Z22 绑定表单失败可重试** | **宇恒已改完；智建无需** | prepare 只做非交互检测；`ask_fields`/`Expecting value` 不冒泡成「绑定引导失败」；必弹绑定表单（§5.20） |
| **Z23 表单推送失败禁止空等** | **宇恒已改完；智建无需** | `ask_fields_via_chat` 推送失败立即抛错；进行中再选中会话给出提示；对齐 ty_host≥1.2.35（§5.21） |
| **Z24 已绑定勿 force 清落点** | **宇恒已改完；智建无需** | 启动自动引导绑定时禁止 `force_rebind` 清 CHANNEL；仅用户点「换绑」才清（§5.22） |
| **Z25 自动引导勿 force + host_key** | **宇恒已改完；智建无需** | prepare 失败只弹表单不清落点；host_key 回落 env/config，禁止空串打智建（§5.23） |
| **Z26 已有 CHANNEL 禁再弹绑表** | **宇恒已改完；智建无需** | 本机已有同步通道时自动引导不再弹「绑定公司同步」；仅换绑才清（§5.24） |
| **仍关注** | 用法 | 通道断了靠智建自愈；表数据靠宇恒双向指纹 / 数据恢复；不是「再点启动」 |

### 0.0 修改流程（冻结）

1. **先写本意见**（§0.2 分 A/B″，§5 写清诉求/分工/验收）→ 双方对齐。  
2. **再改代码**：宇恒只改 `yuhengyihao_client`；智建只改 `ai建站`；**互不代改**。  
3. 接口/产品有变 → 回写 §1 契约与验收；禁止口头改完再补意见。

### 0.1 绑定产品冻结（Z13 · 试运行）

| 约定 | 内容 |
|------|------|
| 联调号 | **`13531041944`**（宇信达）；**禁止**超管 `13531041945` |
| 换票 | 凭票：`YXD_YUHENG_TICKET_SECRET` + `YXD_HOST_KEY` + 手机；**勿**手写登录密码 |
| 短信 | 试运行关：`RequireForBind=false`；`Provider: "off"`（**YAML 必须加引号**） |
| 同号 | 命中成员 → 必须弹窗；`confirm=true` 才绑 |
| 通道 | 默认同步通道 **默认运行中**；开通不依赖用户先点「启动」 |

### 0.2 待改清单（按负责方）

> **分工冻结**：宇恒只改 `yuhengyihao_client`；智建只改 `ai建站`；互不代改对方仓。  
> 智建轨迹：`d195aa4` 凭票 → `4222667` SMS 引号 → `cb76824` 默认启动 → `4fae7ae` Z10d+fingerprint → **`bb59bb5` Z14c**。生产：`bash ./restart.sh --pull`。

#### A. 宇恒 · 已完成

| 编号 | 内容 |
|------|------|
| **Z10c** | `ensure_before_drain`（选同步 force + drain 前 TTL） |
| **Z13b–e** | 绑定码 / 同号确认 / policy / 凭票 / 选「同步」即 Binding+drain |
| **Z14a** | 表级指纹慢心跳（原：不一致 → ensure + full-push） |
| **Z14d** | **双向并集补齐（2026-08-06）**：不一致 → 先 `pull` 灌本机（不进 outbox）再 ensure+full-push；选「同步」同样先 pull 再 push；**默认不做 prune**（`YXD_SYNC_PRUNE_EXTRAS=1` 才清线上多余）。覆盖：线上误删 / 本地误删 / 缺表 |
| **Z15** | **建站技能 config 自生成 + 无权限先绑定（2026-08-06 · 宇恒已改完）**：`ai-site-agent-api` 缺 `config.json` 自动写默认模板；绑定优先；门闸含「绑定账号或手机号」。改动：`config_loader.py` / `host_bootstrap.py` / `forms.py` / `forms_ui.py` / `forms_ui.json` / `reference.md`。**智建无需改** |
| **Z16** | **去掉凭票硬编码手机号（2026-08-06 · 宇恒已改完）**：`_sync_phone()` 不再回落 `13531041944`；仅读 `YXD_SYNC_LOGIN_PHONE` / `YXD_ZHIJIAN_PHONE`；绑定成功则回写该 env。联调号仍可作**人工验收样例**（§0.1），但**禁止写进产品默认值**。**智建无需改** |
| **Z17** | **鉴权失败门闸：绑定或更换绑定（2026-08-06 · 宇恒已改完）**：`pending_auth` 选项含「绑定或更换绑定」；点选后 `clear_local_sync_bind` + `force_rebind` 再弹绑定表单，避免因 agents/me 已绑而跳过。**智建无需改** |
| **Z18** | **模块操作页底部「换绑」按钮（2026-08-06 · 宇恒已改完）**：sendFrom 增加 `showRebind`/`rebindText`；`ask_module_action` 点按钮 → `FormRebindInterrupt` → `force_rebind`。鉴权门闸同步改为按钮而非下拉选项。**智建无需改** |
| **Z19** | **禁静默默认登记 + 修表单 components（2026-08-06 · 宇恒已改完）**：未绑定先弹绑定；`build_sendfrom_form` 补回 `components`。**智建无需改** |
| **Z20** | **启动失败直接弹绑定/换绑表单（2026-08-06 · 宇恒已改完）**。**智建无需改** |
| **Z21** | **绑定 HTTP 空体安全 JSON（2026-08-06 · 宇恒已改完）**：`_safe_http_json` 替代裸 `r.json()`，避免 `Expecting value` 打断启动。**智建无需改** |
| **Z22** | **绑定表单失败可重试（2026-08-06 · 宇恒已改完）**：启动检测改为非交互；表单推送/JSON 解析失败不向用户暴露 `Expecting value`，捕获后重试弹表。**智建无需改** |
| **Z23** | **表单推送失败禁止空等（2026-08-06 · 宇恒已改完）**：推送失败立即抛错触发重试；禁止「只发文字、表单卡死等待」。**智建无需改** |
| **Z24** | **已绑定勿 force 清落点（2026-08-06 · 宇恒已改完）**：自动绑定时 `force_rebind=False`；换绑按钮才清。**智建无需改** |

#### A′. 宇恒 · 配合注意（非阻塞新开发）

| 优先级 | 内容 |
|--------|------|
| **P0** | 端到端见 **§0.3**：绑定 → 库选「同步」→ drain；线上表只反映已 push/ensure/pull |
| **P0** | **`online_db_id`**：生产 pull Z14c 后，智能体 JWT **用** ticket/`agents/me` 的 `{channel}_uN`；`apply_bind_success` 勿再长期吞掉 `_uN` |
| **P1** | 通道失效（push/agents/me 报通道不存在）→ **重新换票**，用返回的新 `channel_id` 覆盖 env（根因自愈见智建 **Z12h**） |
| **P1** | Binding 带可读名；同库串行 drain；指纹优先 `schema/fingerprint`，404 回退 `row_count` |

#### B. 智建 · 已完成（仓内）

| 项 | 说明 |
|----|------|
| gateway 公开 bind / ticket | 生产 health 已通 |
| `SMS.Provider: "off"` | YAML 布尔踩坑已修 |
| 默认同步通道默认启动 | 创建即 enabled；重启自动 Start |
| **Z14c** ACL | 智能体 JWT 放行挂载 `online_db_id`（`agentOwnsOnlineDB`） |
| Podman DNS / 脚本可执行位 | compose aliases；`restart.sh` 100755 |
| **Z12h / Z12h-1 / Z12h-2** | `HealTenantSyncBind`：Ensure 默认通道 + 重挂 Agent/Binding；默认同步**可删**，删后立即 Ensure+heal；列表/换票/`agents/me`/ensure Binding 触发；见 §5.9 |
| **数据恢复（§5.13）** | 成功同步后线上库快照（latest+previous）；SyncPage「数据恢复」；`GET …/checkpoint` + `POST …/restore` |

> 产品一句：**账号已绑定 ⇒ 落点信息固定；通道没了平台自动补，终端无需手填 channel_id。**

#### B″. 智建 · 仍待办（**开发** · 本仓改）

| 优先级 | 编号 | 项 | 说明 |
|--------|------|----|------|
| — | — | **无阻塞开发项** | Z12h、数据恢复已合入；后续仅生产 pull 与运维配号 |

#### B‴. 智建 · 本次明确不改（Z15）

| 项 | 说明 |
|----|------|
| 新机缺 `config.json` | **宇恒技能本地文件**问题；平台不负责生成终端 config |
| 「模块操作 / 等待授权」表单 | 宇恒 `sendFrom` + `yxd_skill_ty_host`；智建已有 bind/register/token API，**契约够用** |
| pending 待管理员启用 | 仍靠控制台赋「生成发布」；**不**为此新开接口 |

> 结论：**Z15 智建无需开发**；仅当后续要把「绑定失败错误码 / pending 文案」标准化进 API 时再开智建项并先改本意见。

#### B′. 智建 · 仍待办（仅运维）

| 优先级 | 项 | 说明 |
|--------|----|------|
| **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复**；`bash ./restart.sh --pull` |
| **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**；勿超管号 |
| **P0** | 通道表白名单 | **勿**「填入测试默认」；形态 B 可空 |
| **P1** | 凭票 Secret | `YuhengTicket.Secret` ≡ `YXD_YUHENG_TICKET_SECRET` |
| **P1** | `DefaultRemoteDSN` | 生产 Postgres；空则 sqlite 联调文件 |
| **P2** | 正式短信 | Provider≠`"off"` 且 `RequireForBind=true` |

#### C. 双方已否决

| 项 | 说明 |
|----|------|
| 终端冲突台 | 仅平台超管 |
| 智建代改宇恒仓 / 宇恒代改智建仓 | **分工冻结** |
| 平台 push 租约 | version 幂等 + 宇恒串行 drain |
| 用户手填 channel_id / DSN 为开通终态 | 已否决；见 Z12 / Z12h |

### 0.3 双方怎么配合（开通 → 看到线上表）

```text
【智建运维】
  1. bash ./restart.sh --pull（health platform=true）
  2. 成员绑 13531041944；通道勿测试表白名单；配凭票 Secret
  3. 启用智能体 → 默认同步通道应「运行中」
  4.（可选）生成绑定码

【智建 · Z12h / 数据恢复 已落实】
  误删默认同步 / Binding 挂死 → SyncPage / 换票 / agents/me 自动 Ensure+重挂
  误删线上数据 → SyncPage「数据恢复」按上次同步快照恢复线上（本机靠下次 pull）

【宇恒终端】
  5. 未绑定 → policy → lookup/confirm 或 redeem 或 ticket-exchange
  6. 本机库选「同步」→ Binding + pull + ensure + drain/full_push（Z13e / Z14d）
  7. 慢心跳指纹（Z14d）：本地缺←pull，线上缺→push；缺列依赖生产已 pull 的 Z10d
  8. 若仍报通道不存在 → 再换票刷新 channel_id（生产须已 pull Z12h）

【智建验收】
  9. 「查看线上表」刷新 → 应有业务表（含曾 0 行空表）
  10. 仍空 = 终端尚未 push/ensure/pull，不是再点一次「启动」就能灌表
  11. 删默认同步后：换票或打开 SyncPage / agents/me → 通道自动回来
  12. 同步成功后误删线上行 →「数据恢复」可选最近一次/上一代快照恢复

| 角色 | 负责 | 不负责 |
|------|------|--------|
| **智建** | 落点/通道自愈/鉴权/ensure/fingerprint API；控制台看线上表 | 不替宇恒灌本机历史行；不改宇恒仓 |
| **宇恒** | 绑定 UX、选同步、outbox、drain、**双向**指纹修复、full-push/pull | 不以手填 DSN/通道为产品终态；不改智建仓 |
| **共同** | 联调号 `13531041944`；Secret 对齐；试运行免短信仍须确认 | 不用超管号 |

宇恒对照脚本：`smoke_zhijian_e2e.py` / `smoke_offline_concurrent.py` / `smoke_zhijian_gateway_burst.py` / `smoke_stress_sync.py`。

复测摘要：gateway 串行 60/60、`eof502=0`；同 version → `skipped`；并发 SQLite 须重试 / 客户端串行。

---

## 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`；**Z10d** 部署后已存在表亦 `ADD COLUMN`）
5. `POST .../schema/fingerprint`（**Z14** · 建议部署）  
   - Body：`{ "online_db_id": "..." }`  
   - 返回各表 `name/row_count/content_hash/pk_column`；与宇恒本机指纹算法对齐（见 §5.11）  
   - **未部署时**宇恒回退只用 `schema.row_count` 对账（可发现行数差，不能发现「同 count 不同内容」）
6. 鉴权：管理员 JWT（含「数据同步」）、智能体 Token，或**登录用户 JWT**（须本人 Binding + `online_db_id`）均可调 agent API
7. 错误体优先带可读 `message`；忙/上游失败宜带 `retryable: true`（502/503）

**联调账号（写入约定，勿再用错租户 / 勿用超管号）**

| 环境 | 用途 | 登录 | 说明 |
|------|------|------|------|
| 本机 / 演示公司 | 租户 1 演示 | `13800000001` / `demo123` | 仅本机联调；**不要**拿去推生产「宇信达」通道 |
| **生产 · 宇信达** | 公司管理 / 同号绑定联调 | **`13531041944`**（密码由开通时设定） | **专用联调号**；智建须在「宇信达」成员/管理员中写入此手机；宇恒 `YXD_SYNC_LOGIN_PHONE` 与 Z13 同号确认均以此号为准 |
| 平台超管（禁止联调） | 控制台「填入超管手机」 | `13531041945` / `ljk_admin` | **最顶级管理员**；seed `ensureDevPhone(ljk_admin)`；**禁止**用作同步登录 / 绑定联调，会与公司账号冲突 |

> **智建待办（运维）**：见 **§0.2 B′**（生产 pull、成员手机、Secret、通道勿测试表白名单）。Z14c/Z10d/fingerprint 属「已合入待 pull」，不在待开发。

---

## 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 → skipped，A 不增行 | ✅ |
| gateway 连续 push ≥20 无 EOF | ✅（burst 25、stress 串行 60） |
| stats `pushed_applied` / `pushed_skipped` | ✅ |
| 并发 push（SQLite） | ⚠️ 需重试；建议客户端串行 |

未在每次脚本覆盖：无「数据同步」权限 → 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`）
- 选「同步」后可拉起本机 agent；push 带头里的用户 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；废弃默认双轨 | **已落实（含 Z8g 展示）** |
| **Z9** | **P0** | **模块默认 import + 存量扫库** | 见 §5.6 | **已落实 Z9a–f** |
| **Z10** | **P0** | **空表两侧建齐** | 见 §5.7：仅靠 outbox 不会建空表 | **双方已接**：智建 API + 宇恒 Z10c（`sync_schema`） |
| **Z11** | **P1** | **蓝图增字段迁库** | 见 §5.8 | **智建已落实** ALTER + 导入提示 |

### 建议验收（智建改后）

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` 可读 |
| — | 经典 e2e（B→outbox→push→A 同 UUID） | ✅ **PASSED** | 首次 push 常 502/503，约 2–4 次重试后成功；二次 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 |
| **Z4–Z6** | 白名单 / 下行 / 文案 | ✅ | Z4 冻结②已落实（任意表+自动建表）；Z5 pull；Z6 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）已打通**。  
- **Z2（2026-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 已通**；空表 **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` 绑定）。  
> **进展**：Z8a–g 已通（本机镜像、绑库、SyncPage 模块展示）；空表 **Z10c 宇恒已接**（`sync_schema`）。

**目标数据流**

```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「智能体/模块」勿长期「未绑模块」；「查看线上表」列出本机已有业务表（含 `模块·…`）；模块列优先展示名 | **智建已落实（2026-08-05）**：模块展示名 + slug 副文案；策略表标「参考/整库同步」 |

**分工建议**

| 方 | 负责 |
|----|------|
| 智建 | 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】本机可见 ↔ 线上可见（展示增强 · 2026-08-05）

> **联调现象（对照两图）**  
> - 宇恒「数据表」已有：`orders`、`填土高度(6标一工区)`、**`模块·测·Sheet1`**（及业务表）。  
> - 智建 SyncPage：有 `app_slug` 时解析已发布模块**展示名**，slug 作副文案；仅缺绑定才「未绑模块」。  
> - 通道表白名单 UI 标为「策略表（参考）」/「整库同步中」，避免误读为只能 sync 名单内表。

| 编号 | 优先级 | 诉求 | 说明 | 负责 |
|------|--------|------|------|------|
| **Z8g-1** | **P1** | **SyncPage 模块列可读** | 展示名 + slug 副文案 | **智建已落实** |
| **Z8g-2** | **P1** | **通道绑定可被宇恒写入** | `PUT` 可写 `agent_id`/`app_slug` | **智建保持契约**；宇恒已接 |
| **Z8g-3** | **P1** | **查看线上表 = 本机业务表子集** | inspect + 默认藏 `_ajz_*`；缺表文案 | **智建已具备** |
| **Z8g-4** | **P2** | **白名单列勿误导** | 「策略表（参考）」/「整库同步中」 | **智建已落实** |

**验收**

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.1（merge 已改）。

**产品结论：采用方案 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（升格）** | **存量模块一次性扫库补齐** | 平台启动扫库 + 管理接口；不依赖用户再发布 | **已落实（2026-08-05）**：启动 `RunBackfill…`；`POST /api/v1/admin/apps/ensure-import`；模块管理「一键开启导入」；超管 `…/platform/apps/ensure-import` |
| **Z9f** | **P1** | **读路径也兜底（双保险）** | Import/Export 入口内存补齐后再校验；可读错误文案 | **已落实**：`ensureImportOpInMemory` + 区分蓝图缺 import vs 权限 |

**存量复现（必须消灭）**

```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/Z9f 已落实**；保持 Z9a–d；错误文案区分「蓝图未开 import」vs「账号无 row.import」 |
| 宇恒 / 智能体 | 导入前尽力自动补齐（已做）；表单选对 slug/resource；**重启 platform 或点「一键开启导入」后**旧模块即可 import |

**验收（含存量）**

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 / 宇恒接 2026-08-05）

> **现象**  
> 本机有空表（如 `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 | **宇恒已接（2026-08-05）**：`sync_schema.ensure_before_drain`；选「同步」force ensure；drain 前 TTL 节流 |

**契约摘要**

```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 建空表。**Z10d（2026-08-05 晚）**：已存在表不再整表跳过，缺列则 `ALTER TABLE … ADD COLUMN`（`platform/internal/dbsync/ensure_table.go`）。**合入后生产 `./restart.sh --pull` 生效**；未 pull 时对「先按 `_row_id` 建表、后 push 注入 `id`」会持续 `has no column named id`。

**验收**

1. 本机空表 ensure 后，智建「查看线上表」可见同名 **0 行**表。  
2. 换机 / 仅线上：schema → 本机建空表 → bootstrap 灌行。  
3. 有数据的表仍走原 push；ensure **不替代** outbox 行同步。  
4. **Z10d**：ensure 带新增列名时，已存在表补列成功，随后 push 含新列不再 503。

### 5.8 【已落实 Z11】蓝图增字段须迁 Postgres（2026-08-05）

> **联调现象**  
> 模块 `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** 业务表补列 | **智建已落实**：`BuildPostgresAlterDDL` + publish 合并执行；字段-only merge 可发布 |
| **Z11b** | **P2** | 导入时：Excel 列映射到蓝图有、库无的字段 → 明确提示迁库 | **智建已落实**：42703 行错误附加「请重新发布以自动补列」 |

**说明**：存量表补列一律可空，避免 NOT NULL 迁库失败。已有库需**再发布一次**（或编辑发布增字段）才会 ALTER；之后新字段随发布自动补。

### 5.9 【Z12 · 已落实含 Z12h】账号绑定即落点，禁止手填通道（2026-08-05 / 修订 2026-08-06）

> **产品约定（生产 · 冻结意向）**  
> 1. **一个登录账号 / 一个智能体 ↔ 本公司同步落点**：绑定后，该账号的本机库只进自己的（或公司共享的）线上库，**不会**接到别人账号的库。  
> 2. **用户不应**：手抄 `channel_id`、也不应先去「数据同步」里点「新建通道」才能用。  
> 3. **通道对用户隐藏或自动生成**：公司开通 / 智能体启用时，平台**自动**准备本公司默认同步通道（Postgres 线上 A）并写到智能体/账号绑定上；控制台「数据同步」留给运维改 DSN，不作为开通必经 UI。  
> 4. **共享库**：仅「公司共享数据库」可被多名成员同步到同一 `online_db_id`；个人库默认隔离。  
> 5. 方向默认 **`local_to_remote`**（松离线 agent push）；「双向」不是用户必选项。  
> 6. SyncPage 列表须展示可复制 **通道 ID**（运维用；普通开通流程不依赖）。

| 编号 | 优先级 | 诉求 | 说明 | 负责 |
|------|--------|------|------|------|
| **Z12a** | **P0** | **换票带回绑定** | `POST /api/v1/auth/token`（及登录）响应含 `channel_id` / `online_db_id` / `database_name` / `sync_bound` | **智建已落实** |
| **Z12b** | **P0** | **智能体自查绑定** | `GET /api/v1/agents/me`：智能体 Bearer 可读自己的通道/线上库/状态 | **智建已落实** |
| **Z12c** | **P0** | **启用即自动绑通道** | 创建/启用智能体 → `EnsureAndStartSystemDefaultChannel`（`IsSystemDefault`，表白名单可空，**默认运行中**）；写回 `channel_id`+`online_db_id`；DSN 见 `DBSync.DefaultRemoteDSN` | **智建已落实**（含默认启动 `cb76824`） |
| **Z12c-1** | **P0** | **账号级隔离** | Binding / push：用户 JWT 只能写本人 Binding；**智能体 JWT** 另按挂载落点（**Z14c 已修**） | **智建已落实** |
| **Z12c-2** | **P1** | **公司共享库** | Binding `shared=true`：同租户成员可访问该 `online_db_id`；仅管理员可设 | **智建已落实** |
| **Z12d** | **P1** | **SyncPage 展示通道 ID** | 运维可见 + copyable | **智建已落实** |
| **Z12e** | **P1** | **唯一默认通道** | 每公司至多 1 条 `is_system_default`；复用已有默认 | **智建已落实** + 宇恒「仅 1 条则写入 env」 |
| **Z12f** | **P1** | **开通 UX** | 启用智能体即可；DSN 用公司级默认 | **智建已落实** |
| **Z12g** | **P1** | **默认同步通道默认启动** | 创建 `Enabled=true`；重启对 `IsSystemDefault` 自动 Start；控制台保存后自动启动 | **智建已落实**（`cb76824`） |
| **Z12h** | **P0** | **通道误删自愈** | `HealTenantSyncBind`：Ensure 默认通道 + 重挂挂死 Agent/Binding；触发：列表/换票/`agents/me`/ensure Binding/删通道后 | **智建已落实** |
| **Z12h-1** | **P1** | **禁删或删后重建默认同步** | `is_system_default` **可删**；删后立即 Ensure+heal 重建并重挂 | **智建已落实** |
| **Z12h-2** | **P1** | **orphan Binding heal** | 列表 / Ensure Binding 时通道缺失 → 静默改挂默认通道 | **智建已落实** |

**缺口说明（2026-08-06 · 已关闭）**

- 原：SyncPage「通道已删除」、`ensureAgentSyncBind` 见非空 `channel_id` 即跳过。  
- 现：列表/换票等路径先 heal；默认同步可删并立即重建。生产须 **pull** 后验收。

**宇恒已做（2026-08-05）**

1. 当前生产通道已写入 env（过渡缓存；Z12c/Z12h 落地后应由换票/自愈自动获得）。  
2. `resolve_sync_channel_id()`：env → Binding → 租户仅 1 通道自动选用。  
3. 不把「手建通道 + 手抄 ID」当作产品终态；通道失效时配合再换票（根因仍靠 Z12h）。

**验收**

1. 新公司：只「启用智能体」，**不**点新建通道，换票已带 `channel_id`，宇恒可 drain。  
2. 账号 A 的本机库 push 不会出现在账号 B 的线上库；共享库仅在显式共享时可见。  
3. 用户全程无需 F12、无需手填 `YXD_SYNC_CHANNEL_ID`。  
4. **（Z12h）** 删除默认同步通道 → 立即重建并重挂；Binding 挂死 ID 时打开 SyncPage / 换票亦自愈。

### 5.10 【双方代码已接 Z13】绑定流程简化：绑定码 / 手机号（2026-08-05）

> **端到端体验（生产目标）**  
> 1. 后台已绑定（或用户用绑定码/手机号完成绑定）→ 宇恒本机库选 **「同步」** → **直接可用**（自动通道、自动 Binding、agent drain），无第二套配置。  
> 2. 绑定流程必须比「登记智能体 + 抄通道 ID + 填 env」简单。  
> 3. **生产·宇信达联调账号**：`13531041944`（公司侧专用；**禁止**用超管号 `13531041945`）。  
> 4. **试运行**：未接短信平台 → **关闭短信验证**；正式接入后再开（见下）。

**推荐交互**

```text
公司管理员（智建后台）
  → 生成「绑定码」→ 发给终端用户

宇恒 · AI建站智能体
  → 检测绑定状态
       ├─ 已绑定 → 静默换票；本机库选「同步」即可用
       └─ 未绑定
            ├─ 宇恒已有手机，且智建同号有成员
            │     → 弹「已有账号是否绑定？」→ 确认 / 取消
            ├─ 表单：【绑定码】或【手机号】
            │     · 宇恒未绑手机 → 仍可直接填手机号
            │     · 试运行：不校验短信，仅确认绑定
            │     · 正式：同号可免短信/凭票；异号须短信
            └─ 无成员 / 无码 → 提示联系管理员
  → 绑定成功 → 选「同步」→ drain，零通道配置
```

**试运行 vs 正式**

| 项 | 试运行（当前） | 正式（接短信后） |
|----|----------------|------------------|
| 配置 | `SMS.RequireForBind=false`（或 `Provider=off`） | `RequireForBind=true` |
| 探测 | `GET /api/v1/auth/bind/policy` → `trial_mode=true`，`sms_required_unless_attested=false` | `trial_mode=false` |
| 填手机号 | 可绑；**不校验短信** | 异号 / 宇恒未绑号须 `sms_code` |
| 同号免验 | 可不传 `sms_code`；确认即可 | 同号 attested 或走 `ticket-exchange` |
| 必保留 | **「已有账号是否绑定」确认**；`confirm=true` | 同左 |

**同号 / 验证规则**

| 场景 | 行为 |
|------|------|
| 试运行 / 短信未接入 | **不校验短信**；填号 + 确认即可 |
| 宇恒未绑手机 | **仍可直接输入手机号**；正式环境须短信 |
| 宇恒已绑且输入 = 已绑号 | 正式可免短信 / 凭票；命中成员仍弹确认 |
| 宇恒已绑且输入 ≠ 已绑号 | 正式**必须短信** |
| 命中公司成员 | **必须询问**「已有账号是否绑定」；不得静默 |
| 用户确认 | 绑定该账号 + 公司默认同步落点 |
| 用户取消 | 不绑；可改绑定码 / 换号 |
| 公司无此号 | 提示用绑定码 |
| 联调样例 | **`13531041944`**；勿用 `13531041945` |

| 编号 | 优先级 | 诉求 | 说明 | 负责 |
|------|--------|------|------|------|
| **Z13a** | **P0** | **后台生成绑定码** | `POST/GET/DELETE /api/v1/admin/bind-codes`；SyncPage「绑定码」区 | **智建已落实** |
| **Z13b** | **P0** | **绑定码兑换** | `POST /api/v1/auth/bind-code/redeem`（`host_key` + `code`） | **双方已接**（智建 API + 宇恒表单） |
| **Z13c** | **P0** | **手机号探测** | `POST /api/v1/auth/bind/phone-lookup`（不直接绑定） | **智建已落实** |
| **Z13c-1** | **P0** | **确认绑定** | `POST /api/v1/auth/bind/phone-confirm`（`confirm=true`） | **双方已接**（宇恒弹窗 + 本机 API） |
| **Z13c-2** | **P0** | **验证策略 / 凭票 / policy** | 试运行关短信；`GET /auth/bind/policy`；正式异号短信；凭票 `ticket-exchange` | **双方已接**（生产须对齐 Secret + 部署 gateway） |
| **Z13d** | **P0** | **未绑定检测与提示** | `sync_bound=false` → lookup / 表单 | **宇恒已接** `ensure_sync_bound_interactive` |
| **Z13e** | **P0** | **选同步即用** | 库选「同步」→ Binding + drain；零手填通道 | **宇恒已接** `POST /database/sync/mode` |
| **Z13f** | **P1** | **安全** | 试运行关短信；正式强制异号短信；审计 | **智建开关已落实** |

**验收**

1. 绑定码 → 填码 → 绑定成功 → 库选「同步」→ 数据进公司线上库。  
2. 同号 `13531041944` → 弹「已有账号是否绑定」；确认绑、取消不绑；禁用 `13531041945`。  
3. 宇恒未绑手机 → 仍可直接填号；试运行免短信，正式须短信。  
4. 试运行：无 `sms_code` 也可 confirm；`GET /auth/bind/policy` → `trial_mode=true`。  
5. 未绑定用户只见绑定码/手机表单，**不见** DSN/通道 ID。  
6. 未确认 / 他人绑定码，不能绑进别的公司或别人账号。  
7. 正式且启凭票：同号走 `ticket-exchange`，不再依赖明文 `attested_same_phone`。

### 5.11 【Z14 · 2026-08-05 / 修订 2026-08-06】表级指纹对账 + 双向补齐 + 联调踩坑

> **诉求**  
> 1. 仅靠 agent「推送心跳」发现不了「本机 96 行、线上 0 行」或「同 count 不同内容」。  
> 2. **已绑定账号后**：线上误删或本地误删，都应能从另一侧按主键**并集**补回；心跳检测到不一致即自动修。  
> 3. **通道被删**不属于表级修复，见 **Z12h**（智建已落实；须生产 pull）。

| 编号 | 优先级 | 诉求 | 现状 |
|------|--------|------|------|
| **Z14a** | **P0** | 宇恒本机指纹 + 对比线上 | **宇恒已接**：`sync_fingerprint.py`；TTL **600s**；`POST /database/sync/fingerprint` |
| **Z14d** | **P0** | **双向并集修复** | **宇恒已接（2026-08-06）**：`sync_pull.py`；本地缺/更少 → pull（直写 B、不进 outbox）；线上缺/更少 → ensure+full_push；hash/count 冲突 → 先 pull 再 push。选「同步」同路径；**默认不 prune** |
| **Z14b** | **P1** | 智建 `content_hash` | **已合入，生产 pull**：`POST .../schema/fingerprint` |
| **Z10d** | **P0** | ensure 已存在表补列 | **已合入，生产 pull**；未 pull 时 `填土高度` 缺 `id` 仍 503 |
| **Z14c** | **P0** | 智能体 `online_db_id` ACL | **已合入，生产 pull**：`agentOwnsOnlineDB`；人类 Binding 规则不变 |

**契约摘要（Z14b）**

```http
POST /api/v1/agent/sync/channels/{id}/schema/fingerprint
{ "online_db_id": "..." }
→ result.tables[]: { name, pk_column, row_count, content_hash }
```

哈希：`sha256( table+"|"+pk+"|" + Σ json(row, sort_keys=True)+"\n" )` 前 32 hex。

下行（宇恒已用，智建保持兼容）：

```http
POST /api/v1/agent/sync/channels/{id}/pull
{ "mode": "bootstrap", "table": "...", "after_pk": "", "limit": 200, "online_db_id": "..." }
→ result: { columns, items[], has_more, next_after_pk, pk_column }
```

**分工**

| 方 | 做什么 |
|----|--------|
| 宇恒 | 本机指纹；404 回退 `row_count`；**Z14d** 双向 pull↔push；选同步默认不 prune |
| 智建 | 生产 pull：fingerprint + Z10d + Z14c + **Z12h** + **数据恢复**；保持 pull/bootstrap 契约 |

**联调踩坑（宇信达 · 已回写）**

1. **行数对不上**：开通同步不会自动灌历史 → full-push / 指纹修复；**Z14d** 后本地缺也会 pull。  
2. **`填土高度` 缺 `id`**：push 默认 pk=`id`；须 Z10d 补列或删表重建后再推（宇恒 `mark_error` 堵队已修）。  
3. **`_uN` 曾 403**：智能体 JWT `user_id=agent_id`，Binding 在人类成员上 → **Z14c** 放行挂载 online。  
4. **删通道后再同步仍空**：Binding「通道已删除」→ 生产 pull **Z12h** 后打开 SyncPage/换票自愈；表级指纹无法重建通道。

**验收**

1. 改本机行数后 ≤TTL 检出 mismatch 并排队修复。  
2. 未 pull fingerprint：仅 count 对账仍可用。  
3. pull 后：同 count 不同内容 → `hash_mismatch` → 双向修。  
4. Z10d pull 后：`填土高度` 可推满。  
5. **（Z14d）** 线上删某表行 → 心跳后本机行仍在且线上恢复；本机删某表行（未 prune）→ 心跳后本机从线上拉回。  
6. **（Z12h）** 见 §5.9 验收第 4 条。

### 5.12 【Z15 · 2026-08-06】建站技能 config 自生成 + 无权限先绑定

**状态**：**宇恒已改完（2026-08-06）**；**智建不改 / 无需开发**。  
落地文件（宇恒仓 `.yxd/skills/user/ai-site-agent-api/`）：`scripts/config_loader.py`（`ensure_config`）、`scripts/host_bootstrap.py`（绑定优先）、`scripts/forms.py`、`scripts/forms_ui.py`、`forms_ui.json`、`reference.md`。

**现象（另一台电脑）**

- 克隆后无 `config.json`（gitignore 密钥文件）→ `默认登记失败: [Errno 2] No such file or directory: '.../config.json'`  
- 随后卡在鉴权/换票，而不是先走绑定码/手机号

**诉求**

1. 缺 `config.json` → 自动写默认模板（可再填绑定码/手机号等），**禁止**裸抛 Errno 2  
2. 启动顺序：**绑定优先** → 再鉴权/换票；未绑定或无权限时门闸选项含「绑定账号或手机号」  
3. 聊天表单仍走宇恒 `sendFrom` / `yxd_skill_ty_host`（与税务 TY 插件无关，但是同一套交互壳）

**分工**

| 方 | 做什么 |
|----|--------|
| 宇恒 | `ai-site-agent-api`：`config_loader.ensure_config`、`host_bootstrap` 绑定优先、`forms`/`forms_ui` 门闸选项 |
| 智建 | **不改**；bind / register / ticket 已够用（见 §0.2 B‴） |

**宇恒落地（已改完）**

1. `ensure_config()`：缺 `config.json` 写默认模板，不再 Errno 2  
2. `host_bootstrap`：绑定码/手机号优先于鉴权换票  
3. `pending_auth` 等门闸选项含「绑定账号或手机号」  

**验收**

1. 新机无 config → 首次跑技能生成模板文件，无 Errno 2  
2. 未绑定 → 弹绑定相关表单，而非只提示换票失败  
3. 已绑定有权限 → 原鉴权/模块操作路径不变  

### 5.13 【智建已落实 · 2026-08-06】数据恢复（恢复上次同步）

**状态**：**智建已落实**（须生产 `bash ./restart.sh --pull`）。

**诉求**：10:00 同步成功 → 11:00 误删 → 可恢复到最后一次成功同步（含自动 push/drain）时的线上库状态。

| 项 | 说明 |
|----|------|
| 快照 | 成功同步后对通道 **Remote 线上库** 打滚动快照（`latest` + `previous`，约 30s 防抖） |
| 触发 | agent push/batch（有 applied）、通道 drain 有变更、控制台「同步修复」成功 |
| UI | SyncPage 通道操作「**数据恢复**」；可选最近一次 / 上一代 |
| API | `GET /api/v1/admin/sync/channels/{id}/checkpoint`；`POST …/restore`（`confirm=true`，`which=latest\|previous`） |
| 恢复范围 | **仅线上库**；本机靠宇恒下次同步/指纹 pull 从线上补回 |
| 注意 | 误删后若又自动同步成功，最新快照可能已含删除态 → 选「上一代」或尽快恢复 |

**验收**

1. 同步成功约 30s 后，checkpoint 有 `latest`。  
2. 删线上若干行 →「数据恢复」选最近一次 → 「查看线上表」行数/内容回到快照。  
3. 无快照时按钮提示「尚无成功同步快照」，不静默失败。  

---

### 5.14 【Z16 · 2026-08-06】禁止硬编码联调手机号

**状态**：**宇恒已改完**；**智建无需开发**。

**诉求**

- `sync_token._sync_phone()` 不得默认 `13531041944`（否则别的用户未配 env 时会误用联调号换票）。
- 手机号来源：用户绑定表单 / 显式 `YXD_SYNC_LOGIN_PHONE`（或 `YXD_ZHIJIAN_PHONE`）；绑定成功后回写本机 env。
- §0.1 联调号仅作**人工联调约定**，不是代码默认值。

**验收**

1. 未配置手机号 → 凭票返回「未配置 YXD_SYNC_LOGIN_PHONE」，不静默用联调号。  
2. 用户用自己的号绑定成功 → env 写入该号，后续换票用该号。  
3. 源码检索无产品路径硬编码 `13531041944`（文档/联调说明除外）。


### 5.15 【Z17 · 2026-08-06】鉴权失败可绑定或更换绑定

**状态**：**宇恒已改完**；**智建无需开发**。

**现象**

- 鉴权/换票失败后只像「重新检查授权」，虽有绑定项，但本机/线上仍判已绑时 `ensure_sync_bound_interactive` 直接 skipped，无法换绑。

**诉求**

1. 鉴权失败门闸选项含 **「绑定或更换绑定」**（优先于仅重新检查）。  
2. 点选后：清空本机 `CHANNEL_ID` / Token / `ONLINE_DB_ID` / `LOGIN_PHONE`，再强制弹出绑定码/手机号表单。  
3. 不要求智建提供解绑 API（本机强制重绑即可联调；线上旧 Binding 由新绑定覆盖/复用）。

**验收**

1. 鉴权失败 → 表单可见「绑定或更换绑定」。  
2. 即使 env 里仍有旧 channel，点选后仍弹出绑定方式表单。  
3. 绑定成功后可再「重新检查授权状态」。


### 5.16 【Z18 · 2026-08-06】模块操作页「换绑」为独立按钮

**状态**：**宇恒已改完**；**智建无需开发**。

**诉求**

- 「模块操作」第一屏底部提供独立按钮「换绑」，**不是**操作下拉里的选项。  
- 鉴权失败门闸同样用按钮触发换绑，避免与「重新检查」混在同一枚举里。  
- 点击后：`clear_local_sync_bind` + 强制绑定表单（同 Z17）。

**改动面**

- 前端 `sendFrom.vue`：`showRebind` / `rebindText`  
- `yxd_skill_ty_host`：表单载荷 + `FormRebindInterrupt`  
- 建站技能：`forms.py` / `host_bootstrap.py` / `forms_ui.json`

**验收**

1. 已登录看到「模块操作」时，底部有「换绑」按钮（与退出、提交并列）。  
2. 点击后弹出绑定码/手机号表单，不依赖下拉选「换绑」。  
3. 操作下拉里无「换绑」项。


### 5.17 【Z19 · 2026-08-06】禁止静默默认登记；未绑定先绑；修表单无组件

**状态**：**宇恒已改完**；**智建无需开发**。

**现象**

- 「默认登记失败：Expecting value…」→ 旧路径静默 `POST /auth/agent/register`，空响应 JSON 解析失败。  
- 紧接「会话异常：chat form has no components」→ `build_sendfrom_form` 漏写 `components` 字段。

**诉求**

1. 无公司同步绑定时：**先绑定**，禁止把「默认登记」当登录。  
2. 登记/换票失败文案引导绑定，不暴露原始 JSON 解析堆栈给用户为主路径。  
3. sendFrom 表单必须带 `components`。


### 5.18 【Z20 · 2026-08-06】启动失败直接弹绑定表单

**状态**：**宇恒已改完**。启动未完成时禁止只 notify 文字；立即 force_rebind 推绑定表单（底部换绑同源）。门闸异常时同样回落弹表。

另：`ty_host`≥1.2.33 禁止用 DONE 永久跳过 bootstrap（旧日志「已自动登记过，跳过」会导致选中会话再也不弹表）。失败后 ACTIVE 清掉，可再拉起。


### 5.19 【Z21 · 2026-08-06】绑定接口空体安全解析

**状态**：**宇恒已改完**；**智建无需开发**。

**现象**

- 日志 `prepare_runtime failed: Expecting value: line 1 column 1` → 某绑定/鉴权 HTTP 返回空体，裸 `r.json()` 抛错，启动被打断；再选会话若被旧 DONE 跳过则只剩文字。

**诉求 / 改动**

1. `yxd/app_fastapi/sync_bind_flow.py`：`_safe_http_json`，空体/非 JSON 返回 `{}`，不抛。  
2. 与 Z19/Z20 配合：解析失败不阻断 → 走「未绑定」→ 直接弹绑定表单。


### 5.20 【Z22 · 2026-08-06】绑定表单失败可重试

**状态**：**宇恒已改完**；**智建无需开发**。

**现象**

- 聊天出现「绑定引导失败：Expecting value: line 1 column 1」且下方无绑定表单。  
- 根因：`prepare` 内过早走交互绑定；`ask_fields`/宿主回包空体 JSON 解析异常冒泡。

**诉求**

1. `prepare_runtime` 仅非交互检测是否已绑（channel/token），不在此处 `ask_fields`。  
2. 交互绑定集中在 `_try_bind_interactive`；失败捕获、友好文案、重试弹表，禁止把 `Expecting value` 原文给用户。  
3. `config.json` 空/坏档自动重建；宿主 command 回包空体安全解析。


### 5.21 【Z23 · 2026-08-06】表单推送失败禁止空等

**状态**：**宇恒已改完**；**智建无需开发**。

**现象**

- 聊天只有「请用绑定码/手机号绑定」等文字，**没有** sendFrom 表单。  
- 日志可能出现旧文案「已自动登记过，跳过」（进程未加载新 ty_host）。

**诉求**

1. `ask_fields_via_chat`：推送 `success=False` 立即抛错，禁止无限 `wait_form_values`。  
2. 推送成功后短延迟再推一次，减轻选中会话连刷盖住首帧表单。  
3. ACTIVE 进行中再选中：解锁输入并提示「表单等待中」，勿静默跳过。  
4. **必须重启** 宇恒进程以加载 ty_host≥1.2.35（否则仍跑旧「已自动登记过」）。


### 5.22 【Z24 · 2026-08-06】已绑定勿 force 清落点

**状态**：**宇恒已改完**；**智建无需开发**。
**诉求**

1. 启动自动引导：仅当本机无 channel 时才弹绑定表；**禁止**默认 force 清落点。  
2. 用户点「换绑 / 绑定或更换绑定」才 `force_rebind=True`。  
3. 已 sync_bound 时 bootstrap_auth 失败不要清绑定；保留 runtime 或走门闸。


### 5.23 【Z25 · 2026-08-06】自动引导禁止 force 清绑；host_key 必须回落

**状态**：**宇恒已改完**。

1. `runtime is None` 自动绑定时 `force_rebind=False`（仅「换绑」/门闸显式换绑才 clear）。此前 `force_rebind=not already_bound` 在未绑时仍会 clear，出现「已清空」+ 空表。
2. `_host_key` / `_host_key_default` 回落 `YXD_HOST_KEY` 与技能 `config.json`；空 host_key 本地拦截，避免智建 `host_key required`。


### 5.24 【Z26 · 2026-08-06】已有 CHANNEL 禁止再弹绑定表

**状态**：**宇恒已改完**。

本机 `YXD_SYNC_CHANNEL_ID` 已存在时：`ensure_sync_bound_interactive` 直接 skip；`prepare` 失败分支也不再打开绑定表单，只走门闸「重新检查/换绑」。避免他机已绑仍反复弹出「绑定公司同步」。

## 6. 联系与附件

- **待改清单（优先看）**：§0.2（**A′ 宇恒配合 · B′ 智建运维**；开发项已清空）；**改代码前先写本意见**（§0.0）；配合见 **§0.3**  
- 方案：`松离线-dbsync方案-最终版.md`（含 2026-08-01 联调建议落地记录）  
- 宇恒使用说明：`宇恒-松离线数据同步使用文档.md`（含 Z10 schema/ensure、Z13 绑定）  
- 开通说明：`docs/数据同步-开通说明.md`（含 **数据恢复**）  
- 绑定策略探测：`GET /api/v1/auth/bind/policy`（`trial_mode` / `require_for_bind`）  
- 本意见如与冻结方案冲突，**以冻结方案为准**；§5.1–5.13 为产品增量与复测记录，不推翻 H1–H6 默认无感约束。  
- **分工**：宇恒只改宇恒仓；智建只改智建仓；互不代改。