本文为您介绍平台型企业如何集成呼叫能力。例如您是做CRM系统,为您的客户提供软件服务,那么集成呼叫能力,助力您的产品实现场景闭环,能够为您的客户提供更优质的服务。
集成架构
流程图
JWT Token 模式集成接入流程图
说明JWT Token 模式集成接入流程图中蓝色文字的内容表示该步骤由Fuyun IDP完成。
OAuth模式集成接入流程图
说明OAuth模式集成接入有两种方式获取access_token,一种为前端直接获取,另一种为客户后端post访问access_token置换接口获取access_token。
前期集成准备
获取AccessKey ID和AccessKey Secret。
登录阿里云账号,单击右上角人像图标,选择AccessKey管理,获取AccessKey ID和AccessKey Secret。
获取实例ID。
登录智能联络中心,在实例管理页面获取实例ID。
创建坐席和热线技能组。
把坐席添加到技能组有以下两种方式,您可以选择其中任一方式。
PaaS方式:首先建热线技能组,再创建坐席,把坐席分配到热线技能组中。
创建坐席可参见CreateAgent,参见示例demo进行创建:
创建热线技能组可参见CreateSkillGroup,参见示例Demo进行创建:
SaaS方式:直接在智能联络中心添加。
创建热线技能组:选择
页面,切换到技能组Tab,单击...按钮,单击添加技能组,选择应用渠道为热线,即可创建新的热线技能组。创建坐席:选择
页面,切换到人员授权Tab,单击页面右上角新增人员,输入基本信息,并把该坐席添加到上述步骤中创建的热线技能组中。
注册开发者门户。
SDK集成服务端步骤
JWT Token 模式集成接入指引
IDP身份管理配置。
登录开发者门户,单击设置,进入身份管理配置页面,单击右上角添加,进行身份管理配置。
基础信息设置。
App Name:客户侧应用名,最长10个字符,支持英文、数字。
授权类型:选择令牌交换模式。
授权范围:scope,选择用户管理和热线,用于标识颁发的access_token可访问的API范围,防止越权调用。
是否免登工作台:不用勾选。
回调地址:请填写认证成功后的回调的URL,客服方用来接收并处智能联络中心颁发的授权码。
开始URL:客户方网站首页。
凭证信息。
完善基础信息设置后,单击下一步,进入凭证信息页面。此处为智能联络中心颁发的client_id和client_secret,用于验证接入方。
单击完成,授权类型即设置成功。
单击上一步按钮,可修改基础信息设置(App Name除外)。
免登模式配置。
授权模式为令牌交换模式时,需要配置免登模式。免登模式选择JWT模式。
免登模式:JWT模式。
公钥:获取公钥,请参见使用OpenSSL生成密钥对。
签发网站域名:即iss。更多详情,请参见JWT token生成规则。
配置完成后,单击保存,即配置成功。单击上一步,支持返回凭证信息页面。
生成JWT Tkoen。
在后端的开发环节生成jwt token,需要用到参数iss,user_name,exp,private_key。payload包含字段:
字段
描述
示例
iss
签发者网站域名
"iss":"http://signin.rhino****.com"
exp
过期时间戳
-
user_name
JWT token颁发给的用户,创建坐席时您设置的AccountName(用来映射唯一坐席)。
-
jti
随机uuid
"b774ef13-a5bc-****-8346-042d879efb1a"
JWT token生成代码Demo。
// pom依赖 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.1.1.RELEASE</version> </dependency> /** * 生成jwtToken * @return */ public static String generateToken(String userName, String iss, long expireTime, String privateKeyStr) { try { privateKeyStr = privateKeyStr.replaceAll("\\s+", ""); byte[] decodedPrivateKey = Base64.getDecoder().decode(privateKeyStr.getBytes()); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedPrivateKey); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(spec); RsaSigner signer = new RsaSigner((RSAPrivateKey) privateKey); // generate token Map<String, Object> payloadMap = new HashMap<>(4); payloadMap.put("iss", iss); payloadMap.put("jti", UUID.randomUUID().toString()); payloadMap.put("user_name", userName); payloadMap.put("exp", expireTime); return JwtHelper.encode(new JSONObject(payloadMap).toJSONString(), signer).getEncoded(); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { e.printStackTrace(); } return null; }
请求头header和请求参数的封装。
请求参数的封装,具体可参考三方账号授权。
请求URL
URL:https://signin.rhinokeen.com/oauth/token_exchange。
请求类型:POST。
请求HEADER
字段
示例
描述
Authorization
"Authorization: Basic YWxpYmFiYS14aWFvZXI6YmNlMTllZDYtYTFhNC00NzA3LTgwZjAtYTM4OGY3MGUxNWQ3"
授权类型,接口使用http basic authentication认证方式Authorization= Basic Base64.encode(client_id:client_secret)
请求参数
字段
示例
描述
grant_type
"urn:ietf:params:oauth:grant-type:token-exchange"x
授权类型
scope
"user_management,hotline"
访问范围(用户管理和热线)
redirect_url
"http://****.com/callback"
回调地址
subject_token
-
第三方JWT token
subject_issuer
"http://****.com"
签发者网站域名
向Fuyun_IDP指定免登地址发出POST请求。
发送post请求的同时并携带您封装好的参数,用表单传输格式。
获取access_token。
Fuyun_IDP会通过response的形式返回access_token等信息。
重要HTTPS的请求的数据是表单传输格式,不要用JSON。
创建技能组会返回技能组ID,通过这个ID让坐席加入相应技能组。
创建坐席设置的AccountName之后会成为生成JWT token的参数。
示例Demo。
/** * 环境:jdk1.8 * 这是一个简单的获取access_token的demo * 在此之前,你需要准备的工作: * 1.创建坐席和技能组,并把坐席分配到技能组中(可下载阿里云官网的demo) * 2.用OpenSSL生成private_key和public_key * 3.用iss/public_key/redirect_url去开发者门户(目前向阿里相关工作人员)获取client_id和client_secret */ @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) @SpringBootTest public class DemoCostmerServer02ApplicationTests { /** * OpenSSL生成端privateKey */ public final String privateKey = ""; /** * fuyun指定置换token的URL */ public final String FuYun_URL = "https://signin.rhinokeen.com/oauth/token_exchange"; /** * 置换得到的client_id */ public final String client_id = ""; /** * 置换得到的client_secret */ public final String client_secret = ""; /** * 拼接成指定的参数格式 */ public final String client_IdAndSecret = client_id + ":" + client_secret; /** * 请求的参数 */ public final String grant_type = "urn:ietf:params:oauth:grant-type:token-exchange"; public final String scope = "fuyun-dev"; public final String redirect_url = "https://bc.****.com/api/ccs/callback"; /** * 签发者域名网站iss */ public final String iss = ""; /** * 您创建坐席是设置的AccountName */ public final String accountName = ""; @Test void contextLoads() { /** * 用Base64对header参数进行加密 */ try { final Base64.Encoder encoder = Base64.getEncoder(); final byte[] client_IdAndSecretByte = client_IdAndSecret.getBytes("UTF-8"); //编码 final String Auth = encoder.encodeToString(client_IdAndSecretByte); /** * 封装header */ Map<String,String> header = new HashMap<>(); header.put("Authorization","Basic "+Auth); /** * 生成jwt token */ Long time = System.currentTimeMillis(); time = time + 1622797200l; // JwtUtil_AliDemo01是根据上文生成token的工具类 String subject_token = JwtUtil_AliDemo01.generateToken(accountName, iss, time, privateKey); /** * 请求参数的封装 */ Map<String,String> pram = new HashMap<>(); pram.put("grant_type",grant_type); pram.put("scope",scope); pram.put("redirect_url",redirect_url); pram.put("subject_token",subject_token); pram.put("subject_issuer",iss); /** * 发出post请求在repsonse返回中得到access_token等相关信息 * HttpUtils是一个http工具类 */ Optional<String> userInfoOptional = HttpUtils.post(FuYun_URL, pram, header,null); String userInfo = userInfoOptional.orNull(); System.out.println("userInfo====>" + userInfo); }catch (Exception e){ e.printStackTrace(); } } } /** * http工具类 */ public class HttpUtils { public static Optional<String> get(String uri) { return fetch(HttpUtils.QueryMethod.GET, uri, null, null, null); } public static Optional<String> get(String uri, Map<String, String> params, Map<String, String> headers) { return fetch(HttpUtils.QueryMethod.GET, uri, params, headers, null); } public static Optional<String> post(String uri, Map<String, String> params) { return fetch(HttpUtils.QueryMethod.POST, uri, params, null, null); } public static Optional<String> post(String uri, Map<String, String> params, Map<String, String> headers, String body) { return fetch(HttpUtils.QueryMethod.POST, uri, params, headers, body); } private static Optional<String> fetch(HttpUtils.QueryMethod method, String uri, Map<String, String> params, Map<String, String> headers, String body) { Optional<String> result = Optional.absent(); InputStream inputStream = null; try { String url = uri; if (StringUtils.isBlank(uri)) { return Optional.absent(); } if (params != null && params.size() > 0) { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append("?"); for (String key : params.keySet()) { String value = URLEncoder.encode(params.get(key), StandardCharsets.UTF_8.toString()); urlBuilder.append(key).append("=").append(value).append("&"); } String s = urlBuilder.toString(); url += s.substring(0, s.length() - 1); } URL u = new URL(url); HttpURLConnection urlConnection = (HttpURLConnection) u.openConnection(); urlConnection.setInstanceFollowRedirects(false); urlConnection.setConnectTimeout(15000); urlConnection.setReadTimeout(15000); if (method != null) { urlConnection.setRequestMethod(method.name()); } if (headers != null) { for (String key : headers.keySet()) { urlConnection.addRequestProperty(key, headers.get(key)); } } if (body != null) { urlConnection.setDoOutput(true); OutputStream out = urlConnection.getOutputStream(); out.write(body.getBytes()); out.flush(); out.close(); } int responseCode = urlConnection.getResponseCode(); inputStream = urlConnection.getInputStream(); if (responseCode == HttpStatus.OK.value()) { result = Optional.fromNullable(streamToString(inputStream)); } else if (responseCode == HttpStatus.MOVED_PERMANENTLY.value() || responseCode == HttpStatus.FOUND.value()) { //TODO:add redirect logic later on result = Optional.of("redirect url found!"); } } catch (MalformedURLException malformedURLException) { } catch (IOException e) { } catch (Exception e) { } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { } } } return result; } static public String streamToString(InputStream in) throws IOException { StringBuilder outputBuilder = new StringBuilder(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in)); String s; while ((s = bufferedReader.readLine()) != null) { outputBuilder.append(s); } return outputBuilder.toString(); } public static enum QueryMethod { POST("post"), GET("get"), PUT("put"), DELETE("delete"); private String name; QueryMethod(String name) { this.name = name; } } }
OAuth模式集成接入指引
IDP身份管理配置。
登录开发者门户,单击设置,进入身份管理配置页面,单击右上角添加,进行身份管理配置。
基础信息设置。
App Name:客户侧应用名,最长10个字符,支持英文、数字。
授权类型:选择令牌交换模式。
授权范围:scope,选择用户管理和热线,用于标识颁发的access_token可访问的API范围,防止越权调用。
是否免登工作台:不用勾选。
回调地址:请填写认证成功后的回调的URL,客服方用来接收并处智能联络中心颁发的授权码。
开始URL:客户方网站首页。
凭证信息。
完善基础信息设置后,单击下一步,进入凭证信息页面。此处为智能联络中心颁发的client_id和client_secret,用于验证接入方。
单击完成,授权类型即设置成功。
单击上一步,可修改基础信息设置(App Name除外)。
免登模式配置。
授权模式为令牌交换模式时,需要配置免登模式。免登模式选择OAuth模式。
免登模式:OAuth模式。
免登访问fuyun open api的redirect_url:用于在第三方系统中申请OAuth client接入时使用,例如:在Salesforce平台中申请OAuth Client 可填入此处提供的redirect_url。
免登访问XP工作台redirect_url:用于在第三方系统中申请OAuth client接入时使用,例如:在Salesforce平台中申请OAuth Client 可填入此处提供的redirect_url。
token endpoint:令牌公共端点,例如微软的token endpoint为https://login.microsoftonline.de/common/oauth2/v2.0/token,可参考微软官方文档,其余OAuth平台文档类似。
user endpoint:UserInfo 端点,例如微软的user endpoint为https://graph.microsoft.com/oidc/userinfo,可参考微软官方文档,其余OAuth平台文档类似。
第三方系统颁发的client id:从第三方系统获取,例如从Salesforce平台申请完成OAuth Client会获得从Salesforce平台颁发的client id。
第三方系统颁发的client secret:从第三方系统获取,例如从Salesforce平台申请完成OAuth Client会获得从Salesforce平台颁发的client secret。
scope:访问范围(用户管理和热线),输入user_management,hotline,例如:在Salesforce平台申请OAuth Client时会要求填入scope访问范围。
mapping field:客户方UserInfo里与智能联络中心客服工作台坐席映射的字段,例如:email、moblie、address等。
配置完成后,单击保存,即配置成功。单击上一步,支持返回凭证信息页面。
获取access_token。
OAuth模式集成接入有两种方式获取access_token,一种为前端直接获取,另一种为客户后端post访问access_token置换接口获取access_token。
前端方式获取access_token。
以微软为例,通过调用微软的OAuth authorize endpoint,回调到智能联络中心端,继而获取访问智能联络中心API的access token。https://login.microsoftonline.com/zxshirley163.onmicrosoft.com/oauth2/v2.0/authorize?client_id=<microsoft颁发的client_id>&response_type=code&redirect_uri=<注册microsoft oauth接入时填入的redirect_url>。该次调用最终会将智能联络中心的access token放置到浏览器Cookie中(key为AC_TOKEN), 亦会返回json response(如下示例),后续集成步骤可根据需求取用cookie中的access_token或者json response中的access token皆可。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vZGV2ZWxvcGVyLnJoaW5va2Vlbi5jb20iLCJleHAiOjE2MjY5NDY2OTUsInVzZXJfbmFtZSI6ImZ1eXVuLXRlc3QtdXNlciIsImp0aSI6ImQ0YmQ4MTM1LTZmZjYtNDZiNi05YTg2LTQ2MWNkMGVjNDI4NyIsImNsaWVudF9pZCI6ImlkcF90ZXN0XzE2UTRveiIsInNjb3BlIjpbImZ1eXVuLWRldiJdfQ.cDE0EuZaBRwsNf7WrbIPsZhw9juk5dpA2GtCEJ4WHf-iwz8tp9xnX4Kb4jmJqwtWjtrvz0mDeU8uFB31oiz4FzRQb30qhaCeJo7totjwZfTr4bI6bd8afb5C3kypgQUYAyg3wkMzF-6nKgnN_a9YViWp2vO1lq3gH7I5vA5CX6bACWu8OO7LtaD-nKf6JRCcdwY2CWDq_jl43mjz_oek0c8MBcnLL11PAk5VnZRYg7pO6AhPOUkqyAqwbBGcgkEw3pNR1aSTbL8-u69RczKaEgXB_lusshLEXeRK6uNlO8SZzx2BR0AG3nHSG9dAGEdaWMhPUR4gY488k4SYPFNNEQ", "token_type": "bearer", "expires_in": 2591999, "scope": "fuyun-dev", "iss": "http://developer.rhinokeen.com", "jti": "d4bd8135-6ff6-46b6-9a86-461cd0ec4287" }
后端方式获取access_token。
请求头header和请求参数的封装
请求URL
URL:https://signin.rhinokeen.com/oauth/token_exchange。
请求类型:POST。
请求HEADER
字段
示例
描述
Authorization
"Authorization: Basic YWxpYmFiYS14aWFvZXI6YmNlMTllZDYtYTFhNC00NzA3LTgwZjAtYTM4OGY3MGUxNWQ3"
授权类型,接口使用http basic authentication认证方式Authorization= Basic Base64.encode(fuyun_client_id:fuyun_client_secret)
请求参数
字段
示例
描述
grant_type
"urn:ietf:params:oauth:grant-type:token-exchange"
授权类型。
scope
"user_management,hotline"
访问范围(用户管理和热线)。
redirect_url
"http://****.com/callback"
回调地址。
subject_token
-
第三方OAuth平台的access token,例如Salesforce的颁发给当前用户的access_token。
subject_issuer
-
在身份管理的基础信息设置页面,配置的APP Name。
返回示例。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vZGV2ZWxvcGVyLnJoaW5va2Vlbi5jb20iLCJleHAiOjE2MjY5NDY2OTUsInVzZXJfbmFtZSI6ImZ1eXVuLXRlc3QtdXNlciIsImp0aSI6ImQ0YmQ4MTM1LTZmZjYtNDZiNi05YTg2LTQ2MWNkMGVjNDI4NyIsImNsaWVudF9pZCI6ImlkcF90ZXN0XzE2UTRveiIsInNjb3BlIjpbImZ1eXVuLWRldiJdfQ.cDE0EuZaBRwsNf7WrbIPsZhw9juk5dpA2GtCEJ4WHf-iwz8tp9xnX4Kb4jmJqwtWjtrvz0mDeU8uFB31oiz4FzRQb30qhaCeJo7totjwZfTr4bI6bd8afb5C3kypgQUYAyg3wkMzF-6nKgnN_a9YViWp2vO1lq3gH7I5vA5CX6bACWu8OO7LtaD-nKf6JRCcdwY2CWDq_jl43mjz_oek0c8MBcnLL11PAk5VnZRYg7pO6AhPOUkqyAqwbBGcgkEw3pNR1aSTbL8-u69RczKaEgXB_lusshLEXeRK6uNlO8SZzx2BR0AG3nHSG9dAGEdaWMhPUR4gY488k4SYPFNNEQ", "token_type": "bearer", "expires_in": 2591999, "scope": "fuyun-dev", "iss": "http://developer.rhinokeen.com", "jti": "d4bd8135-6ff6-46b6-9a86-461cd0ec4287" }
向Fuyun_IDP指定免登地址发出POST请求。
发送post请求的同时并携带您封装好的参数,用表单传输格式。
获取access_token,
Fuyun_IDP会通过response的形式返回access_token等信息。
SDK集成前端步骤
前端通过access_token对JS SDK的接入,具体可参见热线SDK接入(新版)。JS SDK初始化成功可以正常调用JS接口。
录音、通话详情以及数据拉取
企业内部的坐席在自有的CRM系统上使用集成好的电话条进行通话,在通话结束后,您还可以调用API接口获取通话详情和录音文件。
配置管理
PaaS方式
SaaS方式
坐席管理
创建坐席:选择
页面,切换到人员授权Tab,单击页面右上角新增人员,输入基本信息,并把该坐席添加到对应的技能组中。删除坐席:选择
页面,切换到人员授权Tab,在搜索框中输入姓名/对外展示名,找到要删除的坐席,单击更多,选择删除,单击确定,该坐席即被删除。修改坐席信息:选择
页面,切换到人员授权Tab,在搜索框中输入姓名/对外展示名,找到要修改信息的坐席,单击小铅笔图标,即进入坐席信息修改页面。支持修改坐席的真实姓名、对外展示名、服务权限、高级权限、上班设置、在线服务设置和热线服务设置。查询坐席信息:选择
页面,切换到人员授权Tab,在搜索框中输入姓名/对外展示名,即可找到要查询的坐席信息。
技能组管理
创建技能组:选择
页面,切换到技能组Tab,单击...,单击添加技能组,即可创建新的技能组。删除技能组:选择
页面,切换到技能组Tab,搜索框中输入需要删除的技能组名称,鼠标hover到该技能组名称上,右侧出现删除按钮,单击删除按钮进行删除。如果该技能组内还有坐席,请先把人员从该技能组中移除,再删除技能组。修改技能组信息:选择
页面,切换到技能组Tab,搜索框中输入需要修改信息的技能组名称,即可修改该技能组名称、对外展示名、应用渠道、转交是否可见、实操培训和描述,修改完后单击保存,该技能组信息即可修改成功。查询技能组信息:选择
页面,切换到技能组Tab,搜索框中输入需要查询信息的技能组名称,即可查询对应的技能组信息。
部门管理
创建部门:选择
页面,切换到技能组Tab,单击技能组搜索框右侧的+号,即可新建部门,输入技能组分组名称和描述,单击保存按钮,部门即创建成功。删除部门:选择
页面,切换到技能组Tab,鼠标hover到需要删除的部门名称上,右侧出现...按钮,单击删除,再按照提示输入部门名称,单击确认删除即可成功删除该部门。如果该部门中还有技能组,请先把该部门中的技能组删除,再删除部门。修改部门信息:选择
页面,切换到技能组Tab,找到需要更新信息的部门,单击该部门名称,即可修改该部门的技能组分组名称和描述,修改后单击保存即修改成功。查询部门信息:选择
页面,切换到技能组Tab,即可查看所有的部门信息。
号码管理
新增热线号码:选择
页面,切换到号码管理Tab,选择号码,单击新增,即可新增热线号码。删除热线号码:
单个删除:选择
页面,切换到号码管理Tab,选择号码,选择需要删除的热线号码,单击右侧删除按钮,输入该号码以确认更换,单击确定,该号码即删除成功。批量删除:选择
页面,切换到号码管理Tab,选择号码,在号码最左侧勾选需要删除的热线号码,然后单击下方批量删除按钮,输入对应的号码以确认删除,单击确定,勾选的所有号码即删除成功。
重置热线号码:
单个重置:选择
页面,切换到号码管理Tab,选择号码,选择需要重置的热线号码,单击右侧编辑按钮,进入编辑热线号码页面。描述:支持修改描述。
功能:号码默认有呼入功能,支持设置号码是否具有呼出功能。
呼入IVR:支持修改呼入的IVR。
呼出生效范围:支持修改呼出生效范围。
呼入/呼出满意度调查:支持开启/关闭呼入/呼出满意度调查。
批量重置:选择
页面,切换到号码管理Tab,选择号码,在号码最左侧勾选需要重置的热线号码,然后单击下方批量重置,进入批量编辑热线号码页面。描述:支持修改描述。
功能:号码默认有呼入功能,支持设置号码是否具有呼出功能。
呼入IVR:支持修改呼入的IVR。
呼出生效范围:支持修改呼出生效范围。
呼入/呼出满意度调查:支持开启/关闭呼入/呼出满意度调查。
查询热线号码
选择
页面,切换到号码管理Tab,选择号码,支持查看所有的热线号码配置列表。