purser文档

身份令牌:告诉 purser 顾客是谁

在你的服务器上用身份签名密钥签一个短时、一次性的令牌,让聊天窗口认出已登录的顾客。

聊天窗口运行在访客的浏览器里,它自己说「我是某某顾客」是不可信的。所以 purser 的聊天接口里没有任何「顾客 ID」参数:只有你的服务器用身份签名密钥签出来的令牌,才能让 purser 相信这位访客是你店里的哪位顾客。

没有令牌、或者令牌验证不过,对话就是匿名的。匿名对话里 AI 不会透露任何订单信息,问「我的订单到哪了」会被请去登录。

认出顾客之后有什么不同

匿名访客已验证顾客
订单和物流AI 不给任何订单信息AI 直接拿到他最近 5 个订单(状态、商品、金额、物流),不需要再报订单号;收货地址和邮箱不会交给模型
顾客分层不进任何分层按你定义的分层(VIP 等)享受对应的服务策略,见 顾客与分层
退款AI 不会提出退款AI 只会针对他自己的订单、在政策允许时提出退款,见 退款
上一次对话客服只能看到按设备匹配的记录(标注「可能不是同一个人」),AI 看不到30 天内有过对话的,AI 会拿到上一次对话的摘要当背景;客服在收件箱里能看到上一次对话

令牌里的顾客 ID(sub)要和你通过 API 推送订单时用的 customerId 一致,AI 才能找到他的订单。推送方法见 REST API 与 Node SDK。

你需要的两个值

在控制台「接入」页的「4. 识别已登录的顾客」卡片里:

  • 「工作区 ID(iss)」:放进令牌的 iss;
  • 「身份签名密钥」:签名用的密钥。

这张卡片只有工作区的店主和管理员能看到。身份签名密钥和 API 密钥(sk_…)是两回事:API 密钥泄露了也不能拿来冒充顾客。

身份签名密钥泄露时,点卡片里的「更换密钥」。新密钥立即生效,用旧密钥签的令牌马上失效:在你把服务器的环境变量换成新密钥之前,已登录的顾客会被当作匿名访客(能聊天,但看不到订单)。所以先准备好更新服务器,再点更换。

密钥只放在服务器上

身份签名密钥等于「以任何顾客身份登录聊天」的能力。只放在服务器的环境变量里,永远不要写进网页、App 安装包或前端代码。

令牌格式

令牌是一个 HS256 签名的 JWT:

// header
{ "alg": "HS256", "typ": "JWT" }

// payload
{
  "iss": "<工作区 ID>",
  "sub": "<你系统里的顾客 ID>",
  "vid": "<访客 ID>",
  "jti": "<每个令牌唯一的随机值>",
  "iat": 1790000000,
  "exp": 1790000300
}
字段说明
iss工作区 ID,必须和控制台显示的一致
sub你系统里的顾客 ID(字符串)
vid访客 ID。网站上是嵌入脚本写在 localStorage 里的 purser_vid;App 客服页是你传给页面的 vid 参数
jti随机唯一值,例如 UUID
iat签发时间,Unix 秒
exp过期时间,Unix 秒

签名密钥就是「身份签名密钥」这个字符串本身(按 UTF-8 字节)。只接受 HS256,其他算法一律当作无效。

有效期和一次性使用

  • 最长 10 分钟:exp - iat 或 exp - 当前时间 超过 600 秒的令牌会被拒绝,哪怕签名正确。SDK 默认签 300 秒(5 分钟)。
  • 只能用一次:每个 jti 在工作区里只接受一次,同一个令牌第二次出现会被当作重放,对话变成匿名。
  • 绑定访客:vid 必须和发起聊天的那个浏览器(或 App)的访客 ID 一致。从日志或截图里抄走的令牌在别的浏览器里没用。

所以不要在页面渲染时签一个令牌长期放着,而是聊天窗口打开时再去你的服务器要一个新的。下面的写法就是这样做的。聊天窗口和 反馈工具 各自消耗一个令牌,两个都用时要各签一个。

在 Node.js 里签令牌

不依赖任何第三方库,只用 Node 自带的 crypto(Node 20 及以上):

import { createHmac, randomUUID } from "node:crypto";

const b64url = (s) => Buffer.from(s).toString("base64url");

