将“Integrating Tencent Chat with Salesforce: A Comprehensive Guide for Developers”翻译成中文为:腾讯聊天与Salesforce集成:开发人员的综合指南。

10 分钟阅读
May 7, 2025

场景概述

本教程旨在演示如何将聊天SDK集成到Salesforce工作流程中,以利用Salesforce代理与最终用户之间的沟通。

应用产品

聊天

基本集成指南

初步准备

1. 注册腾讯RTC,创建一个应用并注册聊天服务,详见 指南

2. 如果您没有Salesforce开发者账户,请注册,点击 这里

路线图

实现目标需要三个部分。

1. 一个供最终用户启动对话的应用程序。

2. 一个在线服务器,用于创建Salesforce案例和聊天组。同时提供API以邀请/删除Salesforce代理到组,并在案例关闭时解散组。

3. 一个自定义Salesforce实用组件,用于Salesforce内部通信。

以下是集成图:

以下是集成图:

- 最终用户通过实时通信与Web服务器互动。
- 最终用户在Web服务器上创建工单。

最终用户应用程序

聊天为最流行的平台提供多种SDK,您可以简单地选择适合您的平台的SDK,查看我们的SDK: Android, iOS, Web, Flutter, Windows, Unity, 虚幻引擎 从头开始构建应用程序的推荐方法是使用我们的TUIKit来构建界面,查看这里: Android, iOS, Web, Flutter

在线服务器中继

在这里,我们将逐步指导您创建一个Web服务器,以允许最终用户提交Salesforce案例并创建相应的聊天组,同时也允许Salesforce代理被邀请/移除出聊天组,并在案例关闭时删除该组。

代理聊天界面

本教程将为您提供在Salesforce中创建聊天界面的说明,并邀请代理进入聊天组。此外,您还可以使用我们的 Web UIKit来构建此插件小部件。

场景特定实现

创建在线服务器

在线服务器的目的是连接Salesforce和聊天,使最终用户能够创建Salesforce案例并创建相应的聊天组。下面是示例Node服务器:

const express = require("express")
const axios = require("axios")
var TLSSigAPIv2 = require("tls-sig-api-v2") // 生成用户签名用于聊天
const sf = require("node-salesforce") // Salesforce API连接库,用于Node.js应用程序

const YOUR_SDKAPPID = 1400000000
const YOUR_SECRET = ""
const ADMIN_USERID = ""

const app = express()
app.use(express.json())

const port = process.env.PORT || 3000

app.use(express.json())

app.use(function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "https://YOUR_DOMAIN")
  res.header("Access-Control-Allow-Headers", "*")
  next()
})

// 最终用户调用 /createticket 创建一个Salesforce案例和一个聊天组。等待Salesforce代理加入该组。
app.post("/createticket", async (req, res) => {
  const { userId, caseInfo } = req.body
  if (!userId) return res.status(500).send("缺少userId")

  const auth = await getSalesforceAccessToken()
  if (auth.error) return res.status(500).send("Salesforce认证错误")

  const salesforceCase = await createCase(auth.token, caseInfo)
  if (!salesforceCase.success)
    return res.status(500).send("案例创建失败")

  const groupName = salesforceCase.id
  const result = await createGroup(groupName, userId)
  if (result.ErrorCode !== 0)
    return res.status(500).send("组创建失败")

  res.status(200).send(result)
})

// 当检测到新代理分配到组时,Salesforce发送请求将该代理加入聊天组
app.post("/joingroup", async (req, res) => {
  const { groupId, userId } = req.body
  if (!userId) return res.status(500).send("缺少userId")
  if (!groupId) return res.status(500).send("缺少groupId")

  const result = await joinGroup(groupId, userId)
  if (result.ErrorCode !== 0)
    return res.status(500).send("加入组失败")

  res.status(200).send(result)
})

// 当检测到某个代理从组中移除时,Salesforce发送请求将该代理从聊天组中移除
app.post("/leavegroup", async (req, res) => {
  const { groupId, userId } = req.body
  if (!userId) return res.status(500).send("缺少userId")
  if (!groupId) return res.status(500).send("缺少groupId")

  const result = await leaveGroup(groupId, userId)
  if (result.ErrorCode !== 0)
    return res.status(500).send("离开组失败")

  res.status(200).send(result)
})

app.post("/deletegroup", async (req, res) => {
  const { groupId } = req.body
  if (!groupId) return res.status(500).send("缺少groupId")

  const result = await deleteGroup(groupId)
  if (result.ErrorCode !== 0)
    return res.status(500).send("删除组失败")

  res.status(200).send(result)
})

