Files

8.0 KiB
Raw Blame History

OneTalk 结构化消息信息采集分类:技术设计

1. 范围与不变量

本任务扩展 OneTalkMessage.content 的封闭联合类型,而不新增平行消息表、平行投递帧或第二套消息身份。既有消息事实的幂等键始终是 channelAccountId + conversationId + messageId,并继续保留 messageIdconversationIdsenderIddirectionsentAtMsparticipantIdsreadStatusmessageStatusunreadCount

变更只适用于 SDK 历史扁平条目:history.ts 已把它们转换为 sdk_flat_history 内容来源,并将 originalData 仅作为 MAIN-world decoder 的短暂输入。WebSocket raw 分支不扩展业务卡识别;现有 text/image/file decoder、批量诊断与 unsupported_skipped 语义保持不变。

2. 规范化内容合同

apps/onetalk-contract/src/content.ts 是唯一内容类型、exact-shape guard 和 Mind 读取模型的 owner。保留 content.version = 1,在同一版本加入三个 kind

type OneTalkBusinessCardContent = {
    version: 1;
    kind: "business_card";
};

type OneTalkBusinessCardViewContent = OneTalkBusinessCardContent & {
    contactName: string | null;
    companyName: string | null;
    countryCode: string | null;
    avatarUrl: string | null;
};

type OneTalkInquiryContent = {
    version: 1;
    kind: "inquiry";
};

type OneTalkOrderAction = {
    name: string;
    messageKey: string;
    payStep: string | null;
};

type OneTalkOrderContent = {
    version: 1;
    kind: "order";
    orderId: string | number | null;
    bizCode: string | number | null;
    contractId: string | number | null;
    id: string | number | null;
    tenant: string | number | null;
    orderAmount: number;
    orderAmountCurrency: string;
    paymentAmount: number;
    paymentAmountCurrency: string;
    statusMessageKey: string;
    actions: OneTalkOrderAction[];
};

所有内容 kind 继续 exact-shape 验证。消息事实中的名片严格只允许 marker;四字段只存在于读取 view,非空字符串必须符合共享文本/头像 URL 限制。view 由服务端读取同一账号同一会话的当前 profile 以内存组合,profile 缺失时返回 marker,单字段缺失时返回 null,不使用历史条目的 contact。询盘只有类别,不含伪造的空业务字段。订单金额必须是有限非负数,币种、状态键和 action 键必须是非空受限字符串,payStep 可以为 null;五个关联字段只接受原样字符串、safe integer 或 null,不把值 stringify、拼接或从其它字段补偿。动作数和 Base64 输入大小都有固定上限;超限或任一 schema 失败均进入显式 anomaly。

sign、原始 content、完整 contact、完整 params、询盘加密标识、令牌和原始 URL 不出 MAIN world。头像 URL 只沿独立 profile 观察链路以 avatarUrl 白名单字段跨层,并沿用现有绝对 HTTP(S) URL 校验;它不是名片消息事实字段。

3. MAIN-world 分类与解码

content-decoder.tssdk_flat_history 分支集中拥有业务卡识别与投影;不在 history.ts、bridge、Service Worker 或 server 重复判断。

kind 必须同时成立 MAIN-world 投影
business_card messageType="rec"type=1viewType=0msgType=10010subType=57originalData.cardType=1 只输出 { version: 1, kind: "business_card" } marker;不读取 item.contact
inquiry 相同前三项和 msgType=10010,并且 subType=50cardType=6 { version: 1, kind: "inquiry" }
order 相同前三项和 msgType=10010,并且 subType=59cardType=9 五个关联字段和受控 Base64-UTF-8-JSON 订单摘要

为将 messageType/type/viewType 与 decoder 统一,OneTalkMessageContentSource 必须仅为历史 SDK 分支携带所需判别元数据;它仍是 MAIN 内部类型,不能进入结果。历史适配器必须丢弃顶层 content 和完整 contact,名片 decoder 不接收客户资料输入。__conversationListData__ / profile observer 与消息采集异步时,消息流程不得等待或合并 profile。任何联合条件不完整的 10010 卡继续遵循既有 ignore/unsupported 路径,不能被分类为文件、订单或询盘。

