feat: 移除 OneTalk 图片宽高链路

This commit is contained in:
YBF
2026-09-11 14:30:35 +08:00
parent a128940d94
commit 5ee817cf54
40 changed files with 849 additions and 268 deletions
@@ -4,6 +4,8 @@
> 环境:OneTalk SaaS 测试环境,Chromium CDP `127.0.0.1:9222`
> 性质:运行态调查与可行性结论,不包含代码实现
> 隐私约束:本文不记录真实会话 ID、账号 ID、Token、Cookie、媒体完整 URL、URL 查询值或 MD5 原值
>
> 历史快照说明(2026-09-11):下文的“当前”“已实现”和能力结论均指 2026-09-10 的调查环境,不构成当前工作树的 file-send 发布承诺。本轮 v6 只完成并检查了图片无尺寸合同;文件发送、其 pending/matcher 与真实 Chromium 联调须在独立范围按当前代码重新验证。
## 1. 结论摘要
@@ -619,22 +621,24 @@ node --experimental-strip-types --test \
## 9. 文件与图片的不同点
| 维度 | 图片 | 普通文件 |
| ------------------ | -------------------------------- | ---------------------------------------------------- |
| 页面分类 | `imageCard` | `fileCard` |
| 页面兼容 `msgType` | `60` | `53` |
| BaaS 输入类型 | 图片类型,实测历史为 `102` | 页面输入默认 `107`,历史归一化为 `10010` |
| 历史 `subType` | `60` | `61` |
| raw content | `contentType=101/custom.type=7` | `contentType=101/custom.type=10010` |
| 二次判别 | 图片 payload schema | 必须同时满足 `cardType=12``10010` 本身不够 |
| 核心显示字段 | `width/height/isOriginal` | `fileName/parentId/downloadState` |
| 共同字段 | `fileId/extension/size/md5/url` | `fileId/extension/size/md5/url` |
| 大小类型 | raw `size` 为 number | raw `params.size` 为十进制 string |
| 压缩 | 约 1 MB 以上图片可能先压缩 | ZIP/PDF 等普通文件不做图片压缩 |
| 匹配指纹 | 大小 + 宽 + 高 + 可选 MD5/fileId | 文件名 + 扩展 + 大小 + 可选 MD5/fileId/parentId |
| URL 语义 | 主要是 image preview | 可能区分 office preview、download、thumbnail |
| 显式 downloadUrl | 图片合同没有下载状态 | 可为空;可由 `url.fileAction=download` 派生 |
| 文件真实性 | 可由图片解码进一步验证像素 | observer 无二进制,不能验证扩展名与 magic bytes 一致 |
| 维度 | 图片 | 普通文件 |
| ------------------ | ---------------------------------------------------- | ---------------------------------------------------- |
| 页面分类 | `imageCard` | `fileCard` |
| 页面兼容 `msgType` | `60` | `53` |
| BaaS 输入类型 | 图片类型,实测历史为 `102` | 页面输入默认 `107`,历史归一化为 `10010` |
| 历史 `subType` | `60` | `61` |
| raw content | `contentType=101/custom.type=7` | `contentType=101/custom.type=10010` |
| 二次判别 | 图片 payload schema | 必须同时满足 `cardType=12``10010` 本身不够 |
| 核心显示字段 | `extension/sizeBytes/isOriginal` | `fileName/parentId/downloadState` |
| 共同字段 | `fileId/extension/sizeBytes/md5/previewUrl/urlScope` | `fileId/extension/size/md5/url` |
| 大小类型 | raw `size` 为 number | raw `params.size` 为十进制 string |
| 压缩 | 约 1 MB 以上图片可能先压缩 | ZIP/PDF 等普通文件不做图片压缩 |
| 匹配指纹 | 大小 + MD5 + 可选 fileId | 文件名 + 扩展 + 大小 + 可选 MD5/fileId/parentId |
| URL 语义 | 主要是 image preview | 可能区分 office preview、download、thumbnail |
| 显式 downloadUrl | 图片合同没有下载状态 | 可为空;可由 `url.fileAction=download` 派生 |
| 文件真实性 | 仅验证 OneTalk canonical metadata | observer 无二进制,不能验证扩展名与 magic bytes 一致 |
图片 raw payload 仍可能携带 `width` / `height`,但它们只属于 OneTalk 上游证据:v6 MAIN decoder 忽略这两个字段,normalized/public image、post-upload metadata、confirmation fingerprint 与 harness display 均不读取或显示它们。
## 10. 关键注意点
@@ -672,7 +676,7 @@ URL 可能包含会话授权、临时签名、重定向和不同 `fileAction`。
上传、分片、大文件策略和关系建立可能耗时较长。发送确认计时器应只覆盖最终消息发送阶段,而不是整个文件上传阶段。
### 10.6 当前代码能力边界
### 10.6 2026-09-10 调查时的代码能力边界
- 接收/观测合同已经支持 `content.kind="file"`
- 当前工作树中的出站合同正在扩展 `text | image`,尚未包含 `file`
@@ -3,15 +3,17 @@
> 日期:2026-09-10
> 性质:测试环境运行态调查与实现可行性报告,不是 Trellis task,不包含代码实现
> 范围:本地图片发送到非当前打开会话,以及通过 WebSocket observer 确认 sent 图片事实
>
> v6 同步说明(2026-09-11):本报告中的 raw upload、像素尺寸和当时 live 记录是历史调查证据;当前 canonical image、post-upload metadata、confirmation fingerprint 与 harness display 均不使用 `width` / `height`。未在本报告的旧运行环境复跑 v6 live 验收。
## 1. 结论
本次调查确认两件事:
1. **可以在页面当前打开其它会话时,向显式指定的目标会话发送图片。** 最终路由由发送参数中的 `cid` 决定,不要求切换页面 selected conversation。
2. **可以在 WebSocket 观测阶段使用图片的大小、宽度和高度辅助确认发送结果。** live WS 图片会被 MAIN-world 解码器归一化为 `content.kind="image"`保留 `sizeBytes``width``height`
2. **可以在 WebSocket 观测阶段使用图片的已验证 canonical metadata 辅助确认发送结果。** v6 MAIN-world decoder 将 live WS 图片归一化为 `content.kind="image"`保留 `fileId``extension``sizeBytes``isOriginal``md5``previewUrl``urlScope`
`sizeBytes + width + height` 不是唯一键。本次连续发送同一图片后,扩展存储中出现了两条不同的 live sent 消息,它们的三个字段完全相同。因此该三元组只能作为复合匹配条件,不能单独生成 `confirmed_sent`
`sizeBytes` 不是唯一键;即使 `md5``fileId` 可用,重复发送同一文件时也可能相同。本次连续发送同一图片后,扩展存储中出现了两条不同的 live sent 消息。因此 metadata 只能作为无 candidate ID 时的复合匹配条件,不能单独生成 `confirmed_sent`
推荐确认顺序:
@@ -19,7 +21,7 @@
可靠候选 messageId
→ conversationId + direction=sent
→ content.kind=image
→ post-upload sizeBytes + width + height
→ post-upload sizeBytes + md5 + optional fileId
→ 短时间窗口
→ 必须唯一匹配,否则 send_ambiguous
```
@@ -77,17 +79,17 @@ https://onetalk.alibaba.com/message/weblitePWA.htm
运行结果:
| 证据 | 结果 |
| ---------------------------- | ---------------------------------------------- |
| `prepareSendFileWithGroup` | HTTP `200` |
| `buildFileRelationWithGroup` | HTTP `200` |
| OSS 二进制上传 | 未发生,命中文件已存在/去重分支 |
| BaaS `sendMessageBase` | 已进入一次 |
| 页面 `send-msg-success` | 一次 |
| WebSocket 帧 | 出站 3、入站 3 |
| Runtime exception | 0 |
| selected conversation | 全程不变 |
| 目标会话只读历史 | 找到一条与 `I1` 大小和宽高完全一致的 sent 图片 |
| 证据 | 结果 |
| ---------------------------- | ------------------------------------ |
| `prepareSendFileWithGroup` | HTTP `200` |
| `buildFileRelationWithGroup` | HTTP `200` |
| OSS 二进制上传 | 未发生,命中文件已存在/去重分支 |
| BaaS `sendMessageBase` | 已进入一次 |
| 页面 `send-msg-success` | 一次 |
| WebSocket 帧 | 出站 3、入站 3 |
| Runtime exception | 0 |
| selected conversation | 全程不变 |
| 目标会话只读历史 | 找到一条与 `I1` 大小一致的 sent 图片 |
页面 `send-msg-success` 和 SDK Promise 只证明本地受理,不能单独证明发送完成。目标会话只读历史中的 sent 图片事实才排除了“只插入了页面假消息”的情况。
@@ -95,17 +97,17 @@ https://onetalk.alibaba.com/message/weblitePWA.htm
扩展 Service Worker 的 IndexedDB 中,对目标会话和 `I1` 的归一化字段进行只读匹配:
| 项目 | 结果 |
| ------------------------------------ | ---------------- |
| exact image candidate | 3 条 |
| `observationSource="live"` | 2 条 |
| `observationSource="history"` | 1 条 |
| candidate status | 全部 `confirmed` |
| live 记录的 `sizeBytes/width/height` | 与 `I1` 完全一致 |
| 项目 | 结果 |
| --------------------------------------------- | ---------------- |
| exact image candidate | 3 条 |
| `observationSource="live"` | 2 条 |
| `observationSource="history"` | 1 条 |
| candidate status | 全部 `confirmed` |
| 当时 live 记录的 raw `sizeBytes/width/height` | 与 `I1` 完全一致 |
手工调用的只读历史接口没有把返回值送入页面 bridge 或 Service Worker;同时 raw WebSocket history response 会被 observer 主动忽略。因此 `observationSource="live"` 的两条记录证明,图片大小和宽高确实能经过 live WS observer 到达归一化存储边界。
手工调用的只读历史接口没有把返回值送入页面 bridge 或 Service Worker;同时 raw WebSocket history response 会被 observer 主动忽略。因此 `observationSource="live"` 的两条记录证明当时的 live 观察链已能接收图片事实。该记录来自 v6 前的调查,不构成对当前 v6 normalized shape 的运行态验证:当前 MAIN decoder 忽略 raw `width` / `height`,它们不进入归一化存储边界。
这两条 live 消息也构成反例:相同图片重复发送时,大小和宽高完全相同,但它们是不同的消息事实。
这两条 live 消息也构成反例:相同图片重复发送时,媒体 metadata 可以相同,但它们是不同的消息事实。
## 4. 图片发送路径与参数
@@ -292,7 +294,7 @@ mtop.alibaba.interaction.clouddisk.buildFileRelationWithGroup
}
```
成功结果提供后续 `sendFile` 所需的媒体关系,例如 `fileId``fileCardUrl``redirectFileUrl`、图片尺寸和文件节点信息。
成功结果提供后续 `sendFile` 所需的媒体关系,例如 `fileId``fileCardUrl``redirectFileUrl` 和文件节点信息;若上游关系含图片尺寸,它们仍只属于 raw upload evidencev6 downstream 不读取
### 4.7 最终页面发送参数
@@ -341,7 +343,7 @@ conversationCode = input.cid || sdkContext.cid;
因此只要传入非空目标 `cid`,当前页面 selected conversation 不参与最终路由;缺失 `cid` 时才会回退 SDK 当前上下文,存在发错会话风险。
图片被转换为 BaaS `originalData`
图片被转换为 BaaS raw `originalData`
```ts
{
@@ -356,6 +358,8 @@ conversationCode = input.cid || sdkContext.cid;
}
```
这是页面上游的 raw upload payload 示例,不是 v6 canonical contract。即使该 raw payload 含 `width` / `height`MAIN decoder 也会忽略它们,且 post-upload metadata、correlator 和下游 public content 都不会读取或传递这两个字段。
### 4.8 `sendImageMessage` 快捷入口
页面 SDK 还暴露:
@@ -441,8 +445,6 @@ type OneTalkImageContent = {
fileId: string;
extension: string;
sizeBytes: number;
width: number;
height: number;
isOriginal: boolean;
md5: string | null;
previewUrl: string | null;
@@ -454,8 +456,8 @@ send correlator 不需要也不应该继续读取 raw `originalData`。应比较
```text
message.content.sizeBytes
message.content.width
message.content.height
message.content.md5
message.content.fileId (when present)
```
### 5.3 observer 与 correlator 的调用顺序
@@ -469,11 +471,11 @@ observedSink(batch);
因此图片 live batch 在跨 MAIN bridge、写 IndexedDB 或上传 Bright 之前,已经可以交给发送确认 correlator。无需新增第二个 WebSocket observer,也不应增加另一套 raw payload parser。
## 6. 建议的图片确认模型
## 6. 当前 v6 图片确认模型
### 6.1 Pending 数据
现有 [`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts) `PendingSend` 只保存字符串正文。图片支持应改为判别联合,而不是给文本结构追加一组可选字段
v6 的 [`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts) 已使用 text/image/file 判别联合;图片 pending 保存目标会话、候选消息 ID 与最终 post-upload metadata,而不是给文本记录追加可选字段。下列 image 分支是当前实现的形状
```ts
type PendingTextSend = {
@@ -491,21 +493,19 @@ type PendingImageSend = {
candidateMessageIds: Set<string>;
expected: {
sizeBytes: number;
width: number;
height: number;
md5?: string | null;
md5: string;
fileId?: string;
};
};
type PendingSend = PendingTextSend | PendingImageSend;
type PendingObservation = PendingTextSend | PendingImageSend | PendingFileSend;
```
`md5``fileId` 可以提高不同图片之间的区分度,但同一文件去重或重复发送时它们也可能相同,仍不能当作每次发送的唯一 ID。
### 6.2 登记时机
图片 pending 必须在以下时机登记:
图片 pending 在取得最终 media relation 后、最终 native send 前登记:
```text
压缩完成
@@ -516,18 +516,18 @@ type PendingSend = PendingTextSend | PendingImageSend;
→ 立即调用 sendUIMessages
```
更准确地说,应`buildFileRelationWithGroup` 成功、`sendFile` 已拿到最终 `nodeSize/width/height` 后,并在最终 SDK send 之前登记
实现以 `tmpKey` 隔离上传回调;`buildFileRelationWithGroup` 成功、`sendFile` 已拿到最终 `sizeBytes`、非空 `md5` 和可用 `fileId` 后,立即将该 metadata 交给 correlator 并调用最终 SDK send。45 秒预算覆盖下载、上传和 live 确认,correlator 使用其剩余时间
不能在用户选择原始文件时登记,原因有两个:
1. 图片压缩可能改变 `sizeBytes`,甚至改变尺寸
2. 上传时间可能超过当前 correlator 的 `10_000ms` 超时。
1. 图片压缩可能改变最终 `sizeBytes`
2. 上传时间可能超过该历史调查时 correlator 的 `10_000ms` 超时;当前 image 预算为 45 秒
上传阶段和消息发送确认阶段应是两个状态,不要让消息确认定时器覆盖完整上传耗时。
### 6.3 匹配顺序
建议匹配逻辑
已实现的匹配顺序
```text
1. message.direction 必须是 sent
@@ -535,8 +535,8 @@ type PendingSend = PendingTextSend | PendingImageSend;
3. message 必须通过完整 OneTalkMessage guard
4. 如果存在可靠 candidateMessageId:只按 messageId 匹配,不回退媒体指纹
5. 否则要求 pending.kind=image 且 message.content.kind=image
6. 比较 post-upload sizeBytes、width、height
7. 可选比较 md5/fileId,但不能把它们当作单次发送唯一键
6. 比较 post-upload `sizeBytes`、`md5` 和可用 `fileId`
7. 这些 metadata 不能被当作单次发送唯一键
8. 要求消息位于 pending 生命周期和允许的时钟偏差内
9. 一个消息必须只匹配一个 pending;多个匹配立即 send_ambiguous
```
@@ -549,8 +549,8 @@ const imageMatches = (
actual: OneTalkImageContent,
): boolean =>
actual.sizeBytes === expected.sizeBytes &&
actual.width === expected.width &&
actual.height === expected.height;
actual.md5 === expected.md5 &&
(expected.fileId === undefined || actual.fileId === expected.fileId);
```
该函数只能是复合匹配的一部分,不能绕过 conversation、direction、时间窗口和唯一性检查。
@@ -561,21 +561,21 @@ const imageMatches = (
至少需要补充以下测试:
| 用例 | 预期 |
| -------------------------- | ----------------------------------------------------------- |
| live WS 图片 raw payload | 输出 `content.kind=image` 及准确的 `sizeBytes/width/height` |
| 正确会话、方向、指纹和时间 | `confirmed_sent` |
| 错误 conversation | 不匹配 |
| `direction=received` | 不匹配 |
| 宽度、宽高或大小任一不同 | 不匹配 |
| 候选 message ID 匹配 | 即使时间窗口外仍按 ID 确认 |
| 候选 message ID 不匹配 | 不回退图片指纹 |
| 同图两个并发 pending | `send_ambiguous` |
| 图片压缩后大小变化 | 使用 post-upload 大小确认 |
| 超时无 live echo | `delivery_unknown/send_state_lost` |
| 非法 Base64/JSON/schema | anomaly,不进入 correlator |
| 用例 | 预期 |
| --------------------------------------- | ------------------------------------------------------------ |
| live WS 图片 raw payload | 忽略 raw `width` / `height`;输出无尺寸的 v6 canonical image |
| 正确会话、方向、指纹和时间 | `confirmed_sent` |
| 错误 conversation | 不匹配 |
| `direction=received` | 不匹配 |
| `sizeBytes``md5` 或可用 `fileId` 不同 | 不匹配 |
| 候选 message ID 匹配 | 即使时间窗口外仍按 ID 确认 |
| 候选 message ID 不匹配 | 不回退图片指纹 |
| 同图两个并发 pending | `send_ambiguous` |
| 图片压缩后大小变化 | 使用 post-upload 大小确认 |
| 超时无 live echo | `delivery_unknown/send_timeout` |
| 非法 Base64/JSON/schema | anomaly,不进入 correlator |
现有测试已经覆盖 raw 图片解码、flat history 图片归一化、非法 live 媒体隔离和文本 send confirmation;尚缺成功 live 图片直接驱动 correlator 的用例
定向测试覆盖 raw 图片解码、flat history 图片归一化、非法 live 媒体隔离,以及无尺寸 live sent image 驱动实际 image pending 至 `confirmed_sent`。这只是自动化证据;尚未在本报告原有 Chromium 环境执行 v6 真实发送联调
### 7.2 Chromium/CDP 联调
@@ -583,7 +583,7 @@ const imageMatches = (
1. 打开会话 A,但指定目标会话 B。
2. 记录 selected conversation 的内部比较结果,不输出真实 ID。
3. 选择一张已知大小和宽高的测试图片。
3. 选择一张已知大小的测试图片。
4. 对大图额外记录压缩后的最终 metadata。
5. 在最终 `sendUIMessages` 前登记 image pending。
6. 观察 prepare、OSS/去重、build relation 和 BaaS send 的状态。
@@ -619,9 +619,9 @@ SDK send 已执行
## 8. 注意点与风险
### 8.1 大小与宽高不唯一
### 8.1 媒体 metadata 不唯一
同一图片重复发送会产生不同的 messageId,但 `sizeBytes/width/height` 完全相同。本次运行态已经得到两条这样的 live sent 记录。
同一图片重复发送会产生不同的 messageId,但 `sizeBytes``md5``fileId` 可以完全相同。本次运行态已经得到两条这样的 live sent 记录。
不得采用:
@@ -665,20 +665,22 @@ V2 `sendUIMessages` 的 Promise 可以在 local callback 得到 clientId/opId
raw `originalData` 只应在 MAIN world 短暂存在。send correlator 应消费已经归一化的 `OneTalkImageContent`,不要在 correlator、Service Worker 或 Bright 中再实现第二套 Base64/JSON 图片解析。
## 9. 实现边界建议
上游 raw `originalData` 可含 `width` / `height`v6 MAIN decoder 忽略它们,correlator 的 post-upload expected 和 public/read payload 只使用无尺寸的 canonical contract。
该能力技术上可行,建议后续实现限定为:
## 9. 当前实现边界
当前实现限定为:
1. 为页面图片发送定义独立、最小的输入合同;
2. 在上传关系成功后生成 post-upload image fingerprint
3. `PendingSend` 改成 text/image 判别联合;
3. 使用 text/image/file 判别的 pending 集合;
4. 复用现有 live WS observer 和 normalized content,不新增旁路;
5. 候选 ID 优先,媒体指纹仅作无 ID 回退;
6. 保留唯一匹配与 `send_ambiguous`
7. 分离 upload timeout 与 send confirmation timeout
8. 保持 SDK 异常和不确定投递 fail closed,不自动重试。
当前仓库的 [`page-command.ts`](../apps/chrome-extension/src/onetalk/main-page/current-conversation-history/page-command.ts) 仍只接受字符串 `content`[`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts) 仍以文本正文为无 ID 回退条件。本报告确认的是图片扩展方案可行,不表示当前插件已经具备图片发送命令和图片发送确认合同
[`page-command.ts`](../apps/chrome-extension/src/onetalk/main-page/current-conversation-history/page-command.ts) 现已严格接收 outbound `text | image | file` contract,并将 image 交给 `sendOneTalkImage`[`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts) 先按 candidate message ID 确认,缺少候选 ID 时才以同会话、sent 方向、image kind、`sizeBytes``md5`、可用 `fileId`、时间窗和唯一性回退。当前自动化证据不代替未执行的 v6 Chromium live 验收
## 10. 相关代码
@@ -4,6 +4,8 @@
> 调查对象:`https://onetalk.alibaba.com/message/weblitePWA.htm` 以及当前 `trade-message-center` OneTalk 扩展链路
> 调查方式:Chromium DevTools ProtocolCDP`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 忽略 raw `width` / `height`,它们不跨 normalized boundary;本报告不是 v6 live runtime 验收。
## 1. 摘要
@@ -13,7 +15,7 @@ OneTalk 的图片和附件并不是另一条独立的同步通道。它们和文
- 图片:`contentType = 101``content.custom.type = 7``content.custom.data` 是 Base64 编码的 JSON。
- 附件:`contentType = 101``content.custom.type = 10010``content.custom.data` 是 Base64 编码的 JSON。
当前扩展的传输和持久化边界已经能够保留这些原始内容;非文本消息只会令便利字段 `text``null`,不会令 `content` 消失。因此“现在只实现 text”的准确含义是:当没有完成图片/附件的语义投影、Mind 端展示和完整发送适配,而不是 WebSocket 接收层完全收不到媒体。
以下是 2026-09-01 的历史调查结论:当时扩展的传输和持久化边界保留这些原始内容;非文本消息只会令便利字段 `text``null`,不会令 `content` 消失。因此“现在只实现 text”的准确含义是:当没有完成图片/附件的语义投影、Mind 端展示和完整发送适配,而不是 WebSocket 接收层完全收不到媒体。它不描述当前 v6 流程。
本次 CDP 实测在当前登录页面的两个已加载会话中调用了只读历史接口,捕获到一页 20 条消息的历史 WebSocket 帧;现有 `parseOneTalkMessages` 返回了全部 20 条,其中包含一条图片和一条附件。没有点击上传、发送或下载,发送侧结论只来自 SDK 和 bundle 的方法/调用形态分析。
@@ -30,9 +32,9 @@ OneTalk 的图片和附件并不是另一条独立的同步通道。它们和文
5. 对同一帧运行仓库现有 `parseOneTalkMessages` 后,图片和附件仍保留在 `message.content`,但 `message.text``null`
6. 图片和附件的 `custom.data` 经 Base64 解码后是 JSON,而不是二进制图片或文件本体。
### 2.2 代码级确认、尚未做完整端到端实测
### 2.2 历史代码级确认、尚未做完整端到端实测
- Service Worker 的观察、IndexedDB 写入、Bright 上传和 Bright HTTP 返回均使用通用 JSON `content`,类型上没有把内容限制为文本。
- 当时 Service Worker 的观察、IndexedDB 写入、Bright 上传和 Bright HTTP 返回均使用通用 JSON `content`,类型上没有把内容限制为文本。
- 当前没有真实数据库写入后的 Mind 页面媒体渲染回归测试。
- 没有执行真实图片上传、附件上传、发送确认和下载操作,因此不能把 SDK bundle 中的发送能力称为扩展已经支持的能力。
@@ -180,7 +182,7 @@ MessagePack 的数字键本身不携带业务字段名,不能仅凭数组位
}
```
这里的 `url` 是图片资源地址;报告不记录实际 URL,因为它可能包含访问签名或其他会话相关信息。`size`、尺寸、后缀和 MD5 是元数据,不是图片二进制本体。
这里的 `url` 是图片资源地址;报告不记录实际 URL,因为它可能包含访问签名或其他会话相关信息。`size`、尺寸、后缀和 MD5 是 raw 上游元数据,不是图片二进制本体。此处的 `width` / `height` 仅保留为调查证据;当前 v6 MAIN decoder 忽略它们,且它们不会跨出 normalized boundary。
### 5.2 页面 SDK 归一化形态
@@ -207,7 +209,9 @@ MessagePack 的数字键本身不携带业务字段名,不能仅凭数组位
}
```
`subType = 60``msgType = 102` 是页面 SDK/渲染层的归类结果,不应替换原始 `contentType``custom.type`同步事实应继续保留原始内容,归类字段只作为投影依据。
`subType = 60``msgType = 102` 是页面 SDK/渲染层的归类结果,不应替换原始 `contentType``custom.type`上游 raw 内容只在 MAIN 边界短暂存在;同步事实使用其规范化投影,归类字段只作为投影依据。
SDK `originalData` 中的 `width` / `height` 同样只是 raw SDK 证据,不是 current v6 normalized image contract 的字段;MAIN decoder 不读取或传递它们。
## 6. 附件消息格式
@@ -279,28 +283,26 @@ MessagePack 的数字键本身不携带业务字段名,不能仅凭数组位
`subType = 61``msgType = 10010` 是页面显示/消息模型的归类,不是可以脱离 `custom.type = 10010` 单独使用的稳定事实键。
## 7. 当前仓库的数据流追踪
## 7. 历史数据流与当前 v6 边界
### 7.1 页面观察器
历史帧由 [历史解析器](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts:12) 交给 `observedMessage()`。在 [消息模型](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts:65) 中:
2026-09-01 的历史帧由 [历史解析器](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/message-observer/history.ts:12) 交给 `observedMessage()`。在当时的 [消息模型](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/message-observer/model.ts:65) 中:
1. 只要 `message.content` 是对象,就把它作为完整 `content` 保留。
2.`content.contentType` 提取 `contentType`
3. 只有 `content.text.content` 是字符串时,才填充 `text`
4. 图片/附件没有 `content.text.content`,所以 `text``null`
因此,当前代码并没有把图片/附件转换成错误的文本,也没有在这一层删除原始媒体对象。
因此,历史代码并没有把图片/附件转换成错误的文本,也没有在这一层删除原始媒体对象。
### 7.2 页面桥与 Service Worker
[页面桥转换](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/service-worker/sync-engine/helpers.ts:66) 会复制观察消息的所有 JSON 字段;如果原消息已有 `content`,不会用 `text` 覆盖它。`content` 进入 Service Worker 后仍是通用 JSON 值。
这是历史实现:页面桥会复制观察消息的 JSON 字段`content` 进入 Service Worker 后仍是通用 JSON 值。它已被 v6 normalized boundary 取代。
### 7.3 Bright 服务与数据库
[Bright 归一化](/Users/ybf/code/trade-message-center-worktree/apps/server/src/onetalk/service.ts:170) 对 `content` 执行通用 JSON 校验和敏感键过滤,然后将清洗后的 `content` 持久化。当前公共契约中的 [OneTalkMessage](/Users/ybf/code/trade-message-center-worktree/apps/onetalk-contract/src/model.ts:203) 也将 `content` 定义为通用 `OneTalkJsonValue`,没有要求它必须含有 `text`
所以现有事实链可以保存:
历史 Bright 归一化`content` 执行通用 JSON 校验和敏感键过滤,然后持久化。历史事实链为:
```text
OneTalk raw content
@@ -311,28 +313,41 @@ OneTalk raw content
→ Mind history response
```
当前缺少的是在某个明确边界增加媒体语义投影,而不是重新设计这条事实链。
这条 raw 事实链仅为历史调查证据,当前不得使用。v6 的规范路径为:
## 8. 已经可以做到什么
```text
OneTalk raw content
→ MAIN decoderraw 只停留在此处)
→ normalized content
→ page bridge / Service Worker / IndexedDB
→ Bright canonical JSONB / HTTP history
→ Mind read model
```
| 能力 | 当前状态 | 证据/限制 |
| ----------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------- |
| 接收文本历史消息 | 已验证 | 现有观察器提取 `text` |
| 接收图片历史消息 | 已验证 | CDP 实测 `custom.type=7`,现有 parser 保留 `content` |
| 接收附件历史消息 | 已验证 | CDP 实测 `custom.type=10010`,现有 parser 保留 `content` |
| 保留原始媒体元数据 | 代码已支持 | 通用 JSON `content` 贯穿页面桥、Service Worker、Bright |
| `channelAccountId + conversationId + messageId` 幂等保存 | 代码已支持 | 媒体不改变消息业务键 |
| 在 Mind 历史接口返回原始媒体 JSON | 代码路径支持 | 尚未做真实 DB 写入和 Mind UI 回归 |
| 将图片字段投影为 `imageUrl/width/height` | 当前未实现 | 需要新增共享内容解码器/投影器 |
| 将附件字段投影为文件名、大小、预览和下载动作 | 当前未实现 | 需要处理 `downloadUrl` 为空的情况 |
| 在 Mind 页面显示图片 | 当前未实现 | 当前仓库没有对应媒体渲染契约/组件 |
| 在 Mind 页面显示附件卡片 | 当前未实现 | 当前仓库没有对应媒体渲染契约/组件 |
| OneTalk 文本发送 | 已有路径 | `sendUIMessages` 与文本确认逻辑以字符串正文为中心 |
| OneTalk 图片发送 | 页面 SDK 有方法 | bundle 观察到 `sendImageMessage({ cid, picUrl })`;扩展未接入完整发送契约 |
| OneTalk 本地文件/附件发送 | 页面 bundle 有上传流程 | 涉及 `prepareSendFileWithGroup`、OSS 上传和文件卡片;未做真实上传验证 |
| 图片/附件发送确认 | 当前未实现 | 出站确认关联器只按文本内容匹配 |
| 下载二进制到 Bright | 当前未实现 | 当前只保存消息 JSON,不保存媒体本体 |
| 群聊图片/附件全量同步 | 当前未确认 | 既有历史同步对群聊会话有跳过/不支持边界 |
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. 发送侧调查结果
@@ -353,7 +368,7 @@ messageService.sendImageMessage({
});
```
这说明 OneTalk 页面具有图片发送入口,但这不等于扩展已经具备图片发送能力。扩展当前页面命令在 [page-command.ts](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/current-conversation-history/page-command.ts:97) 中要求 `command.content` 是字符串,并交给文本型 `sendUIMessages`需要新增图片输入契约、SDK 调用和发送确认规则后才能接入
这说明 OneTalk 页面具有图片发送入口,但这不等于调查时的扩展已经具备图片发送能力。当时页面命令要求 `command.content` 是字符串,并交给文本型 `sendUIMessages`这个历史限制已被当前 v6 image outbound contract 与确认路径取代
### 9.2 文件/附件发送
@@ -387,17 +402,17 @@ nodeName
### 9.3 发送确认限制
当前 [send-observation.ts](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts:11) 的待确认状态以字符串 `content` 和消息时间窗口进行关联。对于图片/附件:
调查时 [send-observation.ts](/Users/ybf/code/trade-message-center-worktree/apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts:11) 的待确认状态以字符串 `content` 和消息时间窗口进行关联。对于图片/附件:
- 图片通常没有可比较的文本正文。
- 附件卡片的 HTML/URL 可能在发送前后发生变化。
- `sendImageMessage` 或文件上传回调返回的操作 ID不能直接当作最终消息 ID,必须等待完整的 sent-direction OneTalk 观察消息。
因此图片/附件发送确认不能简单复用“比较 `text`”的逻辑,也不能仅凭 SDK Promise resolve 就写入 Bright。
因此图片/附件发送确认不能简单复用“比较 `text`”的逻辑,也不能仅凭 SDK Promise resolve 就写入 Bright。当前 v6 image 实现先按 candidate message ID 匹配;没有候选 ID 时,才以同会话、sent 方向、image kind、`sizeBytes``md5`、可用 `fileId`、时间窗和唯一性回退,歧义为 `send_ambiguous`。这项代码与自动化测试证据不替代尚未执行的 v6 Chromium 真实发送验收。
## 10. 推荐的实现边界
## 10. 历史建议与当前边界
如果后续开始实现,建议保持原始事实与展示投影分离:
以下建议保留其调查背景;当前 v6 已将 raw 停在 MAIN decoder,并只让 normalized content 下游流转。
### 10.1 共享内容解码器
@@ -427,18 +442,15 @@ Base64 解码 → UTF-8 → JSON.parse → 字段校验
不能只看 `contentType=101`,因为它至少同时承载图片和附件。
### 10.2 原始内容必须继续保留
### 10.2 原始内容不得越过 MAIN 边界
投影结果不应替换原始 `content`。推荐消息同时保留:
历史提案曾建议同时保留原始 `content`;当前 v6 明确禁止这样做。页面桥以后的消息只保留:
```text
content 原始 OneTalk JSON,可用于审计、未知类型和未来兼容
contentType 原始数字类型
media 经过严格校验的可选语义投影
text 仅文本便利字段
content 经过严格校验的 normalized content
```
未知类型进入 `unknown`,并保留原始 JSON;不能为了让 UI 正常而把未知媒体伪装成文本
未知类型不能为了让 UI 正常而伪装成文本,也不得携带原始 JSON 离开 MAIN decoder
### 10.3 URL 与二进制边界
@@ -480,10 +492,12 @@ delivery_unknown
## 12. 结论
当前同步系统已经具备“接收并保存图片/附件原始消息”的基础条件,真正缺口集中在三处:
当前同步系统使 raw 图片/附件止于 MAIN decoder,之后只保存 normalized content;原始样本仍仅用于调查证据。当前 v6 图片合同只保留 `fileId``extension``sizeBytes``isOriginal``md5``previewUrl``urlScope`,不保留 raw `width` / `height`
1.`content.custom.data` 从 Base64 JSON 解码为经过校验的图片/附件语义对象。
2. 在 Bright → Mind 的边界定义媒体字段白名单和 URL 生命周期处理。
3. 为图片/附件发送建立独立输入和基于 sent-direction 事实的确认关联。
尚未完成或未验证的缺口集中在三处:
因此不需要重写 OneTalk WebSocket、MessagePack 解码器、会话锚点或消息幂等机制。下一次实现应从共享内容投影器和测试样本开始,并把真实上传/发送抓包作为单独的运行时验证步骤
1. 对附件的当前运行态与发布范围重新验证
2. 对 URL 生命周期做真实环境验证。
3. 在维护窗口执行 v6 Chromium 图片发送与 live-confirmation 验收。
因此不需要重写 OneTalk WebSocket、MessagePack 解码器、会话锚点或消息幂等机制。后续运行时工作应验证真实上传/发送和确认链路,而不能以这份历史调查替代 v6 现场验收。
+9 -10
View File
@@ -138,16 +138,14 @@ sdkMessage.originalData.params
type OneTalkUrlScope = "onetalk_session";
type OneTalkImageContent = {
version: 1;
kind: "image";
fileId: string;
extension: string;
sizeBytes: number;
width: number;
height: number;
isOriginal: boolean;
md5: string | null;
previewUrl: string;
downloadUrl: null;
previewUrl: string | null;
urlScope: OneTalkUrlScope;
};
@@ -232,11 +230,10 @@ type OneTalkImagePayload = {
fileId: string;
suffix: string;
size: number;
width: number;
height: number;
isOriginal: 0 | 1;
md5: string;
url: string;
// OneTalk raw payload may contain width/height. The MAIN decoder ignores them.
};
```
@@ -244,20 +241,20 @@ type OneTalkImagePayload = {
```ts
return {
version: 1,
kind: "image",
fileId: payload.fileId,
extension: payload.suffix.toLowerCase(),
sizeBytes: payload.size,
width: payload.width,
height: payload.height,
isOriginal: payload.isOriginal === 1,
md5: payload.md5 || null,
previewUrl: payload.url,
downloadUrl: null,
urlScope: "onetalk_session",
};
```
OneTalk raw image payload may retain `width` or `height` as upstream evidence, but the MAIN decoder neither reads nor validates them. They never cross this normalized boundary into page bridge, IndexedDB, Bright, Mind, public reads, or confirmation metadata.
图片 URL 必须是绝对 HTTPS URL,当前允许的运行态 host 为 `clouddisk.alibaba.com`,当前动作是 `fileAction=imagePreview`
## 8. 怎么取附件数据
@@ -400,6 +397,8 @@ Service Worker history request
Raw `content.custom.data` 只能在 MAIN world 短暂存在。页面桥、IndexedDB、Bright 和 Mind 不得继续解析或持久化 OneTalk raw payload。
图片 raw payload 中存在的 `width` / `height` 同样止于该边界:v6 canonical image 只包含 `fileId``extension``sizeBytes``isOriginal``md5``previewUrl``urlScope`(以及内容 `version``kind`)。
## 12. 错误处理
- 非法 Base64`invalid_base64`
@@ -412,7 +411,7 @@ Raw `content.custom.data` 只能在 MAIN world 短暂存在。页面桥、Indexe
## 13. 验收标准
- JPEG 样本被规范化为 `kind="image"`字段与 SDK `originalData` 一致
- JPEG 样本被规范化为 `kind="image"`保留 canonical 白名单字段;SDK `originalData` 中可能存在的 raw `width` / `height` 被 MAIN decoder 忽略
- ZIP、PDF 样本都被规范化为 `kind="file"`,共同满足 `cardType=12`
- ZIP 返回 payload 自带的下载 URL。
- PDF 当前返回 `downloadUrl=null``previewUrl=params.url`
+52 -33
View File
@@ -7,16 +7,20 @@
> 调试环境: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`、文件名、扩展名、大小、宽高、缩略图或下载地址合同。
- 图片和附件当时没有规范化的 `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 归一化类型 |
@@ -28,7 +32,7 @@
本次真实附件样本为 PDF;没有抓到 TXT 附件、实时图片 push 或实时文件 push,因此这些场景不能标记为已验证。
另有一个必须优先修复的安全问题:当前文本内容的 `text.extension.basicMessageInfo` 是一段序列化 JSON,真实样本中包含 `chatToken` 键。页面观察器会复制整个原始 `content`,而 Bright 的清洗器对字符串直接原样放行,因此嵌套在字符串或 Base64 中的敏感字段可能进入持久化。
调查时还发现一个必须优先修复的安全问题:文本内容的 `text.extension.basicMessageInfo` 是一段序列化 JSON,真实样本中包含 `chatToken` 键。页面观察器会复制整个原始 `content`,而 Bright 的清洗器对字符串直接原样放行,因此嵌套在字符串或 Base64 中的敏感字段可能进入持久化。
## 2. 调查范围与证据边界
@@ -188,7 +192,7 @@ SDK hasMoreboolean
WebSocket hasMore0 | 1
```
SDK `list[]` 条目确认包含:
调查时 SDK `list[]` 条目确认包含:
```text
autoReply
@@ -217,6 +221,8 @@ uuid
viewType
```
这份 SDK 字段清单是历史 raw/boundary-only 证据,包含的 `originalData.width` / `originalData.height` 只可在 MAIN decoder 边界被忽略,不能成为 canonical 字段、存储字段或下游输入。
其中:
- `content` 已经被 SDK 转成展示字符串。
@@ -313,6 +319,8 @@ Base64 解码结果:JSON object
}
```
这是 OneTalk raw payload 证据,不是 normalized contract。raw `width` / `height` 可保留在此样本中,但 v6 MAIN decoder 忽略它们,绝不将其传过页面桥或写入 canonical content。
### 6.3 SDK 归一化结果
```text
@@ -339,15 +347,15 @@ width
```ts
type OneTalkImageContent = {
version: 1;
kind: "image";
fileId: string;
suffix: string;
extension: string;
sizeBytes: number;
width: number;
height: number;
isOriginal: boolean;
md5?: string;
sourceUrl: string;
md5: string | null;
previewUrl: string | null;
urlScope: "onetalk_session";
};
```
@@ -357,9 +365,9 @@ type OneTalkImageContent = {
- `custom.data` 必须是有大小上限的合法 Base64。
- Base64 解码结果必须是 UTF-8 JSON object。
- `fileId``suffix``url` 必须为非空字符串。
- `size``width``height` 必须为有限非负整数,并设置合理上限
- `size` 必须为有限非负整数,并设置合理上限;raw `width` / `height` 被 MAIN decoder 忽略,不参与校验或输出
- `isOriginal``0/1` 显式转换为 boolean。
- `url` 只接受绝对 HTTPS URL,并在确认真实主机后加入固定 host allowlist
- `url` 只接受绝对 HTTPS URL,并在确认真实主机后映射为 `previewUrl``urlScope` 固定为 `onetalk_session`
- 不根据 `suffix` 猜造 OneTalk 未提供的 MIME;UI 可以使用安全扩展名映射做展示提示。
## 7. 文件附件原始格式
@@ -507,7 +515,7 @@ decoded.params 通过文件 schema
其它合法但未支持的 `cardType` 应返回受控的 `unsupported` 内容,不应丢弃整条消息,也不应保留完整 raw payload。
## 9. 当前代码为什么表现为“只有文本”
## 9. 调查时的代码为什么表现为“只有文本”
### 9.1 页面观察器
@@ -557,7 +565,7 @@ onetalk_sync_candidates
### 9.3 共享协议与 Bright
当前 `OneTalkMessage` 定义:
调查时的 `OneTalkMessage` 定义:
```ts
type OneTalkMessage = {
@@ -586,7 +594,7 @@ text text nullable
content jsonb
```
因此 Bright 能保存媒体 raw JSON,但不能告诉 Mind
因此调查时的 Bright 能保存媒体 raw JSON,但不能告诉 Mind
```text
这是图片还是文件
@@ -617,7 +625,7 @@ unsupported fallback
真实 Mind UI 如果只读取 `message.text`,媒体消息自然不可见。
## 10. 当前安全风险
## 10. 调查时发现的安全风险
### 10.1 字符串内部的敏感字段绕过清洗
@@ -646,7 +654,7 @@ 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”的不变量。
真实 `basicMessageInfo` 解析后已确认含有 `chatToken` 键。调查时的实现把完整 raw content 交给 Bright,违反了数据库注释中“不得写入带认证信息的完整 envelope”的不变量。
### 10.2 推荐的安全边界
@@ -689,7 +697,20 @@ Authorization
在上述行为没有真实验证前,只能称其为 `sourceUrl`,不能承诺“Mind 可直接下载”。
## 11. 推荐的目标内容合同
## 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` 成为消息展示内容的唯一事实源:
@@ -700,15 +721,15 @@ type OneTalkNormalizedContent =
text: string;
}
| {
version: 1;
kind: "image";
fileId: string;
suffix: string;
extension: string;
sizeBytes: number;
width: number;
height: number;
isOriginal: boolean;
md5?: string;
sourceUrl: string;
md5: string | null;
previewUrl: string | null;
urlScope: "onetalk_session";
}
| {
kind: "file";
@@ -740,7 +761,7 @@ message.content.kind === "text" ? message.content.text : null;
## 12. 可以做到什么
### 12.1 当前已经做到
### 12.1 调查时已经做到
- 观察文本、图片和 custom card 的原始 WebSocket content。
- 使用 raw `channelAccountId + conversationId + messageId` 保持消息幂等。
@@ -769,7 +790,7 @@ message.content.kind === "text" ? message.content.text : null;
- 支持音频、视频、语音、压缩包或所有 OneTalk custom card:没有真实样本和枚举。
- checkpoint 已完整收敛:本次末态仍为 `uploading/succeeded`
## 13. 推荐 PRD 要求
## 13. 当前 v6 不变量
### R1. 单一内容合同
@@ -799,7 +820,7 @@ channelAccountId + conversationId + raw messageId
- URL 只允许 HTTPS 和固定 OneTalk host allowlist。
- URL query/hash/userinfo 不进入日志或错误。
- 文件大小、宽高和时间字段必须有上下限。
- 文件大小和时间字段必须有上下限;图片 raw `width` / `height` 由 MAIN decoder 忽略,不能成为 normalized 字段、匹配条件或展示数据
- 文件扩展名规范化为小写有限字符集。
- 文件名按纯文本处理并限制长度。
- MD5 只能作为来源元数据,不能替代消息 ID 或安全签名。
@@ -860,7 +881,7 @@ aliIdEncrypt
## 14. 推荐验收标准
- [ ] 真实文本历史消息归一化为 `{ kind: "text", text }`,并且 content 不含 extension。
- [ ] 真实 JPEG 样本从 `custom.type=7` Base64 JSON 归一化为 image,尺寸、大小、后缀与真实载荷一致
- [ ] 真实 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,不影响同批其它消息。
@@ -872,7 +893,7 @@ aliIdEncrypt
- [ ] 获得真实 live 图片和附件 push 后证明历史与实时共用同一 decoder。
- [ ] 验证媒体 URL 在有 Cookie、无 Cookie、跨 Origin、过期和跳转场景的行为,再决定保存 URL、保存 ID 或增加受控代理。
## 15. 相关代码位置
## 15. 调查时的相关代码位置
| 层 | 文件 | 当前行为 |
| -------------- | --------------------------------------------------------------------------------- | --------------------------------------- |
@@ -890,15 +911,13 @@ aliIdEncrypt
## 16. 最终判断
OneTalk 当前真实载荷已经提供实现图片和文件同步所需的核心元数据,且 raw `custom.data` 可以在页面内稳定识别为 Base64 JSON。第一阶段不需要下载媒体文件,也不需要新建第二套消息表;可以在现有消息链上增加一个唯一的内容 decoder 和跨层 typed contract。
实现工作的核心不是“把 `contentType=101` 放行”,因为当前已经放行;真正需要完成的是:
历史调查证明 OneTalk raw payload 提供实现图片和文件同步所需的核心元数据。当前 v6 不再沿用该报告中 raw content 跨页面桥、IndexedDB、Bright 或 Mind 的历史路径;实现核心已经收敛为 MAIN decoder 的单一白名单边界:
```text
opaque raw content
→ 严格、安全、可测试的 text/image/file/unsupported 合同
→ Bright 持久化与事件保持同一语义
→ Mind 按 kind 展示
→ Bright 持久化与事件只使用 normalized content
→ Mind read model 只接收 normalized content
```
同时必须先关闭 raw 字符串中嵌套凭证可能进入 Bright 的安全缺口,否则新增媒体支持会进一步扩大敏感 payload 的存储范围
raw 字符串中嵌套凭证进入 Bright 是本报告记录的历史风险;当前 v6 边界以不让 raw payload 离开 MAIN decoder 的方式关闭它。真实 v6 Chromium 运行态验收仍未执行