Files
trade-message-center/.trellis/spec/chrome-extension/frontend/onetalk/durable-sync.md
T

10 KiB
Raw Blame History

OneTalk 耐久同步与连接生命周期契约

1. Scope / Trigger

当 OneTalk 页面观察需要经过 Service Worker 进入 Bright,并在浏览器或 Service Worker 重启后恢复历史、增量、实时消息和 ACK 状态时,必须遵循本契约。

页面桥和命令路由遵循 OneTalk 页面桥、Port 与命令路由。设备安装实例身份遵循 OneTalk 设备身份

2. Signatures

Durable keys and records

消息幂等范围:

```text channelAccountId + conversationId + messageId ```

同步检查点范围:

```text channelAccountId + conversationId ```

页面观察写入边界:

```ts type PersistObservedBatch = { channelAccountId: string; conversationId: string; messages: ObservedOneTalkMessage[]; mode: "full" | "incremental"; receivedAt?: number; }; ```

检查点至少表达:

  • `phase`
  • `pageTimeStamp`
  • `historyComplete`
  • `latestMessageId`
  • `completionSent`
  • 分页位置
  • 同步模式
  • 锚点状态
  • 最近观察时间
  • 会话结果

候选状态至少区分:

```text awaiting_anchor pending_ack confirmed anomaly rejected ```

设备、binding、workspace 和用户字段只能作为授权/来源上下文,不能创建设备独立消息或锚点副本。

Profile ledger (independent state machine)

联系人资料不使用消息 candidate/checkpoint/anomaly store。ONE_TALK_SYNC_DATABASE_VERSION=8:从 oldVersion < 7 升级时,transaction 必须清空五个 OneTalk store(消息、candidate、checkpoint、anomaly、onetalk_contact_profiles),不重键或回补旧 ledger;从 v7 升级到 v8 时只新增 onetalk_conversation_bootstraps store,必须保留五个既有 store 的记录。Chrome 配置、deviceId、binding 与其它渠道存储不属于该删除范围;首次升级后必须重新采集 profile 并从 clean state 执行 full syncv7→v8 不得因为新增 bootstrap store 清理既有事实。v7 checkpoint 新增 OneTalk 会话列表来源的 latestMessageAtMs,用于同步完成后更新服务端会话活动时间。

清空后的 onetalk_contact_profiles 业务键为 channelAccountId + conversationId;记录包含 key、账号、conversationId、资料字段中的 aliId、lastUploadedFingerprint、uploaded/rejected observed high-water mark、updatedAt、lastUploadedAt 和最新 pending 清洗 profile。资料上传到 Bright persistence,不调用 Mind profile HTTP。

Observation 先写最新 pending,再由现有 Bright WebSocket 发送 contact.profile.observed。同 fingerprint 且无 pending 时只推进更高 observed high-water mark;任何不高于 uploaded/rejected high-water mark 的不同 fingerprint 也跳过。断线、Service Worker 重启或新页面连接只从 pending 重建发送。收到 contact.profile.ack 后,必须等待 readwrite transaction oncomplete,且只确认仍匹配的 fingerprint 和 observedAtMs;迟到旧 ACK 不得删除新 pending。收到 ws.error profile_observed_at_future 时只丢弃该 request 的精确 pending,避免无限重试。

消息与 profile 的耐久状态仍然分离:历史/实时消息中的 business_card 只持久化 { version: 1, kind: "business_card" } marker。由于 __conversationListData__ 与消息采集可能异步,消息候选不得等待或读取 profile ledger;当前客户资料只在服务端读取时按 [channelAccountId, conversationId] 以内存方式组合到对外 view。没有 profile 时保留 marker,不能 fallback 到登录人资料。

3. Contracts

Durable-first observation

页面观察的顺序固定为:

```text page observation -> validate and normalize -> IndexedDB durable write -> Bright upload -> per-message ACK ```

每批消息先写 IndexedDB,再允许上传或通知上层。durable write 失败时不上传该批次、不调用上层观察回调,并通过稳定错误暴露。

观察消息缺少 `messageId`、conversation ID、sender ID 或其它必填字段时,写独立 anomaly,不写正常消息事实表;后续合法消息仍可继续处理。

Full / incremental / live

  • 没有活动增量检查点时,被动到达的 history observation 按 full 事实处理。
  • 增量扫描在找到旧锚点前,候选保持 `awaiting_anchor`,不得上传、写入 Bright 或通知 TradeMind。
  • 找到旧锚点后,候选按旧到新上传;Bright 逐条确认后才能标记本地记录已确认。
  • 只有页面明确历史结束、全部有效消息获确认且没有待确认有效消息时,才能推进会话锚点。
  • 每页完成后发布 `historyProgress`;还有下一页时保存下一次请求的 `nextTimeStamp`,历史结束或命中锚点时保存为 `null`;同时持久化会话列表来源的 latestMessageAtMs,不得从已过滤后的业务消息反算。
  • 增量期间到达的 live observation 仍先持久化并等待锚点;锚点命中后释放历史/live 候选。
  • 已完成增量会话的新 live observation 按正常待确认事实上传。

Completion and anchor evidence

服务端没有 `sync.complete` 成功 ACK。客户端发送完成声明后:

  1. 只保存 `completionSent=true`
  2. checkpoint 保持 `uploading`
  3. 等待后续 `anchor.snapshot`
  4. 只有 snapshot 的会话和 `latestMessageId` 精确匹配时,才写入 `completed` 并推进本地锚点。

