Files
trade-message-center/.trellis/tasks/archive/2026-09/09-17-onetalk-quote-reply-protocol/prd.md
T
YBF 784bbec2a0 feat: 支持 OneTalk 文本引用回复协议 (#66)
* feat: add OneTalk quote reply support

* chore(task): archive 09-17-onetalk-quote-reply-protocol

* chore: record journal

* docs: format OneTalk DOM card collection plan

* chore: release 0.8.29

* chore: release 0.8.30
2026-09-17 11:56:35 +08:00

6.3 KiB
Raw Blame History

OneTalk 引用回复跨端协议接入

Goal

让 Mind 用户能选择一条 OneTalk 时间线消息并发送引用回复,同时保持 OneTalk 原始载荷只在插件 MAIN world 内解析。协议必须在 Mind、Center WebSocket、Service Worker 和页面 SDK 间传递确定的引用目标,不让任一层按正文、时间或显示名称猜测引用内容。

Confirmed facts

  • 2026-09-17 的真实 ChromiumOneTalk PWA)中,_imBaaSSDK 版本为 5.0.110,其 MessageService 暴露 sendUIMessages。读取已加载 SDK 源码确认:顶层 referMessage 被转换为发送请求的 extParam.quoteMessage,接收侧再还原为 extInfo.referMessage
  • 已用该 SDK 在当前真实会话中发送一条文本引用回复;SDK 返回本地受理对象,随后页面渲染出 quote-wrapper 引用块。该实测证明引用序列化和 UI 渲染,不证明跨会话路由。
  • 扩展当前稳定 SDK 入口是 window.IcbuIM.IMBaaSSDK.default.getMessageService().sendUIMessagessend.tssend-sop.md 都规定 cid 是权威路由字段,conversationCode 只能与它同值作为兼容字段。
  • 当前 SendObservationCorrelator.execute 只将 cidconversationCodecontent 和可选 ext 传给 SDK。SDK 读取的是顶层 referMessage,所以把它放在 ext.referMessage 不会启用引用回复。
  • 扩展的业务 ID 会把数字 .PNM 别名规范化为无后缀形式;引用 SDK 参数需要恢复原始消息 ID 的 .PNM 传输形式。
  • 当前 send.request / send.command 的 payload 严格只允许 conversationIdcontent,且所有 WebSocket 帧的 protocolVersion 都须精确等于共享 ONETALK_PROTOCOL_VERSION。引用 ID 因而需要同时改 shared contract、Mind frame builder/parser、Center 转发和插件 page command;任一方仍使用旧帧都会被拒绝。
  • Mind 的 OneTalk HTTP/实时投影将 Center messageId 原样赋给时间线消息的 idexternalMessageId。它只持有规范化的 contentsenderId、参与者和时间,不持有 OneTalk SDK 生成引用所需的 subTypeoriginalData、展示名等原始字段。
  • 插件当前在历史和实时解析时读取 raw OneTalk message,但只跨 MAIN 边界发布白名单后的 OneTalkMessage,不保留可按 [conversationId, messageId] 查找的引用源。引用功能需要新增仅存在于 MAIN world 的短生命周期索引;raw 数据不得经 WebSocket 发给 Mind 或持久化为第二份业务内容。
  • Mind 现有 WhatsApp 引用使用 replyToMessageId,但其 HTTP schema 要求 UUID。OneTalk 的 messageId 可能是数字字符串,不能直接复用该 UUID 校验或将它误认为 Mind 数据库 ID。

Requirements

  1. Mind 发送请求、Center send.request / send.command 及插件 page command 增加可选顶层 replyToMessageId,值为当前 OneTalk 时间线行的原始 messageId;省略时普通发送的 wire shape 与行为保持不变。
  2. replyToMessageId 只能是非空 OneTalk 业务 ID,必须在 plugin MAIN world 归一化后与同一 conversationId 和当前 channelAccountId 下的原始来源精确匹配。它不接受 Mind 数据库 UUID、.PNM 拼接后的别名、正文、时间或合成 ID。
  3. 插件必须从仅 MAIN-world 的原始引用索引解析 SDK referMessagemsgIdsubTypesenderAliIdsenderNamecontentAbstractoriginalDatasendTime。解析不完整、ID 不存在、会话/账号不匹配或类型未验证时 fail closed 为 rejected_before_send/invalid_request,且不调用 SDK。
  4. 解析成功时唯一的发送入口为 messageService.sendUIMessages({ cid, conversationCode: cid, content, referMessage });不使用 quoteReply、DOM 点击、sendMessagesendTextMessage
  5. cidconversationCode 的路由语义必须保持现有契约:前者为权威目标,二者必须同值,selected 会话和 URL 均不能替代 cid
  6. 协议升级必须协调 Mind 与插件版本;共享帧 decoder 对版本和 payload keys 均 fail closed,不能采用“未知字段忽略”的兼容降级。
  7. SDK 返回值仍只是本地受理;必须沿用 WebSocket 旁路观察完整 direction: sent 消息来确认真实投递。
  8. 第一版 Mind UI 在每条已加载的 OneTalk 普通文本消息上提供回复操作;选中后 composer 显示发送者与文本摘要,并允许取消。发送成功、发送前拒绝或当前会话切换时清除引用目标;历史消息本身不新增引用卡展示。
  9. 第一版仅允许 subType: 1 文本引用(已在真实 Chromium 以 SDK 成功验证)。图片、文件、富卡、系统消息、无正文消息和未解析类型没有回复操作,也不能通过手工帧绕过这一限制。

Out of scope

  • 为非 OneTalk 渠道改变既有 WhatsApp、邮件或通用 TradeBridge 引用协议。
  • 向 Mind、Center WebSocket、数据库或日志传递 SDK raw message / originalData,或建立第二份 raw 消息存储。
  • 用页面私有 EventBusquoteReply 作为扩展发送实现。
  • 为未知消息类型虚构 ID、发送者、原始数据或引用摘要。

Acceptance criteria

  • Mind 选择的时间线消息以其 OneTalk messageId 作为可选 replyToMessageId 发送,且不将 Mind 数据库 ID 或完整 raw 载荷带入 WS 帧。
  • send.requestsend.command 和 page command 都以相同可选字段透传,普通发送保持原 payload shape;版本和 exact-key decoder 的联动升级有测试。
  • MAIN-world 索引只保存构建 SDK referMessage 所需的短生命周期原始引用数据,按 channel account、conversation 和 canonical message ID 隔离,且不跨桥发布。
  • 有效引用调用包含同值的非空 cidconversationCode、正文和顶层 referMessagemsgId.PNM 规则、subTypesenderAliIdsenderNamecontentAbstractoriginalDatasendTime 均有测试。
  • 不存在/不完整/跨会话/跨账号/不受支持的 replyToMessageId 得到 canonical pre-send rejection,绝不发送普通文本作为降级。
  • 端到端测试覆盖 Mind frame、Center 转发、插件 SDK 输入和已确认发送;opId 或页面渲染不能单独作为投递成功证据。