mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
41 KiB
41 KiB
重构 OneTalk 消息收发同步机制
Goal
以 OneTalk 页面为消息唯一事实源,由 TradeBright 服务端保存插件从精确 OneTalk 页面确认过的真实收件与发件;OneTalk 完全绕过现有 outbox 型消息搬运机制,TradeMind 不保存 OneTalk 消息副本,只读 TradeBright 的统一 onetalk_message 表并通过 WebSocket 与新链路交互。
Background
- 用户提供的总体架构图表达了以下边界:Chrome 插件通过 WebSocket 与 TradeBright 双向通信;TradeBright 读写数据库;TradeMind 服务端只读该数据库;TradeMind 消息页面通过 WebSocket 与 TradeBright 双向通信。
- 当前 TradeBridge 入站投递使用
trademind_delivery_outbox,出站发送使用outbound_message。这些表和通用机制继续保留,历史数据不迁移,但 OneTalk 不再进入其中。 - 当前 TradeMind 已有跨渠道通用
messages表,发件还通过 durable communication dispatch task 编排。目标架构中,OneTalk 消息不再写入该通用表或使用 dispatch,其他渠道不受影响。 - 用户提供的发送确认流程为:TradeMind 消息页面向 TradeBright 发起发送;TradeBright 通过 WebSocket 转发给插件;插件驱动精确匹配的 OneTalk 页面;页面事实检查结果返回插件;插件把发送结果回报 TradeBright;TradeBright 向 TradeMind 页面发送对应回执。
- 用户明确接受以下取舍:不持久化发送任务,不自动恢复或自动重试,无法保证恰好发送一次;数据库中只保存经 OneTalk 页面事实闭环确认的真实消息。
- 现有代码从
messageId、msgId、messageID、msgIdStr、id等候选路径读取所谓externalMessageId,并在缺失时生成latest-<hash>或hist_...合成值;这些实现均不能证明 OneTalk 提供了稳定真实消息 ID,且合成值已知可能碰撞。 - 当前 OneTalk 链路没有满足本方案的 IndexedDB 消息账本;已有 Chrome storage 分页状态不能替代逐批落地、逐条确认和中断续传所需的耐久本地事实缓冲。
- 本次重构分为三个主要系统面:Chrome 插件、TradeBright 服务端、TradeMind 业务系统。当前任务先作为父级架构规划确定事实归属、系统边界、接口方向、交付顺序与切换策略;字段、游标、超时和具体表结构留到对应子项目设计。
Architecture Planning Scope
- 父任务负责:总体目标、事实归属、三系统职责、跨系统契约方向、依赖顺序、全局验收和切换/回滚边界。
- 插件子项目负责:OneTalk 页面适配、身份范围、IndexedDB、本地全量/增量状态机、发送确认和异常采集。
- TradeBright 子项目负责:OneTalk 技术域模型、消息事实持久化、binding 精确路由、锚点/异常、WebSocket 协议及只读访问边界。
- TradeMind 子项目负责:消息页面、业务会话/客户关联、权限、摘要/未读/引用等业务能力改造,以及移除 OneTalk 对本地
messages/dispatch 的依赖。 - 低层实现决策必须服从父任务的事实归属和失败边界;当前阶段不因某个现有表或接口方便而倒置系统所有权。
- Bright 直接通过当前 TradeBridge 项目重构实现,不创建新的服务仓库或第三套部署单元。
- 用户将自行拆分后续插件、Bright、Mind 和集成任务;当前父任务不自动创建 Trellis 子任务。
- 跨仓契约在后续拆分时采用共享 type/规则范式:Bright 侧拥有消息/同步/HTTP/WS/错误码契约,Mind 侧拥有最小认证视图契约,两边通过版本和 contract test 防止漂移。
Confirmed Architecture Boundaries
- 系统使用两个独立数据库:Bright DB 与 Mind DB。Bright DB 是 OneTalk 消息正文及关联 ID 的唯一事实源;Mind DB 不保存 OneTalk 消息正文副本。
- Bright 的产品职责是 OneTalk 消息中心,主要持有消息正文、附件/引用内容、OneTalk 原始 ID、关联范围 ID,以及前文确认的同步锚点和异常诊断;不得吸收 TradeMind 的 workspace 权限、客户关系、负责人、摘要、未读或其它 CRM 业务事实。
- Bright 保存 sender ID 与 login user ID 作为消息原始关联 ID,但发送人的名称、客户身份、组织关系、负责人和其它业务资料由 Mind DB 持有。
- TradeMind 继续拥有 workspace 权限、客户关联、负责人、摘要、未读和其它业务能力,并通过 Bright 消息/会话关联 ID 消费 OneTalk 消息事实。
- Mind 服务端读写 Mind DB,不连接 Bright DB,也不建设 Bright 消息投影。Bright DB 只允许 Bright 服务端访问。历史消息不经过 Mind 服务端代理,Mind 页面直接调用 Bright 的受权历史查询接口;浏览器仍不直接连接任一数据库。
- Mind 消息页面通过 Bright HTTP/查询接口读取历史,通过 Bright WebSocket 发送/接收实时 OneTalk 消息,通过 Mind 服务端取得已有客户关联、负责人等业务信息,并按共享 ID 组合。
- Bright 或其消息连接不可用时,TradeMind 可暂时保留页面已加载内容,但必须明确显示连接中断;停止发送和接收,不排队、不回退旧链路。
- 上线采用新代码路径全量一次性切换,不保留新旧双写;某个当前登录用户对应的 OneTalk 账号完成首次初始化后即可工作,不等待其它账号。系统不建立账号级
ready/degraded业务状态。 - 新链路产生第一条 Bright 消息事实后,禁止回退旧 OneTalk/outbox 路径;严重故障只能暂停 OneTalk 收发、修复新链路并恢复。首次启用新写入前仍可整体回滚部署。
- 推荐部署顺序:先部署 Mind 最小认证视图;再部署关闭新写入的 Bright DB/服务端;发布新协议插件;部署但关闭入口的新 Mind 页面;用测试账号完成首次初始化、收件、发件、设备接管和故障验收;随后全局关闭旧 OneTalk 链路并要求最低插件版本;最后开启 Bright 新写入和新页面。第一条 Bright 消息写入后进入不可回退状态。
- 两个数据库使用共享原始字段契约关联,不生成 Bright 内部 account/conversation/message ID。消息关联与唯一键至少为
channelAccountId + conversationId + messageId;binding 和 deviceId 可作为插件授权/来源上下文,mindUserId、workspaceId仅由 Mind/Bright 服务端解析和使用,不是 OneTalk 或插件字段;这些上下文均不参与消息主键、去重或会话锚点。 - Mind DB 可保存相同原始 ID 来关联客户、负责人、摘要、未读等业务数据,但不得复制消息正文。sender ID 与 login user ID 是消息属性,不属于唯一键。
- Mind DB 继续作为认证、激活码、Mind user/workspace、binding 和账号授权事实源。Bright 只读其中认证授权所需的数据,不读取客户、摘要、负责人、未读等业务数据。
- Bright 服务端独占 Bright DB 写入;Mind 服务端独占 Mind DB 业务写入。Mind 只读 Bright 消息,Bright 只读 Mind 认证授权;双方均不得直接写对方数据库。
- 激活码仍由 Mind 认证流程验证和消费,Mind 生成并持有 binding;Bright 只获得验证后的 binding 授权结果。Bright 无法读取 Mind 认证授权数据时,连接、发送和接收全部 fail closed。
- Mind 用户可以属于多个 workspace,每个 workspace 可以拥有多个
channelAccountId归属记录;MVP 同一用户只激活其中一个。当前系统没有账号迁移功能,本项目不设计账号迁移或相关数据处理。 - 同一 Mind 用户同一时刻只允许一个有效 binding;binding 接管时 Mind 必须使旧 binding 失效。插件安装实例可用 deviceId 标识来源,但该标识不改变 binding 的授权结论。
- MVP 中 Mind 侧 active binding 的授权作用域为
mindUserId + workspaceId + channelAccountId与binding。deviceId仅是插件安装实例的来源标识和同一连接的完整性字段,不参与 active binding decision;插件不携带也不校验mindUserId、workspaceId。同一 Mind 用户只允许一个 active binding;切换授权范围或 binding 时必须原子撤销旧 binding。 channelAccountId是 OneTalk 页面运行时提供的登录人原始账号 ID(currentUserAccountId,或IcbuIM.UserUtil.currentUser.accountId),不是 URLactiveAccountId所指向的当前对话账号,也不是 Mind 或 Bright 生成的内部 ID。插件从页面取得该值,并在连接中携带channelAccountId + binding + deviceId;由 Bright/Mind 授权视图精确确认 binding 归属,插件不自行查询 Mind 业务身份。- Bright 只通过 Mind DB 的版本化最小只读认证视图读取 binding、设备、user/workspace、账号范围、权限、状态、撤销时间、版本和更新时间;Bright 数据库凭据不得读取 Mind 客户、摘要、负责人、未读、激活码秘密或其它业务表。
- Bright 复用现有 Mind session/token,不建设独立登录系统。Mind 页面访问 Bright HTTP/WebSocket 时携带现有凭证且不得放在 URL,Bright 校验页面侧的
mindUserId + workspaceId + channelAccountId及 read/send 权限;插件连接只提交channelAccountId + binding + deviceId,由 Bright 通过 Mind 授权视图解析并校验服务端 user/workspace 上下文。 - 插件 popup 负责输入并保存 Bright WebSocket URL、
channelAccountId、deviceId和binding到chrome.storage.local;Service Worker 启动时读取,popup 修改或清除后动态重建或关闭连接。binding不得进入 MAIN world、页面localStorage或消息事实 payload。 - 开发环境不得默认跳过认证。
pnpm dev启动独立的本地 Mind HTTP mock,Bright 通过 development-only loopbackMIND_AUTH_BASE_URL调用 binding/session 授权接口;mock 只校验固定 fixture 的channelAccountId + binding、Session Cookie、授权版本和权限,缺失或不匹配时 fail closed。mindUserId、workspaceId只来自 Mind HTTP 授权响应,不作为插件输入;生产环境仍使用 HTTPS 的真实 Mind 授权服务。 - 插件离线但 Bright 与 Mind 认证库可用时,用户仍可读取已持久化历史;页面显示插件离线,禁止发送且没有实时新消息。
- Bright 消息接口可用但 Mind 业务接口失败时,页面可显示消息和原始 ID,业务区域显示不可用;Mind 业务接口可用但 Bright 不可用时,只显示业务壳并明确消息不可用。Mind 认证库不可用时 Bright 全部 fail closed。
- 首次切换必须保留核心消息能力:会话列表与精确会话打开、历史读取、实时收件、发件三类结果、客户/会话精确关联、基础未读、权限与账号选择。
- AI 摘要、搜索、引用操作和深层分析等非核心能力可后续迁移;未迁移能力必须明确禁用,不得读取 TradeMind 旧 OneTalk 消息伪装可用。
- 首发 Mind 能力重新限定为:权限与账号选择、Bright 已发现会话列表/精确打开、历史读取、实时收件、发件三类结果,以及展示 Mind 中已存在的客户关联和负责人。本次不建设新未读持久化、自动客户关联、自动摘要或 Bright 消息后台投影。
- OneTalk 是会话存在性的唯一来源。插件枚举并同步到 Bright 后,Mind 才能读取该会话并建立业务关联;Mind 不得主动创建尚未被 Bright 发现的 OneTalk 会话。
- Mind 后台消费 Bright 消息、未读/摘要派生和两库最终一致机制属于本次暂缓的边缘功能,不在当前设计与实现范围。客户关联、负责人、摘要、未读和权限的事实归属仍全部在 Mind。
Requirements
Fact ownership and boundaries
- R0. 从现在开始,项目中所有声称来自 OneTalk 的字段、字段路径、类型和语义都视为不可信;现有代码、命名、fixture 和测试只能作为待验证候选,不能据此声称 OneTalk 提供
externalMessageId或其它供应方契约。 - R1. OneTalk 页面是消息唯一事实源。
- R2. 收件与经确认的发件共用 TradeBright 的一张
onetalk_message事实表。 - R3. TradeBright 是
onetalk_message的唯一服务端读写入口;Mind 服务端和浏览器都不直接连接 Bright DB。Mind 页面只能通过 Bright HTTP/WebSocket 访问消息。 - R3a. Mind 页面历史消息直接请求 Bright 服务端;Bright 使用 Mind 登录/授权信息校验
mindUserId + workspaceId + channelAccountId后查询 Bright DB 并返回,Mind 服务端不代理历史消息请求。 - R3b. Bright 历史读取要求
read权限;发送还要求有效 active binding 与send权限。激活码不是 Bright API 凭证,session/token 禁止放入 URL,具体传输机制由部署设计确定。 - R4. TradeMind 不得再把 OneTalk 消息复制到自身通用
messages表;非 OneTalk 渠道继续使用原有messages表。 - R5. TradeMind 对 OneTalk 消息的客户关联、摘要、未读、引用和发送流程必须使用统一事实表的消息主键及 OneTalk
messageId,不得通过维护第二份消息正文或投递状态恢复旧行为。 - R6. 现有 outbox 表、worker、claim/lease API 和其他渠道使用的通用机制全部保留;新的 OneTalk 收件、发件和状态同步必须完全绕过该机制,不得双写、claim、消费或 fallback。
Identity, routing and correlation
- R7. 新链路不使用
externalMessageId概念。消息满足入库结构门槛只要求 OneTalk 提供的messageId、conversation ID、sender ID、当前 login user ID 四项同时存在且为非空标量值。 - R7a. 对这四项只做存在性与基本可序列化检查,不校验格式、长度模式、业务合理性、ID 之间的关系或跨接口一致性;字段值的业务真实性由 OneTalk 负责。
- R7b. 插件必须保留这四项的原始观测来源以供诊断,但不得用
msgId、messageID、msgIdStr、通用id等未经新契约确认的候选字段静默替代缺失的messageId。 - R7c.
latest-*、hist_*、正文/时间/方向 hash 等客户端合成值不得存入messageId,也不得作为去重、增量锚点或重建完成依据。 - R7d. TradeBright 按
channelAccountId + conversationId + messageId建立幂等边界;binding 和 deviceId 只用于证明上传设备获得该账号授权和记录来源,mindUserId、workspaceId由服务端授权适配解析,不进入插件上传帧或幂等键。senderId 与 loginUserId 作为消息属性保存,不做合理性判断。 - R7e. Bright/Mind 服务端上下文只允许:
mindUserId、workspaceId、channelAccountId、deviceId、conversationId、messageId、senderId、loginUserId。其中mindUserId、workspaceId仅属于 Mind 授权/业务上下文,不是 OneTalk 页面字段,也不进入插件认证帧;插件认证字段固定为channelAccountId、deviceId和binding。其中channelAccountId是 OneTalk 页面运行时登录人的原始账号 ID(currentUserAccountId或IcbuIM.UserUtil.currentUser.accountId),URLactiveAccountId只表示当前对话账号。两库必须共享字段名称、类型和空值规则;不得新增sellerAccount、channelAccount或sellerAccountId,除非后续证据证明 OneTalk 提供且业务另行批准。 - R8. 所有缺失或不匹配的页面账号、渠道账号或会话身份必须 fail closed;不得回退到其他 OneTalk 标签页或广播账号特定工作。
- R8a. 设备能否连接、同步或发送由 Mind 的 binding 授权决定;Bright 必须在接受对应操作前通过最小认证视图校验授权,失败或认证数据不可用时拒绝操作。
- R8aa. 插件
ws.hello只携带channelAccountId + deviceId,并在payload.binding中提交 binding;Bright 必须向 Mind 授权视图确认 binding 属于该channelAccountId,再返回authorizationVersion与permissions。deviceId只用于来源和同一 socket 的 scope 完整性校验,不参与授权匹配。插件不得携带或校验mindUserId、workspaceId。 - R8ab.
binding是唯一绑定业务概念;协议、服务端上下文、数据库字段和诊断统一使用binding,不得引入另一套绑定标识命名。 - R8b. 同一 Mind 用户不得同时保持多个有效 binding;binding 接管后旧 binding 立即失效。会话事实和锚点归
channelAccountId + conversationId,不归设备,换安装实例不得形成消息或锚点副本。 - R8c. 因单用户单设备约束,不建设多设备同步 lease、自动发送设备选择或广播发送。所有页面驱动同步和发送只路由到当前有效 binding。
- R8d. Bright 在连接时、每次发送/同步操作前以及 WebSocket heartbeat 周期中复核 Mind 授权版本;发现撤销、接管、账号范围变化或认证视图不可用时立即 fail closed 并断开旧连接。
- R8da. 插件离线不阻止授权用户读取 Bright 已持久化历史,但必须禁用发送并标记实时接收不可用;插件重连后按共享锚点恢复同步。
- R8e. MVP 每个 Mind 用户只有一个 active binding 和一个 active
channelAccountId授权;切换 binding 或 active channelAccountId 必须先撤销旧 binding,不支持并行管理多个 active channel account。 - R8ea. Mind 侧 active binding 固定
mindUserId + workspaceId + channelAccountId + binding;授权范围或 binding 变化都属于接管并原子撤销旧 binding,插件只感知其中的channelAccountId + deviceId + binding,其中 deviceId 不参与授权 decision。 - R8f. 新设备接管发生在旧设备已经触发发送但尚未回报时,旧连接和迟到回报均被拒绝,原请求视为
delivery_unknown;若 OneTalk 实际已发送,新设备后续观察到四字段完整消息时按普通消息事实入库。 - R9. 一次发送沿
TradeMind 页面 → TradeBright → 插件 → 精确 OneTalk 页面传递,并沿相反方向返回结果;每一跳必须携带同一个稳定、非敏感的发送请求 ID。 - R10.
emit和 fallback 必须执行相同的channelAccountId + conversationId精确校验。
Outbound send and confirmation
- R11. TradeBright 不得为发送请求创建
pending、dispatching、confirming、failed或其他占位消息行;发送请求不是消息事实。 - R12. 插件发送前必须冻结当前精确会话的最新消息快照,发送后重新检查最新消息。
- R13. 插件可以综合使用 OneTalk 消息事件/emitter、发送接口返回值、页面 SDK 或消息列表数据、history/fallback 拉取和 DOM 展示结果;DOM 只是观测手段之一。
- R14.
confirmed_sent必须同时满足:页面channelAccountId + conversationId与精确 binding 授权匹配;发送后出现发送前快照中没有的新messageId;该记录同时具备 conversationId、senderId 与 loginUserId;正文、附件或引用与发送请求精确匹配;消息时间不早于本次发送;发送接口提供候选messageId时与页面观测值一致;不存在人工操作或并发发送歧义。除这些确定性条件外,不判断四个 ID 的业务合理性。 - R15. 确认规则必须是确定性证据闭环,不使用模糊置信度或评分;任一关键证据不足都不得返回
confirmed_sent。 - R16. 发送结果仅分为
confirmed_sent、rejected_before_send和delivery_unknown。 - R17.
confirmed_sent表示页面事实闭环,TradeBright 按精确账号/会话范围和messageId幂等写入统一消息表,并在数据库提交成功后向 TradeMind 页面发送最终回执。 - R18.
rejected_before_send只适用于尚未产生发送副作用且已明确拒绝的请求,例如插件离线、没有精确匹配页面或身份不一致;不得创建消息行或排队等待。 - R19. 已尝试发送但无法可靠确认时必须返回
delivery_unknown,不得创建消息行或自动重试。TradeMind 页面提示:“发送结果无法确认。请刷新或打开 OneTalk 页面核对;确认未发送后,再手动重试。” - R20. TradeBright 或 WebSocket 在发送尝试后中断时,不恢复检查、不自动重发;页面刷新后未确认的临时发送气泡允许消失。
- R20a.
sendRequestId只用于本次 TradeMind 页面、TradeBright 与插件之间的内存关联,到终端响应、超时或连接中断即失效,不写入消息表、任务表或其它耐久存储。 - R20b. TradeMind 已收到
delivery_unknown或连接已经中断后,如果插件稍后从 OneTalk 页面观察到四字段齐全的真实发件,TradeBright 仍必须把它作为普通 OneTalk 消息事实幂等入库并推送。 - R20c. 迟到消息不得追溯修改或伪造原临时发送请求的结果;页面允许先显示“结果无法确认”,随后通过正常消息流看到实际发出的消息。
Inbound receive
- R21. 插件从精确 OneTalk 页面观察消息后,只要
messageId、conversation ID、sender ID、login user ID 四项齐全,即可连同消息内容发送给 TradeBright;不校验这四项的业务合理性。 - R22. TradeBright 先通过 Mind 认证授权数据验证当前 binding 有权代表该
channelAccountId上传,再按channelAccountId + conversationId + messageId幂等写入统一表;数据库提交后再通过 WebSocket 通知 TradeMind 页面。 - R23a. 任一必需字段缺失时,该记录不得写入
onetalk_message,必须作为异常信息记录缺失字段与上下文;异常不能阻塞同页后续记录、后续分页或该会话后续增量同步。 - R23b. TradeBright 必须使用独立耐久诊断表
onetalk_message_anomaly保存异常消息,供开发后续确认;该表不是消息事实表或 outbox,不参与发送、claim、worker、自动重试、消息展示或增量锚点。 - R23c. 异常记录至少包含可获得的 binding、mindUserId、workspaceId、channelAccountId、deviceId、conversationId 范围、缺失字段列表、观测来源、清洗后的异常 payload、首次与最近出现时间、出现次数以及
open、resolved、ignored处理状态。 - R23d. 允许对清洗后的异常 payload 生成诊断 fingerprint,以合并同一异常的重复观测;该 fingerprint 只能用于诊断去重,绝不能成为
messageId、消息幂等键或增量锚点。 - R23e. 异常 payload 必须删除 Cookie、CSRF、反爬令牌、认证头和与排查无关的敏感字段;异常保存失败必须显式暴露,但不得把缺字段记录写入正常消息表。
- R23. 收件与经确认的发件最终形成同一种持久化消息事实,只以 direction 和页面事实字段区分。
- R24. 新
onetalk_message表不得直接复制或转换 TradeMind 旧 OneTalk 消息;历史数据只能由插件从精确 OneTalk 页面重新观察、验证并重建。 - R25. 未经插件重新确认的 TradeMind 旧 OneTalk 消息冻结为 legacy 数据,不得参与新链路的发送确认、实时同步、幂等判断或事实读取。
- R25a. 插件从 OneTalk 页面枚举到会话后,Bright 才能创建
channelAccountId + conversationId技术会话同步进度;Mind 只能读取 Bright 已发现的会话,不能主动制造 OneTalk 会话事实。 - R25b. senderId/loginUserId 四字段齐全时消息正常入 Bright;Mind 仅用现有业务逻辑按 ID 查询已有联系人/客户资料,查不到时显示原始 senderId 或“未关联联系人”。消息链路不自动创建客户,也不调用客户详情接口补全消息资料。
- R25c. OneTalk 页面可以在 MAIN world 观察当前已加载单聊的白名单基础资料,并通过独立的 profile observation 链路投递给 Mind;这不改变消息事实归属,也不表示 Mind 已完成 DB 落库。
Full rebuild and incremental anchor
- R26. 首次初始化的范围是当前 active
channelAccountId下页面可枚举的全部会话;初始化是一次同步过程,不形成账号级ready/degraded持久业务状态。 - R26a. 首次初始化后只执行会话级同步:新会话执行该会话首次全量;单个会话找不到旧锚点时只自动重新同步该会话;新消息确认后只推进该会话锚点。
- R27. 首次初始化和会话级重建均按
channelAccountId范围执行,不按 binding 创建设备副本;新设备接管后从 Bright 共享会话锚点重建本地状态。 - R28. 单聊和群聊均纳入全量重建;无法可靠验证身份的会话必须记录明确失败,不得静默跳过。
- R29. “全量”只表示 OneTalk 页面实际可枚举和读取的完整范围,不得宣称覆盖页面未暴露的数据。
- R30. 插件先枚举全部会话,再让每个会话独立从最新向最早分页;每批消息必须先耐久写入插件 IndexedDB,再上传 TradeBright。
- R31. TradeBright 对每条消息执行身份、范围与幂等校验并逐条确认;插件只有收到逐条确认后才能把对应 IndexedDB 项标记为已确认。
- R32. 插件必须耐久保存每个会话的分页位置、页面结束证据、待上传/待确认消息和重建结果,支持浏览器或 service worker 中断后从同一会话检查点继续。
- R33. 会话分页只能在 OneTalk 明确返回历史结束时完成;游标停滞、响应缺失、身份变化或页面不可用必须明确失败或未完成,不得解释为历史结束。
- R34. 单个会话的重建完成判定至少要求 OneTalk 明确返回历史结束、所有四字段齐全的消息均已上传、TradeBright 已逐条确认、IndexedDB 不存在待确认的有效消息;缺字段异常不得中断扫描或阻止后续有效消息同步。
- R34a. 满足完成条件且存在异常时,会话标记
succeeded_with_anomalies;账号汇总必须显示异常会话数和异常记录数,不得伪装成无异常成功。 - R35. 全部会话必须分别记录
succeeded、failed或incomplete;部分成功不得冒充整个账号重建完成。 - R36. 全量同步只有在 OneTalk 明确返回历史结束且有效消息全部获 TradeBright 确认后,才能记录该会话最新有效消息的
messageId作为增量锚点;全量未明确结束时禁止创建或更新锚点。 - R36a. 服务端锚点必须按
channelAccountId + conversationId唯一保存,值为该会话当前latestMessageId;可附带 senderId 与 loginUserId,但 binding、mindUserId、workspaceId 和 deviceId 不参与锚点归属,禁止使用裸messageId跨会话或跨账号查找。 - R36aa.
latestMessageId的归属和作用不因当前授权设备变化而改变;新设备读取并继续推进同一会话锚点,不创建设备独立锚点。 - R36ab. 插件建立连接并通过认证后,Bright 返回当前 active
channelAccountId下的全部服务端会话锚点;插件随后枚举页面全部会话以重建本地同步状态。 - R36ac. 页面会话存在服务端锚点时,插件从最新向该锚点执行增量追赶;不存在锚点时,只对该会话执行首次全量历史同步。
- R36ad. “全量重建同步状态”表示重新枚举会话并用服务端锚点恢复每个会话的本地状态,不等于每次连接都重新分页抓取所有历史;只有无锚点或锚点找不到的会话才抓取全历史。
- R36ae. Bright 必须保存独立的技术性会话同步进度,至少包含
channelAccountId + conversationId、latestMessageId、首次/增量/失败状态和最近观察时间,用于换设备或重连后恢复;该记录不保存消息正文、客户、负责人、摘要或未读。 - R36b. 锚点只定义增量扫描停止边界,不参与消息排序、正常消息入库资格或去重;消息幂等仍使用精确账号/会话范围加
messageId。 - R36c. 增量同步由 OneTalk 页面新消息信号触发,插件从当前最新消息向历史方向分页,直到遇到服务端保存的
latestMessageId;扫描过程中时间上比旧锚点更新的有效消息是本次候选新增消息,旧锚点可重复上报供服务端幂等校验,也可只作为边界。 - R36d. 增量扫描到的四字段完整消息必须先耐久写入插件 IndexedDB 并标记为
awaiting_anchor;找到旧锚点前禁止上传 TradeBright、写入onetalk_message或通知 TradeMind。 - R36e. 找到旧锚点后,插件把锚点之前的候选消息按页面扫描顺序反向整理,从旧到新逐条上传 TradeBright;Bright 按精确会话范围加
messageId幂等确认。 - R36f. 只有本次全部候选消息获得 TradeBright 确认后,才可把锚点推进到本次扫描最前端的最新有效消息,并通知 TradeMind 读取这批已确认事实。
- R36g. 旧锚点本身只作为扫描边界,默认不作为新增消息上传;即使实现选择重新上报,也只能由 Bright 幂等忽略。
- R36h. 最新页面消息缺少
messageId时写异常表并继续扫描,但本次不得推进该会话锚点;后续有效消息同步不受阻塞。 - R36i. 新会话没有锚点时直接执行该会话首次全量同步。
- R36j. 增量扫描直到 OneTalk 明确历史结束仍找不到旧锚点时,必须记录
incremental_anchor_not_found;所有候选继续保留在 IndexedDB,不得按增量结果上传或通知 TradeMind,并自动把当前会话切换为会话级全量重建。 - R36k. 自动会话重建必须复用锚点未找到时的候选消息和分页位置,不重复抓取已经耐久保存的页面消息,并在页面明确返回历史结束后按全量规则写入;其它会话继续独立增量同步。
- R36l. 浏览器或 service worker 重启后,插件必须从 IndexedDB 恢复
awaiting_anchor候选、分页位置和扫描模式,不能丢弃候选或越过锚点边界。
Observability and security
- R37. 每个发送请求、插件检查结果、重建会话、分页批次和消息写入必须保留可关联的结构化、非敏感诊断,至少包含 request ID、binding、账号范围、会话 ID、结果分类和时间戳;诊断不得记录 binding 原值、Cookie、CSRF、令牌或不必要的消息正文。
- R38. 插件不得直接调用 OneTalk CRM 端点、读取或重放 Cookie/CSRF/反爬令牌、接收任意远程 selector/script,或使用模糊客户名和列表位置点击。允许 MAIN world 读取 OneTalk 页面已经加载的、经过固定白名单清洗的基础资料;Service Worker 和服务端不得重放页面 token 或自行拼接 CRM 请求。
- R38a. 基础资料只读取
__conversationListData__初始快照和im-conversation-list:syncData更新;邮箱、注册时间、买家标签、详情微应用、DOM 刷新和群聊成员另行规划,不得以缺字段为由扩大本链路。 - R39. 本项目不设计普通消息删除、账号迁移历史搬运或 OneTalk 召回/编辑生命周期;
onetalk_message不提供常规物理删除路径。未来如需支持,必须单独规划 Bright 事实变化与 Mind 业务引用一致性。 - R40. 本项目不支持
channelAccountId在 workspace 之间迁移,不迁移、复制或重新归属客户、负责人、摘要、未读或 Bright 消息。消息复用只适用于当前有效授权关系下的同一 channelAccountId。 - R41. 新架构首次写入 Bright 消息后,旧 OneTalk outbox/dispatch 路径永久保持禁用;故障处置只能暂停新链路、修复并恢复,不能回退旧事实链路。
- R42. 全量切换后,旧协议插件发起 OneTalk 同步或发送时必须明确返回
onetalk_protocol_upgrade_required并提示升级,禁止静默忽略或写入旧 outbox;旧插件的其它兼容渠道可继续工作。
Acceptance Criteria
- AC1. 同一张
onetalk_message表表达入站与经确认的出站消息,并通过精确范围唯一约束和真实 PostgreSQL 测试保证幂等。 - AC2. 新出站请求在插件完成页面事实闭环前不会产生消息行或显示为最终成功。
- AC3. 插件只返回
confirmed_sent、rejected_before_send或delivery_unknown;只有confirmed_sent产生消息行。 - AC4. 发送接口返回成功但页面事实未闭环、相同文本并发发送、人工发送干扰或页面消息延迟时,结果为
delivery_unknown且数据库零写入。 - AC5.
delivery_unknown不触发自动恢复或自动重试,页面显示约定提示;刷新后临时气泡可以消失。 - AC5a. 临时
sendRequestId在终端响应、超时或断线后不留耐久任务记录。 - AC5b. 超时或断线后迟到的有效发件仍作为普通消息事实入库和推送,但原请求结果保持
delivery_unknown。 - AC6. 插件离线、身份不匹配或精确 OneTalk 页面不存在时返回
rejected_before_send,不排队、不切换页面且数据库零写入。 - AC7. 插件确认一条新发件后,重复上报相同精确范围、conversation ID 和
messageId只返回同一消息事实,不创建重复行。 - AC8. 控制命令按精确授权 binding 路由;消息事实按
channelAccountId + conversationId + messageId幂等,锚点按channelAccountId + conversationId共享,不因不同授权设备产生副本或串号。 - AC8a. 未授权、已撤销或认证视图不可用的设备无法连接、同步或发送;Bright 不通过缓存旧授权继续放行。
- AC8b. 插件 WebSocket 握手只提交
channelAccountId + binding + deviceId;Bright/Mind 授权视图按channelAccountId + binding确认归属后返回authorizationVersion与permissions,deviceId 仅作来源/连接完整性字段,缺失或不匹配时 fail closed。 - AC8c. 插件 popup 配置持久化 Bright WebSocket URL、
channelAccountId、deviceId和binding;配置变更能重建或关闭 Service Worker 连接,且mindUserId、workspaceId不进入插件配置、页面世界或消息事实。 - AC9. 收件与确认发件写入同一张表,TradeMind 页面只在数据库提交后通过 WebSocket 看到该消息事实。
- AC10. 现有 outbox 表和非 OneTalk 使用路径保持可用;集成测试证明 OneTalk 不会创建或消费任何 outbox 或 TradeMind dispatch 行。
- AC11. TradeMind 服务端不能写 TradeBright 的
onetalk_message表,越权写入在数据库权限或等价边界处失败。 - AC11a. Mind 页面只有在 Bright 根据 Mind 登录/授权信息确认当前 workspace/user/channelAccountId 可读后才能取得历史;历史请求不经过 Mind 服务端,也不暴露数据库连接。
- AC11b. Mind 服务端没有 Bright DB 凭据或消息投影;Bright DB 只接受 Bright 服务端连接。
- AC11c. Bright 复用 Mind session/token 完成 HTTP/WS 授权,token 不出现在 URL,read 与 send 权限分别生效。
- AC12. 新 OneTalk 消息不会写入 TradeMind 通用
messages表;其页面仍能读取、关联、发送并实时显示 OneTalk 消息。 - AC13. WhatsApp、邮件等非 OneTalk 渠道继续通过 TradeMind 原有
messages与 dispatch 机制工作。 - AC14. 缺少发送请求 ID,或消息缺少
messageId、conversation ID、sender ID、login user ID 任一项时,不写消息表并产生不含业务正文的结构化异常;同批和后续消息继续处理。 - AC14a.
senderId缺失的消息进入异常诊断而非正常消息表,开发能看到异常,且同批和后续同步继续。 - AC15. TradeBridge 与 TradeMind 的相关类型检查、单元测试及真实 PostgreSQL 迁移、幂等和断线边界测试通过。
- AC16. 新表的历史消息均可追溯到插件重新观察的精确 OneTalk 页面事实;没有任何 TradeMind legacy 消息被直接复制进新表。
- AC17.
latest-*、hist_*或正文/时间 hash 不会写入messageId,也不会被用作增量锚点。 - AC18. 当前登录用户的 OneTalk 账号首次初始化枚举全部会话;初始化完成后,新会话或锚点缺失只触发对应会话同步,不维护账号级 ready/degraded 状态。
- AC19. 全量重建枚举当前精确账号下页面可见的全部单聊和群聊,并为每个会话独立保存分页检查点与结果。
- AC20. 每批消息在上传前已写入 IndexedDB;断线或 service worker 重启后从耐久检查点续传,已确认消息不会重复创建。
- AC21. 游标停滞、会话/页面账号身份无法建立精确 binding 范围或任一有效消息未确认都会阻止该会话成功;单条缺字段异常不会阻塞其后的有效消息。
- AC22. 首次初始化结果准确列出每个会话的成功、失败与未完成状态;该结果是同步报告,不形成账号级 ready/degraded 业务事实。
- AC23. 缺字段异常耐久写入独立诊断表;重复观测只增加出现次数,开发可将其标记为
resolved或ignored。 - AC24. 异常诊断表不被任何消息读取、发送、增量定位或重试流程消费,且清洗测试证明不保存认证秘密。
- AC25. 重建扫描到异常后继续处理后续消息;满足其它完成条件时结果为
succeeded_with_anomalies并准确汇总异常数。 - AC26. 全量同步只有在页面明确返回历史结束且所有有效消息获确认后才保存精确范围的
latestMessageId锚点。 - AC27. 增量同步从最新向历史扫描并在旧锚点停止;只有旧锚点之前遇到的候选消息被处理,锚点本身不影响排序或入库资格。
- AC28. 找到旧锚点前,候选消息全部处于 IndexedDB
awaiting_anchor,TradeBright 消息表和 TradeMind 通知均为零变化。 - AC29. 找到旧锚点后,候选消息从旧到新逐条上传并幂等确认;全部确认后才推进锚点并通知 TradeMind。
- AC30. 成功增量完成后锚点推进到最新有效消息;最新消息缺少
messageId时记录异常且不推进锚点。 - AC31. 无锚点的新会话执行首次全量;找不到旧锚点时记录
incremental_anchor_not_found,候选留在 IndexedDB,不按增量结果写消息表或通知 TradeMind,并自动转入当前会话全量。 - AC32. 会话自动全量复用已有候选和分页位置;重启后仍能恢复,不重复抓取或丢失候选,也不阻塞其它会话。
- AC33. 新设备连接后获得服务端全部会话锚点并重新枚举页面会话;有锚点会话只做增量,无锚点会话才执行首次全量。
- AC34. 新设备接管会撤销旧 binding;旧设备无法继续连接、同步或发送,新设备从共享服务端锚点恢复且不产生设备副本。
- AC35. Bright 认证账号只能读取 Mind DB 的版本化最小授权视图,连接、操作和 heartbeat 均能感知授权撤销或设备接管。
- AC36. 同一 Mind 用户无法同时激活第二台设备或第二个精确账号 binding;接管会原子撤销旧 binding。
- AC37. 接管期间旧设备迟到回报被拒绝且请求为
delivery_unknown;实际已发消息可由新设备后续观察并正常入库。 - AC38. 系统没有账号迁移入口或隐式归属变更;消息复用只发生在当前 active binding 授权的同一 channelAccountId。
- AC39. 第一条新 Bright 消息写入后,任何配置或错误处理都不能重新启用旧 OneTalk outbox/dispatch 路径。
- AC40. 全量切换后旧插件的 OneTalk 请求明确返回
onetalk_protocol_upgrade_required,不会进入旧 outbox;其它兼容渠道继续工作。 - AC41. Bright 技术会话同步进度能表示零消息会话、首次/增量/失败状态、锚点与最近观察时间,且不含消息正文或 Mind 业务字段。
- AC42. Mind 无法创建 Bright 尚未由 OneTalk 插件发现的会话;插件枚举并提交技术会话后,Mind 才能读取并建立业务关联。
- AC43. senderId/loginUserId 完整但 Mind 无对应资料时消息仍显示原始 ID/未关联占位;消息链路不会自动创建客户或触发详情资料补全。
- AC44. 插件离线时历史仍可读取、发送和实时接收禁用;插件重连后从服务端锚点恢复。
- AC45. Bright 消息与 Mind 业务接口能按确认的部分故障规则独立降级;Mind 认证视图不可用时 Bright 历史、连接、同步和发送全部拒绝。
- AC46. 首次有效 OneTalk 页面 hello 仅对当前已加载单聊产生 profile snapshot;新单聊和白名单资料变化产生独立 profile event,经现有 Bright WebSocket 和 binding/read 授权投递 Mind;Bright 不保存 profile,任意 HTTP response 才 ACK,无 response 保留 pending。
Out of Scope
- 删除、改造或迁移现有 outbox 表及其中历史数据。
- 重构 WhatsApp、邮件等非 OneTalk 渠道,除非共享契约需要最小兼容调整。
- 为未确认发送增加任务表、占位消息、自动恢复或自动重试。
- 保证恰好发送一次。
- 把 TradeMind 旧 OneTalk 消息直接转换成新表事实。
- 本次重新设计或实现 Mind 后台消息消费者、未读/摘要派生、两库消费 cursor 和最终一致重试机制;这些业务事实继续归 Mind,后续另立子项目。
Accepted Risks And Trade-offs
- 无法保证恰好发送一次;用户未经核对就手动重试仍可能造成重复。
- Bright 或 WebSocket 在发送后中断时只能视为结果未知,不能自动恢复检查。
- 未确认发送不持久化,TradeMind 刷新后临时发送气泡可以消失。
- 相同文本、并发发送、人工操作或页面消息延迟都可能造成歧义;系统宁可返回
delivery_unknown,也不误报成功。 - TradeMind 原先依赖自身
messages表的 OneTalk 关联、摘要、未读和引用功能需要改造。
Open Questions
- Q1. WebSocket 断线补读、精确字段类型/空值、游标、超时、索引、表结构、部署域名和凭证传输方式等低层决策,明确推迟到用户后续拆分的对应子项目设计,不阻塞父级架构确认。