Files
trade-message-center/docs/archive/重构 OneTalk 消息收发同步机制-server.md

22 KiB
Raw Permalink Blame History

OneTalk 消息中心服务端重构 PRD

本文从根目录 prd.md 拆分,仅约束当前仓库 apps/server 所承载的 TradeBright 服务端。父 PRD 的事实归属、失败边界和切换约束优先级高于本文;如两者冲突,必须先回到父 PRD 澄清,不能由实现自行改变系统边界。

1. 目标

在当前 TradeBridge 仓库内把 apps/server 建设为 TradeBright 的 OneTalk 消息中心服务端,使其成为 OneTalk 消息事实的唯一服务端读写入口,并同时服务 Chrome 插件与 TradeMind 消息页面。

服务端必须做到:

  • 只持久化插件从精确 OneTalk 页面观察到的真实收件,以及完成页面事实闭环确认的真实发件。
  • 使用统一的消息事实模型表达收件和确认发件,不创建发送任务、发送占位消息或第二份消息投影。
  • 基于 Mind 的最小认证授权视图按 channelAccountId + binding 校验插件授权,并在服务端解析对应的 Mind user/workspace 授权上下文;deviceId 只保留为来源和连接完整性字段。
  • 为插件提供消息逐条确认、技术会话进度和共享增量锚点,为 Mind 页面提供受权历史查询、会话读取、实时消息与临时发送回执。
  • OneTalk 全链路绕过现有 outbox/worker/claim/dispatch 机制,同时保持其它渠道原有链路可用。

2. 当前基线

  • 服务端包位于 apps/server/,当前只有 package.json
  • 当前唯一运行时依赖是 fastify@^5.12.1;尚无启动入口、HTTP 路由、WebSocket、配置、数据库、迁移、日志和测试实现。
  • 根配置要求 Node.js >=22.22.2 <23、pnpm 11.7.0、TypeScript strict、ES2022 和 NodeNext。
  • 本任务是服务端能力从零落地,不得假设现有代码已经提供数据库模型、认证协议、错误码或消息兼容层。

3. 事实归属与系统边界

3.1 TradeBright 服务端拥有

  • OneTalk 收件与确认发件的消息正文、附件/引用内容、方向及 OneTalk 原始关联 ID。
  • channelAccountId + conversationId + messageId 范围内的消息幂等事实。
  • channelAccountId + conversationId 范围内的技术会话存在性、同步进度和 latestMessageId 锚点。
  • 缺字段、同步失败和协议异常的耐久诊断事实。
  • Bright 的消息、同步、HTTP、WebSocket 和错误码契约及其版本。
  • 当前有效插件连接的内存路由,以及页面与插件之间临时发送请求的内存关联。

3.2 TradeBright 服务端不拥有

  • OneTalk 页面的抓取、分页、身份提取、IndexedDB 账本和发件事实判断;这些属于插件。
  • Mind 的用户、workspace、客户、联系人、负责人、摘要、未读、激活码和其它 CRM 业务事实。
  • Mind 页面展示状态、临时发送气泡和业务数据组合。
  • 未确认发送的耐久任务、自动恢复、自动重试或恰好一次保证。
  • TradeMind 的 OneTalk 消息副本或 Bright 消息后台投影。

3.3 数据访问边界

  • Bright 服务端独占 Bright DB 的读写,浏览器、插件和 Mind 服务端均不得直连 Bright DB。
  • Bright 只能以只读方式访问 Mind DB 暴露的版本化最小认证授权视图,不得读取客户、负责人、摘要、未读、激活码秘密等业务表。
  • Mind DB 或最小认证授权视图不可用时,Bright 的历史查询、连接、同步和发送全部 fail closed;不得用过期授权缓存继续放行。
  • Mind 页面读取历史时直接访问 Bright 的受权接口,不经过 Mind 服务端代理。

4. 服务端能力拆分

S1. 服务基础设施

建立可运行、可配置、可测试的 Fastify 服务端边界,包括 HTTP、WebSocket、配置校验、统一错误协议、数据库连接与迁移、结构化日志和优雅停机能力。

本 PRD 不指定 ORM/驱动、目录名称、路由路径、帧格式或日志库;这些决策必须在服务端 design 中确定并同步到 .trellis/spec/server/backend/

S2. Mind 认证授权适配

