将腾讯聊天与Salesforce集成:开发者综合指南

10 分钟阅读
Feb 18, 2025

场景概述

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

应用产品

聊天

基本集成指南

初步准备

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

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

路线图

实现目标需要三个部分。

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

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

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

以下是集成地图:

Integrated Workflow Diagram for User Interaction, Ticket Creation, and Case Management

最终用户应用程序

聊天提供了多种流行平台的 SDK,您可以简单地选择适合您平台的一个,查看我们的 SDK 在这里: 安卓, iOS, Web, Flutter, Windows, Unity, 虚幻引擎 从头开始构建应用程序的推荐方式是利用我们的 TUIKit 来叠加界面,参见这里: 安卓, 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") // 为聊天生成 UserSig
const sf = require("node-salesforce") // Node.js 应用程序的 Salesforce API 连接库

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 获取 accessToken,更多信息请参见 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. 为聊天生成 UserSig

 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 容器 将第三方 i-frame 作为静态资源上传,并使用 lightning:container 在 Aura 组件中托管内容。您可以使用 聊天 Web UIKit 构建代理聊天组件,并将其部署在 Lightning 容器中,作为位于底部的 Salesforce 实用工具栏小部件。

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

Chat Web UI Kit Development Example
Static Resource构建
Compressed ZIP file
Chat component initialization
Agent ID usage
UI design for messaging apps

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

    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 容器

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. 点击“▾”并在名为服务控制台的应用中点击“编辑” 
  3. 在应用设置中,点击“实用工具项目(仅限桌面)” 
  4. 点击“添加实用工具项目” 
  5. 选择“tim_utilities_bar” 
  6. 设置宽度和高度 
  7. 勾选“自动启动” 
  8. 点击“保存”

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

  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 容器

  • 一旦 Lightning 容器准备就绪,发送 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 容器加载时渲染您的应用。

// 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 Callout 加入组。

转到 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。 我们很高兴为您提供有关解决方案或任何其他通过聊天构建现代实时通信系统的更多细节。

立即订购

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