13 KiB
OneTalk 媒体消息格式二次运行态复核方案
日期:2026-09-02
性质:方案讨论与只读运行态复核清单,不是 Trellis task,不包含实现承诺
目标:通过真实运行态证据复核两份调查文档的共同结论、冲突结论和仍不明确的内容
1. 复核对象
本次复核以以下两份文档为输入:
/Users/ybf/code/trade-message-center-worktree/docs/onetalk-media-message-format-investigation.md/Users/ybf/code/trade-message-center-worktree/docs/onetalk-message-content-formats.md
复核时必须区分三类结论:
- 运行态事实:能够从真实 WebSocket、SDK 返回、页面桥、IndexedDB、Bright 或 Mind 中直接观察。
- 代码事实:能够从当前代码路径确认,但尚未完成真实端到端验证。
- 方案决策:例如 raw content 是否保留、normalized content 放在哪一层。这类结论不能伪装成运行态事实。
2. 安全边界与停止条件
2.1 默认只读
默认允许:
- 读取当前 Chromium/CDP 目标、版本和已加载 OneTalk SDK 方法。
- 调用
getConversationListByPagination。 - 调用
fetchMessagesWithoutUpdateToRead。 - 观察历史 WebSocket 响应和同一次调用的 SDK 归一化结果。
- 读取扩展 IndexedDB、Service Worker 状态和 Bright 的只读查询结果。
- 在内存中执行 Base64、UTF-8 和 JSON 解码,并只输出字段名、类型、长度和布尔判断。
默认禁止:
- 调用
fetchMessages、updateMessageToRead或其它会改变已读状态的方法。 - 点击发送、上传、下载、文件选择器或会改变页面 store 的操作。
- 将 Cookie、SID、token、
chatToken、加密账号、消息正文、完整 URL、URL query 值或原始 payload 写入文件、日志或报告。 - 为了便于复核而修改生产数据、IndexedDB、Local Storage、Service Worker 数据或 Bright 数据库。
图片/文件发送、真实上传、二进制下载和主动制造 live push 必须作为单独步骤,由用户明确同意后再执行;它们不属于默认只读复核范围。
2.2 立即停止
出现以下任一情况时停止当前检查并清除未保存的临时输出:
- DevTools、终端或脚本准备输出认证值、完整 URL 或消息正文。
- 所调用的方法实际改变已读状态、会话状态或页面数据。
- URL 验证开始返回二进制正文,而当前步骤只获准检查响应元数据。
- 无法区分测试账号、真实业务账号或目标会话。
3. 实际调试复核范围
3.1 环境基线
| 编号 | 检查项 | 需要记录的安全证据 | 完成条件 |
|---|---|---|---|
| R01 | Chromium 与 CDP | 浏览器版本、CDP protocol 版本、目标页面 URL 的 origin/path | 能确认本次检查对应 OneTalk PWA |
| R02 | 扩展与代码版本 | 扩展版本、仓库 commit、工作树是否有影响复核的未提交改动 | 复核结果能够绑定到唯一代码快照 |
| R03 | SDK 方法 | 方法是否存在及函数类型,不记录函数闭包或运行时凭证 | 确认只读历史方法可用 |
| R04 | 数据状态 | IndexedDB schema 版本及各表计数,不输出记录正文 | 能区分本次复核前后的已有数据 |
3.2 历史消息原始格式
至少选择包含下列样本的历史会话;每个样本只使用 T1、I1、F1、C1 等本地别名,不在报告中记录真实会话 ID 或消息 ID。
| 编号 | 样本 | 必须检查 | 不得记录 |
|---|---|---|---|
| R10 | 文本 T1 |
contentType、text.content 类型、text.extension 的键名 |
正文、extension 字符串值 |
| R11 | 图片 I1 |
contentType、custom.type、Base64/UTF-8/JSON 是否成立、解码后字段名和字段类型 |
MD5、fileId、完整 URL 和 query 值 |
| R12 | PDF 文件 F1 |
custom.type、cardType、params 字段名和类型 |
文件名原值、文件 ID、完整 URL |
| R13 | 非文件卡片 C1 |
custom.type、cardType、SDK subType/msgType |
卡片业务值和账号信息 |
每个样本都需要同时保留两种独立、脱敏的证据:
- WebSocket 原始
body.userMessageModels[].message.content的结构证据。 - 同一次请求对应的 SDK
list[]归一化类型和originalData字段结构。
不能只根据 SDK 的 msgType/subType 反推 WebSocket 原始类型,也不能只根据 WebSocket 的 custom.type 推断业务含义。
4. 冲突结论的第二次确认
每项冲突必须得到两份相互独立的证据后才能定案。代码引用和同一份运行输出的重复截图不算两份独立证据。
C1. custom.type=10010 是否等于附件
第一次证据
- 真实 PDF 样本:
custom.type=10010,解码后cardType=12,SDK 归一化为文件类型。
第二次证据
- 真实非文件卡片:同样为
custom.type=10010,但解码后cardType=2000或其它非文件值,且 SDK 不把它归一化为文件。
定案条件
file = contentType=101
&& custom.type=10010
&& decoded.cardType=12
&& decoded.params 通过文件 schema
若缺少非文件卡片样本,只能说“当前 PDF 样本是附件”,不能泛化为“所有 custom.type=10010 都是附件”。
C2. 附件样本的 cardType 是 0 还是 12
第一次证据
- 直接读取 PDF WebSocket 原始
custom.data的解码结果,只记录cardType数字。
第二次证据
- 对照同一条消息的 SDK
originalData.cardType。
定案条件
- 两个边界均为
12:文档中的0必须标记为错误或不当占位。 - 两个边界不一致:记录为 SDK 转换差异,不能先选择其中一个。
cardType 是判别字段,文档示例不得用 0 冒充脱敏占位;未知值应写成 <number>。
C3. raw content 应继续保留还是在 MAIN world 归一化
第一次证据
- 当前观察器确实复制完整 raw content,Bright 当前清洗器确实对字符串原样放行。
第二次证据
- 真实运行链检查证明序列化 JSON 或 Base64 中的敏感键名能够到达页面桥、IndexedDB、Bright 输入或持久化边界中的至少一处。
定案条件
- 若敏感内容能跨边界:raw content 不得继续作为跨层合同,必须在 MAIN world 白名单解码。
- 即使当前样本未穿透,也不能据此证明任意 raw content 安全;还需要负向 fixture 覆盖字符串和 Base64 嵌套敏感键。
建议的目标不变量是:
raw content 仅在 MAIN world 短暂存在
→ 输出 text | image | file | unsupported
→ 下游只校验 normalized contract,不再解析 OneTalk raw payload
C4. content 是 raw 事实源,还是 normalized 事实源
这不是单靠抓包能够决定的事实冲突,而是架构决策。运行态复核只需要确认现有消费者分别读取了什么;方案确认需要满足:
- 跨层只有一个可写内容事实源。
- 顶层
message.text如需兼容,只能从content.kind === "text"派生。 - 有效但暂未支持的 card 使用
kind="unsupported",不伪装成文本或文件。 - 非法 Base64、非法 JSON、字段错误和超限数据进入明确 anomaly 或受控失败,不静默变成空文本。
- 合同含义从 raw 改为 normalized 时必须提升协议版本,不能让同一协议版本同时承载两种语义。
C5. 完整 sourceUrl 是否可以持久化和发送给 Mind
第一次证据
- 解析图片
url、文件url/downloadUrl/thumbnailUrl,只记录 scheme、host、path 后缀、query 键名和是否为空,不记录 query 值。
第二次证据
- 分别验证 OneTalk 登录上下文和无登录上下文中的响应状态、跳转次数、最终 host、Cookie 依赖和 CORS;只记录状态与布尔结果。
定案条件
- 未确认 URL 生命周期、授权依赖和 query 敏感性前,
sourceUrl不能同时被定义为必填字段并承诺跨网络/数据库持久化。 - 如果 URL query 含短期授权或身份值,第一阶段只同步安全元数据和资源 ID;是否增加临时 URL 换取或受控代理另行决策。
- 如果需要实际 GET、重定向跟随或读取二进制,本项必须先获得用户明确同意;默认只允许检查响应元数据。
5. 不明确内容的再次确认
| 编号 | 当前不明确内容 | 再确认方法 | 定案所需证据 |
|---|---|---|---|
| U01 | 图片/文件 URL 是否依赖 OneTalk Cookie | 登录上下文与干净上下文分别检查响应元数据 | 两种上下文的状态、跳转和 Cookie 依赖结论 |
| U02 | URL 有效期 | 同一脱敏样本在不同时间点检查响应元数据,不记录 URL | 至少能观察有效、过期或续期行为中的一种明确规则 |
| U03 | URL 能否跨 Origin 给 Mind 使用 | 在 Mind 所在 Origin 做只读请求或实际 <img>/链接测试 |
CORS、Cookie、跳转和 CSP 的真实结果 |
| U04 | downloadUrl 为空时 params.url 的真实语义 |
页面行为与 SDK/bundle 调用点交叉确认 | 能区分预览、下载、操作入口或临时跳转 |
| U05 | TXT 是否与 PDF 同构 | 获取一条真实入站 TXT 历史样本并按 R12 检查 | cardType、params schema 和 SDK 归一化结果 |
| U06 | live 图片/文件 push 是否与历史同构 | 观察真实入站 live 媒体消息,不主动发送 | live envelope 与 history content 的字段级比较 |
| U07 | 群聊媒体是否同构 | 在明确允许的群聊样本上执行只读历史检查 | 群聊 raw content、身份和 participant 结构 |
| U08 | custom.type/cardType 完整枚举 |
对已有历史样本做类型和 cardType 计数,只输出数字集合 | 至少确认当前账号样本范围,不宣称全局完整枚举 |
| U09 | confirmed 是否等于真实持久化 |
将样本 ACK 与 Bright 数据库行只读对应 | accepted/duplicate 与数据库事实的区别 |
| U10 | Mind 是否能显示媒体 | 检查真实 history/event 消费和 UI 模型 | 真实渲染结果,不以服务启动或 JSON 可返回代替 |
| U11 | MessagePack live/sync push 的媒体路径 | 捕获真实媒体 MessagePack 样本并与明文历史消息交叉对应 | 数字路径和业务字段一一对应,不靠数组位置猜测 |
| U12 | 图片/文件发送和确认 | 仅在用户明确授权后执行一次受控发送 | 上传结果、最终 sent-direction 消息和三态发送结果 |
U01–U04、U06、U07 和 U12 依赖特定运行环境或真实样本;缺少样本时状态必须写为 blocked_by_sample,不能写成已验证或默认同构。
6. 复核输出格式
每个检查项使用以下固定格式记录:
### Rxx / Cx / Uxx:标题
- 状态:verified | contradicted | not_verified | blocked_by_sample | blocked_by_environment
- 环境:浏览器版本、扩展版本、commit
- 样本别名:T1 / I1 / F1 / C1
- 证据 A:来源边界、字段名、字段类型或布尔结果
- 证据 B:独立来源边界、字段名、字段类型或布尔结果
- 结论:只描述证据能够支持的范围
- 未覆盖:明确列出不能外推的消息类型或场景
最终报告必须分别列出:
- 两份文档共同成立的结论。
- 经两次独立证据确认的冲突及采用哪一侧。
- 被新证据推翻的原结论。
- 尚未确认、缺样本或缺环境的内容。
- 纯方案决策,不得写成运行态事实的内容。
7. 本轮不做
- 不修改两份原调查文档。
- 不创建 Trellis task、PRD 或实现计划。
- 不修改协议、数据库、扩展、Bright 或 Mind 代码。
- 不执行图片/文件发送、上传或二进制下载。
- 不以静态代码检查代替真实运行态结论。