开发者指南

访客识别 API

以编程方式识别已登录的用户,让他们不再看到邮箱提示,他们的对话也会自动关联到联系人。

适用场景

如果你的网站有已登录用户(SaaS 应用、仪表板、会员门户),你本来就知道他们是谁。使用 identify 命令把他们的邮箱传给 Sonny,用户就无需再次输入。

  • 已登录用户完全跳过邮箱提示
  • 自动将聊天对话关联到其联系人记录
  • 立即在收件箱中看到访客的姓名和邮箱
  • 借助已验证身份,在不同浏览器和设备间延续历史记录

跨设备延续

已验证的访客身份

基本的邮箱识别很方便,但浏览器可以声称任何邮箱。已验证身份增加了一个由你的服务器签名的短期 JWT。这样 Sonny 就能安全地把客户的联系人作为其网站聊天历史的归属,让同样的对话出现在另一个浏览器或设备上。

关

默认设置。现有的嵌入代码和访客都不受影响;历史记录仍与浏览器的访客 ID 绑定。

可选验证(推荐)

持有有效 JWT 的访客可获得跨设备历史记录。没有 JWT 的访客保持现有的匿名或邮箱识别体验。

需要证明才能识别身份

匿名聊天仍然可用,但 identify 和自定义属性调用需要有效的签名 JWT。

验证模式不会让必填的聊天前字段变为可选。各模式如何满足必填邮箱,请参阅必填的聊天前信息。

1. 生成签名密钥

打开渠道 → 你的渠道 → 在线聊天,找到安全访客身份,选择一种模式并生成密钥。明文只显示一次。将其作为 SONNY_IDENTITY_SECRET 保存到后端的密钥管理工具中。切勿把签名密钥放在浏览器代码、嵌入代码片段、公开的环境变量或源代码仓库中。

2. 在后端签发短期 JWT

使用 HS256 签名。Sonny 要求包含 user_id、email、iat 和 exp。user_id 必须是你自己数据库中稳定的 ID,而不是邮箱地址。令牌有效期最长 24 小时;15 分钟是不错的默认值。允许最多 60 秒的时钟误差。

你还可以加入一个服务器签名的 traits 对象,提供可信的客服上下文,例如 merchantName、subdomain、plan、platform 和 role。特征值必须是字符串、有限数字、布尔值或 null。Sonny 会把它们保存在网站范围的已验证联系人身份上。浏览器提供的属性仍为未验证状态,永远不会被提升为这些签名特征。

Node.js
import { SignJWT } from 'jose';

