mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
docs(onetalk): record image send observation feasibility
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
# OneTalk 非当前会话图片发送与 WebSocket 观测可行性报告
|
||||
|
||||
> 日期:2026-09-10
|
||||
> 性质:测试环境运行态调查与实现可行性报告,不是 Trellis task,不包含代码实现
|
||||
> 范围:本地图片发送到非当前打开会话,以及通过 WebSocket observer 确认 sent 图片事实
|
||||
|
||||
## 1. 结论
|
||||
|
||||
本次调查确认两件事:
|
||||
|
||||
1. **可以在页面当前打开其它会话时,向显式指定的目标会话发送图片。** 最终路由由发送参数中的 `cid` 决定,不要求切换页面 selected conversation。
|
||||
2. **可以在 WebSocket 观测阶段使用图片的大小、宽度和高度辅助确认发送结果。** live WS 图片会被 MAIN-world 解码器归一化为 `content.kind="image"`,并保留 `sizeBytes`、`width`、`height`。
|
||||
|
||||
但 `sizeBytes + width + height` 不是唯一键。本次连续发送同一图片后,扩展存储中出现了两条不同的 live sent 消息,它们的三个字段完全相同。因此该三元组只能作为复合匹配条件,不能单独生成 `confirmed_sent`。
|
||||
|
||||
推荐确认顺序:
|
||||
|
||||
```text
|
||||
可靠候选 messageId
|
||||
→ conversationId + direction=sent
|
||||
→ content.kind=image
|
||||
→ post-upload sizeBytes + width + height
|
||||
→ 短时间窗口
|
||||
→ 必须唯一匹配,否则 send_ambiguous
|
||||
```
|
||||
|
||||
## 2. 调查范围与安全边界
|
||||
|
||||
本次使用已启动的 Chromium CDP `127.0.0.1:9222`,目标页面为:
|
||||
|
||||
```text
|
||||
https://onetalk.alibaba.com/message/weblitePWA.htm
|
||||
```
|
||||
|
||||
运行环境:
|
||||
|
||||
| 项目 | 结果 |
|
||||
| ---------------------------------------- | ------------------------- |
|
||||
| CDP protocol | `1.3` |
|
||||
| OneTalk host | `seller_pwa` |
|
||||
| SaaS channel | `true` |
|
||||
| GGS proxy | `false` |
|
||||
| 活动 chat bundle | `im-weblite-chat/1.0.700` |
|
||||
| 页面 selected conversation | 恰好一个 |
|
||||
| selected conversation 与目标会话 | 不同 |
|
||||
| 发送过程中是否切换 selected conversation | 否 |
|
||||
|
||||
测试图片记为 `I1`:
|
||||
|
||||
```text
|
||||
格式:JPEG
|
||||
文件大小:370256 bytes
|
||||
像素尺寸:1192 × 1188
|
||||
```
|
||||
|
||||
报告不记录以下内容:
|
||||
|
||||
- 真实目标会话 ID;
|
||||
- 登录账号、联系人及加密身份值;
|
||||
- Cookie、token、请求头和 POST body;
|
||||
- MD5 实值、fileId、messageId;
|
||||
- 完整媒体 URL 及 URL query。
|
||||
|
||||
## 3. 本次运行态证据
|
||||
|
||||
### 3.1 非当前会话定向发送
|
||||
|
||||
发送前确认:
|
||||
|
||||
- 当前 selected conversation 与目标会话不同;
|
||||
- 目标会话在 `window.__conversationListFullData__` 中唯一存在;
|
||||
- 文件选择器中保留一个与 `I1` 对应的 `File`;
|
||||
- 页面目标组件具备 `sendFileToOss`;
|
||||
- 目标是单聊会话。
|
||||
|
||||
执行时显式向 `sendFileToOss` 传入目标会话对象,没有修改页面 selected conversation。
|
||||
|
||||
运行结果:
|
||||
|
||||
| 证据 | 结果 |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| `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 图片事实才排除了“只插入了页面假消息”的情况。
|
||||
|
||||
### 3.2 live WebSocket 归一化证据
|
||||
|
||||
扩展 Service Worker 的 IndexedDB 中,对目标会话和 `I1` 的归一化字段进行只读匹配:
|
||||
|
||||
| 项目 | 结果 |
|
||||
| ------------------------------------ | ---------------- |
|
||||
| exact image candidate | 3 条 |
|
||||
| `observationSource="live"` | 2 条 |
|
||||
| `observationSource="history"` | 1 条 |
|
||||
| candidate status | 全部 `confirmed` |
|
||||
| live 记录的 `sizeBytes/width/height` | 与 `I1` 完全一致 |
|
||||
|
||||
手工调用的只读历史接口没有把返回值送入页面 bridge 或 Service Worker;同时 raw WebSocket history response 会被 observer 主动忽略。因此 `observationSource="live"` 的两条记录证明,图片大小和宽高确实能经过 live WS observer 到达归一化存储边界。
|
||||
|
||||
这两条 live 消息也构成反例:相同图片重复发送时,大小和宽高完全相同,但它们是不同的消息事实。
|
||||
|
||||
## 4. 图片发送路径与参数
|
||||
|
||||
### 4.1 页面普通本地图片路径
|
||||
|
||||
OneTalk 页面普通图片发送链路为:
|
||||
|
||||
```text
|
||||
隐藏的 input[type=file][multiple]
|
||||
→ FileList 转成 File[],为每个 File 分配 uid
|
||||
→ Upload.beforeUpload 返回 false,阻止通用 Upload 自动请求
|
||||
→ OneTalk beforeUpload(File) 接管
|
||||
→ 文件校验、类型判断、本地 preview URL
|
||||
→ 适用时压缩图片
|
||||
→ 计算文件 MD5
|
||||
→ prepareSendFileWithGroup
|
||||
→ OSS 上传,或命中文件已存在后跳过上传
|
||||
→ buildFileRelationWithGroup
|
||||
→ sendFile
|
||||
→ messageBox/sendMessage
|
||||
→ getMessageServiceV2().sendUIMessages(...)
|
||||
→ PaaS send / live WebSocket echo
|
||||
```
|
||||
|
||||
页面 UI 还会用 `tmpKey` 插入本地假消息和上传进度。假消息只属于展示层,不是 sent 消息事实。
|
||||
|
||||
### 4.2 文件输入参数
|
||||
|
||||
文件选择阶段的核心对象是浏览器 `File`:
|
||||
|
||||
```ts
|
||||
type UploadImageFile = File & {
|
||||
uid: string;
|
||||
};
|
||||
```
|
||||
|
||||
业务流程实际使用的字段包括:
|
||||
|
||||
```text
|
||||
file.name
|
||||
file.type
|
||||
file.size
|
||||
file.uid
|
||||
```
|
||||
|
||||
图片大于等于约 `1_000_000` bytes 时,页面会尝试压缩。压缩后的 `File` 才进入后续上传,因此原始 `file.size` 不一定等于最终消息中的 `originalData.size`。
|
||||
|
||||
### 4.3 目标会话参数
|
||||
|
||||
本地图片上传不只需要 `cid`。建立云盘文件关系和构造消息还需要目标会话的完整页面模型,至少涉及:
|
||||
|
||||
```ts
|
||||
type TargetConversationContext = {
|
||||
cid: string;
|
||||
conversationType: number;
|
||||
accountId: string | number;
|
||||
accountIdEncrypt?: string;
|
||||
aliId?: string | number;
|
||||
aliIdEncrypt?: string;
|
||||
chatToken?: string;
|
||||
contact?: {
|
||||
aliId?: string | number;
|
||||
};
|
||||
owner?: {
|
||||
accountId?: string | number;
|
||||
aliId?: string | number;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
这些字段只应留在 OneTalk MAIN world 内使用,不应作为新的 Bright 或 Mind 原始合同整体向外传递。
|
||||
|
||||
文件关系使用的发送双方参数形态为:
|
||||
|
||||
```ts
|
||||
type FileRelationParticipants = {
|
||||
from: string | number;
|
||||
to: string | number;
|
||||
fromAliId: string | number;
|
||||
toAliId: string | number;
|
||||
};
|
||||
```
|
||||
|
||||
本次定向发送传给页面内部上传函数的参数等价于:
|
||||
|
||||
```ts
|
||||
sendFileToOss({
|
||||
file,
|
||||
fromTo: {
|
||||
from: currentUserAccountId,
|
||||
to: targetConversation.accountId,
|
||||
fromAliId: targetConversation.owner?.aliId,
|
||||
toAliId: targetConversation.contact?.aliId ?? targetConversation.aliId,
|
||||
},
|
||||
tmpKey,
|
||||
contact: targetConversation,
|
||||
previewUrl,
|
||||
traceId,
|
||||
});
|
||||
```
|
||||
|
||||
### 4.4 准备上传参数
|
||||
|
||||
页面通过 MTop 调用:
|
||||
|
||||
```text
|
||||
mtop.alibaba.interaction.clouddisk.prepareSendFileWithGroup
|
||||
```
|
||||
|
||||
核心业务参数:
|
||||
|
||||
```ts
|
||||
{
|
||||
appKey: "OneChat",
|
||||
fileName: `${md5}.${extension}`,
|
||||
scene: JSON.stringify({
|
||||
sceneType: isGroupChat ? "2" : "1",
|
||||
idType: "2",
|
||||
from: fromAliId,
|
||||
to: toAliId,
|
||||
}),
|
||||
isToken: false,
|
||||
}
|
||||
```
|
||||
|
||||
较大文件存在单独的 token/上传分支,不能假设所有文件始终使用完全相同的 prepare 参数。
|
||||
|
||||
prepare 成功后会返回上传目录、最大文件限制、文件是否已存在和 OSS policy。报告只记录这些字段是否存在,不记录其值。
|
||||
|
||||
### 4.5 OSS 上传参数
|
||||
|
||||
未命中去重时,页面向 prepare 返回的动态 OSS host 上传:
|
||||
|
||||
```ts
|
||||
{
|
||||
file,
|
||||
filename,
|
||||
action: policyDTO.host,
|
||||
data: {
|
||||
key,
|
||||
policy,
|
||||
OSSAccessKeyId,
|
||||
success_action_status: "200",
|
||||
callback,
|
||||
signature,
|
||||
},
|
||||
onProgress,
|
||||
}
|
||||
```
|
||||
|
||||
本次 `I1` 命中文件已存在分支,因此没有实际发生 OSS 二进制 POST;这不影响后续文件关系建立和消息发送。
|
||||
|
||||
### 4.6 建立文件关系参数
|
||||
|
||||
页面随后调用:
|
||||
|
||||
```text
|
||||
mtop.alibaba.interaction.clouddisk.buildFileRelationWithGroup
|
||||
```
|
||||
|
||||
概念参数为:
|
||||
|
||||
```ts
|
||||
{
|
||||
appKey: "OneChat",
|
||||
scene: {
|
||||
sceneType,
|
||||
idType,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
node: {
|
||||
nodeName,
|
||||
nodeSize,
|
||||
materialType,
|
||||
md5,
|
||||
bucket,
|
||||
extend,
|
||||
},
|
||||
params: {
|
||||
source: "",
|
||||
needCardUrl: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
成功结果提供后续 `sendFile` 所需的媒体关系,例如 `fileId`、`fileCardUrl`、`redirectFileUrl`、图片尺寸和文件节点信息。
|
||||
|
||||
### 4.7 最终页面发送参数
|
||||
|
||||
图片扩展名被归类为 `imageCard`。页面的 `sendFile` 构造:
|
||||
|
||||
```ts
|
||||
const data = {
|
||||
cid: targetConversation.cid,
|
||||
mediaInfo: {
|
||||
...relationInfo,
|
||||
nodeName,
|
||||
},
|
||||
baasMsgType: 102,
|
||||
content: relationInfo.fileCardUrl,
|
||||
msgType: 60,
|
||||
accountId: targetConversation.accountId,
|
||||
accountIdEncrypt: targetConversation.accountIdEncrypt,
|
||||
bizType,
|
||||
bizId,
|
||||
clientVersion,
|
||||
chatToken: targetConversation.chatToken,
|
||||
};
|
||||
|
||||
sendMessage({
|
||||
sendType: "file",
|
||||
tmpKey,
|
||||
data,
|
||||
traceId,
|
||||
contentType: "imageCard",
|
||||
scene: "_sendFile",
|
||||
callback,
|
||||
});
|
||||
```
|
||||
|
||||
`messageBox/sendMessage` 会补齐页面 scene、`ext`、client 信息和必要身份字段,当前 SaaS 分支最终调用:
|
||||
|
||||
```ts
|
||||
window.IcbuIM.IMBaaSSDK.default.getMessageServiceV2().sendUIMessages(data);
|
||||
```
|
||||
|
||||
V2 转换器显式以 `cid` 选择会话:
|
||||
|
||||
```ts
|
||||
conversationCode = input.cid || sdkContext.cid;
|
||||
```
|
||||
|
||||
因此只要传入非空目标 `cid`,当前页面 selected conversation 不参与最终路由;缺失 `cid` 时才会回退 SDK 当前上下文,存在发错会话风险。
|
||||
|
||||
图片被转换为 BaaS `originalData`:
|
||||
|
||||
```ts
|
||||
{
|
||||
height,
|
||||
width,
|
||||
url: redirectFileUrl,
|
||||
isOriginal: 1,
|
||||
size: nodeSize ?? size,
|
||||
md5,
|
||||
fileId,
|
||||
suffix: materialType,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.8 `sendImageMessage` 快捷入口
|
||||
|
||||
页面 SDK 还暴露:
|
||||
|
||||
```ts
|
||||
messageService.sendImageMessage({
|
||||
cid: targetConversationId,
|
||||
picUrl: [uploadedImageUrl],
|
||||
});
|
||||
```
|
||||
|
||||
它对应 SDK command:
|
||||
|
||||
```text
|
||||
im.singlemsg.sendImageMessage
|
||||
```
|
||||
|
||||
该入口适用于已经拥有可用图片 URL 的场景。它不能直接接收本地 `File`;本次本地文件定向发送验证走的是 `sendFileToOss → sendFile → sendUIMessages`,没有把 `sendImageMessage` 快捷入口冒充为已验证的本地上传路径。
|
||||
|
||||
## 5. WebSocket 观测方法
|
||||
|
||||
### 5.1 观测入口
|
||||
|
||||
扩展 MAIN-world observer 只旁路观察:
|
||||
|
||||
```text
|
||||
wss-icbu.dingtalk.com
|
||||
```
|
||||
|
||||
入口实现位于:
|
||||
|
||||
- [`websocket.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/websocket.ts)
|
||||
- [`index.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts)
|
||||
- [`new.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts)
|
||||
|
||||
处理顺序:
|
||||
|
||||
```text
|
||||
WebSocket message event
|
||||
→ 跳过 heartbeat
|
||||
→ JSON.parse
|
||||
→ 要求 frame.code === 200
|
||||
→ 忽略 body.userMessageModels 历史响应
|
||||
→ 仅处理 live body[]
|
||||
→ singleChatUserConversation.lastMessage.message
|
||||
→ 验证消息身份和 sent/received 方向
|
||||
→ 解码 content
|
||||
→ 输出 ObservedOneTalkMessage
|
||||
```
|
||||
|
||||
MessagePack `/s/sync` push 当前不会被当作普通 live 媒体消息处理,因此不能将 JSON live 路径的结论直接外推到所有 WebSocket frame。
|
||||
|
||||
### 5.2 图片内容解码
|
||||
|
||||
live 图片的 raw 内容形态:
|
||||
|
||||
```ts
|
||||
{
|
||||
contentType: 101,
|
||||
custom: {
|
||||
type: 7,
|
||||
data: "<Base64 JSON>",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
[`content-decoder.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts) 在 MAIN world 执行:
|
||||
|
||||
```text
|
||||
Base64
|
||||
→ UTF-8
|
||||
→ JSON.parse
|
||||
→ exact field validation
|
||||
→ normalized image content
|
||||
```
|
||||
|
||||
输出合同定义于 [`content.ts`](../apps/onetalk-contract/src/content.ts):
|
||||
|
||||
```ts
|
||||
type OneTalkImageContent = {
|
||||
version: 1;
|
||||
kind: "image";
|
||||
fileId: string;
|
||||
extension: string;
|
||||
sizeBytes: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isOriginal: boolean;
|
||||
md5: string | null;
|
||||
previewUrl: string | null;
|
||||
urlScope: "onetalk_session";
|
||||
};
|
||||
```
|
||||
|
||||
send correlator 不需要也不应该继续读取 raw `originalData`。应比较已经验证、清洗后的:
|
||||
|
||||
```text
|
||||
message.content.sizeBytes
|
||||
message.content.width
|
||||
message.content.height
|
||||
```
|
||||
|
||||
### 5.3 observer 与 correlator 的调用顺序
|
||||
|
||||
[`page-script-entry.ts`](../apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts) 当前顺序是:
|
||||
|
||||
```ts
|
||||
sendObservation.observe(batch.messages);
|
||||
observedSink(batch);
|
||||
```
|
||||
|
||||
因此图片 live batch 在跨 MAIN bridge、写 IndexedDB 或上传 Bright 之前,已经可以交给发送确认 correlator。无需新增第二个 WebSocket observer,也不应增加另一套 raw payload parser。
|
||||
|
||||
## 6. 建议的图片确认模型
|
||||
|
||||
### 6.1 Pending 数据
|
||||
|
||||
现有 [`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts) 的 `PendingSend` 只保存字符串正文。图片支持应改为判别联合,而不是给文本结构追加一组可选字段:
|
||||
|
||||
```ts
|
||||
type PendingTextSend = {
|
||||
kind: "text";
|
||||
conversationId: string;
|
||||
content: string;
|
||||
sentAfterMs: number;
|
||||
candidateMessageIds: Set<string>;
|
||||
};
|
||||
|
||||
type PendingImageSend = {
|
||||
kind: "image";
|
||||
conversationId: string;
|
||||
sentAfterMs: number;
|
||||
candidateMessageIds: Set<string>;
|
||||
expected: {
|
||||
sizeBytes: number;
|
||||
width: number;
|
||||
height: number;
|
||||
md5?: string | null;
|
||||
fileId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type PendingSend = PendingTextSend | PendingImageSend;
|
||||
```
|
||||
|
||||
`md5` 和 `fileId` 可以提高不同图片之间的区分度,但同一文件去重或重复发送时它们也可能相同,仍不能当作每次发送的唯一 ID。
|
||||
|
||||
### 6.2 登记时机
|
||||
|
||||
图片 pending 必须在以下时机登记:
|
||||
|
||||
```text
|
||||
压缩完成
|
||||
→ 上传/去重完成
|
||||
→ buildFileRelationWithGroup
|
||||
→ 得到最终 mediaInfo
|
||||
→ 登记 PendingImageSend
|
||||
→ 立即调用 sendUIMessages
|
||||
```
|
||||
|
||||
更准确地说,应在 `buildFileRelationWithGroup` 成功、`sendFile` 已拿到最终 `nodeSize/width/height` 后,并在最终 SDK send 之前登记。
|
||||
|
||||
不能在用户选择原始文件时登记,原因有两个:
|
||||
|
||||
1. 图片压缩可能改变 `sizeBytes`,甚至改变尺寸;
|
||||
2. 上传时间可能超过当前 correlator 的 `10_000ms` 超时。
|
||||
|
||||
上传阶段和消息发送确认阶段应是两个状态,不要让消息确认定时器覆盖完整上传耗时。
|
||||
|
||||
### 6.3 匹配顺序
|
||||
|
||||
建议匹配逻辑:
|
||||
|
||||
```text
|
||||
1. message.direction 必须是 sent
|
||||
2. message.conversationId 必须等于 pending.conversationId
|
||||
3. message 必须通过完整 OneTalkMessage guard
|
||||
4. 如果存在可靠 candidateMessageId:只按 messageId 匹配,不回退媒体指纹
|
||||
5. 否则要求 pending.kind=image 且 message.content.kind=image
|
||||
6. 比较 post-upload sizeBytes、width、height
|
||||
7. 可选比较 md5/fileId,但不能把它们当作单次发送唯一键
|
||||
8. 要求消息位于 pending 生命周期和允许的时钟偏差内
|
||||
9. 一个消息必须只匹配一个 pending;多个匹配立即 send_ambiguous
|
||||
```
|
||||
|
||||
概念判断:
|
||||
|
||||
```ts
|
||||
const imageMatches = (
|
||||
expected: PendingImageSend["expected"],
|
||||
actual: OneTalkImageContent,
|
||||
): boolean =>
|
||||
actual.sizeBytes === expected.sizeBytes &&
|
||||
actual.width === expected.width &&
|
||||
actual.height === expected.height;
|
||||
```
|
||||
|
||||
该函数只能是复合匹配的一部分,不能绕过 conversation、direction、时间窗口和唯一性检查。
|
||||
|
||||
## 7. 验证方法
|
||||
|
||||
### 7.1 自动化测试
|
||||
|
||||
至少需要补充以下测试:
|
||||
|
||||
| 用例 | 预期 |
|
||||
| -------------------------- | ----------------------------------------------------------- |
|
||||
| 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 |
|
||||
|
||||
现有测试已经覆盖 raw 图片解码、flat history 图片归一化、非法 live 媒体隔离和文本 send confirmation;尚缺成功 live 图片直接驱动 correlator 的用例。
|
||||
|
||||
### 7.2 Chromium/CDP 联调
|
||||
|
||||
推荐的最小真实联调步骤:
|
||||
|
||||
1. 打开会话 A,但指定目标会话 B。
|
||||
2. 记录 selected conversation 的内部比较结果,不输出真实 ID。
|
||||
3. 选择一张已知大小和宽高的测试图片。
|
||||
4. 对大图额外记录压缩后的最终 metadata。
|
||||
5. 在最终 `sendUIMessages` 前登记 image pending。
|
||||
6. 观察 prepare、OSS/去重、build relation 和 BaaS send 的状态。
|
||||
7. 捕获 live WS normalized message,并验证来源为 `live`。
|
||||
8. 断言 selected conversation 未变化。
|
||||
9. 断言目标会话只读历史存在同一 sent 图片。
|
||||
10. 断言 correlator 返回完整 `confirmed_sent`,而不是仅凭页面 success event。
|
||||
|
||||
### 7.3 完成判据
|
||||
|
||||
只有同时满足以下条件,才算图片发送确认链闭合:
|
||||
|
||||
```text
|
||||
SDK send 已执行
|
||||
+ live WS observer 输出完整 sent OneTalkMessage
|
||||
+ conversationId 精确匹配目标会话
|
||||
+ candidate ID 或媒体复合指纹唯一匹配
|
||||
+ 完整消息通过 OneTalkMessage guard
|
||||
```
|
||||
|
||||
以下证据不能单独生成 `confirmed_sent`:
|
||||
|
||||
- `prepareSendFileWithGroup` HTTP 200;
|
||||
- OSS 上传成功;
|
||||
- `buildFileRelationWithGroup` HTTP 200;
|
||||
- 页面出现图片或上传进度;
|
||||
- 页面 `send-msg-success`;
|
||||
- SDK Promise resolve;
|
||||
- 只看到 WebSocket 帧数量增加,但没有解析业务消息;
|
||||
- 历史接口稍后出现图片,但实时 correlator 没有观察到对应 live 消息。
|
||||
|
||||
历史查询适合作为联调和审计的独立验证,不应成为每次实时发送确认的必需轮询路径。
|
||||
|
||||
## 8. 注意点与风险
|
||||
|
||||
### 8.1 大小与宽高不唯一
|
||||
|
||||
同一图片重复发送会产生不同的 messageId,但 `sizeBytes/width/height` 完全相同。本次运行态已经得到两条这样的 live sent 记录。
|
||||
|
||||
不得采用:
|
||||
|
||||
```text
|
||||
找到第一条同尺寸图片 → confirmed_sent
|
||||
```
|
||||
|
||||
### 8.2 压缩改变待比较字段
|
||||
|
||||
本次 `I1` 小于压缩阈值,最终消息大小与本地文件一致。大图会先压缩,必须使用压缩后、上传关系返回的实际 metadata。
|
||||
|
||||
### 8.3 URL 不能作为稳定关联键
|
||||
|
||||
媒体 URL 可能包含临时授权、重定向和会话作用域,上传前后也可能改变。`previewUrl`、`redirectFileUrl`、`fileCardUrl` 不适合作为首要关联键,更不能写入普通诊断日志。
|
||||
|
||||
### 8.4 MD5 和 fileId 也不是每次发送唯一键
|
||||
|
||||
它们能区分多数不同文件,但文件去重意味着重复发送同一图片时可能继续相同。真正可靠的候选 messageId 应优先于所有媒体指纹。
|
||||
|
||||
### 8.5 本地受理不等于真实发送
|
||||
|
||||
V2 `sendUIMessages` 的 Promise 可以在 local callback 得到 clientId/opId 后 resolve。后续 PaaS change 和 live sent message 才能证明消息事实。
|
||||
|
||||
不能把 clientId/opId 不经验证地当成最终 messageId。如果需要以候选 ID 为主键,必须先验证它与 live messageId 的稳定映射。
|
||||
|
||||
### 8.6 目标会话上下文要求
|
||||
|
||||
本次目标会话已经存在于 `window.__conversationListFullData__`,因此能够取得文件关系所需的双方身份、账号和 chat token。只有一个任意 `cid`、但没有完整目标会话上下文时,本地文件上传路径尚未验证为安全可用。
|
||||
|
||||
不应从会话 ID 字符串、participant 顺序或当前页面联系人猜测缺失身份字段。
|
||||
|
||||
### 8.7 群聊未验证
|
||||
|
||||
页面上传代码存在 group scene 分支,但本次仅验证单聊。群聊 participant、关系建立和 live message envelope 需要独立验证。
|
||||
|
||||
### 8.8 不确定发送禁止自动重试
|
||||
|
||||
上传完成后如果 SDK 连接丢失、live echo 缺失或多个 pending 发生歧义,应返回 `delivery_unknown`,不得自动重发图片,以免产生重复消息。
|
||||
|
||||
### 8.9 保持单一解析边界
|
||||
|
||||
raw `originalData` 只应在 MAIN world 短暂存在。send correlator 应消费已经归一化的 `OneTalkImageContent`,不要在 correlator、Service Worker 或 Bright 中再实现第二套 Base64/JSON 图片解析。
|
||||
|
||||
## 9. 实现边界建议
|
||||
|
||||
该能力技术上可行,建议后续实现限定为:
|
||||
|
||||
1. 为页面图片发送定义独立、最小的输入合同;
|
||||
2. 在上传关系成功后生成 post-upload image fingerprint;
|
||||
3. 将 `PendingSend` 改成 text/image 判别联合;
|
||||
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 回退条件。本报告确认的是图片扩展方案可行,不表示当前插件已经具备图片发送命令和图片发送确认合同。
|
||||
|
||||
## 10. 相关代码
|
||||
|
||||
- 页面 WebSocket tap:[`websocket.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/websocket.ts)
|
||||
- live frame 分发:[`index.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/index.ts)
|
||||
- live message 解析:[`new.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/new.ts)
|
||||
- MAIN 内容归一化:[`content-decoder.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts)
|
||||
- 归一化媒体合同:[`content.ts`](../apps/onetalk-contract/src/content.ts)
|
||||
- observer/correlator 接线:[`page-script-entry.ts`](../apps/chrome-extension/src/onetalk/main-page/page-script-entry.ts)
|
||||
- 当前文本发送确认:[`send-observation.ts`](../apps/chrome-extension/src/onetalk/main-page/message-observer/send-observation.ts)
|
||||
- 当前页面命令入口:[`page-command.ts`](../apps/chrome-extension/src/onetalk/main-page/current-conversation-history/page-command.ts)
|
||||
- 媒体解码测试:[`onetalk-media-content-decoder.test.js`](../apps/chrome-extension/test/onetalk-media-content-decoder.test.js)
|
||||
- WebSocket observer 测试:[`onetalk-websocket-tap.test.js`](../apps/chrome-extension/test/onetalk-websocket-tap.test.js)
|
||||
Reference in New Issue
Block a user