8.0 KiB
OneTalk 结构化消息信息采集分类:技术设计
1. 范围与不变量
本任务扩展 OneTalkMessage.content 的封闭联合类型,而不新增平行消息表、平行投递帧或第二套消息身份。既有消息事实的幂等键始终是 channelAccountId + conversationId + messageId,并继续保留 messageId、conversationId、senderId、direction、sentAtMs、participantIds、readStatus、messageStatus 和 unreadCount。
变更只适用于 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.ts 在 sdk_flat_history 分支集中拥有业务卡识别与投影;不在 history.ts、bridge、Service Worker 或 server 重复判断。
| kind | 必须同时成立 | MAIN-world 投影 |
|---|---|---|
business_card |
messageType="rec"、type=1、viewType=0、msgType=10010、subType=57、originalData.cardType=1 |
只输出 { version: 1, kind: "business_card" } marker;不读取 item.contact |
inquiry |
相同前三项和 msgType=10010,并且 subType=50、cardType=6 |
仅 { version: 1, kind: "inquiry" } |
order |
相同前三项和 msgType=10010,并且 subType=59、cardType=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;新三类仅在完整历史联合条件下改变为可上传消息。 - 订单不能因解码失败降级为
inquiry、text、空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. 验证矩阵
- contract:新 kind exact-shape、额外键拒绝、null/URL/ID/金额/action 边界及原有 kind 回归。
- MAIN decoder:每个完整联合条件成功;名片只生成 marker 且不读取
item.contact;相邻10010的附件与未知卡不误判;订单各解码失败分别可观测;业务卡原始content不进入结果。 - bridge + durable sync:新 kind 可以通过既有 decoder、写入 candidate、创建 canonical frame,敏感原始键不存在。
- server:wire guard、repository copy、marker exact CHECK、
0011旧行归一化、按账号/会话 profile 的读投影(无 profile/部分字段)和 publish 均接受新 kind;复合幂等键、ACK 和旧 content 回归。 - quality:定向测试、typecheck、build、全量测试、format check 与 migration check;真实 Chromium 只作为未来历史样本 smoke,WebSocket raw 不在本期验收。