22 KiB
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、pnpm11.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 + deviceId;Bright 使用channelAccountId + binding向 Mind 授权视图查询 active binding。 - Mind 授权视图确认 binding 属于该
channelAccountId,并返回服务端使用的mindUserId、workspaceId、authorizationVersion和permissions;deviceId 不参与 active binding decision。 - 用户、workspace、账号和 binding 状态有效且未撤销;deviceId 仅作为来源标识,不参与该 decision。
- 历史和会话读取具备
read权限;发送具备send权限和有效 active binding。 - 同一 Mind 用户在 MVP 中只有一个 active binding 和一个 active
channelAccountId;deviceId 不参与 active binding decision。
mindUserId 与 workspaceId 只属于 Mind/Bright 服务端授权上下文,不是插件输入,也不是 OneTalk 字段;插件不负责校验这两个字段。开发环境由独立 Mind HTTP mock 提供最小授权视图,Bright 通过 development-only loopback MIND_AUTH_BASE_URL 调用固定 binding/session endpoint;mock 只按 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。 binding、mindUserId、workspaceId和deviceId只作为授权/来源上下文,不进入消息唯一键;数据库列名也统一为binding,不再保留旧的绑定标识命名。senderId与loginUserId是消息属性,不进入唯一键,也不由 Bright 校验其业务关系。- 正常入库的结构门槛是非空且可序列化的
messageId、conversationId、senderId、loginUserId;不额外推断 ID 格式或业务合理性。 - 禁止把
latest-*、hist_*、正文/时间/方向 hash 或其它合成值写入messageId。 - 不生成 Bright 内部 account/conversation/message ID 来替代共享原始 ID。
- 不提供普通消息物理删除路径,不导入或转换 TradeMind legacy OneTalk 消息。
重复上报必须返回同一事实的幂等确认,不得产生重复消息。消息事务提交成功后,才能确认插件上传并向 Mind 页面发布实时事实。
S5. 技术会话与同步锚点
Bright 必须保存独立的技术会话同步进度。技术会话只能由插件从 OneTalk 页面枚举后创建,Mind 不得主动创建尚未被 Bright 发现的会话。
- 唯一范围为
channelAccountId + conversationId,不按 binding 或设备复制。 - 至少表达首次/增量/失败状态、
latestMessageId和最近观察时间,并能表示零消息会话。 - 会话级结果需能区分
succeeded、succeeded_with_anomalies、failed和incomplete;不得形成账号级ready/degraded持久业务状态。 - 插件通过认证后,Bright 返回当前 active
channelAccountId下的全部服务端会话锚点。 - 锚点只作为增量扫描停止边界,不参与消息排序、消息入库资格或幂等。
- 只有插件明确报告页面历史结束、所有有效消息已获 Bright 逐条确认且不存在待确认有效消息后,Bright 才能创建或推进锚点。
- 最新页面消息缺少
messageId、全量未完成或本次有效消息未全部确认时,不得推进锚点。
S6. 消息上传与逐条确认
服务端接收插件从 IndexedDB 耐久账本上传的消息,并对每条消息独立执行:
- 协议版本与输入结构校验。
- 当前连接的
channelAccountId + binding授权校验;deviceId 只校验同一 socket 的 scope 完整性。 - 必需 OneTalk 原始字段存在性校验。
- 精确范围幂等写入。
- 数据库提交后的逐条确认与实时发布。
一条记录失败不得阻塞同批或后续有效记录。服务端不负责保存插件分页位置、awaiting_anchor 候选或 IndexedDB 状态,也不得在插件找到旧锚点前接收候选增量消息作为正常事实。
S7. 异常诊断
Bright DB 必须使用独立耐久表 onetalk_message_anomaly 保存缺字段和协议观测异常。
- 至少记录可获得的 binding、Mind 授权视图解析出的用户/workspace、账号、设备、会话范围,缺失字段,观测来源,清洗后的异常 payload,首次/最近出现时间,出现次数以及
open、resolved、ignored状态。 - 可使用诊断 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只存在于当前进程的临时关联中;终端响应、超时、断线或进程终止后即失效,不写消息表、任务表或其它耐久存储。- 不创建
pending、dispatching、confirming、failed等占位消息行。 - 对外结果仅允许
confirmed_sent、rejected_before_send、delivery_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_outbox、outbound_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 服务端身份与关联上下文只允许以下命名作为当前基线;这不表示插件必须提交全部字段:
mindUserIdworkspaceIdchannelAccountIddeviceIdconversationIdmessageIdsenderIdloginUserId
其中 channelAccountId 是 OneTalk 页面运行时提供的登录人原始账号 ID(currentUserAccountId 或 IcbuIM.UserUtil.currentUser.accountId);URL 的 activeAccountId 仅是当前对话账号,不能作为 channel account。字段具体类型、空值规则和序列化形式必须由服务端 design 与共享契约统一确定;未经新的页面证据和业务批准,不新增 sellerAccount、channelAccount 或 sellerAccountId 等替代概念。
插件 OneTalk WebSocket ws.hello 的字段子集固定为 channelAccountId + deviceId,binding 固定放在 payload.binding;Bright 按 channelAccountId + binding 执行授权匹配,deviceId 只用于来源和同一 socket 完整性。mindUserId 与 workspaceId 由 Bright 从 Mind 登录/授权结果或开发授权视图获得,仅用于服务端授权,不进入插件 popup、插件协议或页面事实。
6. 交付分区与依赖顺序
以下是能力依赖,不是具体代码目录或提交清单:
- 契约与基础边界:先确定配置、错误、共享字段、协议版本、认证适配、数据库与测试策略。
- 事实存储:建立消息、技术会话/锚点、异常诊断及真实 PostgreSQL 迁移验证。
- 授权连接与上传:完成 Mind 最小认证视图接入、插件连接、会话发现、逐条上传确认和 publish-after-commit 边界。
- Mind 读取与实时链路:开放受权历史/会话读取及提交后的实时消息事件。
- 临时发件闭环:接入内存请求关联、精确插件路由、三态回执和断线/接管边界。
- 切换保护:旧协议拒绝、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 + deviceId;Bright 能在 Mind 授权视图或开发.env.development.local视图中按channelAccountId + binding确认授权后返回authorizationVersion与permissions,deviceId 不参与 decision,缺失或不匹配时 fail closed。 - SAC11.
read与send权限分别生效;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、脱敏和诊断数据保留周期。
- 新写入开关、最低插件协议版本、灰度验收、暂停和不可回退保护的部署实现。