Files
trade-message-center/docs/onetalk-media-message-format-investigation.md

22 KiB
Raw Permalink Blame History

OneTalk 图片与附件消息格式调查报告

调查日期:2026-09-01
调查对象:https://onetalk.alibaba.com/message/weblitePWA.htm 以及当前 trade-message-center OneTalk 扩展链路
调查方式:Chromium DevTools ProtocolCDP127.0.0.1:9222)只读运行时探查、历史 WebSocket 帧捕获、已加载 SDK bundle 静态检索、仓库代码追踪
安全边界:本报告不保存或展示 Cookie、sidchatToken、加密账号、签名 URL、消息正文和二进制内容;示例只保留字段名、类型和脱敏结构。

v6 同步说明(2026-09-11):raw WebSocket/SDK 样本仍是历史调查证据;当前规范性图片合同只包含 fileIdextensionsizeBytesisOriginalmd5previewUrlurlScope(另有 versionkind)。MAIN decoder 忽略 raw width / height,它们不跨 normalized boundary;本报告不是 v6 live runtime 验收。

1. 摘要

OneTalk 的图片和附件并不是另一条独立的同步通道。它们和文本一样出现在历史消息 WebSocket 帧的 body.userMessageModels[].message 中,但内容形态不同:

  • 文本:contentType = 1,正文在 content.text.content
  • 图片:contentType = 101content.custom.type = 7content.custom.data 是 Base64 编码的 JSON。
  • 附件:contentType = 101content.custom.type = 10010content.custom.data 是 Base64 编码的 JSON。

以下是 2026-09-01 的历史调查结论:当时扩展的传输和持久化边界会保留这些原始内容;非文本消息只会令便利字段 textnull,不会令 content 消失。因此“现在只实现 text”的准确含义是:当时没有完成图片/附件的语义投影、Mind 端展示和完整发送适配,而不是 WebSocket 接收层完全收不到媒体。它不描述当前 v6 流程。

本次 CDP 实测在当前登录页面的两个已加载会话中调用了只读历史接口,捕获到一页 20 条消息的历史 WebSocket 帧;现有 parseOneTalkMessages 返回了全部 20 条,其中包含一条图片和一条附件。没有点击上传、发送或下载,发送侧结论只来自 SDK 和 bundle 的方法/调用形态分析。

2. 调查范围与证据等级

2.1 已直接验证

  1. Chromium CDP 调试端口 127.0.0.1:9222 可用,目标页面是 OneTalk PWA。
  2. window.IcbuIM.IMBaaSSDK.default.getMessageService() 存在,且暴露 fetchMessagesWithoutUpdateToReadsendImageMessagesendTextMessagesendUIMessages 等方法。
  3. 历史读取产生 code = 200 的 JSON WebSocket 帧,消息列表位于 body.userMessageModels
  4. 当前页面真实历史数据中出现:
    • 图片:type = 1subType = 60msgType = 102
    • 附件:type = 1subType = 61msgType = 10010
  5. 对同一帧运行仓库现有 parseOneTalkMessages 后,图片和附件仍保留在 message.content,但 message.textnull
  6. 图片和附件的 custom.data 经 Base64 解码后是 JSON,而不是二进制图片或文件本体。

2.2 历史代码级确认、尚未做完整端到端实测

  • 当时 Service Worker 的观察、IndexedDB 写入、Bright 上传和 Bright HTTP 返回均使用通用 JSON content,类型上没有把内容限制为文本。
  • 当前没有真实数据库写入后的 Mind 页面媒体渲染回归测试。
  • 没有执行真实图片上传、附件上传、发送确认和下载操作,因此不能把 SDK bundle 中的发送能力称为扩展已经支持的能力。

2.3 当前未确认

  • 所有历史 content.custom.type 的完整枚举。
  • 图片/附件 URL 的长期有效期、签名参数和权限续期规则。
  • 群聊媒体消息是否使用完全相同的 custom 结构。
  • 视频、音频、表情、富文本卡片、商品卡片等其他非文本类型的完整字段契约。