const getSalesforceAccessToken = async function () {
  const url = "https://{your_instance}.salesforce.com"
  const conn = new sf.Connection({ loginUrl: url })
  try {
    await conn.login("SF_EMAIL", "SF_PASSWORDSF_TOKEN")
    return { error: undefined, token: conn.accessToken }
  } catch (e) {
    return { error: e, token: undefined }
  }
}

const createCase = async function (token, caseInfo) {
  const { subject, desc, name, email } = caseInfo
  const body = {
    Subject: subject,
    Description: desc,
    SuppliedName: name,
    SuppliedEmail: email,
  }
  const headers = {
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + token,
    },
  }
  const url =
    "https://{your_instance}.salesforce.com/services/data/v{api_version}/sobjects/Case"
  try {
    const result = await axios.post(url, body, headers)
    return result.data
  } catch (e) {
    return { id: undefined, success: false, error: e }
  }
}

const generateUserSig = function () {
  const expires = 600
  const api = new TLSSigAPIv2.Api(YOUR_SDKAPPID, YOUR_SECRET)
  return api.genSig(ADMIN_USERID, expires)
}

const generateRandom = function () {
  return Math.floor(Math.random() * 4294967295)
}

const createGroup = async function (groupName, userId) {
  const sig = generateUserSig()
  const random = generateRandom()
  // 使用salesforceCase.id作为组ID
  const data = { Owner_Account: userId, Type: "Public", Name: groupName, GroupId: groupName }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/create_group?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
    const groupRes = await axios.post(url, data)
    return groupRes.data
  } catch (e) {
    return { ErrorCode: -1, ErrorInfo: e }
  }
}

const joinGroup = async function (groupId, userId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { GroupId: groupId, MemberList: [{ Member_Account: userId }] }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/add_group_member?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
    const groupRes = await axios.post(url, data)
    return groupRes.data
  } catch (e) {
    return { ErrorCode: -1, ErrorInfo: e }
  }
}

const leaveGroup = async function (groupId, userId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { GroupId: groupId, MemberToDel_Account: [userId] }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/delete_group_member?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
    const groupRes = await axios.post(url, data)
    return groupRes.data
  } catch (e) {
    return { ErrorCode: -1, ErrorInfo: e }
  }
}

const deleteGroup = async function (groupId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { GroupId: groupId }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/destroy_group?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
    const groupRes = await axios.post(url, data)
    return groupRes.data
  } catch (e) {
    return { ErrorCode: -1, ErrorInfo: e }
  }
}

app.listen(process.env.PORT || port, () =>
  console.log(`示例应用正在监听端口 ${port}!`)
)

服务器支持一个路由 /createticket,供最终用户在Salesforce中创建一个案例并在聊天中创建一个组。我们在这里做的事情如下:

1. 首先,我们从Salesforce获取访问令牌,更多信息请参见 SF_TOKEN

const getSalesforceAccessToken = async function () {
  const url = "https://{your_instance}.salesforce.com"
  const conn = new sf.Connection({ loginUrl: url })
  try {
 await conn.login("SF_EMAIL", "SF_PASSWORDSF_TOKEN")
 return { error: undefined, token: conn.accessToken }
  } catch (e) {
 return { error: e, token: undefined }
  }
}

2. 创建一个Salesforce案例。

const createCase = async function (token, caseInfo) {
  const { subject, desc, name, email } = caseInfo
  const body = {
 Subject: subject,
 Description: desc,
 SuppliedName: name,
 SuppliedEmail: email,
  }
  const headers = {
 headers: {
   "Content-Type": "application/json",
   Authorization: "Bearer " + token,
 },
  }
  const url =
 "https://{your_instance}.salesforce.com/services/data/v{api_version}/sobjects/Case"
  try {
 const result = await axios.post(url, body, headers)
 return result.data
  } catch (e) {
 return { id: undefined, success: false, error: e }
  }
}

3. 生成用户签名用于聊天

 const generateUserSig = function () {
  const expires = 600
  const api = new TLSSigAPIv2.Api(YOUR_SDKAPPID, YOUR_SECRET)
  return api.genSig(ADMIN_USERID, expires)
}

4. 根据案例ID和用户ID创建聊天组

const createGroup = async function (groupName, userId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { Owner_Account: userId, Type: "Public", Name: groupName }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/create_group?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
 const groupRes = await axios.post(url, data)
 return groupRes.data
  } catch (e) {
 return { ErrorCode: -1, ErrorInfo: e }
  }
}

此外,Web服务器提供了通过案例ID和代理ID加入/离开/删除聊天组的路由。这些在Salesforce触发器检测到案例代理变更时使用。更多细节将在第3步中讨论。

