25 KiB
OneTalk 客户资料获取方法与参数
记录日期:2026-08-27
适用页面:
https://onetalk.alibaba.com/message/weblitePWA.htm本文记录一次对真实 Chromium 页面运行时的只读探查结果,重点是如何获取会话对应的客户姓名、公司、登录 ID、阿里 ID 等资料,以及如何继续刷新客户详情。页面和静态 bundle 版本可能变化,生产代码必须保留特征检测、超时和字段白名单。
当前任务边界(2026-08-31):本文是历史探查证据,不是当前跨层实现合同。当前第一阶段只允许 MAIN world 读取已经加载的白名单基础资料,并通过现有 Bright WebSocket 投递;详情刷新、邮箱、注册时间、买家标签、DOM 适配和群聊成员属于未来范围。文中出现的 token、加密 ID 和内部详情调用只用于说明探查结果,禁止进入页面桥、Service Worker、Bright、Mind、日志或持久化。
1. 结论
当前 OneTalk 页面有三层资料入口:
-
首选:
window.__conversationListData__会话列表模块已经把客户资料放在这个全局对象中。对于已经加载到会话列表的联系人,不需要额外请求,就能取得
aliId、loginId、姓名、公司等字段。 -
延期的详情刷新探查:
conversationServiceHttp.getConversationContactDetailList()window.IcbuIM.IMBaaSSDK中具体的IcbuConversationServiceImpl实例带有内部 HTTP 适配器。该适配器可以用页面已有的加密 ID 和 chat token 刷新联系人资料。 -
延期的完整客户详情:客户详情微应用自己的
contactMemberInfo请求右侧“客户详情”面板中的邮箱、注册时间等字段不在当前基础联系人对象中。静态 bundle 显示客户详情微应用会调用
/message/contact/detail/contactMemberInfo.htm。这部分应优先通过页面微应用已有逻辑或已渲染 DOM 获取,不要在插件 Service Worker 中自行拼接 CRM 请求。
最简单、最稳定的基础资料获取路径是:
window.__conversationListData__
→ 读取 contact
→ 白名单提取字段
→ 发送给 Mind
缺资料时再走(历史探查建议,当前任务禁止):
IcbuConversationServiceImpl.getInstance()
.conversationServiceHttp
.getConversationContactDetailList(...)
2. 本次运行时探查结果
2.1 页面和 SDK 根对象
页面上的根对象存在:
window.IcbuIM;
window.IcbuIM.IMBaaSSDK;
window.IcbuIM.IMBaaSSDK.default 当前只有:
{
sdkVersion: "...";
}
客户资料相关对象在 IMBaaSSDK 顶层,而不在 default 中。已确认的相关入口包括:
AuthService
ConversationService
MessageService
IcbuAuthServiceImpl
IcbuConversationServiceImpl
IcbuMessageServiceImpl
chatSdk
dataManager
baasDataManager
chatSdk.getConversationService() 返回的是公共会话服务,当前没有暴露客户详情方法。具体实现实例的原型包含:
getConversationList
getConversationListByPagination
getConversationContactDetailList
listGroupMemberDetails
...
2.2 当前页面的客户资料全局对象
页面存在:
window.__conversationListData__;
window.__conversationListMapFullData__;
window.__conversationListFullData__;
其中:
window.__conversationListMapFullData__ === window.__conversationListData__;
会话列表 bundle 中的同步逻辑大致如下:
window.__conversationListData__ = map;
window.__conversationListMapFullData__ = window.__conversationListData__;
window.EventBus.publish("im-conversation-list:syncData", map);
因此应当把 __conversationListData__ 作为主读取入口,并订阅 im-conversation-list:syncData 获取后续更新。
本次页面上该对象有 2 个已加载会话。典型条目如下:
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 转发的字段:
aliIdEncrypt
accountIdEncrypt
loginIdEncrypt
chatToken
ext.custom
latestMessage
msgCache
不能把整个会话对象直接序列化后发送给 Mind。
3. 直接读取已有客户资料
推荐使用 contact 优先、条目自身字段兜底的方式。这样可以兼容不同版本把资料放在顶层或 contact 下的情况。
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 是文档化的输出白名单;生产实现应显式构造对象,而不是使用:
JSON.stringify(row);
3.1 订阅会话资料更新
会话列表模块会通过页面 EventBus 发布全量 map:
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
下面这个路径在当前页面版本中存在,但实现为空:
window.IcbuIM.IMBaaSSDK.IcbuConversationServiceImpl.getInstance().getConversationContactDetailList;
运行时函数体表现为:
function (e) {
return new Promise(function (resolve, reject) {});
}
调用它不会发出网络请求,也不会完成 Promise。
ConversationService.getInstance().getConversationContactDetailList() 当前同样是空实现。不要把这两个外层方法作为生产入口。
4.2 已验证可用的入口:内部 HTTP 适配器
真正可用的实现位于具体实例的 conversationServiceHttp:
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 会话执行了该调用:
请求:POST /message/listRecentConversationContactDetail.htm
HTTP 状态:200
返回联系人数量:1
返回资料中确认包含:
serviceType
loginId
kHTAccessToken
companyName
complianceCountryCode
aliId
currentTimeZone
fullPortrait
accountId
name
productRelation
block
loginIdEncrypt
返回结果至少可以取得:
{
aliId,
accountId,
loginId,
name,
companyName,
complianceCountryCode,
currentTimeZone,
serviceType,
block,
}
4.3 刷新入参来源
row 可以从 window.__conversationListData__ 获得:
const row = window.__conversationListData__[String(aliId)];
当前会话条目的顶层和 contact 对象都可以看到这些参数键:
| 参数 | 作用 | 是否可跨边界发送 |
|---|---|---|
aliId |
联系人的 OneTalk 阿里 ID | 可以,按白名单发送 |
encryptAliId |
aliId 的页面请求加密值 |
不可以,仅在页面内部使用 |
encryptAccountId |
账号 ID 的页面请求加密值 | 不可以,仅在页面内部使用 |
chatToken |
当前页面请求令牌 | 不可以,不能记录或发送给 Mind |
内部实现读取参数的规则是:
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) 内部会把输入转换为:
{
contactEncryptAliIdList: ["..."],
contactEncryptAccountIdList: [],
chatTokens: "..."
}
实际接口路径为:
POST https://onetalk.alibaba.com/message/listRecentConversationContactDetail.htm
生产代码不应在 Service Worker 或服务端手工调用这个 URL。正确做法是让 MAIN world 页面适配器调用页面已有服务,再返回已清洗的结果。
4.5 建议的安全包装
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);
};
实际实现还应增加超时、页面身份检查和结果状态,例如:
confirmed 已拿到资料且 ID 范围匹配
partial 只拿到姓名/公司等部分字段
not_found 页面返回空联系人
unavailable SDK 或页面服务不存在
timeout 页面请求未完成
identity_mismatch 返回资料和目标联系人不匹配
5. 返回资料与右侧完整详情的边界
5.1 基础联系人接口不包含的字段
本次 listRecentConversationContactDetail.htm 返回结果没有看到:
email
registerTime
registrationTime
buyerTags
而右侧客户详情 DOM 中确实能看到:
邮箱
注册时间
买家标签
因此不能假设 getConversationContactDetailList 会返回右侧卡片的全部字段。
5.2 右侧客户详情模块的请求
公开静态 bundle 中存在以下页面函数:
getContactInfo;
其请求路径为:
POST /message/contact/detail/contactMemberInfo.htm
页面代码传入的参数形状为:
{
contactAccountIdEncrypt: contact.accountIdEncrypt,
chatToken: contact.chatToken,
}
页面逻辑随后从响应的 data 更新客户详情状态。这个请求属于 CRM 客户详情微应用,不是当前 IMBaaSSDK 顶层公开的简单方法。
建议优先级:
- 让页面自己的客户详情微应用加载数据,再从稳定的页面状态或已渲染 DOM 读取;
- 如果后续确认页面存在稳定的、页面自有的资料方法,再在 MAIN world 内调用;
- 不在 Service Worker 中保存或重放
chatToken; - 不把邮箱、注册时间等字段误认为消息字段。
5.3 DOM 兜底定位
本次页面看到的可用 DOM 特征包括:
.alicrm-buyerLoginId-text
.name-text
.base-information-form-item
.base-information-form-item-label
.base-information-form-item-content .content
例如:
<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 映射规则
重要:URL 的
activeAccountId是当前选中对话账号(例如243340382),不是登录人。channelAccountId必须读取页面运行时的currentUserAccountId或IcbuIM.UserUtil.currentUser.accountId(本次探查为286995452)。
6.1 当前页面观察到的 ID
当前 Heena Liu 会话中同时出现了:
aliId = 2208314000798
loginId = hzhago
accountId = 243340382
右侧卡片显示的 ID: hzhago 是 loginId,不是 aliId。
消息对象中的 sender.targetId 当前观察到更接近 aliId,例如:
sender.targetId = 2208314000798
所以不能只保存一个“ID”字段。建议 Mind 侧至少保存:
channelAccountId
aliId
loginId
name
companyName
资料映射主键建议为:
channelAccountId + aliId
loginId 作为可展示和辅助匹配的别名保存。
6.2 不要把 accountId 误当成联系人 ID
当前联系人对象中的 accountId 与页面 active account 相关,不能仅凭名称判断它是买家的独立联系人 ID。生产实现应保留原始字段并在多个样本上验证:
消息 sender.targetId
会话对象 aliId
资料对象 aliId
资料对象 loginId
页面 ID 文本
只有确认字段对应关系后,才能建立自动映射。
6.3 单聊和群聊
- 单聊:通常可以用会话对象的
contact直接得到一个联系人。 - 群聊:会话标题不是联系人资料;需要使用成员列表服务逐个得到成员,再分别匹配
aliId/loginId。 - 当前
getConversationContactDetailList可以批量传多个联系人对象,但每个联系人仍必须带自己的加密 ID和页面 token。
7. 向 Mind 发送的历史建议(不是当前实现契约)
以下结构保留作 2026-08-27 探查记录。当前实现以共享 OneTalk contract、profile page envelope、Bright frame 和 Mind profile HTTP 规范为准,不使用本节的旧顶层字段结构。
客户资料应作为独立资料事件发送,不要附加到消息正文,也不要把原始会话对象当作 payload:
{
type: "contact.profile.observed",
source: "onetalk.page",
channelAccountId: "286995452",
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",
],
}
建议增加:
observationStatus: confirmed | partial | unavailable | mismatch
profileFingerprint
lastObservedAt
manualLock / manuallyVerified
Mind 侧如果已有联系人表,优先复用。没有合适结构时,可以增加一个资料观察/映射表,而不是把这些字段写入消息表。
8. 推荐插件实现结构
建议新增独立的页面资料观察模块,例如:
apps/chrome-extension/src/onetalk/main-page/contact-observer/
模块职责:
- 读取
window.__conversationListData__初始快照; - 订阅
im-conversation-list:syncData; - 未来范围:对资料缺失的会话调用
conversationServiceHttp.getConversationContactDetailList; - 未来范围:对邮箱、注册时间等完整详情使用页面微应用状态或 DOM 补充;
- 对返回对象执行字段白名单清洗;
- 通过已有 page bridge / Service Worker 发送给 Mind;
- 以
channelAccountId + aliId去重; - 对每个联系人记录成功、部分成功、超时和 ID 不匹配状态。
上面的详情刷新策略是未来范围。当前第一阶段策略:
实时会话列表更新 → 立即采集已加载的白名单基础资料
首次有效页面 hello → 请求当前已加载单聊 snapshot
邮箱/注册时间/买家标签/显式回填 → 另立 task
不要对每一条消息重复请求客户资料;应按联系人去重。
9. 错误处理与可观测性
9.1 必须区分的错误
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 诊断字段
日志可以记录:
requestId
channelAccountId
conversationId
aliId(必要时)
结果状态
返回字段名集合
耗时
时间戳
日志禁止记录:
chatToken
aliIdEncrypt
accountIdEncrypt
loginIdEncrypt
kHTAccessToken
Cookie
CSRF
完整原始响应
9.3 页面离线与未来详情刷新
本次第一次调用外层空 Promise 时没有任何请求;改用内部 HTTP 适配器后能收到 200 响应。这是历史探查结果,不是当前任务的调用授权。若未来任务重新启用详情刷新,必须另行定义 endpoint、token 边界和超时;当前 profile snapshot 命令本身是 fire-and-forget,不能阻塞消息 bootstrap。
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 返回邮箱/注册时间 | 当前未包含 |
10.1 当前仓库的本地 profile receipt(仅测试)
当前仓库的 apps/mind-test-harness 提供一个仅供本地验证的接口:
POST /internal/bright/onetalk/contact-profiles
Body: { channelAccountId, binding, profiles[] }
Response: 204 No Content
它复用共享 OneTalk profile/头像 URL 校验,只按 channelAccountId + aliId 在注入或默认的内存 store 中保存白名单字段;有效头像 URL 会保留,显式 avatarUrl: null 会覆盖旧值。账号或 binding 不匹配、额外或敏感字段会被拒绝,未知的 /collector/v1/sync-batches 路径仍返回 404。
该 receipt 只证明本地 mock 的请求边界和 URL/null 存储语义,不代表真实 Mind 服务已接通。仓库仍没有 TradeMind Workbench、/collector/v1/sync-batches、customers[].avatarUrl 或 tm-binding-avatar 实现;真实 Mind/Workbench/collector/Chromium 联调保持 external_unverified。
11. 历史最小示例(不可直接用于当前任务)
本节代码展示当时探查到的内部详情刷新方式,故意保留为未来任务的证据。当前实现不得调用其中的 HTTP 详情方法,也不得让 chatToken 或加密 ID 离开 MAIN world。
下面是只返回安全字段的最小示例。它应当在 OneTalk 页面 MAIN world 中执行:
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,
};
}
该示例故意没有返回:
chatToken
aliIdEncrypt
accountIdEncrypt
loginIdEncrypt
kHTAccessToken
fullPortrait
productRelation
完整原始响应
如果需要邮箱、注册时间或买家标签,应在此基础上另加一个“完整详情观察”步骤,而不是扩大基础联系人接口的敏感字段转发范围。