Files
trade-message-center/docs/onetalk-customer-profile-fetch.md

25 KiB
Raw Permalink Blame History

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 页面有三层资料入口:

  1. 首选:window.__conversationListData__

    会话列表模块已经把客户资料放在这个全局对象中。对于已经加载到会话列表的联系人,不需要额外请求,就能取得 aliIdloginId、姓名、公司等字段。

  2. 延期的详情刷新探查:conversationServiceHttp.getConversationContactDetailList()

    window.IcbuIM.IMBaaSSDK 中具体的 IcbuConversationServiceImpl 实例带有内部 HTTP 适配器。该适配器可以用页面已有的加密 ID 和 chat token 刷新联系人资料。

  3. 延期的完整客户详情:客户详情微应用自己的 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 顶层公开的简单方法。

建议优先级:

  1. 让页面自己的客户详情微应用加载数据,再从稳定的页面状态或已渲染 DOM 读取;
  2. 如果后续确认页面存在稳定的、页面自有的资料方法,再在 MAIN world 内调用;
  3. 不在 Service Worker 中保存或重放 chatToken
  4. 不把邮箱、注册时间等字段误认为消息字段。

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 必须读取页面运行时的 currentUserAccountIdIcbuIM.UserUtil.currentUser.accountId(本次探查为 286995452)。

6.1 当前页面观察到的 ID

当前 Heena Liu 会话中同时出现了:

aliId    = 2208314000798
loginId  = hzhago
accountId = 243340382

右侧卡片显示的 ID: hzhagologinId,不是 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/

模块职责:

  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 不匹配状态。

上面的详情刷新策略是未来范围。当前第一阶段策略:

实时会话列表更新 → 立即采集已加载的白名单基础资料
首次有效页面 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-batchescustomers[].avatarUrltm-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
完整原始响应

如果需要邮箱、注册时间或买家标签,应在此基础上另加一个“完整详情观察”步骤,而不是扩大基础联系人接口的敏感字段转发范围。