const joinGroup = async function (groupId, userId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { GroupId: groupId, MemberList: [{ Member_Account: userId }] }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/add_group_member?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
 const groupRes = await axios.post(url, data)
 return groupRes.data
  } catch (e) {
 return { ErrorCode: -1, ErrorInfo: e }
  }
}

const leaveGroup = async function (groupId, userId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { GroupId: groupId, MemberToDel_Account: [userId] }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/delete_group_member?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
 const groupRes = await axios.post(url, data)
 return groupRes.data
  } catch (e) {
 return { ErrorCode: -1, ErrorInfo: e }
  }
}

const deleteGroup = async function (groupId) {
  const sig = generateUserSig()
  const random = generateRandom()
  const data = { GroupId: groupId }
  const url = `https://console.tim.qq.com/v4/group_open_http_svc/destroy_group?sdkappid=${YOUR_SDKAPPID}&identifier=${ADMIN_USERID}&usersig=${sig}&random=${random}&contenttype=json`
  try {
 const groupRes = await axios.post(url, data)
 return groupRes.data
  } catch (e) {
 return { ErrorCode: -1, ErrorInfo: e }
  }
}

这就是Web服务器端的所有内容。一旦您设置好服务器,调用端点 /createticket 并:

  1. 检查Salesforce中是否创建了案例。
  2. 检查聊天控制台中是否有以案例ID作为组ID的组。

使用聊天Web UI Kit构建Salesforce实用组件

在这里,我们展示如何使用聊天UI Kit创建Salesforce实用组件的步骤。在Salesforce中,您可以使用 Lightning Container 将第三方i-frame作为静态资源上传,并使用 lightning:container 在Aura组件中托管内容。您可以使用 聊天Web UIKit 来构建代理聊天组件,并将其部署在Lightning Container中,作为位于底部的Salesforce实用工具栏小部件。

1. 首先,通过使用 聊天Web UIKit 开发聊天组件。将其构建为一个带有根index.html的静态资源,并将其压缩为zip文件。案例代理的ID将传递给聊天组件,使用该ID在聊天组件中初始化和登录聊天。

聊天组件开发
UI Kit使用
静态资源压缩
代理ID传输
初始化和登录

2. 为您的组件创建一个Lightning Container,详细信息请参见 这里

    2.1 转到Salesforce 开发者控制台 

    2.2 点击 文件 -> 新建 -> Lightning组件 

    2.3 名称 = "tim_utilities_bar" 

    2.4 点击提交

3. 将自定义组件渲染到您的工具栏小部件中。3.1 设置 aura:component 为实用工具栏,并提供 aura:id

<!-- tim_utilities_bar.cmp -->
<aura:component implements="flexipage:availableForAllPageTypes" access="global">
  <lightning:utilityBarAPI aura:id='utilitybar'" />
</aura:component>

3.2 将静态资源上传到Lightning Container

a. 转到Salesforce 静态资源,创建一个名为"tim_bar"的新资源

b. 上传资源的zip文件,并将"缓存控制"设置为"公开" 

c. 点击保存

注意事项:

  • Index.html应始终位于.zip文件的根级别
  • 上传文件后务必点击"保存"。
  • Salesforce保存的是资源名称,而不是.zip文件的名称。
  • 您在Lightning组件中使用的100%的代码和资产都需要包含在.zip文件中。任何外部代码依赖关系即使在CSP受信任站点列表中也无法正常工作。

3.3 在实用工具栏小部件中引用静态资源"tim_bar"

a. 在实用工具栏小部件中添加一个 lightning:container 标签。aura:id 应为 "TIM_Bar"。b. 引用静态资源 "{!$Resource.tim_bar + '/index.html'}" 。请注意,tim_bar是保存的"静态资源",而不是上传的.zip文件的名称。

<!-- tim_utilities_bar.cmp -->
<aura:component implements="flexipage:availableForAllPageTypes" access="global">
  <lightning:utilityBarAPI aura:id='utilitybar'" />
  <aura:attribute name="recordId" type="String" />
  <aura:attribute name="data" type="String" />
  <lightning:navigation aura:id='navService'" />
  <lightning:container
    aura:id='TIM_Bar'"
    src="{!$Resource.tim_bar + '/index.html'}"
  />
</aura:component>