3. OneTalk 消息的三层格式

3.1 外层 WebSocket JSON 帧

OneTalk WebSocket 返回的历史消息帧形态如下。字段值已抽象为类型,敏感值不展示:

{
    "code": 200,
    "headers": {
        "mid": "<message-frame-id>",
        "sid": "<session-id>",
        "dt": "<frame-type>"
    },
    "body": {
        "degradeFailover": 0,
        "hasMore": 1,
        "nextCursor": 0,
        "userMessageModels": [
            {
                "message": {
                    "cid": "<conversation-id>",
                    "messageId": "<message-id>",
                    "createAt": 0,
                    "sender": {
                        "uid": "<sender-id>"
                    },
                    "content": {
                        "contentType": 101,
                        "custom": {
                            "type": 7,
                            "data": "<base64-json>"
                        }
                    },
                    "extension": {
                        "basicMessageInfo": "<json-string>",
                        "messageDisplayInfo": "<json-string>",
                        "messageEventInfo": "<json-string>"
                    },
                    "searchableContent": {
                        "summary": "<summary>"
                    }
                },
                "readStatus": 0,
                "msgStatus": 1,
                "recallFeature": {}
            }
        ]
    }
}

文本和媒体共享 message 外壳。不能通过外层 typemessageType 判断是否为图片;媒体识别必须进入 message.content

3.2 历史同步的 MessagePack 推送

旧的同步推送帧还会把 syncPushPackage.data[].data 作为 Base64 字符串传输:

外层 JSON
  → body.syncPushPackage.data[].data
  → Base64 解码
  → MessagePack 解码
  → 数字键对象

已验证的 objectType = 40000 消息结构包含以下数字路径(仅列出已经和明文 JSON 交叉确认的路径):

MessagePack 路径 含义
1.2 会话 ID cid
1.3 消息 ID
1.5 创建时间,Unix 毫秒
1.6.1 contentType
1.6.2.1 文本内容类型名,例如 text
1.6.2.2 内容类型对应的扩展对象
1.7 读取状态
1.8 消息读取设置
1.9 展示样式
1.10.basicMessageInfo JSON 字符串形式的基础消息扩展
1.12 记录状态

MessagePack 的数字键本身不携带业务字段名,不能仅凭数组位置推断图片、附件或其他媒体含义。媒体的业务字段名必须由明文 userMessageModels、SDK 归一化对象或协议定义交叉确认。

仓库已有的 MessagePack 解码器 能解码数字、字符串、数组、对象、二进制、浮点数和 64 位整数;它目前是通用解码器,不负责把 content.custom 转成图片或附件领域对象。

4. 文本消息格式

文本样本的结构为:

{
    "contentType": 1,
    "text": {
        "content": "<text>",
        "extension": {
            "basicMessageInfo": "<json-string>",
            "messageDisplayInfo": "<json-string>",
            "messageEventInfo": "<json-string>"
        }
    }
}

当前仓库将 content 整体保留,同时额外提取 content.text.content 到便利字段 text。这个便利字段只适用于真正带 content.text.content 的消息。

5. 图片消息格式

5.1 原始 WebSocket 内容

本次真实历史帧确认图片消息的原始内容形态为:

{
    "contentType": 101,
    "custom": {
        "type": 7,
        "data": "<base64-json>"
    }
}

custom.data Base64 解码后是 JSON 对象,已确认字段形态如下:

{
    "fileId": "<file-id>",
    "height": 0,
    "isOriginal": 0,
    "md5": "<md5>",
    "size": 0,
    "suffix": "<suffix>",
    "url": "<image-url>",
    "width": 0
}

这里的 url 是图片资源地址;报告不记录实际 URL,因为它可能包含访问签名或其他会话相关信息。size、尺寸、后缀和 MD5 是 raw 上游元数据,不是图片二进制本体。此处的 width / height 仅保留为调查证据;当前 v6 MAIN decoder 忽略它们,且它们不会跨出 normalized boundary。