export function signIdentityToken({ secret, workspaceId, customerId, visitorId, ttlSeconds = 300 }) {
  const now = Math.floor(Date.now() / 1000);
  const header = { alg: "HS256", typ: "JWT" };
  const payload = {
    iss: workspaceId,
    sub: String(customerId),
    vid: String(visitorId),
    jti: randomUUID(),
    iat: now,
    exp: now + Math.min(ttlSeconds, 600),
  };
  const data = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
  const sig = createHmac("sha256", secret).update(data).digest("base64url");
  return `${data}.${sig}`;
}

这和 purser 服务端验签的方式完全一致。任何语言的标准 JWT 库也可以,只要用 HS256、字段如上、有效期不超过 10 分钟。

@purser-ai/node 里有同名的 signIdentityToken(参数相同,返回 Promise)。这个包还没有发布到 npm,发布之前请用上面这段代码。

接到网站上

1. 服务器:给已登录的顾客签令牌。 以 Express 为例:

app.get("/purser-token", (req, res) => {
  const customer = req.session?.customer;          // 你自己的登录态
  const vid = req.query.vid;
  if (!customer || typeof vid !== "string") return res.status(204).end();
  const token = signIdentityToken({
    secret: process.env.PURSER_IDENTITY_SECRET,
    workspaceId: process.env.PURSER_WORKSPACE_ID,
    customerId: customer.id,
    visitorId: vid,
  });
  res.set("cache-control", "no-store").type("text/plain").send(token);
});

2. 页面:告诉嵌入脚本去哪里拿令牌。 放在嵌入代码前后都可以:

<script>
  async function purserToken() {
    const vid = localStorage.getItem("purser_vid");
    if (!vid) return null;
    const r = await fetch("/purser-token?vid=" + encodeURIComponent(vid), { credentials: "same-origin" });
    return r.status === 200 ? r.text() : null;
  }
  window.Purser = window.Purser || {};
  window.Purser.identity = purserToken;
</script>

Purser.identity 可以是一个令牌字符串,也可以是返回令牌(或 Promise)的函数。嵌入脚本在聊天窗口打开时才调用它,每次打开都能拿到新令牌。聊天窗口最多等 1.5 秒,超时就按匿名开始,所以这个接口要快。

单页应用里登录、退出时,调用 Purser.identify():

// 顾客登录后
Purser.identify(purserToken);

// 顾客退出后
Purser.identify(null);

身份一变(匿名变已登录、换了一个顾客、退出登录),聊天窗口会重新加载,开始一段新对话。不同顾客的对话记录永远不会串在一起。

嵌入代码本身的写法见 网站聊天窗口。

App 客服页

App 里打开 /c/<公开密钥> 这个 H5 页时,把令牌放在链接的 # 后面:

https://askpurser.com/c/pk_…?vid=<访客ID>&lang=zh-CN#token=<身份令牌>

# 后面的部分不会发给任何服务器,页面读到后立刻从地址栏擦掉。vid 由 App 自己生成一个 UUID 存在本地、每次都用同一个,签令牌时用它作 visitorId。也可以由 App 在页面里注入 window.__PURSER_IDENTITY__ = "<令牌>"。详见 App 客服页。

身份永远不继承

每次开始聊天时,身份都要由页面用新令牌重新声明一次。没有令牌就是匿名,而且是一段新对话:

  • 同一台电脑上,上一位顾客已验证的会话不会被下一位访客沿用;
  • 已验证的对话和匿名对话不会互相接续;
  • 同一位顾客在同一个标签页里刷新页面、带着新令牌回来,才会接着原来的对话。

这是有意的:共用电脑时,只有这样下一个人才看不到上一位顾客的订单和物流链接。

排查令牌问题

令牌出问题时聊天窗口不会报错,只是按匿名处理。要看原因:打开浏览器开发者工具的「网络」面板,找聊天窗口 iframe 发出的 sessions 请求(POST /api/v1/widget/sessions),响应里的 identity 是 verified_hmac 就说明认出来了,否则看 identityError:

identityError原因
malformed不是合法的 JWT,或缺少 sub / jti / exp
wrong_algorithm不是 HS256
bad_signature签名不对:密钥用错了,或者签的不是这个工作区
wrong_issueriss 不是这个工作区的 ID
expired已过期(检查服务器时钟)
lifetime_too_long有效期超过 10 分钟
visitor_mismatchvid 和这个浏览器的访客 ID 不一致
replayed这个令牌已经用过了

控制台「接入」页顶部的「上线清单」里,「认出已登录的顾客」这一步会在第一次有已验证的对话后自动打勾。

本页内容