消费 Mind 提供的版本化最小只读认证视图,统一完成以下校验:

  • 插件认证输入只包含 channelAccountId + binding + deviceIdBright 使用 channelAccountId + binding 向 Mind 授权视图查询 active binding。
  • Mind 授权视图确认 binding 属于该 channelAccountId,并返回服务端使用的 mindUserIdworkspaceIdauthorizationVersionpermissionsdeviceId 不参与 active binding decision。
  • 用户、workspace、账号和 binding 状态有效且未撤销;deviceId 仅作为来源标识,不参与该 decision。
  • 历史和会话读取具备 read 权限;发送具备 send 权限和有效 active binding。
  • 同一 Mind 用户在 MVP 中只有一个 active binding 和一个 active channelAccountIddeviceId 不参与 active binding decision。

mindUserIdworkspaceId 只属于 Mind/Bright 服务端授权上下文,不是插件输入,也不是 OneTalk 字段;插件不负责校验这两个字段。开发环境由独立 Mind HTTP mock 提供最小授权视图,Bright 通过 development-only loopback MIND_AUTH_BASE_URL 调用固定 binding/session endpointmock 只按 channelAccountId + binding、Session Cookie、授权版本和权限校验,配置缺失或不匹配时 fail closed。生产环境仍由真实 Mind Session/授权服务提供该上下文。

Bright 必须在连接建立、每次读取/发送/同步操作以及 WebSocket heartbeat 周期中复核授权版本。binding 接管、撤销、账号范围变化或授权依赖异常时,应立即拒绝后续操作并断开失效连接。

S3. 连接注册与精确路由

服务端维护两类受权实时连接:插件连接和 Mind 页面连接。插件连接必须绑定到精确的 channelAccountId + binding,并保留 deviceId 作为来源/同一 socket 完整性字段;所有账号特定命令只允许路由到该精确连接。Mind 页面连接的 mindUserId + workspaceId 仍由页面登录/授权上下文处理,不下发给插件。

  • 不广播账号特定工作。
  • 不建设多设备 lease、设备自动选择或 fallback 到其它 OneTalk 标签页。
  • emit、fallback 或任何替代传输分支都必须执行同一套 channelAccountId + conversationId 校验。
  • 插件离线时,受权用户仍可读取已持久化历史,但发送和实时接收能力必须明确不可用。

S4. 统一消息事实持久化

Bright DB 必须包含统一的 onetalk_message 事实表,收件与确认发件只通过方向和页面事实字段区分。

  • 唯一幂等范围固定为 channelAccountId + conversationId + messageId
  • bindingmindUserIdworkspaceIddeviceId 只作为授权/来源上下文,不进入消息唯一键;数据库列名也统一为 binding,不再保留旧的绑定标识命名。
  • senderIdloginUserId 是消息属性,不进入唯一键,也不由 Bright 校验其业务关系。
  • 正常入库的结构门槛是非空且可序列化的 messageIdconversationIdsenderIdloginUserId;不额外推断 ID 格式或业务合理性。
  • 禁止把 latest-*hist_*、正文/时间/方向 hash 或其它合成值写入 messageId
  • 不生成 Bright 内部 account/conversation/message ID 来替代共享原始 ID。
  • 不提供普通消息物理删除路径,不导入或转换 TradeMind legacy OneTalk 消息。

重复上报必须返回同一事实的幂等确认,不得产生重复消息。消息事务提交成功后,才能确认插件上传并向 Mind 页面发布实时事实。

S5. 技术会话与同步锚点

Bright 必须保存独立的技术会话同步进度。技术会话只能由插件从 OneTalk 页面枚举后创建,Mind 不得主动创建尚未被 Bright 发现的会话。

  • 唯一范围为 channelAccountId + conversationId,不按 binding 或设备复制。
  • 至少表达首次/增量/失败状态、latestMessageId 和最近观察时间,并能表示零消息会话。
  • 会话级结果需能区分 succeededsucceeded_with_anomaliesfailedincomplete;不得形成账号级 ready/degraded 持久业务状态。
  • 插件通过认证后,Bright 返回当前 active channelAccountId 下的全部服务端会话锚点。
  • 锚点只作为增量扫描停止边界,不参与消息排序、消息入库资格或幂等。
  • 只有插件明确报告页面历史结束、所有有效消息已获 Bright 逐条确认且不存在待确认有效消息后,Bright 才能创建或推进锚点。
  • 最新页面消息缺少 messageId、全量未完成或本次有效消息未全部确认时,不得推进锚点。

S6. 消息上传与逐条确认

服务端接收插件从 IndexedDB 耐久账本上传的消息,并对每条消息独立执行:

  1. 协议版本与输入结构校验。
  2. 当前连接的 channelAccountId + binding 授权校验;deviceId 只校验同一 socket 的 scope 完整性。
  3. 必需 OneTalk 原始字段存在性校验。
  4. 精确范围幂等写入。
  5. 数据库提交后的逐条确认与实时发布。