断线重连时重新发送会话发现、未确认事实和待锚点确认的完成声明。

Connection lifecycle

  • Service Worker 建立唯一插件-Bright WebSocket,并在连接、heartbeat 和每个操作前复核 binding/授权版本。
  • WebSocket 心跳用于保持 Bright 连接,不得把页面 `runtime.Port` 当作持久化或 keepalive 机制。
  • Bright 连接断开时停止发送/接收;发送尝试后的结果为 `delivery_unknown`,不得自动重发。
  • Service Worker 重启后重新认证和注册页面;服务器 command 不从旧内存队列恢复。
  • `delivery_unknown` 不创建发送任务记录;迟到的有效页面消息仍可作为普通事实进入既有 observation pipeline。

Service Worker restart

Service Worker 重启后必须从 IndexedDB 恢复:

  • 候选消息;
  • 分页位置;
  • 同步模式;
  • checkpoint phase
  • anchor state
  • completion state。

不能从内存游标继续,也不能因重启重新生成消息或锚点。

4. Validation & Error Matrix

条件 行为
binding 缺失、撤销或 Mind 授权服务不可用 fail closed;拒绝连接、同步或发送
IndexedDB durable write 失败 不上传、不通知上层;暴露稳定错误,保留会话未完成
页面消息缺少必填字段 记录独立 anomaly,不写正常消息表
增量锚点未找到 候选保持 `awaiting_anchor`,记录 `incremental_anchor_not_found`
Bright 逐条确认缺失 不标记 confirmed,不推进锚点
`sync.complete` 只有 socket send 成功 只保留 `completionSent=true`,等待匹配 `anchor.snapshot`
Bright 连接断开 停止发送/接收;发送尝试返回 `delivery_unknown`,不自动重试
Service Worker 重启 从 IndexedDB checkpoint、candidate 和 mode 恢复
设备切换 复用账号/会话范围的消息和锚点,不创建 device 副本
页面同步 route 无精确会话 由页面桥规范返回明确同步 route reason
Profile ledger 写入/commit 失败 不发送 profilepending 不被伪造为已上传
Profile ACK fingerprint 与当前 pending 不匹配 no-op;保留当前 pending
Profile ACK observedAtMs 与当前 pending 不匹配 no-op;保留当前 pending
Profile observed time 超过 Bright receive time 五分钟 整批不落库/不 ACK;只丢弃匹配 pending
Profile snapshot command 未返回 不阻塞消息 bootstrap;只产生独立 profile diagnostic

5. Good / Base / Bad Cases

  • Good:观察批次先 durable write,再按消息上传并等待 ACK;重复观察按复合业务键幂等。
  • Good:增量命中旧锚点后才释放等待中的历史/live 候选。
  • BaseService Worker 暂停或重启后恢复 `awaiting_anchor` 和分页位置,不信任旧内存状态。
  • Base:完成声明已发送但尚未收到匹配锚点快照时保持 `uploading`。
  • Bad:在 durable write 前上传、把 socket send 成功当作同步完成、或将发送 unknown 写成自动重试任务。
  • Bad:按 deviceId 复制消息/锚点,或者使用内存 cursor 跳过 IndexedDB 恢复。
  • Good:资料 ledger 独立于消息 stores,按 [channelAccountId, conversationId] 严格 observedAt latest-wins;旧 ACK 只影响同 fingerprint、同 observation time。
  • Bad:把 profile ACK 当成 message ACK、用 store.put 完成前返回已确认、按 aliId 建事实键,或用无限 timer 重试 future-skew/未响应的上传。

6. Tests Required

  • 两条 WebSocket 的连接类型、消息方向和状态互不混用。
  • durable write 完成前不会调用上传或上层回调;写入失败不会产生成功 ACK。
  • 同一 `channelAccountId + conversationId + messageId` 只有一条消息事实。
  • full/incremental/live 的候选状态和锚点等待边界正确。
  • `historyProgress` 每页都能恢复,重启后从持久化游标继续。
  • `sync.complete` 后没有匹配 snapshot 时仍为 `uploading`,匹配后才为 `completed`。
  • 断线恢复会重新发现会话并恢复未确认事实和 completion 声明。
  • `delivery_unknown` 不创建发送任务、不自动重发;迟到消息继续进入普通 observation。
  • 页面同步路由、Bright ACK 和 IndexedDB 事务顺序在重启/断线下保持一致。
  • oldVersion < 7 升级到 v8 必须断言五个 OneTalk store 均为空、没有 profile 重键/重试分支,且下一次 bootstrap 重新采集 profile、会话活动时间并执行 full sync;v7→v8 必须断言新增 bootstrap store 且五个既有 store 逐条保留;配置、deviceId 与其它渠道数据在两条路径都保持不变。

7. Wrong vs Correct

```ts // Wrong: 尚未持久化就上传。 await bright.upload(candidate); await store.putCandidate(candidate);

// Correct: durable write 是上传前置条件。 await store.putCandidate(candidate); await bright.upload(candidate); ```

```ts // Wrong: socket.send() 成功就推进同步锚点。 checkpoint.phase = "completed";

// Correct: 等待服务端 anchor.snapshot 证据。 checkpoint.phase = "uploading"; checkpoint.completionSent = true; ```