将 Voice AI 与 Salesforce 集成,实现增强的客户支持

发布: (2025年12月3日 GMT+8 08:01)
6 min read
原文: Dev.to

Source: Dev.to

TL;DR

大多数 Salesforce 语音集成在通话量激增或 CRM 查询超时时会出现故障。本文指南展示了如何构建一个稳健的集成,能够在不丢失数据的情况下处理 1000+ 并发通话,将 VAPI 的语音 AI 通过 Twilio 连接到 Salesforce。系统实现实时案件查询、通话中更新联系人记录,并自动记录转录,从而缩短解决时间并实现零手动数据录入。

Stack: VAPI(语音 AI)、Twilio(电话)、Salesforce REST API(CRM 操作)、Node.js webhook 服务器(编排层)。

API Access & Credentials

  • VAPI API Key – 已启用电话号码供应的生产账户。
  • Twilio Account SID & Auth Token – 确认账户未处于试用模式(试用模式会阻止外呼)。
  • Salesforce Connected App – OAuth 2.0 凭证(Client ID、Client Secret、Refresh Token)。
  • Salesforce API Version – v58.0 或更高(实时事件流所必需)。

Technical Requirements

  • Node.js 18+ – 需要原生 fetch 支持(不使用 Axios polyfills)。
  • Public HTTPS endpoint – 使用 Ngrok 或生产域名提供 webhook 回调。
  • Salesforce profile permissions – API Enabled、View All Data、Modify All Data(用于 Case/Contact 的增删改查)。

System Constraints

  • Webhook timeout tolerance – Salesforce OAuth 令牌刷新会增加 200‑400 ms 延迟。
  • Rate limits – Salesforce:每位用户每 20 秒最多 100 次 API 调用;VAPI:每个账户最多 50 个并发通话。
  • Sandbox note – 沙盒组织使用不同的 OAuth 端点(test.salesforce.com)。硬编码 login.salesforce.com 会在沙盒环境中失效。

Data Flow Overview

flowchart LR
    A[Customer Call] --> B[Twilio]
    B --> C[VAPI Voice Agent]
    C --> D[Your Webhook Server]
    D --> E[Salesforce API]
    E --> D
    D --> C
    C --> B
    B --> A
  • VAPI 负责语音转录和合成。
  • Your server 充当 VAPI 与 Salesforce 之间的桥梁,包含所有 CRM 逻辑。
  • Do NOT 让 VAPI 直接调用 Salesforce;那样会绕过身份验证和错误处理。

VAPI Assistant Configuration

const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{
      role: "system",
      content: "You are a customer support agent. Extract: customer name, issue type, account number. Confirm details before creating case."
    }],
    functions: [{
      name: "createSalesforceCase",
      description: "Creates support case in Salesforce CRM",
      parameters: {
        type: "object",
        properties: {
          accountNumber: { type: "string" },
          issueType: {
            type: "string",
            enum: ["billing", "technical", "account"]
          },
          description: { type: "string" }
        },
        required: ["accountNumber", "issueType", "description"]
      }
    }]
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM"
  },
  transcriber: {
    provider: "deepgram",
    model: "nova-2",
    language: "en"
  },
  serverUrl: process.env.WEBHOOK_URL, // Your server receives function calls here
  serverUrlSecret: process.env.WEBHOOK_SECRET
};

Why it matters: The functions array tells GPT‑4 when to trigger Salesforce writes with well‑structured parameters, preventing garbage data from entering the CRM.

Webhook Server (Node.js)

// server.js
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Validate webhook signature – prevents unauthorized CRM writes
function validateSignature(req) {
  const signature = req.headers['x-vapi-signature'];
  const payload = JSON.stringify(req.body);
  const hash = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  return signature === hash;
}

app.post('/webhook/vapi', async (req, res) => {
  if (!validateSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = req.body;

  // Handle VAPI function‑call requests
  if (
    message.type === 'function-call' &&
    message.functionCall.name === 'createSalesforceCase'
  ) {
    const { accountNumber, issueType, description } = message.functionCall.parameters;

    try {
      // Obtain Salesforce OAuth token
      const authResponse = await fetch('https://login.salesforce.com/services/oauth2/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: process.env.SF_CLIENT_ID,
          client_secret: process.env.SF_CLIENT_SECRET
        })
      });

      if (!authResponse.ok) throw new Error(`Salesforce auth failed: ${authResponse.status}`);
      const { access_token, instance_url } = await authResponse.json();

      // Create the case in Salesforce
      const caseResponse = await fetch(`${instance_url}/services/data/v58.0/sobjects/Case`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${access_token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          AccountNumber: accountNumber,
          Type: issueType,
          Description: description,
          Origin: 'Phone',
          Status: 'New'
        })
      });

      if (!caseResponse.ok) throw new Error(`Case creation failed: ${caseResponse.status}`);
      const caseData = await caseResponse.json();

      // Respond to VAPI – the agent will speak this to the caller
      return res.json({
        result: `Case ${caseData.id} created. Reference number: ${caseData.CaseNumber}`
      });
    } catch (error) {
      console.error('Salesforce API Error:', error);
      return res.json({
        result: "System error. Case not created. Please call back."
      });
    }
  }

  // Fallback for other messages
  res.json({ received: true });
});

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Production Considerations

  • Token expiry: Salesforce OAuth 令牌在 2 小时后过期。请实现刷新逻辑或维护令牌池;单次请求模式在令牌失效后会出错。
  • Rate‑limit handling:429 响应加入指数退避并重试。

Local Testing with Ngrok

ngrok http 3000
# Use the generated HTTPS URL as `serverUrl` in `assistantConfig`

Simulating a VAPI Payload

// test-webhook.js – simulate VAPI function call
const crypto = require('crypto');
const fetch = require('node-fetch');

const payload = JSON.stringify({
  message: {
    type: 'function-call',
    functionCall: {
      name: 'createSalesforceCase',
      parameters: {
        accountNumber: 'ACC-12345',
        issueType: 'billing',
        description: 'Customer reports unexpected charge on invoice.'
      }
    }
  }
});

const signature = crypto
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(payload)
  .digest('hex');

fetch('https://.ngrok.io/webhook/vapi', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-vapi-signature': signature
  },
  body: payload
})
  .then(res => res.json())
  .then(console.log)
  .catch(console.error);

验证 webhook 能返回包含新建案例 ID 的成功信息。

Audio Processing Pipeline

graph LR
    A[Microphone] --> B[Audio Buffer]
    B --> C[Voice Activity Detection]
    C -->|Speech Detected| D[Speech‑to‑Text]
    C -->|No Speech| E[Error: No Input Detected]
    D --> F[Large Language Model]
    F --> G[Response Generation]
    G --> H[Text‑to‑Speech]
    H --> I[Speaker]
    D -->|Error: Unrecognized Speech| J[Error Handling]
    F -->|Error: Processing Failed| J
    J --> K[Log Error]
    K --> L[Notify User]

常见故障点:静默的 webhook 错误、缺失签名或令牌过期。务必在将系统投入生产前先使用 Ngrok 在本地进行完整测试。

Back to Blog

相关文章

阅读更多 »

切换账户

@blink_c5eb0afe3975https://dev.to/blink_c5eb0afe3975 正如大家所知,我正重新开始记录我的进展,我认为最好在一个不同的…

Strands 代理 + Agent Core AWS

入门指南:Amazon Bedrock AgentCore 目录 - 前置要求(requisitos‑previos) - 工具包安装(instalación‑del‑toolkit) - 创建…