5.2 页面 SDK 归一化形态

同一类消息经过 OneTalk 页面 SDK 归一化后,观察到:

{
    "type": 1,
    "subType": 60,
    "msgType": 102,
    "viewType": 0,
    "messageType": "send",
    "content": "<serialized-content>",
    "originalData": {
        "fileId": "<file-id>",
        "height": 0,
        "isOriginal": 0,
        "md5": "<md5>",
        "size": 0,
        "suffix": "<suffix>",
        "url": "<image-url>",
        "width": 0
    }
}

subType = 60msgType = 102 是页面 SDK/渲染层的归类结果,不应替换原始 contentTypecustom.type。上游 raw 内容只在 MAIN 边界短暂存在;同步事实使用其规范化投影,归类字段只作为投影依据。

SDK originalData 中的 width / height 同样只是 raw SDK 证据,不是 current v6 normalized image contract 的字段;MAIN decoder 不读取或传递它们。

6. 附件消息格式

6.1 原始 WebSocket 内容

本次真实历史帧确认附件消息的原始内容形态为:

{
    "contentType": 101,
    "custom": {
        "type": 10010,
        "data": "<base64-json>"
    }
}

custom.data Base64 解码后是一个卡片/文件描述对象:

{
    "cardType": 0,
    "params": {
        "ctime": "<timestamp-string>",
        "downloadUrl": "<download-url-or-empty>",
        "extensionType": "<extension>",
        "id": "<file-id>",
        "md5": "<md5>",
        "name": "<file-name>",
        "parentId": "<parent-id>",
        "size": "<size-string>",
        "thumbnailUrl": "<thumbnail-url>",
        "type": "<file-type>",
        "url": "<file-url>",
        "version": "<version>"
    }
}

本次样本中 downloadUrl 可以为空,而 urlthumbnailUrl 仍存在。因此附件展示逻辑不能简单地写成“只有 downloadUrl 非空才是有效附件”。应根据 type/extensionType 选择预览方式,并把下载地址缺失视为一个需要按页面 SDK 规则处理的状态。

6.2 页面 SDK 归一化形态

页面 SDK 对同一类消息的归类为:

{
    "type": 1,
    "subType": 61,
    "msgType": 10010,
    "viewType": 0,
    "messageType": "send",
    "content": "<serialized-content>",
    "originalData": {
        "cardType": 0,
        "params": {
            "id": "<file-id>",
            "name": "<file-name>",
            "size": "<size-string>",
            "type": "<file-type>",
            "extensionType": "<extension>",
            "url": "<file-url>",
            "thumbnailUrl": "<thumbnail-url>",
            "downloadUrl": "<download-url-or-empty>"
        }
    }
}

subType = 61msgType = 10010 是页面显示/消息模型的归类,不是可以脱离 custom.type = 10010 单独使用的稳定事实键。

7. 历史数据流与当前 v6 边界

7.1 页面观察器

2026-09-01 的历史帧由 历史解析器 交给 observedMessage()。在当时的 消息模型 中:

  1. 只要 message.content 是对象,就把它作为完整 content 保留。
  2. content.contentType 提取 contentType
  3. 只有 content.text.content 是字符串时,才填充 text
  4. 图片/附件没有 content.text.content,所以 textnull

因此,历史代码并没有把图片/附件转换成错误的文本,也没有在这一层删除原始媒体对象。

7.2 页面桥与 Service Worker

这是历史实现:页面桥会复制观察消息的 JSON 字段,content 进入 Service Worker 后仍是通用 JSON 值。它已被 v6 normalized boundary 取代。

7.3 Bright 服务与数据库

历史 Bright 归一化对 content 执行通用 JSON 校验和敏感键过滤,然后持久化。历史事实链为:

OneTalk raw content
  → page observer content
  → page bridge content
  → Service Worker durable observation
  → Bright content JSON
  → Mind history response