订单嵌套 params.params 解码复用当前严格 Base64 → UTF-8 fatal → JSON 流程,但与媒体 anomaly 分开命名为 card anomaly。Base64、UTF-8、JSON、对象/字段类型、金额、动作数组或上限不合规均保留可计数的异常;合法非纳入类别仍为 unsupported_skipped,不是异常。

4. 跨层与持久化

消息事实数据流保持不变,名片资料在读取边界独立组合:

SDK flat history (MAIN only)
  -> exact card classifier + whitelist decoder
  -> shared OneTalkMessage.content (business_card marker only)
  -> page bridge / runtime.Port
  -> IndexedDB candidate (durable first)
  -> messages.observed Bright frame
  -> onetalk_message.content JSONB
  -> existing Mind history / message.created projection
Bright onetalk_message marker
  + scoped onetalk_contact_profile(channelAccountId, conversationId)
  -> server in-memory read projection
  -> optional OneTalkBusinessCardViewContent

page-bridge、Service Worker、IndexedDB candidate、Bright creator、server service/repository 和 Mind read projection 只复用扩展后的 shared guard 与 content JSON;它们不得读取 SDK 字段或重算分类。消息先 durable write,再上传、服务端提交、ACK 与 publish 的顺序不变。

服务端 onetalk_message_content_v1_chk 必须在 schema 与 migration 中要求 kind=business_card 的 JSONB 精确等于 marker。0011 先把已有带资料字段的名片行归一化为 marker,再建立 exact CHECK;新 JSONB 仍受 contract guard 约束。读取层从同一 content JSONB 读取 marker,再按 [channelAccountId, conversationId] 受限读取当前 profile 并以内存投影,Mind 不新增写入端点,也不把 view 回写数据库。

5. 兼容性、失败与回滚

  • 已有 text/image/file exact-shape、media URL 和 raw WebSocket 路径不变。
  • 既有未知业务卡仍产生 unsupported_skipped;新三类仅在完整历史联合条件下改变为可上传消息。
  • 订单不能因解码失败降级为 inquirytext、空 order 或正常 unsupported;必须生成安全、可观测 card anomaly,且不携带 raw payload。
  • 名片观察不读取或写入 contact.profile.observed;消息事实 marker 与当前 profile 是两条独立事实。读取 view 缺 profile 时必须保留 marker,不能用登录人 contact 或其他会话资料补齐。
  • 旧数据库行若曾保存名片资料,迁移只清理名片 content 的额外键,不修改 profile 当前事实;回滚前必须评估已经写入的新 marker/view 读语义。
  • 回滚顺序是先停止产生新 kind,再在部署允许时执行反向 DB check 迁移;现有 JSONB 中的新 kind 会使旧 server 约束/guard 不兼容,因此必须按“contract → server migration → extension”正向顺序发布,回滚前评估已写入的新行。

6. 验证矩阵

  1. contract:新 kind exact-shape、额外键拒绝、null/URL/ID/金额/action 边界及原有 kind 回归。
  2. MAIN decoder:每个完整联合条件成功;名片只生成 marker 且不读取 item.contact;相邻 10010 的附件与未知卡不误判;订单各解码失败分别可观测;业务卡原始 content 不进入结果。
  3. bridge + durable sync:新 kind 可以通过既有 decoder、写入 candidate、创建 canonical frame,敏感原始键不存在。
  4. serverwire guard、repository copy、marker exact CHECK、0011 旧行归一化、按账号/会话 profile 的读投影(无 profile/部分字段)和 publish 均接受新 kind;复合幂等键、ACK 和旧 content 回归。
  5. quality:定向测试、typecheck、build、全量测试、format check 与 migration check;真实 Chromium 只作为未来历史样本 smokeWebSocket raw 不在本期验收。