Files
trade-message-center/docs/onetalk-message-content-formats.md

924 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OneTalk 消息内容原始格式与媒体同步能力调查报告
> 调查日期:2026-09-02
>
> 适用页面:`https://onetalk.alibaba.com/message/weblitePWA.htm`
>
> 调试环境:Chromium `154.0.8012.0`CDP `127.0.0.1:9222`Trade Message Center `0.8.6`
>
> 调查方式:通过 Chromium CDP 观察真实 OneTalk WebSocket 响应、调用页面只读历史 SDK,并对照扩展 IndexedDB、共享协议和 Bright 存储代码。调查过程中没有发送消息、没有调用会改变已读状态的 API,也没有记录正文、账号、token、完整 URL 或 URL 查询参数值。
>
> v6 同步说明(2026-09-11):下列 raw 样本和当时能力结论保留为调查证据;当前规范性图片合同为无尺寸的 `fileId`、`extension`、`sizeBytes`、`isOriginal`、`md5`、`previewUrl`、`urlScope`(另有 `version`、`kind`)。MAIN decoder 忽略 raw `width` / `height`,它们不跨 normalized boundary;本报告不是 v6 live runtime 验收。
## 1. 结论摘要
以下结论是本报告调查时(2026-09-02)的历史快照:当时系统并不是完全没有采集图片和附件,而是只实现了文本的**语义化解析**:
- 文本能够从 `content.text.content` 提取为 `message.text`
- 图片和附件能够作为原始 `contentType=101/custom` JSON 被观察、写入 IndexedDB,并通过 Bright ACK。
- 图片和附件当时没有规范化的 `kind`、文件名、扩展名、大小、缩略图或下载地址合同;当前 v6 image canonical contract 见第 6 节。
- 消费端如果只读取 `message.text`,就会表现为“文本存在,图片和附件不存在”。
这不是当前 v6 数据流。当前 raw OneTalk content 只在 MAIN decoder 内解码和收窄;页面桥、Service Worker、IndexedDB、Bright JSONB、HTTP read 和 Mind 下游只接收 normalized contentBright 不保存 raw JSON。
真实样本已确认以下映射:
| 业务类型 | WebSocket 原始类型 | Base64 解码后的判定 | OneTalk SDK 归一化类型 |
| ------------ | ------------------------------------ | -------------------- | --------------------------- |
| 文本 | `contentType=1` | `text.content` | `msgType=101, subType=1` |
| 图片 | `contentType=101, custom.type=7` | 图片元数据对象 | `msgType=102, subType=60` |
| 文件附件 | `contentType=101, custom.type=10010` | `cardType=12` | `msgType=10010, subType=61` |
| 其它业务卡片 | `contentType=101, custom.type=10010` | 例如 `cardType=2000` | 不是附件 |
本次真实附件样本为 PDF;没有抓到 TXT 附件、实时图片 push 或实时文件 push,因此这些场景不能标记为已验证。
调查时还发现一个必须优先修复的安全问题:文本内容的 `text.extension.basicMessageInfo` 是一段序列化 JSON,真实样本中包含 `chatToken` 键。页面观察器会复制整个原始 `content`,而 Bright 的清洗器对字符串直接原样放行,因此嵌套在字符串或 Base64 中的敏感字段可能进入持久化。
## 2. 调查范围与证据边界
### 2.1 使用的 Chromium/CDP 启动方式
```bash
/Applications/Chromium.app/Contents/MacOS/Chromium \
--remote-debugging-address=127.0.0.1 \
--remote-debugging-port=9222 \
--user-data-dir="/Users/ybf/work/demo-playwright/.chromium-profile"
```
CDP 确认:
```text
Browser: Chrome/154.0.8012.0
Protocol-Version: 1.3
```
### 2.2 只读历史方法
调查只调用了页面现有的只读方法:
```js
const sdk = window.IcbuIM.IMBaaSSDK.default;
const conversationService = sdk.getConversationServiceV2();
const messageService = sdk.getMessageService();
await conversationService.getConversationListByPagination({
cursor: 0,
count: 20,
});
await messageService.fetchMessagesWithoutUpdateToRead(options, conversation);
```
没有调用以下会改变已读状态或页面 store 的方法:
```text
fetchMessages
updateMessageToRead
```
### 2.3 本次运行态证据
本次调查完成时,扩展 IndexedDB 快照为:
```text
数据库:trade-message-center
版本:4
onetalk_messages135
contentType=1125
contentType=101/custom.type=71
contentType=101/custom.type=100109
onetalk_sync_candidates135
confirmed135
anomaly0
onetalk_sync_checkpoints2
phase=uploading2
syncResult=succeeded2
```
注意:
- `custom.type=10010` 同时包含文件和非文件业务卡片,不能把 9 条全部解释为附件。
- `confirmed` 表示插件已经收到 Bright 的 `accepted``duplicate` ACK,不表示 Mind UI 已经能渲染媒体。
- checkpoint 仍为 `uploading`,说明同步完成/锚点最终收敛是另一个独立问题;它不改变本报告对消息内容格式的判断。
## 3. OneTalk 历史 WebSocket 原始响应
### 3.1 顶层 envelope
真实 WebSocket 历史响应结构如下。所有业务值都已省略:
```json
{
"headers": {
"<protocol-header>": "<redacted>"
},
"code": 200,
"body": {
"degradeFailover": "<value>",
"hasMore": 1,
"nextCursor": "<cursor>",
"userMessageModels": []
}
}
```
本次网络原帧中的分页字段为:
```text
body.hasMore0 | 1
body.nextCursornumber | null
```
### 3.2 单条历史消息包装
```json
{
"message": {
"cid": "<raw-conversation-id>",
"messageId": "<raw-message-id>",
"createAt": 0,
"content": {},
"displayStyle": "<value>",
"extension": "<value>",
"msgReadStatusSetting": "<value>",
"receiverCount": 0,
"receivers": [],
"redPointPolicy": "<value>",
"searchableContent": "<value>",
"sender": {
"uid": "<participant-id>"
},
"unreadCount": 0
},
"msgStatus": 1,
"readStatus": 2,
"recallFeature": "<value>",
"userExtension": "<value>"
}
```
当前页面观察器正是从这一层读取:
```text
message.cid
message.messageId
message.createAt
message.content
message.sender.uid
message.unreadCount
item.msgStatus
item.readStatus
```
## 4. OneTalk SDK 返回结构
同一次请求在 SDK Promise 中返回的不是 WebSocket 原始 envelope,而是 SDK 归一化后的扁平结果:
```json
{
"hasMore": true,
"list": [],
"nextCursor": 0
}
```
这里的字段类型与网络原帧不同:
```text
SDK hasMoreboolean
WebSocket hasMore0 | 1
```
调查时 SDK `list[]` 条目确认包含:
```text
autoReply
contact
contactRead
content
conversationCode
extInfo
localExt
messageId
messageType
msgType
opId
originExt
originalData
owner
receiver
sendTime
sender
spamStatus
status
subType
type
unread
uuid
viewType
```
这份 SDK 字段清单是历史 raw/boundary-only 证据,包含的 `originalData.width` / `originalData.height` 只可在 MAIN decoder 边界被忽略,不能成为 canonical 字段、存储字段或下游输入。
其中:
- `content` 已经被 SDK 转成展示字符串。
- 媒体的结构化字段位于 `originalData`
- 现有扩展历史观察链使用 WebSocket 原始消息作为事实来源,不应直接切换为 SDK 扁平消息并替换现有消息 ID。
- 更安全的做法是直接解码 WebSocket `custom.data`,从而保留当前 `channelAccountId + conversationId + messageId` 幂等边界。
## 5. 文本原始格式
### 5.1 WebSocket 原始 content
```json
{
"contentType": 1,
"text": {
"content": "<message-text-redacted>",
"extension": {
"basicMessageInfo": "<serialized-json-string>",
"messageDisplayInfo": "<serialized-json-string>",
"messageEventInfo": "<serialized-json-string>"
}
}
}
```
### 5.2 SDK 归一化结果
```text
msgType101
type1
subType1
viewType0
contentstring
originalData keystext
```
### 5.3 当前可以安全保留的字段
```json
{
"kind": "text",
"text": "<message-text>"
}
```
不应跨页面边界发送:
```text
text.extension
basicMessageInfo
messageDisplayInfo
messageEventInfo
```
真实 `basicMessageInfo` 字符串解析后包含 `chatToken` 键,因此不能把这段字符串当作普通文本保存。
## 6. 图片原始格式
### 6.1 WebSocket 原始 content
```json
{
"contentType": 101,
"custom": {
"type": 7,
"data": "<base64-json>",
"degrade": "",
"summary": "",
"title": ""
}
}
```
本次图片样本:
```text
custom.data 字符长度:532
Base64 解码后 UTF-8 长度:397
Base64 解码结果:JSON object
```
### 6.2 Base64 解码后的 JSON
```json
{
"fileId": "<file-id>",
"height": 1188,
"isOriginal": 1,
"md5": "<md5>",
"size": 370256,
"suffix": "jpeg",
"url": "<https-action-url>",
"width": 1192
}
```
这是 OneTalk raw payload 证据,不是 normalized contract。raw `width` / `height` 可保留在此样本中,但 v6 MAIN decoder 忽略它们,绝不将其传过页面桥或写入 canonical content。
### 6.3 SDK 归一化结果
```text
msgType102
type1
subType60
viewType0
```
SDK `originalData` 的字段与 Base64 解码结果一致:
```text
fileId
height
isOriginal
md5
size
suffix
url
width
```
### 6.4 可实现的规范化图片合同
```ts
type OneTalkImageContent = {
version: 1;
kind: "image";
fileId: string;
extension: string;
sizeBytes: number;
isOriginal: boolean;
md5: string | null;
previewUrl: string | null;
urlScope: "onetalk_session";
};
```
规范化规则:
- `custom.type` 必须为 `7`
- `custom.data` 必须是有大小上限的合法 Base64。
- Base64 解码结果必须是 UTF-8 JSON object。
- `fileId``suffix``url` 必须为非空字符串。
- `size` 必须为有限非负整数,并设置合理上限;raw `width` / `height` 被 MAIN decoder 忽略,不参与校验或输出。
- `isOriginal``0/1` 显式转换为 boolean。
- `url` 只接受绝对 HTTPS URL,并在确认真实主机后映射为 `previewUrl``urlScope` 固定为 `onetalk_session`
- 不根据 `suffix` 猜造 OneTalk 未提供的 MIME;UI 可以使用安全扩展名映射做展示提示。
## 7. 文件附件原始格式
### 7.1 WebSocket 原始 content
```json
{
"contentType": 101,
"custom": {
"type": 10010,
"data": "<base64-json>",
"degrade": "",
"summary": "",
"title": ""
}
}
```
本次 PDF 样本:
```text
custom.data 字符长度:912
Base64 解码后 UTF-8 长度:680
Base64 解码结果:JSON object
```
### 7.2 Base64 解码后的 JSON
```json
{
"cardType": 12,
"params": {
"ctime": "<epoch-string>",
"downloadUrl": "",
"extensionType": "pdf",
"id": "<file-id>",
"md5": "<md5>",
"name": "<redacted>.pdf",
"parentId": "<parent-id>",
"size": "<decimal-string>",
"thumbnailUrl": "<https-action-url>",
"type": "<value>",
"url": "<https-action-url>",
"version": "<value>"
}
}
```
重要事实:
- 文件判定不能只看 `custom.type=10010`
- 真实附件还需要 `cardType=12`
- `params.size` 是 decimal string,不是 number。
- `params.downloadUrl` 在本次样本中为空。
- 实际可用候选位于 `params.url``thumbnailUrl` 是另一个操作地址。
- URL 路径表现为 `.htm` 操作入口,并带有 `appkey``fileAction``id``parentId``scene``secOperateAliId` 等查询键。报告没有记录查询值。
### 7.3 SDK 归一化结果
```text
msgType10010
type1
subType61
viewType0
originalData.cardType12
originalData.params.extensionTypepdf
```
### 7.4 可实现的规范化文件合同
```ts
type OneTalkFileContent = {
kind: "file";
fileId: string;
fileName: string;
extension: string;
sizeBytes: number;
md5?: string;
sourceUrl: string;
thumbnailUrl?: string;
};
```
规范化规则:
- `contentType` 必须为 `101`
- `custom.type` 必须为 `10010`
- Base64 JSON 的 `cardType` 必须为 `12`
- `params.id``params.name``params.extensionType``params.size``params.url` 必须通过边界校验。
- `params.size` 只接受十进制正整数字符串,并转换为安全整数。
- `name` 后缀和 `extensionType` 不一致时 fail closed 或降级为 unsupported,不静默选择其中一个。
- `url`、非空的 `downloadUrl``thumbnailUrl` 只接受绝对 HTTPS URL 和固定 host allowlist。
- 文件名用于展示时必须由 UI 作为文本渲染,不得作为 HTML。
PDF 已有真实样本。TXT 是否沿用完全相同的 `cardType=12` 结构尚无真实证据;实现可以设计为通用文件合同,但 TXT 的验收必须等待真实样本。
## 8. `custom.type=10010` 不等于附件
本次还抓到以下 WebSocket 原始类型:
```text
contentType=101
custom.type=10010
custom.data=<base64-json>
```
但 Base64 解码结果为:
```json
{
"cardType": 2000,
"params": {
"country": "<value>",
"ctime": "<value>",
"ids": "<value>",
"type": "<value>"
}
}
```
SDK 将其归一化为:
```text
msgType=10010
subType=2000
```
这是一类业务卡片,不是附件。若代码只写:
```ts
if (content.custom.type === 10010) return file;
```
就会错误地把商品、国家或其它业务卡片当成文件。
正确判定必须至少包含:
```text
contentType=101
custom.type=10010
decoded.cardType=12
decoded.params 通过文件 schema
```
其它合法但未支持的 `cardType` 应返回受控的 `unsupported` 内容,不应丢弃整条消息,也不应保留完整 raw payload。
## 9. 调查时的代码为什么表现为“只有文本”
### 9.1 页面观察器
当前 `apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts`
```ts
const contentRecord = isRecord(message.content) ? message.content : null;
if (contentRecord && isFiniteNumber(contentRecord.contentType)) {
output.contentType = contentRecord.contentType;
}
if (contentRecord) output.content = contentRecord;
const contentText = contentRecord?.text;
if (isRecord(contentText) && typeof contentText.content === "string") {
output.text = contentText.content;
} else {
output.text = null;
}
```
因此:
- 文本:得到 `contentType=1`、完整 raw content 和 `text=正文`
- 图片/文件:得到 `contentType=101`、完整 raw content 和 `text=null`
- 当前没有任何代码把 Base64 `custom.data` 变成 `image``file`
### 9.2 Service Worker 与 IndexedDB
`pageMessageToObserved()` 会复制页面观察字段。IndexedDB 只检查:
```text
content 是合法 JSON value
contentType 是有限整数
text 是 string | null | undefined
```
所以图片和文件不会因为 `text=null` 被拒绝。它们以 opaque JSON 进入:
```text
onetalk_messages
onetalk_sync_candidates
```
本次运行态的 `confirmed` 计数证明图片/附件 raw 记录确实通过了现有 ACK 链路。
### 9.3 共享协议与 Bright
调查时的 `OneTalkMessage` 定义:
```ts
type OneTalkMessage = {
messageId: string;
conversationId: string;
senderId: string;
direction: "sent" | "received";
sentAtMs: number;
content: OneTalkJsonValue;
contentType: number;
text?: string | null;
participantIds: string[];
readStatus: number;
messageStatus: number;
unreadCount: number;
};
```
`content` 只有“可序列化 JSON”约束,没有 `text/image/file` 语义。
Bright 数据库当前保存:
```text
content_type integer
text text nullable
content jsonb
```
因此调查时的 Bright 能保存媒体 raw JSON,但不能告诉 Mind
```text
这是图片还是文件
哪个 URL 用于预览
哪个 URL 用于下载
文件名和大小是什么
未知 card 应如何展示
```
### 9.4 当前展示边界
仓库内的 Bright harness 只执行:
```js
JSON.stringify({ content: message.content }, null, 2);
```
它没有:
```text
<img>
附件链接
文件名
文件大小
类型图标
unsupported fallback
```
真实 Mind UI 如果只读取 `message.text`,媒体消息自然不可见。
## 10. 调查时发现的安全风险
### 10.1 字符串内部的敏感字段绕过清洗
Bright 当前的敏感键规则能删除普通对象中的:
```text
token
cookie
csrf
authorization
secret
password
credential
sid
app-key
```
但清洗器遇到字符串会立即原样返回:
```ts
if (typeof value === "string" || typeof value === "boolean") return value;
```
以下两种载荷因此不会被递归检查:
1. `text.extension.basicMessageInfo` 中的序列化 JSON。
2. `custom.data` 中的 Base64 JSON。
真实 `basicMessageInfo` 解析后已确认含有 `chatToken` 键。调查时的实现把完整 raw content 交给 Bright,违反了数据库注释中“不得写入带认证信息的完整 envelope”的不变量。
### 10.2 推荐的安全边界
媒体支持不能靠“服务端收到 raw 后再尽量清洗”。应改为:
```text
OneTalk raw message
→ MAIN world 严格解码
→ 显式构造白名单 OneTalkMessageContent
→ 页面桥
→ Service Worker exact decoder
→ Bright exact decoder
→ JSONB / message.created / HTTP history
```
禁止跨边界的字段包括:
```text
raw content object
text.extension
basicMessageInfo
custom.data
chatToken
Cookie
Authorization
完整 OneTalk response/envelope
原始 SDK row
```
### 10.3 URL 风险
图片和文件样本中的 URL 是带查询参数的 HTTPS 操作地址,而不是直接媒体 URL。实现前还必须验证:
- 是否依赖 OneTalk 登录 Cookie。
- 是否包含短期授权或一次性参数。
- 是否可以从 Mind 页面所在 Origin 使用。
- 是否会发生 302 跳转,以及跳转地址的生命周期。
- 是否允许服务端代理;若允许,如何做授权和 SSRF 防护。
- URL 是否适合长期持久化,还是只能保存媒体 ID 并在需要时换取临时地址。
在上述行为没有真实验证前,只能称其为 `sourceUrl`,不能承诺“Mind 可直接下载”。
## 11. 当前 v6 内容边界
当前规范性数据流是:
```text
OneTalk raw message
→ MAIN decoderraw 仅在此处出现)
→ exact normalized content
→ page bridge / Service Worker / IndexedDB
→ Bright canonical JSONB / message.created / HTTP history
→ Mind read model
```
因此 Bright 不保存 raw payload、`custom.data` 或 SDK row;它只保存共享 guard 已接受的 normalized content。下列合同说明该边界的目标形状。
建议让 `content` 成为消息展示内容的唯一事实源:
```ts
type OneTalkNormalizedContent =
| {
kind: "text";
text: string;
}
| {
version: 1;
kind: "image";
fileId: string;
extension: string;
sizeBytes: number;
isOriginal: boolean;
md5: string | null;
previewUrl: string | null;
urlScope: "onetalk_session";
}
| {
kind: "file";
fileId: string;
fileName: string;
extension: string;
sizeBytes: number;
md5?: string;
sourceUrl: string;
thumbnailUrl?: string;
}
| {
kind: "unsupported";
sourceContentType: number;
sourceCustomType?: number;
cardType?: number;
};
```
兼容期可以保留顶层 `message.text`,但它必须始终从:
```ts
message.content.kind === "text" ? message.content.text : null;
```
派生,不能成为第二个可独立写入的事实源。
现有 JSONB 列可以保存这个 union。只有在需要文件类型索引、全文搜索或独立媒体生命周期时,才需要新增数据库列或媒体表;第一阶段不应为了渲染图片/PDF创建第二套消息表。
## 12. 可以做到什么
### 12.1 调查时已经做到
- 观察文本、图片和 custom card 的原始 WebSocket content。
- 使用 raw `channelAccountId + conversationId + messageId` 保持消息幂等。
- 将非文本消息写入 IndexedDB。
- 将非文本消息作为 JSON observation 发送给 Bright。
- Bright 可以 ACK 图片/附件 raw observation。
- Bright JSONB 可以保存规范化后的 `text/image/file/unsupported` union,无需立即增加第二张消息表。
### 12.2 基于现有证据可以立即实现
- 文本严格白名单提取,删除全部 extension。
- `custom.type=7` 图片 Base64 JSON 解码和字段校验。
- `custom.type=10010 + cardType=12` 通用文件解码。
- PDF 文件名、扩展名、大小、源 URL、缩略图元数据同步。
- 未支持 card 的安全 fallback,不丢失消息身份和时间线位置。
- Mind 侧按 `content.kind` 渲染文本、图片占位/预览和附件链接。
- 在共享协议、HTTP history 和 `message.created` 中使用同一 typed content。
- 添加敏感字段负向测试,证明 raw content、Base64 和序列化 JSON 不再跨边界。
### 12.3 当前还不能承诺
- TXT 附件与 PDF 完全同构:没有真实 TXT 样本。
- 实时图片/文件 push 与历史响应完全同构:没有实时媒体 push 样本。
- `sourceUrl` 能在 Mind 页面直接打开:未验证 Cookie、CORS、跳转和有效期。
- 图片/附件可以永久下载:当前 URL 可能只是临时操作入口。
- 支持音频、视频、语音、压缩包或所有 OneTalk custom card:没有真实样本和枚举。
- checkpoint 已完整收敛:本次末态仍为 `uploading/succeeded`
## 13. 当前 v6 不变量
### R1. 单一内容合同
扩展、Bright 和 Mind 必须共用一个 `OneTalkNormalizedContent` 判别联合。页面 raw payload、服务端 JSONB 和 UI 本地模型不得分别维护不同的媒体判定逻辑。
### R2. 页面边界白名单解码
MAIN world 是 OneTalk raw content 的唯一解码所有者:
- 文本只取 `text.content`
- 图片只接受 `contentType=101/custom.type=7` 的有效 Base64 JSON。
- 文件只接受 `contentType=101/custom.type=10010/cardType=12`
- 其它类型输出有限 `unsupported` 元数据。
- raw content 和 custom data 不得进入页面桥。
### R3. 保留事实身份
媒体规范化不得改变现有幂等键:
```text
channelAccountId + conversationId + raw messageId
```
不得用 SDK 展示层 ID、时间戳、hash 或合成 ID 代替 raw message ID。
### R4. URL 与文件元数据校验
- URL 只允许 HTTPS 和固定 OneTalk host allowlist。
- URL query/hash/userinfo 不进入日志或错误。
- 文件大小和时间字段必须有上下限;图片 raw `width` / `height` 由 MAIN decoder 忽略,不能成为 normalized 字段、匹配条件或展示数据。
- 文件扩展名规范化为小写有限字符集。
- 文件名按纯文本处理并限制长度。
- MD5 只能作为来源元数据,不能替代消息 ID 或安全签名。
### R5. 历史与实时一致
历史 `body.userMessageModels[].message.content` 和实时 `lastMessage.message.content` 必须走同一个 decoder。只有在真实 live 媒体样本证明 envelope 不同后,才允许增加窄化适配层。
### R6. 未支持类型行为
合法但未支持的 custom card
- 不伪装成文本或附件。
- 不保存 raw content。
- 保留消息事实并使用 `kind=unsupported`
- 输出稳定的安全类型码或诊断,不输出 raw data。
非法 Base64、非法 JSON、字段类型错误、超限或非 HTTPS URL:
- 进入明确 anomaly 或受控 unsupported 策略。
- 不得静默返回空文本。
- 不得阻塞同批其它有效消息。
### R7. 跨层一致性
以下边界必须同时升级并由共享 decoder 约束:
```text
MAIN observed message
page bridge
Service Worker storage/candidate
message.observed
Bright normalization
PostgreSQL JSONB
message.created
HTTP history
Mind UI model
```
如果合同为不兼容变更,应提升 OneTalk 协议版本并让旧插件得到明确升级错误,不能在同一版本内让 `content` 同时表示 raw 与 normalized 两种含义。
### R8. 安全清除
任何通过网络、数据库、日志或诊断的消息都必须证明不包含:
```text
chatToken
basicMessageInfo
custom.data
Cookie
Authorization
accountIdEncrypt
aliIdEncrypt
原始 URL query 值
完整 raw payload
```
## 14. 推荐验收标准
- [ ] 真实文本历史消息归一化为 `{ kind: "text", text }`,并且 content 不含 extension。
- [ ] 真实 JPEG 样本从 `custom.type=7` Base64 JSON 归一化为无尺寸 v6 image`fileId``extension``sizeBytes``isOriginal``md5``previewUrl``urlScope` 通过 canonical contractraw `width` / `height` 不跨 MAIN 边界。
- [ ] 真实 PDF 样本从 `custom.type=10010/cardType=12` 归一化为 file,文件名、扩展名、大小和 URL 元数据一致。
- [ ] `custom.type=10010/cardType=2000` 不会被识别成文件。
- [ ] 非法 Base64、非法 JSON、超限数据、非 HTTPS URL、缺字段和大小溢出均 fail closed,不影响同批其它消息。
- [ ] page bridge、IndexedDB、Bright DB、HTTP history 和 `message.created` 中均只存在 normalized content。
- [ ] 序列化最终 frame、数据库输入和诊断事件,断言不包含 `chatToken``basicMessageInfo``custom.data` 或加密身份字段。
- [ ] Mind UI 能渲染文本、图片状态和 PDF 附件;URL 不可用时显示明确失败,不显示空白消息。
- [ ] unknown custom card 显示“暂不支持的消息类型”,同时保留正确的消息时间线和幂等身份。
- [ ] 获得真实 TXT 样本后补充同构性测试;未获得前不宣称 TXT 已验证。
- [ ] 获得真实 live 图片和附件 push 后证明历史与实时共用同一 decoder。
- [ ] 验证媒体 URL 在有 Cookie、无 Cookie、跨 Origin、过期和跳转场景的行为,再决定保存 URL、保存 ID 或增加受控代理。
## 15. 调查时的相关代码位置
| 层 | 文件 | 当前行为 |
| -------------- | --------------------------------------------------------------------------------- | --------------------------------------- |
| WebSocket 分流 | `apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts` | 识别 history/new envelope |
| 页面消息提取 | `apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts` | 复制 raw content,仅提取 text |
| 历史只读 SDK | `apps/chrome-extension/src/onetalk/main-page/current-conversation-history/sdk.ts` | 调用 `fetchMessagesWithoutUpdateToRead` |
| 页面到 SW | `apps/chrome-extension/src/onetalk/service-worker/sync-engine/helpers.ts` | 原样复制观察字段 |
| IndexedDB 校验 | `apps/chrome-extension/src/onetalk/service-worker/storage.ts` | 只检查 JSON 与基础字段类型 |
| 共享合同 | `packages/onetalk-contract/src/model.ts` | `content` 为任意 JSON value |
| 共享 decoder | `packages/onetalk-contract/src/decoder.ts` | 不理解 text/image/file 语义 |
| Bright 清洗 | `apps/server/src/onetalk/service.ts` | 按键过滤对象,字符串原样通过 |
| Bright 持久化 | `apps/server/src/database/schema/onetalk.ts` | `content jsonb``text nullable` |
| 开发展示 | `apps/mind-test-harness/src/harness.ts` | 只展示 raw JSON |
| 现有媒体覆盖 | `apps/chrome-extension/test/onetalk-websocket-tap.test.js` | 只有简化 `custom.type=10010` fixture |
## 16. 最终判断
历史调查证明 OneTalk raw payload 提供实现图片和文件同步所需的核心元数据。当前 v6 不再沿用该报告中 raw content 跨页面桥、IndexedDB、Bright 或 Mind 的历史路径;实现核心已经收敛为 MAIN decoder 的单一白名单边界:
```text
opaque raw content
→ 严格、安全、可测试的 text/image/file/unsupported 合同
→ Bright 持久化与事件只使用 normalized content
→ Mind read model 只接收 normalized content
```
raw 字符串中嵌套凭证进入 Bright 是本报告记录的历史风险;当前 v6 边界以不让 raw payload 离开 MAIN decoder 的方式关闭它。真实 v6 Chromium 运行态验收仍未执行。