这条 raw 事实链仅为历史调查证据,当前不得使用。v6 的规范路径为:

OneTalk raw content
  → MAIN decoderraw 只停留在此处)
  → normalized content
  → page bridge / Service Worker / IndexedDB
  → Bright canonical JSONB / HTTP history
  → Mind read model

Bright 不保存 raw JSON、custom.data 或 SDK row。

8. 2026-09-01 时已经可以做到什么

下表除明确标为 v6 的行外,均是历史能力快照;其中 raw content 贯穿页面桥、IndexedDB、Bright 和 Mind 的行不得作为当前实现或发布依据。

能力 历史状态 证据/限制
接收文本历史消息 已验证 现有观察器提取 text
接收图片历史消息 已验证 CDP 实测 custom.type=7,现有 parser 保留 content
接收附件历史消息 已验证 CDP 实测 custom.type=10010,现有 parser 保留 content
保留原始媒体元数据 历史实现 通用 JSON content 曾贯穿页面桥、Service Worker、Bright
channelAccountId + conversationId + messageId 幂等保存 代码已支持 媒体不改变消息业务键
在 Mind 历史接口返回原始媒体 JSON 历史路径 已由 v6 normalized-only boundary 取代
将图片字段投影为 v6 canonical image metadata 当前已实现 只保留 fileId、extension、sizeBytes、isOriginal、md5、previewUrl、urlScope
将附件字段投影为文件名、大小、预览和下载动作 当前未实现 需要处理 downloadUrl 为空的情况
在 Mind 页面显示图片 当前未实现 当前仓库没有对应媒体渲染契约/组件
在 Mind 页面显示附件卡片 当前未实现 当前仓库没有对应媒体渲染契约/组件
OneTalk 文本发送 已有路径 sendUIMessages 与文本确认逻辑以字符串正文为中心
OneTalk 图片发送 页面 SDK 有方法 bundle 观察到 sendImageMessage({ cid, picUrl });扩展未接入完整发送契约
OneTalk 本地文件/附件发送 页面 bundle 有上传流程 涉及 prepareSendFileWithGroup、OSS 上传和文件卡片;未做真实上传验证
图片/附件发送确认 当前未实现 出站确认关联器只按文本内容匹配
下载二进制到 Bright 当前未实现 当前只保存消息 JSON,不保存媒体本体
群聊图片/附件全量同步 当前未确认 既有历史同步对群聊会话有跳过/不支持边界

9. 发送侧调查结果

9.1 图片发送

当前页面 SDK 原型暴露:

sendImageMessage(input)

已加载的 im-weblite-chat bundle 中观察到 Native PC 分支调用形态:

messageService.sendImageMessage({
    cid: conversationId,
    picUrl: selectedPicture,
});

这说明 OneTalk 页面具有图片发送入口,但这不等于调查时的扩展已经具备图片发送能力。当时页面命令要求 command.content 是字符串,并交给文本型 sendUIMessages;这个历史限制已被当前 v6 image outbound contract 与确认路径取代。

9.2 文件/附件发送

bundle 中观察到的文件流程不是“直接把本地文件放进消息 JSON”,而是:

File
  → 文件类型/大小判断
  → 图片或视频压缩(适用时)
  → OSS/云盘上传
  → prepareSendFileWithGroup 建立文件关系
  → 构造文件卡片消息
  → sendMessage / sendUIMessages

文件卡片消息中出现过以下业务字段:

fileId
fileCardUrl
downloadUrl
previewUrl
fileName / name
fileSize / size
fileType / materialType
md5
nodeName

这些字段来自页面上传流程的内部模型,不能直接假设它们和历史 custom.data.params 的字段名一一相同。发送侧必须先抓取一次真实、用户明确触发的上传网络流程,再固定契约。

9.3 发送确认限制

