mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
feat: 移除 OneTalk 图片宽高链路
This commit is contained in:
@@ -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 evidence,v6 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 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 忽略 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 decoder(raw 只停留在此处)
|
||||
→ 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 现场验收。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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 content,Bright 不保存 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 hasMore:boolean
|
||||
WebSocket hasMore:0 | 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 decoder(raw 仅在此处出现)
|
||||
→ 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 contract,raw `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 运行态验收仍未执行。
|
||||
|
||||
Reference in New Issue
Block a user