一条记录失败不得阻塞同批或后续有效记录。服务端不负责保存插件分页位置、awaiting_anchor 候选或 IndexedDB 状态,也不得在插件找到旧锚点前接收候选增量消息作为正常事实。

S7. 异常诊断

Bright DB 必须使用独立耐久表 onetalk_message_anomaly 保存缺字段和协议观测异常。

  • 至少记录可获得的 binding、Mind 授权视图解析出的用户/workspace、账号、设备、会话范围,缺失字段,观测来源,清洗后的异常 payload,首次/最近出现时间,出现次数以及 openresolvedignored 状态。
  • 可使用诊断 fingerprint 合并重复观测;fingerprint 只用于诊断去重,不得作为消息 ID、消息幂等键或锚点。
  • payload 必须移除 Cookie、CSRF、反爬令牌、认证头和无关敏感字段,并避免保存无排障必要的消息正文。
  • 异常表不得被消息读取、发送、增量定位、worker 或重试流程消费。
  • 异常写入失败必须显式返回/记录失败;无论如何不得把不合格记录降级写入消息表。

incremental_anchor_not_found 等同步异常需要可诊断,但候选消息与分页位置仍由插件持有,Bright 不得把未闭环的候选当作消息事实。

S8. 历史与会话读取

Bright 为 Mind 页面提供受权的会话列表、精确会话打开和历史读取能力。

  • 每次 Mind 页面查询必须校验页面侧 mindUserId + workspaceId + channelAccountId 的 read 权限;这些字段不由插件提交或校验。
  • 只能返回 Bright 已由插件发现的技术会话和已提交的消息事实。
  • 不返回或拼装客户、负责人、摘要、未读等 Mind 业务字段。
  • 不读取 TradeMind legacy OneTalk 消息补齐历史。
  • HTTP/查询游标、排序规则、分页大小和断线补读策略在服务端 design 中确定,但必须只基于稳定的 Bright 消息事实,不得复用同步锚点冒充查询游标。

S9. 临时发件协调

服务端承担 Mind 页面 → Bright → 精确插件连接 → OneTalk 页面 的临时请求协调,并沿反向链路传递结果。

  • 每一跳使用同一个稳定、非敏感 sendRequestId
  • sendRequestId 只存在于当前进程的临时关联中;终端响应、超时、断线或进程终止后即失效,不写消息表、任务表或其它耐久存储。
  • 不创建 pendingdispatchingconfirmingfailed 等占位消息行。
  • 对外结果仅允许 confirmed_sentrejected_before_senddelivery_unknown
  • 插件离线、精确连接不存在或发送副作用发生前身份校验失败时,返回 rejected_before_send,不排队。
  • 已尝试发送但证据不足、超时、断线或结果丢失时,返回 delivery_unknown,不恢复检查、不自动重发。
  • 只有插件返回满足契约的 confirmed_sent 页面事实,Bright 才按普通消息规则幂等入库;提交成功后才向页面返回最终成功并发布消息。
  • 旧 binding 的迟到回报必须拒绝,原请求保持 delivery_unknown。之后由有效设备重新观察到的真实发件,按普通消息上传入库,不追溯修改原请求结果。

页面应展示的提示文案由 Mind 负责;Bright 负责返回稳定、可区分的结果码。

S10. 协议版本、切换与旧链路隔离

  • Bright 侧拥有消息、同步、HTTP、WebSocket 和错误码契约,需提供共享类型/规则及 contract test,供插件和 Mind 使用。
  • 全量切换后,旧协议插件发起 OneTalk 同步或发送必须明确返回 onetalk_protocol_upgrade_required
  • 新 OneTalk 收件、发件和同步不得创建、claim、消费或 fallback 到现有 trademind_delivery_outboxoutbound_message 或 Mind dispatch。
  • 其它渠道的既有 outbox/dispatch 行为保持不变。
  • 第一条新 Bright 消息事实写入前允许整体回滚部署;写入后旧 OneTalk 事实链路永久禁用,只能暂停新链路、修复并恢复。

S11. 可观测性与安全

服务端必须为发送请求、插件检查结果、同步会话、分页批次、异常记录和消息写入提供可关联的结构化诊断,至少覆盖:

  • request ID / sendRequestId(适用时)
  • binding(仅记录非敏感摘要)、mindUserId、workspaceId、channelAccountId、deviceId(按日志规范脱敏)
  • conversationId、协议版本、操作类型、结果分类和时间戳

日志、错误和诊断中禁止记录 session/token、Cookie、CSRF、认证头、连接串、OneTalk 反爬令牌或无必要的完整消息正文。内部异常不得直接序列化给客户端。