调查时 send-observation.ts 的待确认状态以字符串 content 和消息时间窗口进行关联。对于图片/附件:

  • 图片通常没有可比较的文本正文。
  • 附件卡片的 HTML/URL 可能在发送前后发生变化。
  • sendImageMessage 或文件上传回调返回的操作 ID不能直接当作最终消息 ID,必须等待完整的 sent-direction OneTalk 观察消息。

因此图片/附件发送确认不能简单复用“比较 text”的逻辑,也不能仅凭 SDK Promise resolve 就写入 Bright。当前 v6 image 实现先按 candidate message ID 匹配;没有候选 ID 时,才以同会话、sent 方向、image kind、sizeBytesmd5、可用 fileId、时间窗和唯一性回退,歧义为 send_ambiguous。这项代码与自动化测试证据不替代尚未执行的 v6 Chromium 真实发送验收。

10. 历史建议与当前边界

以下建议保留其调查背景;当前 v6 已将 raw 停在 MAIN decoder,并只让 normalized content 下游流转。

10.1 共享内容解码器

packages/onetalk-contract 或其最近共同父目录增加一个唯一的内容投影器,输入原始 content,输出判别联合:

text
image
attachment
unknown

最小识别规则:

content.contentType === 1 && content.text.content 是字符串 → text
content.contentType === 101 && content.custom.type === 7 → image
content.contentType === 101 && content.custom.type === 10010 → attachment
其他 → unknown

custom.data 必须经过:

Base64 解码 → UTF-8 → JSON.parse → 字段校验

不能只看 contentType=101,因为它至少同时承载图片和附件。

10.2 原始内容不得越过 MAIN 边界

历史提案曾建议同时保留原始 content;当前 v6 明确禁止这样做。页面桥以后的消息只保留:

content       经过严格校验的 normalized content

未知类型不能为了让 UI 正常而伪装成文本,也不得携带原始 JSON 离开 MAIN decoder。

10.3 URL 与二进制边界

  • 首发只保存消息元数据和页面提供的 URL,不把图片/文件二进制下载到 Bright。
  • 向 Mind 转发时使用字段白名单,不直接序列化整段 OneTalk 原始对象。
  • 需要确认 URL 是否带签名、有效期多久、是否需要 Cookie;这些信息本次没有读取,不能预设“永久公开 URL”。
  • downloadUrl 为空时,应明确返回“可预览但不可直接下载”或由页面 SDK 提供刷新动作,不能静默拼接 URL。

10.4 发送侧单独建模

图片发送和附件发送应分别建模,不把文件上传过程塞进文本 content 字段:

send.text
send.image
send.file

每一种发送都必须继续遵循现有三态结果:

confirmed_sent
rejected_before_send
delivery_unknown

只有观察到完整的 sent-direction 消息事实后,才能产生 confirmed_sent

11. 调查中没有做的事情

为避免对真实账号产生副作用,本次没有:

  • 点击图片发送、文件选择器、上传或下载按钮。
  • 修改 OneTalk 页面状态、IndexedDB、Cookie、Local Storage 或 Service Worker 数据。
  • 把任何真实 token、账号加密值、会话 SID、消息正文或完整 URL 写入报告。
  • 把 SDK bundle 中的内部函数名直接当成稳定公共 API。
  • 声称图片/附件发送已经端到端成功。
  • 创建 Trellis task 或修改业务代码。

12. 结论

当前同步系统使 raw 图片/附件止于 MAIN decoder,之后只保存 normalized content;原始样本仍仅用于调查证据。当前 v6 图片合同只保留 fileIdextensionsizeBytesisOriginalmd5previewUrlurlScope,不保留 raw width / height

尚未完成或未验证的缺口集中在三处:

  1. 对附件的当前运行态与发布范围重新验证。
  2. 对 URL 生命周期做真实环境验证。
  3. 在维护窗口执行 v6 Chromium 图片发送与 live-confirmation 验收。

因此不需要重写 OneTalk WebSocket、MessagePack 解码器、会话锚点或消息幂等机制。后续运行时工作应验证真实上传/发送和确认链路,而不能以这份历史调查替代 v6 现场验收。