3.4 添加实用工具栏小部件以在Salesforce中显示 

  1. 点击"设置",搜索"应用管理器",以设置实用工具栏小部件将出现的位置 
  2. 点击"▾"和"编辑"在名为Service Console的应用中 
  3. 在应用设置中,点击"实用项(仅限桌面)" 
  4. 点击"添加实用项" 
  5. 选择"tim_utilities_bar" 
  6. 设置宽度和高度 
  7. 勾选"自动启动" 
  8. 点击 -> "保存"

3.5 更新Salesforce CSP文件以授予访问权限以在Salesforce中访问聊天 

  1. 转到Salesforce -> 设置 -> 搜索 -> "CSP受信任网站" 
  2. 添加"新受信任网站"(允许所有CSP指令):wss://wss.im.qcloud.com 
  3. 转到Salesforce -> 设置 -> 搜索 -> "CORS" 
  4. 添加"新"允许的来源列表: https://.qq.com https://.qcloud.com 
  5. 转到Salesforce -> 设置 -> 搜索 -> "" 
  6. 添加"新远程站点"列表:Web服务器的URL!

4. 初始化Lightning Container

  • 一旦Lightning Container准备就绪,发送LLC消息进行通知 
  • 实用工具栏小部件。实用工具栏小部件需要将代理的ID发送到我们的组件中 
  • 一旦接收到来自实用工具栏小部件的消息,我们渲染UIKit。

请遵循以下代码:

// tim_utilities_barController.js
({
handleMessage: function(component, message, helper) {
var payload = message.getParams().payload
// 一旦容器准备就绪,初始化UIKit
if (payload === "READY") helper.initUIKit(component, message, helper)
}
});
({
initUIKit: function (component, message, helper) {
// 获取代理的ID
var userId = $A.get("$SObjectType.CurrentUser.Id")
var message = { userId: userId }
try {
// 将ID发送到组件
component.find("TIM_Bar").message(message)
} catch (err) {
console.error("来自实用工具栏的错误:", err)
}
},
})

向实用工具栏小部件添加 onMessage 处理程序:

<!-- tim_utilities_bar.cmp -->
<lightning:container
aura:id='TIM_Bar'"
src="{!$Resource.tim_bar + '/index.html'}"
onmessage="{!c.handleMessage}"
/>

在您的脚本中,使用 LLC包 在Lightning Container加载时渲染您的应用。

// index.js
try {
const clientState = "READY";
LLC.sendMessage(clientState);
console.warn("Lightning Container --> TO SALESFORCE --> Sent:", clientState);
} catch (e) {
console.error("LLC未工作", e);
}
try {
LLC.addErrorHandler((error) => console.log("LLC错误:", error));
LLC.addMessageHandler((salesforceMessage) => {
console.warn("SALESFORCE --> Lightning Container --> 到达:", salesforceMessage);
const app = createApp(App, {
user: salesforceMessage
});
app
.use(store)
.use(router)
.use(TUIKit)
.use(Aegis)
.use(ElementPlus)
.mount('#app');
});
} catch (e) {
console.error("来自LLC的错误!!", e);
}

监听Salesforce案例分配并设置聊天组成员

在Salesforce中,案例手动或自动分配给代理。因此,我们需要邀请新的代理进入组,并让之前的代理离开组。我们使用Salesforce Apex Callout来监听案例代理分配的变化。以下是通过调用Salesforce Apex Callout的步骤。

1. 监听手动案例分配

当指定的代理更改为一个案例时,Apex案例更改触发器调用Apex Callout。在调用中,我们 a. 将以前的代理从聊天组中删除,并 b. 邀请新的代理进入该组。当案例被删除时,相应地解散聊天组。

转到Salesforce开发者控制台 –> 新建 –> Apex触发器 –> 名称 = "AssignAgent" & sObject = "Case"

// AssignAgent.apxt
trigger AssignAgent on Case (after update, after delete) {
     if(trigger.isUpdate){
       // 案例正在更新
       System.debug('案例更新触发:');
       for(Case a : trigger.new){
         Case oldCase = trigger.oldMap.get(a.ID);
         if(String.valueOf(a.OwnerId).substring(0, 3) == '005'){
           // 所有代理ID发生更改,005前缀表示代理ID
           System.debug('邀请代理 :' + a.OwnerId);
           // 将新所有者分配给聊天组
           String[] data = new String[2];
           data[0] = a.Id; // 案例ID是组ID
           data[1] = a.OwnerId;
           TimCallouts.joinGroup(data); // 自定义调用类
         }
         // 新案例代理与当前代理不同
         if(String.valueOf(oldCase.OwnerId).substring(0, 3) == '005' && oldCase.OwnerId != a.OwnerId && String.valueOf(a.OwnerId).substring(0, 3) == '005'){
           // 将旧代理从组中删除。
           // 注意:案例的第一个所有者将是系统所有者。
           System.debug('旧代理将从组中移除' + a.Id);
           System.debug('leaveGroup: ' + oldCase.OwnerId);
           String[] removeData = new String[2];
           removeData[0] = a.Id; // 案例ID是组ID
           removeData[1] = oldCase.OwnerId;
           TIMCallouts.leaveGroup(removeData);
         }
       }
   }
   if(trigger.isDelete ){
     // 案例正在删除
     System.debug('案例删除触发:');
     for(Case a : trigger.old){
       if(String.valueOf(a.OwnerId).substring(0, 3) == '005'){
         System.debug('删除组 :' + a.Id);
         String[] data = new String[1];
         data[0] = a.Id;
         TimCallouts.deleteGroup(data); // 自定义调用类
       }
     }
   }
 }