5. 服务端需求

5.1 不可违背的事实规则

  • SR1. OneTalk 页面是消息唯一事实源;Bright 只接受插件重新观察或确认的页面事实。
  • SR2. 入站和确认出站共用一张 onetalk_message 表。
  • SR3. 未闭环发送不是消息事实,不持久化、不排队、不重试。
  • SR4. Bright 是 Bright DB 唯一读写入口,且只读 Mind 最小认证授权视图。
  • SR5. 消息、会话和锚点按 OneTalk 原始账号/会话 ID 共享,不绑定当前设备。
  • SR6. 所有账号、会话、binding 或权限不匹配均 fail closed。
  • SR7. 消息必须提交后再确认和推送;不得先广播再补写数据库。
  • SR8. 缺字段记录只能进入独立异常诊断,不得进入正常消息表。
  • SR9. OneTalk 新链路必须彻底绕过旧 outbox/dispatch,且不得影响其它渠道。
  • SR10. Bright 不吸收 Mind CRM 业务事实,也不建立 OneTalk 消息的 Mind 投影。

5.2 共享字段基线

Bright/Mind 服务端身份与关联上下文只允许以下命名作为当前基线;这不表示插件必须提交全部字段:

  • mindUserId
  • workspaceId
  • channelAccountId
  • deviceId
  • conversationId
  • messageId
  • senderId
  • loginUserId

其中 channelAccountId 是 OneTalk 页面运行时提供的登录人原始账号 ID(currentUserAccountIdIcbuIM.UserUtil.currentUser.accountId);URL 的 activeAccountId 仅是当前对话账号,不能作为 channel account。字段具体类型、空值规则和序列化形式必须由服务端 design 与共享契约统一确定;未经新的页面证据和业务批准,不新增 sellerAccountchannelAccountsellerAccountId 等替代概念。

插件 OneTalk WebSocket ws.hello 的字段子集固定为 channelAccountId + deviceIdbinding 固定放在 payload.bindingBright 按 channelAccountId + binding 执行授权匹配,deviceId 只用于来源和同一 socket 完整性。mindUserIdworkspaceId 由 Bright 从 Mind 登录/授权结果或开发授权视图获得,仅用于服务端授权,不进入插件 popup、插件协议或页面事实。

6. 交付分区与依赖顺序

以下是能力依赖,不是具体代码目录或提交清单:

  1. 契约与基础边界:先确定配置、错误、共享字段、协议版本、认证适配、数据库与测试策略。
  2. 事实存储:建立消息、技术会话/锚点、异常诊断及真实 PostgreSQL 迁移验证。
  3. 授权连接与上传:完成 Mind 最小认证视图接入、插件连接、会话发现、逐条上传确认和 publish-after-commit 边界。
  4. Mind 读取与实时链路:开放受权历史/会话读取及提交后的实时消息事件。
  5. 临时发件闭环:接入内存请求关联、精确插件路由、三态回执和断线/接管边界。
  6. 切换保护:旧协议拒绝、OneTalk 旧链路隔离、最低插件版本和不可回退开关。

外部前置依赖:

  • Mind 提供版本化最小认证授权视图及 session/token 校验方式。
  • 插件确认 OneTalk 页面真实字段来源,并实现 IndexedDB、会话枚举、全量/增量扫描和发送事实闭环。
  • Mind 页面接入 Bright 的历史、实时和发件三态契约。

服务端设计和本地实现可以先于外部系统完成,但在前置契约未明确时只能使用可替换的测试适配,不能把猜测固化为生产协议。

