在 WhatsApp 后面接 DeskClerk 的 AI 回答

在 WhatsApp 后面接 DeskClerk 的 AI 回答

1. 这一页适合谁

你已经有 WhatsApp 商务号(或者正在搭),想让网页上那套 AI 也能回答 WhatsApp 里的消息——通过你们自己的后端去接。

2. 你能得到什么 / 你得不到什么

你能得到

  • 你网站上那套 AI 回答,变成两个简单的 HTTPS 接口,你们自己的代码直接调。
  • 会话记忆:一个 WhatsApp 会话对应一个 conversationId,AI 记得前面说过什么。
  • 平台替你判好的转人工信号:该换人时,返回里带一个 escalation 对象,你们不用自己写判断规则。
  • 所有渠道共用一套接口。加 WhatsApp 不用改你在 DeskClerk 里的任何设置。

你得不到

  • 我们不在 WhatsApp 里面运行。我们没有 WhatsApp 的应用、机器人或连接器,也没有任何市场上架。
  • 我们不收也不发 WhatsApp 的消息。收和发,仍然是你自己的后端(或者你在这个渠道用的那套平台)在做。
  • 我们不知道你渠道里的用户 ID。external_user_id 是你自己传的,只要在你那边稳定就行。
  • 用量按你套餐的月度额度算,按回答条数计,不够了随时在工作台加购。

3. 开始之前要准备什么

  • 一个至少发布过一次的 DeskClerk 站点。 没发布,AI 手里就没有可答的内容。
  • 你的 Site ID(一个 UUID,例如 7b0c9f5e-2a8d-4f31-8e6b-1c2d3e4f5a6b)。工作台的站点设置里能看到。
  • 一把带 chat:write 权限的 API 钥匙。 在工作台建,只放在你们服务器上——不要放进手机 App,也不要放进前端 JavaScript。
  • 一台能发 HTTPS 请求的服务器,以及一个存 conversationId 的地方(每个用户会话存一个)。
  • 你要当 locale 传的那个语言代码。 必须是你这个站点已开启的语言(例如 en、zh),照站点设置里显示的写法原样传。
  • 你 WhatsApp 的消息后端。 WhatsApp 里收消息、发消息都在你那边;DeskClerk 只是你调用的回答引擎。

4. 分步操作

  1. 保留你自己的 WhatsApp 消息后端(接收 Meta 回调、负责发消息的那个服务)。DeskClerk 不接收 WhatsApp 的消息。
  2. 消息进来时,你的后端为每个 WhatsApp 会话建一个 DeskClerk 会话,把返回的 conversationId 存到你自己的会话 ID 旁边。
  3. 每收到一条新消息,把原文 POST 到发消息接口,把 aiMessage.content 当作回复发出去。
  4. 如果返回里没有 aiMessage,或者带了 escalation,就停止回答,把这条会话转给你们的人工。
  5. 同一会话的后续消息继续用同一个 conversationId,这样 AI 能记住上下文。

下面两步就是全部对接:先建会话,再逐条发消息。没有别的必做项;我们的接口只增不改,你今天写的代码以后一直能用。

5. 可复制的完整示例

先把三个值定下来,剩下的都是固定写法:

变量 示例值 从哪来
BASE_URL https://chat.example.com 承载你挂件和帮助中心的那个域名
SITE_ID 7b0c9f5e-2a8d-4f31-8e6b-1c2d3e4f5a6b 工作台 → 站点设置
DESKCLERK_API_KEY 你的钥匙 工作台 → API 钥匙(chat:write 权限)

第一步 · 建会话(每个会话建一次):

curl -sS -X POST "$BASE_URL/api/v1/integrations/sites/$SITE_ID/conversations" \
  -H "X-Integration-Api-Key: $DESKCLERK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: conv-whatsapp-12025550123-1" \
  -d '{"channel":"whatsapp","external_user_id":"12025550123","user_name":"张三","locale":"zh","timezone":"Asia/Shanghai"}'

返回里的 data.id 就是 conversationId,存到你自己的会话 ID 旁边。示例里存下来的会话 ID 是 0f4c2a91-5d63-4b8e-9f27-3a1b6c8d5e40。

第二步 · 逐条发消息、拿回答:

curl -sS -X POST "$BASE_URL/api/v1/integrations/sites/$SITE_ID/conversations/$CONVERSATION_ID/messages" \
  -H "X-Integration-Api-Key: $DESKCLERK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: msg-whatsapp-12025550123-1" \
  -d '{"content":"你们发货到加拿大吗?"}'

一段完整的 Node.js 处理代码,两个必须处理的分支都写了:

