Files
trade-message-center/docs/onetalk-image-send-observation-feasibility.md

697 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OneTalk 非当前会话图片发送与 WebSocket 观测可行性报告
> 日期: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 观测阶段使用图片的已验证 canonical metadata 辅助确认发送结果。** v6 MAIN-world decoder 将 live WS 图片归一化为 `content.kind="image"`,只保留 `fileId``extension``sizeBytes``isOriginal``md5``previewUrl``urlScope`
`sizeBytes` 不是唯一键;即使 `md5``fileId` 可用,重复发送同一文件时也可能相同。本次连续发送同一图片后,扩展存储中出现了两条不同的 live sent 消息。因此 metadata 只能作为无 candidate ID 时的复合匹配条件,不能单独生成 `confirmed_sent`
推荐确认顺序:
```text
可靠候选 messageId
→ conversationId + direction=sent
→ content.kind=image
→ post-upload sizeBytes + md5 + optional fileId
→ 短时间窗口
→ 必须唯一匹配,否则 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 记录的 raw `sizeBytes/width/height` | 与 `I1` 完全一致 |
手工调用的只读历史接口没有把返回值送入页面 bridge 或 Service Worker;同时 raw WebSocket history response 会被 observer 主动忽略。因此 `observationSource="live"` 的两条记录证明当时的 live 观察链已能接收图片事实。该记录来自 v6 前的调查,不构成对当前 v6 normalized shape 的运行态验证:当前 MAIN decoder 忽略 raw `width` / `height`,它们不进入归一化存储边界。
这两条 live 消息也构成反例:相同图片重复发送时,媒体 metadata 可以相同,但它们仍是不同的消息事实。
## 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` 和文件节点信息;若上游关系含图片尺寸,它们仍只属于 raw upload evidencev6 downstream 不读取。
### 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 raw `originalData`
```ts
{
height,
width,
url: redirectFileUrl,
isOriginal: 1,
size: nodeSize ?? size,
md5,
fileId,
suffix: materialType,
}
```
这是页面上游的 raw upload payload 示例,不是 v6 canonical contract。即使该 raw payload 含 `width` / `height`MAIN decoder 也会忽略它们,且 post-upload metadata、correlator 和下游 public content 都不会读取或传递这两个字段。
### 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`](../packages/onetalk-contract/src/content.ts)
```ts
type OneTalkImageContent = {
version: 1;
kind: "image";
fileId: string;
extension: string;
sizeBytes: number;
isOriginal: boolean;
md5: string | null;
previewUrl: string | null;
urlScope: "onetalk_session";
};
```
send correlator 不需要也不应该继续读取 raw `originalData`。应比较已经验证、清洗后的:
```text
message.content.sizeBytes
message.content.md5
message.content.fileId (when present)
```
### 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. 当前 v6 图片确认模型
### 6.1 Pending 数据
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 = {
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;
md5: string;
fileId?: string;
};
};
type PendingObservation = PendingTextSend | PendingImageSend | PendingFileSend;
```
`md5``fileId` 可以提高不同图片之间的区分度,但同一文件去重或重复发送时它们也可能相同,仍不能当作每次发送的唯一 ID。
### 6.2 登记时机
图片 pending 在取得最终 media relation 后、最终 native send 前登记:
```text
压缩完成
→ 上传/去重完成
→ buildFileRelationWithGroup
→ 得到最终 mediaInfo
→ 登记 PendingImageSend
→ 立即调用 sendUIMessages
```
实现以 `tmpKey` 隔离上传回调;在 `buildFileRelationWithGroup` 成功、`sendFile` 已拿到最终 `sizeBytes`、非空 `md5` 和可用 `fileId` 后,立即将该 metadata 交给 correlator 并调用最终 SDK send。45 秒预算覆盖下载、上传和 live 确认,correlator 使用其剩余时间。
不能在用户选择原始文件时登记,原因有两个:
1. 图片压缩可能改变最终 `sizeBytes`
2. 上传时间可能超过该历史调查时 correlator 的 `10_000ms` 超时;当前 image 预算为 45 秒。
上传阶段和消息发送确认阶段应是两个状态,不要让消息确认定时器覆盖完整上传耗时。
### 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`、`md5` 和可用 `fileId`
7. 这些 metadata 不能被当作单次发送唯一键
8. 要求消息位于 pending 生命周期和允许的时钟偏差内
9. 一个消息必须只匹配一个 pending;多个匹配立即 send_ambiguous
```
概念判断:
```ts
const imageMatches = (
expected: PendingImageSend["expected"],
actual: OneTalkImageContent,
): boolean =>
actual.sizeBytes === expected.sizeBytes &&
actual.md5 === expected.md5 &&
(expected.fileId === undefined || actual.fileId === expected.fileId);
```
该函数只能是复合匹配的一部分,不能绕过 conversation、direction、时间窗口和唯一性检查。
## 7. 验证方法
### 7.1 自动化测试
至少需要补充以下测试:
| 用例 | 预期 |
| --------------------------------------- | ------------------------------------------------------------ |
| 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 媒体隔离,以及无尺寸 live sent image 驱动实际 image pending 至 `confirmed_sent`。这只是自动化证据;尚未在本报告原有 Chromium 环境执行 v6 真实发送联调。
### 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 媒体 metadata 不唯一
同一图片重复发送会产生不同的 messageId,但 `sizeBytes``md5``fileId` 可以完全相同。本次运行态已经得到两条这样的 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 图片解析。
上游 raw `originalData` 可含 `width` / `height`v6 MAIN decoder 忽略它们,correlator 的 post-upload expected 和 public/read payload 只使用无尺寸的 canonical contract。
## 9. 当前实现边界
当前实现限定为:
1. 为页面图片发送定义独立、最小的输入合同;
2. 在上传关系成功后生成 post-upload image fingerprint
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) 现已严格接收 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. 相关代码
- 页面 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`](../packages/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)