22 KiB
OneTalk 图片与附件消息格式调查报告
调查日期:2026-09-01
调查对象:https://onetalk.alibaba.com/message/weblitePWA.htm以及当前trade-message-centerOneTalk 扩展链路
调查方式:Chromium DevTools Protocol(CDP,127.0.0.1:9222)只读运行时探查、历史 WebSocket 帧捕获、已加载 SDK bundle 静态检索、仓库代码追踪
安全边界:本报告不保存或展示 Cookie、sid、chatToken、加密账号、签名 URL、消息正文和二进制内容;示例只保留字段名、类型和脱敏结构。v6 同步说明(2026-09-11):raw WebSocket/SDK 样本仍是历史调查证据;当前规范性图片合同只包含
fileId、extension、sizeBytes、isOriginal、md5、previewUrl、urlScope(另有version、kind)。MAIN decoder 忽略 rawwidth/height,它们不跨 normalized boundary;本报告不是 v6 live runtime 验收。
1. 摘要
OneTalk 的图片和附件并不是另一条独立的同步通道。它们和文本一样出现在历史消息 WebSocket 帧的 body.userMessageModels[].message 中,但内容形态不同:
- 文本:
contentType = 1,正文在content.text.content。 - 图片:
contentType = 101,content.custom.type = 7,content.custom.data是 Base64 编码的 JSON。 - 附件:
contentType = 101,content.custom.type = 10010,content.custom.data是 Base64 编码的 JSON。
以下是 2026-09-01 的历史调查结论:当时扩展的传输和持久化边界会保留这些原始内容;非文本消息只会令便利字段 text 为 null,不会令 content 消失。因此“现在只实现 text”的准确含义是:当时没有完成图片/附件的语义投影、Mind 端展示和完整发送适配,而不是 WebSocket 接收层完全收不到媒体。它不描述当前 v6 流程。
本次 CDP 实测在当前登录页面的两个已加载会话中调用了只读历史接口,捕获到一页 20 条消息的历史 WebSocket 帧;现有 parseOneTalkMessages 返回了全部 20 条,其中包含一条图片和一条附件。没有点击上传、发送或下载,发送侧结论只来自 SDK 和 bundle 的方法/调用形态分析。
2. 调查范围与证据等级
2.1 已直接验证
- Chromium CDP 调试端口
127.0.0.1:9222可用,目标页面是 OneTalk PWA。 window.IcbuIM.IMBaaSSDK.default.getMessageService()存在,且暴露fetchMessagesWithoutUpdateToRead、sendImageMessage、sendTextMessage、sendUIMessages等方法。- 历史读取产生
code = 200的 JSON WebSocket 帧,消息列表位于body.userMessageModels。 - 当前页面真实历史数据中出现:
- 图片:
type = 1、subType = 60、msgType = 102。 - 附件:
type = 1、subType = 61、msgType = 10010。
- 图片:
- 对同一帧运行仓库现有
parseOneTalkMessages后,图片和附件仍保留在message.content,但message.text为null。 - 图片和附件的
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 外壳。不能通过外层 type 或 messageType 判断是否为图片;媒体识别必须进入 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 = 60、msgType = 102 是页面 SDK/渲染层的归类结果,不应替换原始 contentType 和 custom.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 可以为空,而 url 和 thumbnailUrl 仍存在。因此附件展示逻辑不能简单地写成“只有 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 = 61、msgType = 10010 是页面显示/消息模型的归类,不是可以脱离 custom.type = 10010 单独使用的稳定事实键。
7. 历史数据流与当前 v6 边界
7.1 页面观察器
2026-09-01 的历史帧由 历史解析器 交给 observedMessage()。在当时的 消息模型 中:
- 只要
message.content是对象,就把它作为完整content保留。 - 从
content.contentType提取contentType。 - 只有
content.text.content是字符串时,才填充text。 - 图片/附件没有
content.text.content,所以text为null。
因此,历史代码并没有把图片/附件转换成错误的文本,也没有在这一层删除原始媒体对象。
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 decoder(raw 只停留在此处)
→ 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、sizeBytes、md5、可用 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 图片合同只保留 fileId、extension、sizeBytes、isOriginal、md5、previewUrl 与 urlScope,不保留 raw width / height。
尚未完成或未验证的缺口集中在三处:
- 对附件的当前运行态与发布范围重新验证。
- 对 URL 生命周期做真实环境验证。
- 在维护窗口执行 v6 Chromium 图片发送与 live-confirmation 验收。
因此不需要重写 OneTalk WebSocket、MessagePack 解码器、会话锚点或消息幂等机制。后续运行时工作应验证真实上传/发送和确认链路,而不能以这份历史调查替代 v6 现场验收。