2. 监听Salesforce Omni Channel的自动案例分配

Omni Channel检测到案例分配,Salesforce会自动创建一个AgentWork对象。如果代理接受该分配,则AgentWork触发器可能会使用Salesforce调用加入组。

转到Salesforce开发者控制台 –> 新建 –> Apex触发器 –> 名称 = "AgentOmniChannel" & sObject = "AgentWork"

// AgentOmniChanne.apxt
trigger AgentOmniChannel on AgentWork (after update, after insert) {
    if(Trigger.isUpdate){
      for(AgentWork a : Trigger.new){
            AgentWork oldCase = Trigger.oldMap.get(a.ID);
            if(a.Status == 'Opened' && String.valueOf(a.OwnerId).substring(0, 3) == '005' ){
              String[] data = new String[2];
              data[0] = a.WorkItemId;
              data[1] = a.OwnerId;
              TIMCallouts.joinGroup(data);
            }
      }
    }
}

3. 设置Salesforce调用以邀请/移除代理。

转到Salesforce开发者控制台 –> 新建 –> Apex类 –> 名称 = "TIMCallOuts"

// TIMCallOuts.apxc
public class TIMCallouts {
  @future(callout=true)
  public static void joinGroup(String[] data) {
    String groupId = data[0];
    String userId = data[1];
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint('https://{your_web_server}/joingroup');
    request.setMethod('POST');
    request.setHeader('Content-Type', 'application/json;charset=UTF-8');
    request.setBody('{"userId":"'+ userId +'", "groupId":"'+ groupId +'"}');
    HttpResponse response = http.send(request);
    // 解析JSON响应
      if (response.getStatusCode() != 200) {
        System.debug('加入组失败: '+response.getStatusCode()+' '+response.getStatus());
      } else {
        System.debug('腾讯聊天响应: ' + response.getBody());
      }
  }

  @future(callout=true)
  public static void leaveGroup(String[] data) {
      String groupId = data[0];
      String userId = data[1];
      Http http = new Http();
      HttpRequest request = new HttpRequest();
      request.setEndpoint('https://{your_web_server}/leavegroup');
      request.setMethod('POST');
      request.setHeader('Content-Type', 'application/json;charset=UTF-8');
      // 将主体设置为JSON对象
      request.setBody('{"userId":"'+ userId +'", "groupId":"'+ groupId +'"}');
      HttpResponse response = http.send(request);
      // 解析JSON响应
      if (response.getStatusCode() != 200) {
        System.debug('离开组失败: ' + response.getStatusCode() + ' ' + response.getStatus());
      } else {
        System.debug('腾讯聊天响应: ' + response.getBody());
      }
    }

    @future(callout=true)
    public static void deleteGroup(String data) {
      String groupId = data;
      Http http = new Http();
      HttpRequest request = new HttpRequest();
      request.setEndpoint('http://{your_web_server}/deletegroup');
      request.setMethod('POST');
      request.setHeader('Content-Type', 'application/json;charset=UTF-8');
      // 将主体设置为JSON对象
      request.setBody('{"groupId":"'+ groupId + '}');
      HttpResponse response = http.send(request);
      // 解析JSON响应
      if (response.getStatusCode() != 200) {
        System.debug('删除组失败: ' + response.getStatusCode() + ' ' + response.getStatus());
      } else {
        System.debug('腾讯聊天响应: ' + response.getBody());
      }
    }
}

这就是您所需了解的所有内容,以允许来自任何应用程序的最终用户与Salesforce代理开始聊天。如果您有任何进一步的问题,请发送电子邮件至 info_rtc@tencent.com。 我们非常乐意为您提供有关该解决方案或通过聊天构建现代实时通信系统的其他解决方案的更多细节。

立即订购

点击 这里 快速访问购买页面进行订单。