const BASE_URL = "https://chat.example.com";
const SITE_ID = "7b0c9f5e-2a8d-4f31-8e6b-1c2d3e4f5a6b";
const API_KEY = process.env.DESKCLERK_API_KEY;

async function reply(channel, externalUserId, locale, text) {
  const convRes = await fetch(`${BASE_URL}/api/v1/integrations/sites/${SITE_ID}/conversations`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Integration-Api-Key": API_KEY,
      "Idempotency-Key": `conv-${channel}-${externalUserId}-${Date.now()}`
    },
    body: JSON.stringify({
      channel,
      external_user_id: externalUserId,
      locale,
      timezone: "Asia/Shanghai"
    })
  });
  const conv = await convRes.json();
  if (conv.code !== 0) {
    throw new Error(`create conversation failed: ${conv.code} ${conv.message}`);
  }
  const conversationId = conv.data.id;

  const msgRes = await fetch(
    `${BASE_URL}/api/v1/integrations/sites/${SITE_ID}/conversations/${conversationId}/messages`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Integration-Api-Key": API_KEY,
        "Idempotency-Key": `msg-${conversationId}-${Date.now()}`
      },
      body: JSON.stringify({ content: text })
    }
  );
  const msg = await msgRes.json();
  if (msg.code !== 0) {
    throw new Error(`send message failed: ${msg.code} ${msg.message}`);
  }

  // 这一轮没有 AI 回答:转到你们那边的人工。
  if (!msg.data.aiMessage) {
    return { conversationId, text: "稍后会有同事在这里回复您。", escalate: true };
  }

  // 该不该转人工,平台已经判好了。
  const escalate = Boolean(msg.data.escalation);
  return { conversationId, text: msg.data.aiMessage.content, escalate };
}

reply("whatsapp", "12025550123", "zh", "你们发货到加拿大吗?").then(console.log);

发消息这个接口是同步的,最长可能要等 45 秒,所以你的 HTTP 客户端超时要设得比这长。要边生成边显示,就改用流式的那个接口 .../messages/stream,请求体一样。

6. 常见问题

  • 报 403 Forbidden / code 40301。 钥匙没带、被停用,或者这把钥匙没有这个接口要的权限。先确认请求头名字就是 X-Integration-Api-Key(或者 Authorization: Bearer <key>)、钥匙状态是 ACTIVE、这把钥匙的站点列表里有这个 Site ID。

  • 报 429 Too Many Requests / code 42901。 请求太快,超过这个站点的频率上限。读响应里的 Retry-After,等够秒数再退避重试,别在循环里死磕。

  • 网页右下角没有出现客服气泡。 在浏览器里查看页面源代码,搜 widget.js:搜不到说明这段代码没进最终页面(缓存插件或主题设置把它丢了);搜得到就打开浏览器控制台看有没有 Content-Security-Policy 报错,需要把 https://chat.example.com 加进 script-src 和 connect-src。广告拦截插件也会把气泡藏掉,换一个干净浏览器配置再试。

  • 气泡出来了,但回答老是说不知道。 知识是空的、还在处理中,或者知识的语言标签跟访客提问的语言对不上。到工作台「站点 → 知识」里看:总条数要大于 0、待处理条数要是 0、语言标签要和访客提问一致。

  • 建会话报 400,提示 Locale must be enabled for this site's Help Center。 locale 必须填你这个站点已经开启的语言代码(例如 en、zh),不能填 en-US、zh-CN 这种带地区的写法。到站点设置里看一眼语言列表,照那里显示的代码原样传。

  • aiMessage 是 null,但接口是成功的。 这不是报错:说明这一轮助手没有给出回答(比如这个会话的助手被暂停了)。显示你们自己的兜底话术,并把会话转给人工。

  • 发消息报 404。 基本都是 conversationId 不对——要用你存下来的 data.id(UUID),不是你自己的会话 ID。Site ID 写错也是同样的状态码。

  • sources[].score 是什么? 0 到 1 的检索相似度,适合做「查看依据」的界面。它不是回答准确率。

  • 要不要把聊天历史一起传过去? 不用。只传这一条新消息,平台按 conversationId 自己记着上下文。

7. 出问题找谁

  • 产品和配置问题: [email protected]
  • 每次联系都请带上: 你的 Site ID、出问题的请求(方法和路径)、HTTP 状态码、响应体里的 code、以及你用的 Idempotency-Key。有这几样我们就能在日志里把这笔请求找出来。
  • 安全提醒: 别把 API 钥匙明文发给我们。给我们钥匙的前几位(前缀)就够我们定位了。