7. 服务端验收标准

  • SAC1. apps/server 具备可重复运行的 build、typecheck、test 和真实启动入口,配置缺失时明确失败。
  • SAC2. 真实 PostgreSQL 迁移可从空库执行;消息唯一约束能阻止 channelAccountId + conversationId + messageId 重复行。
  • SAC3. 收件与 confirmed_sent 发件写入同一消息表;其它两种发送结果及超时/断线均为消息表零写入。
  • SAC4. 重复上报返回同一消息事实,数据库提交后才向插件确认并向 Mind 页面推送。
  • SAC5. 缺少任一必需原始 ID 的记录不进入消息表,独立异常表可合并重复观测、更新状态且不阻塞后续有效消息。
  • SAC6. 清洗测试证明异常、日志和错误响应不包含认证秘密或无必要的消息正文。
  • SAC7. 技术会话和锚点以 channelAccountId + conversationId 唯一,支持零消息会话和四类会话结果,设备接管后不产生副本。
  • SAC8. 全量未结束、消息未全部确认或最新消息缺少 messageId 时,服务端拒绝推进锚点。
  • SAC9. 新设备取得同一账号的共享锚点;旧设备在 binding 被撤销后无法继续 heartbeat、上传、发送或提交迟到回报。
  • SAC10. Mind 授权视图不可用、授权撤销、范围不匹配或权限不足时,HTTP 与 WebSocket 均 fail closed,且不会依赖旧授权缓存放行。
  • SAC10a. 插件握手只接受 channelAccountId + binding + deviceIdBright 能在 Mind 授权视图或开发 .env.development.local 视图中按 channelAccountId + binding 确认授权后返回 authorizationVersionpermissionsdeviceId 不参与 decision,缺失或不匹配时 fail closed。
  • SAC11. readsend 权限分别生效;Mind session/token 不出现在 URL、日志或业务错误中。
  • SAC12. Mind 页面只能查询已发现会话和已提交事实;不能通过 Bright 创建 OneTalk 会话,也不会从响应得到 Mind CRM 业务字段。
  • SAC13. 插件离线时历史仍可读取,发送明确拒绝且不排队;实时状态可被页面识别为不可用。
  • SAC14. 发件结果只出现三态;sendRequestId 在终态、超时、断线或重启后没有任何耐久记录。
  • SAC15. 发送后断线、设备接管、迟到回报和迟到真实消息测试证明:原请求保持 delivery_unknown,真实消息仅通过普通上传事实入库。
  • SAC16. 集成测试证明 OneTalk 请求不会创建或消费任何旧 outbox/dispatch 行,且非 OneTalk 渠道行为不受影响。
  • SAC17. legacy OneTalk 消息和 latest-*hist_*、正文/时间 hash 均无法进入新消息表或锚点。
  • SAC18. 旧协议 OneTalk 请求稳定返回 onetalk_protocol_upgrade_required,不会静默忽略或回退旧链路。
  • SAC19. contract test 覆盖插件上传/确认、锚点、Mind 历史读取、实时事件、发件三态和稳定错误码。
  • SAC20. 故障测试覆盖数据库失败、Mind 认证依赖失败、WebSocket 中断、重复消息、并发发送关联和进程重启;不出现伪成功、提前广播或自动重试。

8. 明确不在服务端任务范围内

  • 插件对 OneTalk 页面字段的发现、验证和适配。
  • 插件 IndexedDB、分页游标、页面结束证据、awaiting_anchor 候选与中断续传。
  • TradeMind 页面、客户关联、负责人、摘要、未读、搜索、引用和深层分析。
  • Mind 的 binding 创建/接管事务、激活码验证和账号选择 UI;Bright 只消费其最终授权事实。
  • TradeMind 后台消息消费者、Bright 消息投影、两库消费 cursor 和最终一致重试。
  • 删除、迁移或转换旧 outbox 及历史数据。
  • 普通消息删除、OneTalk 撤回/编辑生命周期和账号跨 workspace 迁移。
  • 对未确认发送提供任务表、恢复检查、自动重试或恰好一次保证。

9. 已接受风险

  • 发送无法保证恰好一次;用户手动重试前必须自行核对 OneTalk 页面。
  • 服务端或 WebSocket 在发送尝试后中断时,结果只能是 delivery_unknown
  • 临时发送状态不持久化,页面刷新后允许消失。
  • 进程重启会丢失尚未终结的内存发送关联,这是“不恢复、不自动重试”边界的一部分。
  • Mind 认证视图故障会使 Bright 全部 fail closed,即使 Bright DB 本身仍可用。

10. 后续 design 必须回答的问题

以下问题不阻塞本 PRD,但必须在写服务端代码前定稿:

  • Fastify 启动/模块边界、HTTP 路径、WebSocket 握手和帧格式。
  • 数据库驱动或 ORM、连接池、迁移工具、事务边界及生产数据库权限模型。
  • onetalk_message、技术会话进度和异常表的精确字段类型、空值、索引和时间语义。
  • 消息历史排序、查询游标、分页、实时断线补读和事件序列规则。
  • Mind session/token 的跨服务验证方式,以及凭证在 HTTP/WS 中的安全传输方式。
  • heartbeat、授权复核、发送请求和数据库操作的具体超时值。
  • 进程内连接注册和水平扩容时的路由方案;该方案不得演变为耐久发送队列。
  • 消息上传批次、逐条 ack、会话完成声明与锚点推进之间的原子性协议。
  • 统一错误结构、稳定错误码、HTTP 状态码和 WS close code 映射。
  • 结构化日志、指标、trace、脱敏和诊断数据保留周期。
  • 新写入开关、最低插件协议版本、灰度验收、暂停和不可回退保护的部署实现。