export async function createSonnyIdentityToken(user) {
  const secret = new TextEncoder().encode(process.env.SONNY_IDENTITY_SECRET);

  return new SignJWT({
    user_id: String(user.id),
    email: user.email,
    traits: {
      merchantName: user.merchant.name,
      subdomain: user.merchant.subdomain,
      plan: user.merchant.plan,
      platform: user.merchant.platform,
      role: user.role
    }
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m')
    .sign(secret);
}
Ruby
require 'jwt'

def sonny_identity_token(user)
  now = Time.now.to_i
  payload = {
    user_id: user.id.to_s,
    email: user.email,
    traits: {
      merchantName: user.merchant.name,
      subdomain: user.merchant.subdomain,
      plan: user.merchant.plan,
      platform: user.merchant.platform,
      role: user.role
    },
    iat: now,
    exp: now + 15 * 60
  }

  JWT.encode(payload, ENV.fetch('SONNY_IDENTITY_SECRET'), 'HS256')
end
Python
import os
from datetime import datetime, timedelta, timezone
import jwt

def sonny_identity_token(user):
    now = datetime.now(timezone.utc)
    return jwt.encode({
        "user_id": str(user.id),
        "email": user.email,
        "traits": {
            "merchantName": user.merchant.name,
            "subdomain": user.merchant.subdomain,
            "plan": user.merchant.plan,
            "platform": user.merchant.platform,
            "role": user.role,
        },
        "iat": now,
        "exp": now + timedelta(minutes=15),
    }, os.environ["SONNY_IDENTITY_SECRET"], algorithm="HS256")
PHP
use Firebase\JWT\JWT;

function sonnyIdentityToken($user): string {
    $now = time();
    return JWT::encode([
        'user_id' => (string) $user->id,
        'email' => $user->email,
        'traits' => [
            'merchantName' => $user->merchant->name,
            'subdomain' => $user->merchant->subdomain,
            'plan' => $user->merchant->plan,
            'platform' => $user->merchant->platform,
            'role' => $user->role,
        ],
        'iat' => $now,
        'exp' => $now + (15 * 60),
    ], $_ENV['SONNY_IDENTITY_SECRET'], 'HS256');
}

3. 只把 JWT 传给聊天组件

从受你常规应用会话保护的端点返回 JWT。在 init 之前或之后调用 identify。聊天组件只在内存中保存 JWT:绝不会保存到 localStorage、Cookie 或 URL 中。

JavaScript
// Fetch a short-lived JWT from your authenticated backend.
// Your signing secret never reaches this code.
const { userJwt } = await fetch('/api/sonny-identity').then((res) => res.json());

sonny('identify', { userJwt });
sonny('init', { siteId: 'YOUR_SITE_ID' });

// Sonny asks for a fresh token after expiry or an emergency key change.
window.addEventListener('sonny:identity-required', async () => {
  const { userJwt } = await fetch('/api/sonny-identity').then((res) => res.json());
  sonny('identify', { userJwt });
});

安全地刷新、退出和轮换

  • 刷新:在 JWT 过期前刷新——15 分钟的令牌大约在第 10 分钟刷新——然后调用 sonny('identify', { userJwt })。同时监听 sonny:identity-required,作为令牌过期或签名密钥变更时的兜底。
  • 退出登录:调用 sonny('reset')。这会丢弃内存中的 JWT,清除浏览器的聊天组件会话,并开始一个新的匿名访客会话。
  • 切换账户:在获取用户 B 的 JWT 并调用 sonny('identify', { userJwt }) 之前,先为用户 A 调用 sonny('reset')。切勿把一个用户的令牌带入另一个已登录会话。
  • 常规轮换:Sonny 会在 24 小时内继续接受旧密钥,让你有时间更新每个后端实例。重叠期结束前,无法进行第二次常规轮换。
  • 怀疑密钥泄露:选择立即替换。两个旧签名密钥会立即停止认证新请求;已连接的聊天组件会向宿主页面请求新的 JWT。
  • 关闭验证:Sonny 会丢弃当前和之前的签名密钥,并断开已验证的聊天组件。再次启用验证前,请先生成新密钥。

身份冲突不会自动合并

如果稳定的用户 ID 和邮箱指向不同的 Sonny 联系人,验证会返回冲突,而不是悄悄合并客户记录。请更正令牌或在 Sonny 中合并联系人,然后重试。

仅通过早前未签名的邮箱关联的聊天,不会被自动视为已验证的历史记录。这可以防止浏览器提供的邮箱解锁其他客户的对话。

帮助中心登录

将你现有的客户登录连接到托管或自定义域名的帮助页面。使用与聊天组件相同的网站签名密钥和短期 JWT。基本的邮箱识别和 Sonny 员工登录都不会授予已验证读者的访问权限。

  1. 先为该渠道完成上文的已验证访客身份设置。在“渠道 → 你的渠道 → 在线聊天”下启用“可选验证”或“需要证明才能识别身份”。
  2. 在你的应用中实现一个经过认证的端点,返回 { "userJwt": "SIGNED_TOKEN" }。在服务器上根据已登录客户和可信特征签发令牌;绝不接受浏览器请求的套餐或角色。
  3. 在“渠道 → 你的渠道 → 帮助中心 → 帮助中心访问权限”中,选择“已验证客户”,并可选择一个“客户受众”。在“客户登录 URL”中填写你应用的登录页面,然后选择“保存访问权限”。
  4. 客户登录后,运行下面的交换流程,并将 YOUR_SLUG 替换为帮助中心的“地址”。仅有登录 URL 是不够的:你的应用必须完成此交换。
JavaScript
// Run after your application authenticates the customer.
// This endpoint must use the server's session and trusted customer traits.
const response = await fetch('/api/sonny-identity');
if (!response.ok) throw new Error('Unable to sign in to the help center');
const { userJwt } = await response.json();
const signIn = new URL('https://www.usesonny.com/help/YOUR_SLUG/auth');
signIn.searchParams.set('token', userJwt);
signIn.searchParams.set('return', '/help/YOUR_SLUG');
window.location.assign(signIn);

对于自定义域名,将 signIn 设为 https://help.example.com/auth,并返回到 / 或 /articles/getting-started。托管页面的返回路径以 /help/YOUR_SLUG 开头。返回目标必须位于同一主机。

交换流程会设置一个安全的 HTTP-only Cookie,并通过重定向把令牌从 URL 中移除。读者会话随 JWT 一同过期,最长 24 小时。请使用短期令牌,并避免记录交换 URL。JWT 只用于这一次登录交换,不应出现在共享的文章链接中。

测试登录和访问权限

  1. 在未登录状态下访问私密帮助中心。点击其登录按钮,登录你的应用,确认你会返回到同一托管域名或自定义域名上的帮助中心。
  2. 分别通过浏览、搜索和直接 URL,检查一篇允许访问和一篇拒绝访问的文章。不在帮助中心受众范围内的客户会看到访问提示;被拒绝的文章会返回“未找到”。
  3. 在帮助中心中使用“退出登录”,确认私密内容消失。再用具有不同特征的客户重复一遍。

刷新与退出登录

聊天组件会自动发送当前的 JWT。当 Sonny 请求时刷新它,并在客户退出登录时调用 sonny('reset')。聊天组件身份与帮助中心 Cookie 相互独立:重置聊天组件不会退出托管帮助中心的会话。

要退出托管页面,请访问 /help/YOUR_SLUG/auth/logout;在你的自定义域名上,使用 /auth/logout。如果需要同时结束两个会话,请把这个操作接入你应用的退出登录流程。无效或过期的凭据只有访客访问权限。关闭身份验证时,所有读者都只有访客访问权限。

客户特征变化时,签发新的 JWT 并在聊天组件中重新识别;对托管页面则重新执行登录交换。已有的读者 Cookie 会在过期前保留之前的签名特征。受众规则的变更会在下一次请求时生效。

创建受众、预览访问权限并排查限制问题

邮箱识别不等于身份认证

由浏览器提供邮箱的方式可以丰富客服上下文,但并不能认证访客身份。访客可以在自己的页面上检查并运行 JavaScript,因此邮箱、姓名和自定义属性永远无法解锁其他客户的 Sonny 历史记录。已验证身份需要前文所述的服务器签名 JWT。你自己产品中的操作授权,请保留在已登录的应用内部处理。

启用访客验证后,收件箱中的已验证徽章表示该对话是通过服务器签名的 JWT 关联的。未验证徽章表示没有服务器签名的身份认证过该对话。显示的任何姓名或邮箱都只是访客提供的客服上下文,不能证明身份。

代码示例

最简单的调用——只传入用户的邮箱:

JavaScript
// Identify a logged-in user
sonny('identify', {
  email: 'jane@example.com'
});

同时传入用户的姓名,让客服在收件箱中看到:

JavaScript
// Identify with full name
sonny('identify', {
  email: 'jane@example.com',
  name: 'Jane Smith'
});

在识别时添加有用的客服上下文,稍后更新,或在页面打开期间监听会变化的值:

JavaScript
// Identify with support context
sonny('identify', {
  email: 'jane@example.com',
  name: 'Jane Smith',
  attributes: {
    plan: 'starter',
    seats: 5,
    trial: true
  }
});

// Update only the values that changed
sonny('setAttributes', {
  plan: 'growth',
  seats: 8,
  trial: null // Clears this property
});

// Keep a changing value in sync (checks every 10 seconds)
sonny('watchAttributes', () => ({
  monthly_usage: window.currentUsage
}), { interval: 10000 });

使用异步代码片段的完整示例。注意 identify 可以在 init 之前调用——身份信息会进入队列,并在聊天组件连接后立即发送:

HTML
<!-- Sonny widget snippet -->
<script>
  (function(w,d,s,o,f,js,fjs){
    w['Sonny']=o;w[o]=w[o]||function(){
    (w[o].q=w[o].q||[]).push(arguments)};
    js=d.createElement(s);fjs=d.getElementsByTagName(s)[0];
    js.id=o;js.src=f;js.async=1;fjs.parentNode.insertBefore(js,fjs);
  })(window,document,'script','sonny','https://www.usesonny.com/widget.js');

  // Identify before init — identity is queued and sent on connect
  sonny('identify', {
    email: 'jane@example.com',
    name: 'Jane Smith'
  });

  sonny('init', { siteId: 'YOUR_SITE_ID' });
</script>

用户退出登录时调用 reset,清除其身份并开始新的会话:

JavaScript
// Reset on logout — clears identity and starts a fresh session
sonny('reset');

工作原理

  1. 01

    聊天组件加载并连接

    聊天组件连接到 Sonny,并在加入时一并发送已保存的身份信息。

  2. 02

    身份信息发送到服务器

    Sonny 会在你的工作区中查找使用该邮箱的联系人,如果还不存在就创建一个。

  3. 03

    对话被关联

    当前浏览器访客会话中所有未关联的对话都会关联到已识别的联系人。识别不会合并不同浏览器或设备之间的历史记录。

  4. 04

    跳过邮箱提示

    由于访客已被识别,聊天组件内的邮箱提示会被隐藏——用户不会被打扰。

自定义属性规则

  • 每次调用最多发送 50 个属性。
  • 值可以是字符串、数字、布尔值或 null。字符串最长 1000 个字符。传入 null 或空字符串即可清除已保存的值。
  • 键必须以字母开头,只能包含字母、数字和下划线,最长 64 个字符。
  • 以下键为保留键,会被忽略:email, name, id, phone, createdAt, updatedAt。

API 参考

sonny('identify', { email, name?, attributes? })
识别当前访客。设置邮箱和可选的姓名,跳过邮箱提示,并将身份信息发送到服务器。可以在 init 之前或之后调用。
  • emailstring访客的邮箱地址
  • namestring?访客的显示名称
  • attributesobject?要附加到联系人的自定义属性(键和值的规则请参阅聊天组件设置指南)
sonny('identify', { userJwt, name?, attributes? })
通过服务器生成的 JWT 安全地识别当前已登录的客户。令牌只保存在内存中,并且只在经过认证的请求或 socket 负载中发送。
  • userJwtstring由你已认证的后端签发的新 HS256 JWT
  • namestring?访客的显示名称
  • attributesobject?要附加到已验证联系人的自定义属性
sonny('setAttributes', { ... })
更新已识别访客的自定义属性。只发送发生变化的值。如果访客尚未被识别,更新会等待,在 identify 执行后再发送。
  • attributesobject要设置的键值对。将值设为 null 即可清除属性。
sonny('watchAttributes', getter, { interval? })
定时调用你的 getter 函数,并自动同步有变化的属性。适用于套餐或用量等在页面打开期间会变化的值。
  • getterfunction返回当前属性对象的函数
  • intervalnumber?检查频率,单位为毫秒。默认 10000,最小 2000。
sonny('reset')
清除当前浏览器的聊天组件 ID、邮箱、姓名和本地对话历史,然后开始新的访客会话。它不会删除 Sonny 中的联系人或其已保存的属性。请在退出登录时使用。

需要帮助?

查看聊天组件设置指南,或联系我们的团队。

相关文档