feat: normalize OneTalk business card content

This commit is contained in:
YBF
2026-09-11 19:23:32 +08:00
parent 3be0b3f44d
commit f92f00dca5
36 changed files with 2410 additions and 47 deletions
@@ -0,0 +1,96 @@
# OneTalk 名片消息:运行态格式观察
> 观察日期:2026-09-11
> 证据边界:现有已登录 OneTalk PWA 的 Chromium CDP,只读调用页面 SDK;不保存或输出原始消息、联系人资料、令牌、加密标识或正文。
> 性质:单个真实样本的运行态观察,不是当前跨层数据合同。
## 1. 怎么判断是名片
SDK 历史条目的名片判别条件为:
```ts
const isBusinessCardMessage = (message: Record<string, unknown>): boolean =>
message.messageType === "rec" &&
message.type === 1 &&
message.viewType === 0 &&
message.msgType === 10010 &&
message.subType === 57 &&
message.originalData?.cardType === 1;
```
这里的联合条件不可缩减成 `msgType=10010`:附件、询盘和订单也使用该 `msgType`
## 2. JSON 中已有的数据
### 2.1 名片卡片参数
```text
originalData.cardType = 1
originalData.params keys =
ctime, from, showCertifications, showCompanyName,
showEmailAddress, sign, to
```
`showCompanyName``showEmailAddress``showCertifications` 是展示开关;它们不是公司名、邮箱或认证详情本身。
### 2.2 会话联系人对象
同一条 SDK 条目的 `contact` 中可观察到以下候选字段:
```text
accountId, accountIdEncrypt, aliId, aliIdEncrypt,
loginId, loginIdEncrypt, name, fullPortrait,
companyName, complianceCountryCode, currentTimeZone, serviceType
```
可谨慎使用的资料含义如下:
| 字段 | 可表达的信息 | 限制 |
| ------------------------------- | ------------- | ---------------------------------------- |
| `contact.name` | 联系人显示名 | 是会话联系人资料,不能证明是名片固定字段 |
| `contact.companyName` | 联系人公司名 | 可为空或滞后 |
| `contact.complianceCountryCode` | 国家/地区代码 | 国旗由 UI 按代码渲染,不是消息图片 |
| `contact.fullPortrait` | 头像候选 URL | 本样本未填;不能假设必有 |
截图中的邮箱没有观察到独立的 `email` JSON 字段。本样本 `content` 是非 JSON 的普通字符串;邮箱可能出现在其中的展示文本,但没有验证出可复用的字段格式。
`extInfo.icbuData` 同样只有 `chatEvent` 有实际值,`title``iconUrl``cardUrls``actions``defaultContent` 都不可直接使用。
## 3. 怎么获取
名片和其它业务卡共享同一只读历史入口;区别只在过滤条件。
```js
const getBusinessCardMessages = async (conversation) => {
const service = window.IcbuIM.IMBaaSSDK.default.getMessageService();
const response = await service.fetchMessagesWithoutUpdateToRead(
{
conversationCode: conversation.cid,
contactAccountId: conversation.accountId,
contactAccountIdEncrypt: conversation.accountIdEncrypt,
aliId: conversation.aliId,
aliIdEncrypt: conversation.aliIdEncrypt,
searchMessageId: "",
timeSlide: { forward: false, timeStamp: Date.now(), pageSize: 20 },
},
conversation,
);
return (response.list ?? []).filter(
(message) =>
message.messageType === "rec" &&
message.type === 1 &&
message.viewType === 0 &&
message.msgType === 10010 &&
message.subType === 57 &&
message.originalData?.cardType === 1,
);
};
```
如果后续业务需要联系人名称、公司、国家代码或头像,必须明确它们是 `contact` 资料观察,不是名片内容的权威声明。跨层同步只应传递经业务批准的白名单字段;不能透传 `contact` 整体对象、加密标识、`chatToken``content``sign`
## 4. 已验证与未覆盖
- 已验证:`10010/57/cardType=1` 判别组合,`originalData.params` 键集合,联系人候选资料和 `icbuData` 可用性。
- 未覆盖:`content` 的名片展示文本格式,独立邮箱字段来源,头像字段在不同名片中的填充率,以及名片详情/跳转链接。
@@ -0,0 +1,101 @@
# OneTalk 询盘消息:运行态格式观察
> 观察日期:2026-09-11
> 证据边界:现有已登录 OneTalk PWA 的 Chromium CDP,只读调用页面 SDK;不切换会话、不更新已读状态、不保存或输出原始消息、令牌、加密标识、媒体 URL 或正文。
> 性质:单个真实样本的运行态观察,不是 OneTalk 全局类型枚举,也不是当前跨层数据合同。
## 1. 怎么判断是询盘
SDK 历史返回的单条扁平消息必须同时满足以下条件:
```ts
const isInquiryMessage = (message: Record<string, unknown>): boolean =>
message.messageType === "rec" &&
message.type === 1 &&
message.viewType === 0 &&
message.msgType === 10010 &&
message.subType === 50 &&
message.originalData?.cardType === 6;
```
其中 `msgType=10010` 不是充分条件:当前已观察到的附件、名片和订单同样使用它。
| 类型 | `msgType` | `subType` | `originalData.cardType` |
| ---- | --------: | --------: | ----------------------: |
| 询盘 | 10010 | 50 | 6 |
| 名片 | 10010 | 57 | 1 |
| 订单 | 10010 | 59 | 9 |
| 附件 | 10010 | 61 | 12 |
## 2. JSON 中已有的数据
SDK 返回是 `{ hasMore, list }`,本样本的询盘条目具有以下顶层字段:
```text
autoReply, contact, contactRead, content, conversationCode, extInfo,
localExt, messageId, messageType, msgType, opId, originExt,
originalData, owner, receiver, sendTime, sender, spamStatus, status,
subType, type, unread, uuid, viewType
```
询盘专有的稳定结构位于 `originalData`
```text
originalData.cardType = 6
originalData.params keys =
ctime, encryFeedbackId, encryTradeId, fbType, from, marketType,
sign, source, to, version
```
这些字段可用于将消息关联到询盘/贸易实体,但 `encryFeedbackId``encryTradeId``sign` 属于不应跨 MAIN-world 边界的敏感原始值。
`extInfo.icbuData` 具有 `actions``cardUrls``title``iconUrl``defaultContent``chatEvent` 等候选键名;本样本实际只有 `chatEvent` 有值。不能把键存在误认为标题、商品图片或详情链接可直接取得。
本样本的 `content` 是普通字符串,不是 JSON、URI-JSON 或 Base64-JSON。它不是稳定的业务字段合同。
## 3. 展示卡与 SDK 字段的差异
页面渲染的询盘卡可显示商品标题、采购量、需求、商品图片和按钮,但这些并非本次 SDK 对象中直接可用的结构化字段:
- 商品标题、采购量、详细需求和图片可以在**已渲染且当前可见**的卡片 DOM 中读取;这只是 UI 观察,不可代替消息事实源。
- 本样本的 SDK `cardUrls` 为空,卡片内也没有静态 `<a href>`;“查看详情 / 立即报价”由前端点击逻辑生成,不能从原始消息直接宣称存在详情链接。
- 不应通过 DOM 抓取的展示文本反推稳定的后端协议字段。
## 4. 怎么获取
在 OneTalk PWA 的 MAIN world 中,从精确会话对象调用只读历史方法。不要从昵称、列表顺序或页面文本推断 `conversationId`
```js
const getInquiryMessages = async (conversation) => {
const service = window.IcbuIM.IMBaaSSDK.default.getMessageService();
const response = await service.fetchMessagesWithoutUpdateToRead(
{
conversationCode: conversation.cid,
contactAccountId: conversation.accountId,
contactAccountIdEncrypt: conversation.accountIdEncrypt,
aliId: conversation.aliId,
aliIdEncrypt: conversation.aliIdEncrypt,
searchMessageId: "",
timeSlide: { forward: false, timeStamp: Date.now(), pageSize: 20 },
},
conversation,
);
return (response.list ?? []).filter(
(message) =>
message.messageType === "rec" &&
message.type === 1 &&
message.viewType === 0 &&
message.msgType === 10010 &&
message.subType === 50 &&
message.originalData?.cardType === 6,
);
};
```
这个函数的返回值只应在 MAIN world 中用于归类或进一步受控解析。跨页面桥、Service Worker、IndexedDB、Bright 或 Mind 时,应传递白名单化的业务字段或明确的 `unsupported` 结果,绝不能传递整个 `message``content``params``chatToken` 或序列化的原始对象。
## 5. 已验证与未覆盖
- 已验证:上述判别组合、`originalData.params` 键集合、`icbuData` 字段可用性、单页 SDK 返回形态。
- 未覆盖:询盘 `content` 的稳定文本格式、商品详情 API、详情链接生成规则、不同站点/订单状态下的变体。
@@ -0,0 +1,130 @@
# OneTalk 订单消息:运行态格式观察
> 观察日期:2026-09-11
> 证据边界:现有已登录 OneTalk PWA 的 Chromium CDP,只读调用页面 SDK;不保存或输出原始订单、地址、令牌、加密标识、消息正文或完整 URL。
> 性质:单个真实样本的运行态观察,不是订单系统接口契约,也不是当前跨层数据合同。
## 1. 怎么判断是订单
SDK 历史条目的订单判别条件为:
```ts
const isOrderMessage = (message: Record<string, unknown>): boolean =>
message.messageType === "rec" &&
message.type === 1 &&
message.viewType === 0 &&
message.msgType === 10010 &&
message.subType === 59 &&
message.originalData?.cardType === 9;
```
`msgType=10010` 是业务卡片族而不是订单标记;必须同时检查 `subType=59``cardType=9`
## 2. JSON 中已有的数据
### 2.1 订单关联字段
```text
originalData.cardType = 9
originalData.params keys =
orderId, bizCode, contractId, sign, ctime,
from, to, id, params, tenant
```
`orderId``contractId``id``bizCode` 可用于同一受控边界内的订单关联。`sign`、参与方标识和加密/令牌型字段不得跨 MAIN world 传输或持久化。
### 2.2 Base64 订单摘要
`originalData.params.params` 在本样本中是 Base64 编码的 UTF-8 JSON。解码后具有以下结构:
```ts
type OrderSummary = {
id: number;
orderAmount: number;
orderAmountCurrency: string;
paymentAmount: number;
paymentAmountCurrency: string;
statusMessageKey: string;
actionList: Array<{
name: string;
messageKey: string;
properties: {
payStep?: string;
};
}>;
};
```
因此当前可直接获得:
- 订单和实付金额及币种;
- 非本地化的订单状态键 `statusMessageKey`
- 动作列表及部分付款阶段 `payStep`
- 订单关联 ID。
### 2.3 当前不能从结构化摘要直接获得的数据
样本 UI 显示的商品数量、商品明细/图片、人类可读状态、收件地址和详情链接不在上述已解码摘要内。
本样本 `content` 为非 JSON 的普通字符串模板。它不能直接作为稳定 schema 使用,也不能未经白名单处理跨层传递。
`extInfo.icbuData` 中的 `title``iconUrl``cardUrls``actions``defaultContent` 在本样本为空;只有 `chatEvent` 有值,不能据此承诺订单详情链接可用。
## 3. 怎么获取
### 3.1 取得订单消息
```js
const getOrderMessages = async (conversation) => {
const service = window.IcbuIM.IMBaaSSDK.default.getMessageService();
const response = await service.fetchMessagesWithoutUpdateToRead(
{
conversationCode: conversation.cid,
contactAccountId: conversation.accountId,
contactAccountIdEncrypt: conversation.accountIdEncrypt,
aliId: conversation.aliId,
aliIdEncrypt: conversation.aliIdEncrypt,
searchMessageId: "",
timeSlide: { forward: false, timeStamp: Date.now(), pageSize: 20 },
},
conversation,
);
return (response.list ?? []).filter(
(message) =>
message.messageType === "rec" &&
message.type === 1 &&
message.viewType === 0 &&
message.msgType === 10010 &&
message.subType === 59 &&
message.originalData?.cardType === 9,
);
};
```
### 3.2 在 MAIN world 内解码摘要
必须限制大小、验证 Base64/UTF-8/JSON 和字段 schema;示例仅展示解码入口,不构成跨层透传授权。
```js
const decodeOrderSummary = (encoded) => {
if (typeof encoded !== "string" || encoded.length === 0) return null;
try {
const bytes = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
if (!parsed || typeof parsed !== "object") return null;
return parsed;
} catch {
return null;
}
};
```
生产实现不能将 `null` 静默当作“无订单”:Base64、UTF-8、JSON 或 schema 失败应成为可观测的显式异常/受控 `unsupported` 结果。
## 4. 已验证与未覆盖
- 已验证:`10010/59/cardType=9` 判别组合,订单参数键,嵌套 `params` 的 Base64-UTF-8-JSON 编码,以及摘要键集合。
- 未覆盖:商品数量和明细、图片、状态键到本地化文案的映射、收件地址、详情链接、不同订单状态及多商品订单的字段变体。