mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
docs: record OneTalk customer profile fetching
This commit is contained in:
@@ -0,0 +1,760 @@
|
||||
# OneTalk 客户资料获取方法与参数
|
||||
|
||||
> 记录日期:2026-08-27
|
||||
>
|
||||
> 适用页面:`https://onetalk.alibaba.com/message/weblitePWA.htm`
|
||||
>
|
||||
> 本文记录一次对真实 Chromium 页面运行时的只读探查结果,重点是如何获取会话对应的客户姓名、公司、登录 ID、阿里 ID 等资料,以及如何继续刷新客户详情。页面和静态 bundle 版本可能变化,生产代码必须保留特征检测、超时和字段白名单。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
当前 OneTalk 页面有三层资料入口:
|
||||
|
||||
1. **首选:`window.__conversationListData__`**
|
||||
|
||||
会话列表模块已经把客户资料放在这个全局对象中。对于已经加载到会话列表的联系人,不需要额外请求,就能取得 `aliId`、`loginId`、姓名、公司等字段。
|
||||
|
||||
2. **补刷新:`conversationServiceHttp.getConversationContactDetailList()`**
|
||||
|
||||
`window.IcbuIM.IMBaaSSDK` 中具体的 `IcbuConversationServiceImpl` 实例带有内部 HTTP 适配器。该适配器可以用页面已有的加密 ID 和 chat token 刷新联系人资料。
|
||||
|
||||
3. **完整客户详情:客户详情微应用自己的 `contactMemberInfo` 请求**
|
||||
|
||||
右侧“客户详情”面板中的邮箱、注册时间等字段不在当前基础联系人对象中。静态 bundle 显示客户详情微应用会调用 `/message/contact/detail/contactMemberInfo.htm`。这部分应优先通过页面微应用已有逻辑或已渲染 DOM 获取,不要在插件 Service Worker 中自行拼接 CRM 请求。
|
||||
|
||||
最简单、最稳定的基础资料获取路径是:
|
||||
|
||||
```text
|
||||
window.__conversationListData__
|
||||
→ 读取 contact
|
||||
→ 白名单提取字段
|
||||
→ 发送给 Mind
|
||||
```
|
||||
|
||||
缺资料时再走:
|
||||
|
||||
```text
|
||||
IcbuConversationServiceImpl.getInstance()
|
||||
.conversationServiceHttp
|
||||
.getConversationContactDetailList(...)
|
||||
```
|
||||
|
||||
## 2. 本次运行时探查结果
|
||||
|
||||
### 2.1 页面和 SDK 根对象
|
||||
|
||||
页面上的根对象存在:
|
||||
|
||||
```js
|
||||
window.IcbuIM;
|
||||
window.IcbuIM.IMBaaSSDK;
|
||||
```
|
||||
|
||||
`window.IcbuIM.IMBaaSSDK.default` 当前只有:
|
||||
|
||||
```js
|
||||
{
|
||||
sdkVersion: "...";
|
||||
}
|
||||
```
|
||||
|
||||
客户资料相关对象在 `IMBaaSSDK` 顶层,而不在 `default` 中。已确认的相关入口包括:
|
||||
|
||||
```text
|
||||
AuthService
|
||||
ConversationService
|
||||
MessageService
|
||||
IcbuAuthServiceImpl
|
||||
IcbuConversationServiceImpl
|
||||
IcbuMessageServiceImpl
|
||||
chatSdk
|
||||
dataManager
|
||||
baasDataManager
|
||||
```
|
||||
|
||||
`chatSdk.getConversationService()` 返回的是公共会话服务,当前没有暴露客户详情方法。具体实现实例的原型包含:
|
||||
|
||||
```text
|
||||
getConversationList
|
||||
getConversationListByPagination
|
||||
getConversationContactDetailList
|
||||
listGroupMemberDetails
|
||||
...
|
||||
```
|
||||
|
||||
### 2.2 当前页面的客户资料全局对象
|
||||
|
||||
页面存在:
|
||||
|
||||
```js
|
||||
window.__conversationListData__;
|
||||
window.__conversationListMapFullData__;
|
||||
window.__conversationListFullData__;
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
```js
|
||||
window.__conversationListMapFullData__ === window.__conversationListData__;
|
||||
```
|
||||
|
||||
会话列表 bundle 中的同步逻辑大致如下:
|
||||
|
||||
```js
|
||||
window.__conversationListData__ = map;
|
||||
window.__conversationListMapFullData__ = window.__conversationListData__;
|
||||
window.EventBus.publish("im-conversation-list:syncData", map);
|
||||
```
|
||||
|
||||
因此应当把 `__conversationListData__` 作为主读取入口,并订阅 `im-conversation-list:syncData` 获取后续更新。
|
||||
|
||||
本次页面上该对象有 2 个已加载会话。典型条目如下:
|
||||
|
||||
```js
|
||||
window.__conversationListData__["2208314000798"] = {
|
||||
cid: "2208314000798-2500002169502#11011@icbu",
|
||||
aliId: 2208314000798,
|
||||
accountId: 243340382,
|
||||
loginId: "hzhago",
|
||||
name: "Heena Liu",
|
||||
companyName: "Hangzhou Hago Enterprise Development Co., Ltd.",
|
||||
complianceCountryCode: "CN",
|
||||
contact: {
|
||||
aliId: 2208314000798,
|
||||
accountId: 243340382,
|
||||
loginId: "hzhago",
|
||||
name: "Heena Liu",
|
||||
companyName: "Hangzhou Hago Enterprise Development Co., Ltd.",
|
||||
complianceCountryCode: "CN",
|
||||
currentTimeZone: -9,
|
||||
serviceType: "cgs",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
实际条目还可能包含以下敏感或不应向 Mind 转发的字段:
|
||||
|
||||
```text
|
||||
aliIdEncrypt
|
||||
accountIdEncrypt
|
||||
loginIdEncrypt
|
||||
chatToken
|
||||
ext.custom
|
||||
latestMessage
|
||||
msgCache
|
||||
```
|
||||
|
||||
不能把整个会话对象直接序列化后发送给 Mind。
|
||||
|
||||
## 3. 直接读取已有客户资料
|
||||
|
||||
推荐使用 `contact` 优先、条目自身字段兜底的方式。这样可以兼容不同版本把资料放在顶层或 `contact` 下的情况。
|
||||
|
||||
```js
|
||||
const PROFILE_FIELDS = [
|
||||
"aliId",
|
||||
"accountId",
|
||||
"loginId",
|
||||
"name",
|
||||
"companyName",
|
||||
"complianceCountryCode",
|
||||
"currentTimeZone",
|
||||
"serviceType",
|
||||
];
|
||||
|
||||
const readConversationProfiles = () => {
|
||||
const source = window.__conversationListData__ || {};
|
||||
|
||||
return Object.values(source).map((row) => {
|
||||
const contact = row.contact || row;
|
||||
|
||||
return {
|
||||
conversationId: row.cid || null,
|
||||
aliId: contact.aliId ?? row.aliId ?? null,
|
||||
accountId: contact.accountId ?? row.accountId ?? null,
|
||||
loginId: contact.loginId ?? row.loginId ?? null,
|
||||
name: contact.name ?? row.name ?? null,
|
||||
companyName: contact.companyName ?? row.companyName ?? null,
|
||||
countryCode: contact.complianceCountryCode ?? row.complianceCountryCode ?? null,
|
||||
currentTimeZone: contact.currentTimeZone ?? row.currentTimeZone ?? null,
|
||||
serviceType: contact.serviceType ?? row.serviceType ?? null,
|
||||
};
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
`PROFILE_FIELDS` 是文档化的输出白名单;生产实现应显式构造对象,而不是使用:
|
||||
|
||||
```js
|
||||
JSON.stringify(row);
|
||||
```
|
||||
|
||||
### 3.1 订阅会话资料更新
|
||||
|
||||
会话列表模块会通过页面 `EventBus` 发布全量 map:
|
||||
|
||||
```js
|
||||
const consume = (map) => {
|
||||
for (const row of Object.values(map || {})) {
|
||||
const contact = row.contact || row;
|
||||
|
||||
const profile = {
|
||||
conversationId: row.cid || null,
|
||||
aliId: contact.aliId ?? row.aliId ?? null,
|
||||
accountId: contact.accountId ?? row.accountId ?? null,
|
||||
loginId: contact.loginId ?? row.loginId ?? null,
|
||||
name: contact.name ?? row.name ?? null,
|
||||
companyName: contact.companyName ?? row.companyName ?? null,
|
||||
countryCode: contact.complianceCountryCode ?? row.complianceCountryCode ?? null,
|
||||
};
|
||||
|
||||
// 只发送 profile,不发送原始 row。
|
||||
publishProfileToMind(profile);
|
||||
}
|
||||
};
|
||||
|
||||
consume(window.__conversationListData__ || {});
|
||||
|
||||
const unsubscribe = window.EventBus?.on?.("im-conversation-list:syncData", consume);
|
||||
```
|
||||
|
||||
`EventBus.on` 返回取消订阅函数。页面重新初始化或扩展卸载时应执行 `unsubscribe?.()`。
|
||||
|
||||
注意:`__conversationListData__` 只代表页面当前已经同步到会话列表的数据,不代表所有历史消息或所有曾经存在的联系人。做全量回填时,仍然需要配合会话分页结果,对缺资料会话逐个补刷新。
|
||||
|
||||
## 4. SDK 刷新入口
|
||||
|
||||
### 4.1 错误入口:外层方法当前是空 Promise
|
||||
|
||||
下面这个路径在当前页面版本中存在,但实现为空:
|
||||
|
||||
```js
|
||||
window.IcbuIM.IMBaaSSDK.IcbuConversationServiceImpl.getInstance().getConversationContactDetailList;
|
||||
```
|
||||
|
||||
运行时函数体表现为:
|
||||
|
||||
```js
|
||||
function (e) {
|
||||
return new Promise(function (resolve, reject) {});
|
||||
}
|
||||
```
|
||||
|
||||
调用它不会发出网络请求,也不会完成 Promise。
|
||||
|
||||
`ConversationService.getInstance().getConversationContactDetailList()` 当前同样是空实现。不要把这两个外层方法作为生产入口。
|
||||
|
||||
### 4.2 已验证可用的入口:内部 HTTP 适配器
|
||||
|
||||
真正可用的实现位于具体实例的 `conversationServiceHttp`:
|
||||
|
||||
```js
|
||||
const sdk = window.IcbuIM?.IMBaaSSDK;
|
||||
const service = sdk?.IcbuConversationServiceImpl?.getInstance?.();
|
||||
const http = service?.conversationServiceHttp;
|
||||
|
||||
const [contact] = await http.getConversationContactDetailList([
|
||||
{
|
||||
aliId: row.aliId,
|
||||
encryptAliId: row.aliIdEncrypt,
|
||||
encryptAccountId: row.accountIdEncrypt,
|
||||
chatToken: row.chatToken,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
本次使用当前页面的 Heena Liu 会话执行了该调用:
|
||||
|
||||
```text
|
||||
请求:POST /message/listRecentConversationContactDetail.htm
|
||||
HTTP 状态:200
|
||||
返回联系人数量:1
|
||||
```
|
||||
|
||||
返回资料中确认包含:
|
||||
|
||||
```text
|
||||
serviceType
|
||||
loginId
|
||||
kHTAccessToken
|
||||
companyName
|
||||
complianceCountryCode
|
||||
aliId
|
||||
currentTimeZone
|
||||
fullPortrait
|
||||
accountId
|
||||
name
|
||||
productRelation
|
||||
block
|
||||
loginIdEncrypt
|
||||
```
|
||||
|
||||
返回结果至少可以取得:
|
||||
|
||||
```js
|
||||
{
|
||||
aliId,
|
||||
accountId,
|
||||
loginId,
|
||||
name,
|
||||
companyName,
|
||||
complianceCountryCode,
|
||||
currentTimeZone,
|
||||
serviceType,
|
||||
block,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 刷新入参来源
|
||||
|
||||
`row` 可以从 `window.__conversationListData__` 获得:
|
||||
|
||||
```js
|
||||
const row = window.__conversationListData__[String(aliId)];
|
||||
```
|
||||
|
||||
当前会话条目的顶层和 `contact` 对象都可以看到这些参数键:
|
||||
|
||||
| 参数 | 作用 | 是否可跨边界发送 |
|
||||
| ------------------ | ------------------------ | ----------------------------- |
|
||||
| `aliId` | 联系人的 OneTalk 阿里 ID | 可以,按白名单发送 |
|
||||
| `encryptAliId` | `aliId` 的页面请求加密值 | 不可以,仅在页面内部使用 |
|
||||
| `encryptAccountId` | 账号 ID 的页面请求加密值 | 不可以,仅在页面内部使用 |
|
||||
| `chatToken` | 当前页面请求令牌 | 不可以,不能记录或发送给 Mind |
|
||||
|
||||
内部实现读取参数的规则是:
|
||||
|
||||
```js
|
||||
if (item.encryptAliId) {
|
||||
contactEncryptAliIdList.push(item.encryptAliId);
|
||||
chatTokens.push(item.chatToken || "");
|
||||
} else if (item.encryptAccountId) {
|
||||
contactEncryptAccountIdList.push(item.encryptAccountId);
|
||||
chatTokens.push(item.chatToken || "");
|
||||
}
|
||||
```
|
||||
|
||||
因此:
|
||||
|
||||
- 优先传 `encryptAliId`;
|
||||
- 没有 `encryptAliId` 时传 `encryptAccountId`;
|
||||
- `chatToken` 只在 OneTalk 页面上下文内部使用;
|
||||
- 不要把加密字段和 token 放到插件 WebSocket、Mind 请求或日志中。
|
||||
|
||||
### 4.4 内部请求形状
|
||||
|
||||
`conversationServiceHttp.getConversationContactDetailList(items)` 内部会把输入转换为:
|
||||
|
||||
```js
|
||||
{
|
||||
contactEncryptAliIdList: ["..."],
|
||||
contactEncryptAccountIdList: [],
|
||||
chatTokens: "..."
|
||||
}
|
||||
```
|
||||
|
||||
实际接口路径为:
|
||||
|
||||
```text
|
||||
POST https://onetalk.alibaba.com/message/listRecentConversationContactDetail.htm
|
||||
```
|
||||
|
||||
生产代码不应在 Service Worker 或服务端手工调用这个 URL。正确做法是让 MAIN world 页面适配器调用页面已有服务,再返回已清洗的结果。
|
||||
|
||||
### 4.5 建议的安全包装
|
||||
|
||||
```js
|
||||
const CONTACT_OUTPUT_FIELDS = [
|
||||
"aliId",
|
||||
"accountId",
|
||||
"loginId",
|
||||
"name",
|
||||
"companyName",
|
||||
"complianceCountryCode",
|
||||
"currentTimeZone",
|
||||
"serviceType",
|
||||
"block",
|
||||
];
|
||||
|
||||
const pickContactProfile = (value, conversationId) => {
|
||||
const profile = { conversationId };
|
||||
|
||||
for (const field of CONTACT_OUTPUT_FIELDS) {
|
||||
if (value && Object.prototype.hasOwnProperty.call(value, field)) {
|
||||
profile[field] = value[field];
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
};
|
||||
|
||||
const refreshContactProfile = async (row) => {
|
||||
const sdk = window.IcbuIM?.IMBaaSSDK;
|
||||
const service = sdk?.IcbuConversationServiceImpl?.getInstance?.();
|
||||
const http = service?.conversationServiceHttp;
|
||||
|
||||
if (!http || typeof http.getConversationContactDetailList !== "function") {
|
||||
throw new Error("onetalk_contact_detail_service_unavailable");
|
||||
}
|
||||
|
||||
const request = {
|
||||
aliId: row.aliId,
|
||||
encryptAliId: row.aliIdEncrypt,
|
||||
encryptAccountId: row.accountIdEncrypt,
|
||||
chatToken: row.chatToken,
|
||||
};
|
||||
|
||||
const list = await http.getConversationContactDetailList([request]);
|
||||
const contact = Array.isArray(list) ? list[0] : null;
|
||||
|
||||
return pickContactProfile(contact, row.cid || null);
|
||||
};
|
||||
```
|
||||
|
||||
实际实现还应增加超时、页面身份检查和结果状态,例如:
|
||||
|
||||
```text
|
||||
confirmed 已拿到资料且 ID 范围匹配
|
||||
partial 只拿到姓名/公司等部分字段
|
||||
not_found 页面返回空联系人
|
||||
unavailable SDK 或页面服务不存在
|
||||
timeout 页面请求未完成
|
||||
identity_mismatch 返回资料和目标联系人不匹配
|
||||
```
|
||||
|
||||
## 5. 返回资料与右侧完整详情的边界
|
||||
|
||||
### 5.1 基础联系人接口不包含的字段
|
||||
|
||||
本次 `listRecentConversationContactDetail.htm` 返回结果没有看到:
|
||||
|
||||
```text
|
||||
email
|
||||
registerTime
|
||||
registrationTime
|
||||
buyerTags
|
||||
```
|
||||
|
||||
而右侧客户详情 DOM 中确实能看到:
|
||||
|
||||
```text
|
||||
邮箱
|
||||
注册时间
|
||||
买家标签
|
||||
```
|
||||
|
||||
因此不能假设 `getConversationContactDetailList` 会返回右侧卡片的全部字段。
|
||||
|
||||
### 5.2 右侧客户详情模块的请求
|
||||
|
||||
公开静态 bundle 中存在以下页面函数:
|
||||
|
||||
```js
|
||||
getContactInfo;
|
||||
```
|
||||
|
||||
其请求路径为:
|
||||
|
||||
```text
|
||||
POST /message/contact/detail/contactMemberInfo.htm
|
||||
```
|
||||
|
||||
页面代码传入的参数形状为:
|
||||
|
||||
```js
|
||||
{
|
||||
contactAccountIdEncrypt: contact.accountIdEncrypt,
|
||||
chatToken: contact.chatToken,
|
||||
}
|
||||
```
|
||||
|
||||
页面逻辑随后从响应的 `data` 更新客户详情状态。这个请求属于 CRM 客户详情微应用,不是当前 `IMBaaSSDK` 顶层公开的简单方法。
|
||||
|
||||
建议优先级:
|
||||
|
||||
1. 让页面自己的客户详情微应用加载数据,再从稳定的页面状态或已渲染 DOM 读取;
|
||||
2. 如果后续确认页面存在稳定的、页面自有的资料方法,再在 MAIN world 内调用;
|
||||
3. 不在 Service Worker 中保存或重放 `chatToken`;
|
||||
4. 不把邮箱、注册时间等字段误认为消息字段。
|
||||
|
||||
### 5.3 DOM 兜底定位
|
||||
|
||||
本次页面看到的可用 DOM 特征包括:
|
||||
|
||||
```css
|
||||
.alicrm-buyerLoginId-text
|
||||
.name-text
|
||||
.base-information-form-item
|
||||
.base-information-form-item-label
|
||||
.base-information-form-item-content .content
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```html
|
||||
<div class="alicrm-buyerLoginId-text">ID: <span>hzhago</span></div>
|
||||
|
||||
<div class="base-information-form-item">
|
||||
<div class="base-information-form-item-label" title="公司名称">公司名称</div>
|
||||
<div class="base-information-form-item-content">
|
||||
<span class="content">Hangzhou Hago Enterprise Development Co., Ltd.</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
DOM 读取必须以标签和精确容器为锚点,不能按客户显示名、列表序号或模糊文本点击。DOM 只作为完整详情补充,不应替代基础联系人全局数据。
|
||||
|
||||
## 6. ID 映射规则
|
||||
|
||||
### 6.1 当前页面观察到的 ID
|
||||
|
||||
当前 Heena Liu 会话中同时出现了:
|
||||
|
||||
```text
|
||||
aliId = 2208314000798
|
||||
loginId = hzhago
|
||||
accountId = 243340382
|
||||
```
|
||||
|
||||
右侧卡片显示的 `ID: hzhago` 是 `loginId`,不是 `aliId`。
|
||||
|
||||
消息对象中的 `sender.targetId` 当前观察到更接近 `aliId`,例如:
|
||||
|
||||
```text
|
||||
sender.targetId = 2208314000798
|
||||
```
|
||||
|
||||
所以不能只保存一个“ID”字段。建议 Mind 侧至少保存:
|
||||
|
||||
```text
|
||||
channelAccountId
|
||||
aliId
|
||||
loginId
|
||||
name
|
||||
companyName
|
||||
```
|
||||
|
||||
资料映射主键建议为:
|
||||
|
||||
```text
|
||||
channelAccountId + aliId
|
||||
```
|
||||
|
||||
`loginId` 作为可展示和辅助匹配的别名保存。
|
||||
|
||||
### 6.2 不要把 `accountId` 误当成联系人 ID
|
||||
|
||||
当前联系人对象中的 `accountId` 与页面 active account 相关,不能仅凭名称判断它是买家的独立联系人 ID。生产实现应保留原始字段并在多个样本上验证:
|
||||
|
||||
```text
|
||||
消息 sender.targetId
|
||||
会话对象 aliId
|
||||
资料对象 aliId
|
||||
资料对象 loginId
|
||||
页面 ID 文本
|
||||
```
|
||||
|
||||
只有确认字段对应关系后,才能建立自动映射。
|
||||
|
||||
### 6.3 单聊和群聊
|
||||
|
||||
- 单聊:通常可以用会话对象的 `contact` 直接得到一个联系人。
|
||||
- 群聊:会话标题不是联系人资料;需要使用成员列表服务逐个得到成员,再分别匹配 `aliId/loginId`。
|
||||
- 当前 `getConversationContactDetailList` 可以批量传多个联系人对象,但每个联系人仍必须带自己的加密 ID和页面 token。
|
||||
|
||||
## 7. 向 Mind 发送的建议数据契约
|
||||
|
||||
客户资料应作为独立资料事件发送,不要附加到消息正文,也不要把原始会话对象当作 payload:
|
||||
|
||||
```js
|
||||
{
|
||||
type: "contact.profile.observed",
|
||||
source: "onetalk.page",
|
||||
channelAccountId: "243340382",
|
||||
conversationId: "2208314000798-2500002169502#11011@icbu",
|
||||
aliId: "2208314000798",
|
||||
loginId: "hzhago",
|
||||
name: "Heena Liu",
|
||||
companyName: "Hangzhou Hago Enterprise Development Co., Ltd.",
|
||||
countryCode: "CN",
|
||||
observedAt: "2026-08-27T...Z",
|
||||
sourceFields: [
|
||||
"conversation.contact",
|
||||
"conversationListData",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
建议增加:
|
||||
|
||||
```text
|
||||
observationStatus: confirmed | partial | unavailable | mismatch
|
||||
profileFingerprint
|
||||
lastObservedAt
|
||||
manualLock / manuallyVerified
|
||||
```
|
||||
|
||||
Mind 侧如果已有联系人表,优先复用。没有合适结构时,可以增加一个资料观察/映射表,而不是把这些字段写入消息表。
|
||||
|
||||
## 8. 推荐插件实现结构
|
||||
|
||||
建议新增独立的页面资料观察模块,例如:
|
||||
|
||||
```text
|
||||
apps/chrome-extension/src/onetalk/main-page/contact-observer/
|
||||
```
|
||||
|
||||
模块职责:
|
||||
|
||||
1. 读取 `window.__conversationListData__` 初始快照;
|
||||
2. 订阅 `im-conversation-list:syncData`;
|
||||
3. 对资料缺失的会话调用 `conversationServiceHttp.getConversationContactDetailList`;
|
||||
4. 对邮箱、注册时间等完整详情使用页面微应用状态或 DOM 补充;
|
||||
5. 对返回对象执行字段白名单清洗;
|
||||
6. 通过已有 page bridge / Service Worker 发送给 Mind;
|
||||
7. 以 `channelAccountId + aliId` 去重;
|
||||
8. 对每个联系人记录成功、部分成功、超时和 ID 不匹配状态。
|
||||
|
||||
推荐采集策略:
|
||||
|
||||
```text
|
||||
实时会话列表更新 → 立即采集基础资料
|
||||
用户打开会话 → 按需刷新完整资料
|
||||
显式“回填客户资料” → 对缺资料会话批量补采集
|
||||
```
|
||||
|
||||
不要对每一条消息重复请求客户资料;应按联系人去重。
|
||||
|
||||
## 9. 错误处理与可观测性
|
||||
|
||||
### 9.1 必须区分的错误
|
||||
|
||||
```text
|
||||
onetalk_contact_profile_global_missing
|
||||
onetalk_contact_detail_service_unavailable
|
||||
onetalk_contact_detail_input_missing
|
||||
onetalk_contact_detail_timeout
|
||||
onetalk_contact_detail_empty
|
||||
onetalk_contact_profile_identity_mismatch
|
||||
onetalk_contact_profile_partial
|
||||
```
|
||||
|
||||
### 9.2 诊断字段
|
||||
|
||||
日志可以记录:
|
||||
|
||||
```text
|
||||
requestId
|
||||
channelAccountId
|
||||
conversationId
|
||||
aliId(必要时)
|
||||
结果状态
|
||||
返回字段名集合
|
||||
耗时
|
||||
时间戳
|
||||
```
|
||||
|
||||
日志禁止记录:
|
||||
|
||||
```text
|
||||
chatToken
|
||||
aliIdEncrypt
|
||||
accountIdEncrypt
|
||||
loginIdEncrypt
|
||||
kHTAccessToken
|
||||
Cookie
|
||||
CSRF
|
||||
完整原始响应
|
||||
```
|
||||
|
||||
### 9.3 页面离线
|
||||
|
||||
本次第一次调用外层空 Promise 时没有任何请求;改用内部 HTTP 适配器后能收到 200 响应。若页面自身网络状态为断开,内部方法可能长时间不完成,因此必须在插件侧设置明确超时,并把结果标记为 `timeout`,不能无限等待。
|
||||
|
||||
## 10. 验证记录
|
||||
|
||||
本次只读探查验证了以下事实:
|
||||
|
||||
| 检查项 | 结果 |
|
||||
| --------------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| 远程 Chromium 9222 | 可连接 |
|
||||
| OneTalk 页面 | 已找到 |
|
||||
| `window.IcbuIM.IMBaaSSDK` | 存在 |
|
||||
| `IMBaaSSDK.default` | 只有 `sdkVersion` |
|
||||
| `window.__conversationListData__` | 存在,当前有 2 个会话条目 |
|
||||
| 基础资料 | 已包含 `aliId/loginId/name/companyName` |
|
||||
| 外层 `getConversationContactDetailList` | 空 Promise,不可用 |
|
||||
| 内部 `conversationServiceHttp.getConversationContactDetailList` | 可用 |
|
||||
| 客户详情请求 | `POST /message/listRecentConversationContactDetail.htm` 返回 200 |
|
||||
| 右侧 DOM | 包含公司、邮箱、注册时间、买家标签 |
|
||||
| 基础 SDK 返回邮箱/注册时间 | 当前未包含 |
|
||||
|
||||
## 11. 最小可用实现示例
|
||||
|
||||
下面是只返回安全字段的最小示例。它应当在 OneTalk 页面 MAIN world 中执行:
|
||||
|
||||
```js
|
||||
async function collectOneTalkContact(aliId) {
|
||||
const row = window.__conversationListData__?.[String(aliId)];
|
||||
if (!row) {
|
||||
return { status: "not_found", aliId: String(aliId) };
|
||||
}
|
||||
|
||||
const contact = row.contact || row;
|
||||
const sdk = window.IcbuIM?.IMBaaSSDK;
|
||||
const service = sdk?.IcbuConversationServiceImpl?.getInstance?.();
|
||||
const http = service?.conversationServiceHttp;
|
||||
|
||||
let refreshed = null;
|
||||
|
||||
if (
|
||||
http &&
|
||||
typeof http.getConversationContactDetailList === "function" &&
|
||||
(row.aliIdEncrypt || row.accountIdEncrypt) &&
|
||||
row.chatToken
|
||||
) {
|
||||
const [detail] = await http.getConversationContactDetailList([
|
||||
{
|
||||
aliId: contact.aliId || row.aliId,
|
||||
encryptAliId: contact.aliIdEncrypt || row.aliIdEncrypt,
|
||||
encryptAccountId: contact.accountIdEncrypt || row.accountIdEncrypt,
|
||||
chatToken: contact.chatToken || row.chatToken,
|
||||
},
|
||||
]);
|
||||
refreshed = detail || null;
|
||||
}
|
||||
|
||||
const source = refreshed || contact;
|
||||
|
||||
return {
|
||||
status: refreshed ? "confirmed" : "partial",
|
||||
conversationId: row.cid || null,
|
||||
aliId: source.aliId ?? contact.aliId ?? row.aliId ?? null,
|
||||
accountId: source.accountId ?? contact.accountId ?? row.accountId ?? null,
|
||||
loginId: source.loginId ?? contact.loginId ?? row.loginId ?? null,
|
||||
name: source.name ?? contact.name ?? row.name ?? null,
|
||||
companyName: source.companyName ?? contact.companyName ?? row.companyName ?? null,
|
||||
countryCode:
|
||||
source.complianceCountryCode ??
|
||||
contact.complianceCountryCode ??
|
||||
row.complianceCountryCode ??
|
||||
null,
|
||||
serviceType: source.serviceType ?? contact.serviceType ?? row.serviceType ?? null,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
该示例故意没有返回:
|
||||
|
||||
```text
|
||||
chatToken
|
||||
aliIdEncrypt
|
||||
accountIdEncrypt
|
||||
loginIdEncrypt
|
||||
kHTAccessToken
|
||||
fullPortrait
|
||||
productRelation
|
||||
完整原始响应
|
||||
```
|
||||
|
||||
如果需要邮箱、注册时间或买家标签,应在此基础上另加一个“完整详情观察”步骤,而不是扩大基础联系人接口的敏感字段转发范围。
|
||||
Reference in New Issue
Block a user