本文为您介绍如何将呼叫能力集成到企业自有系统(如CRM等)。
集成架构
流程图
- JWT Token模式集成接入流程图说明 JWT Token模式集成接入流程图中蓝色文字的内容表示该步骤由Fuyun IDP完成。
- OAuth模式集成接入流程图说明 OAuth模式集成接入有两种方式获取access_token,一种为前端直接获取;另一种为客户后端post访问access_token置换接口获取access_token。
前期集成准备
- 获取AccessKey ID和AccessKey Secret。具体操作,请参见获取AccessKey。
- 获取实例ID。
登录智能联络中心,在实例管理页面获取实例ID。
- 创建坐席和热线技能组。
把坐席添加到技能组有以下两种方式,您可以选择其中任一方式。
- PaaS方式:首先创建热线技能组,再创建坐席,把坐席分配到热线技能组中。
- 创建坐席,请参见CreateAgent示例Demo进行创建。
- 创建热线技能组,请参见CreateSkillGroup示例Demo进行创建。
- SaaS方式:直接在智能联络中心添加。
- 创建热线技能组:选择技能组Tab,单击...按钮,单击添加技能组,选择应用渠道为热线,即可创建新的热线技能组。 页面,切换到
- 创建坐席:选择人员授权Tab,单击页面右上角新增人员,输入基本信息,并把该坐席添加到上述步骤中创建的热线技能组中。 页面,切换到
- PaaS方式:首先创建热线技能组,再创建坐席,把坐席分配到热线技能组中。
- 注册开发者门户
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 Token。
在后端的开发环节生成JWT Token,需要用到参数iss,user_name,exp,private_key。payload包含字段:
字段 描述 示例 iss 签发者网站域名。 "iss":"http://signin.rhino****.com" exp 过期时间戳。 1632465581000 user_name JWT Token颁发给的用户,创建坐席时您设置的AccountName(用来映射唯一坐席)。 xxxx 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> /** * 生成JWT Token * @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" 授权类型 scope "user_management,hotline" 访问范围(用户管理和热线) redirect_url "http://****.com/callback" 回调地址 subject_token eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vZGV2ZWxvcGVyLnJoaW5va2Vlbi5jb20iLCJleHAiOjE2MjY5NDY2OTUsInVzZXJfbmFtZSI**** 第三方JWT Token subject_issuer "http://a****.com" 签发者网站域名
- 请求URL。
- 向Fuyun_IDP指定免登地址发出post请求。
发送post请求的同时并携带您封装好的参数,用表单传输格式。
- 获取access_token。
Fuyun_IDP会通过response的形式返回access_token等信息。示例Demo说明
- HTTPS的请求的数据是表单传输格式,不要用JSON。
- 创建技能组会返回技能组ID,通过这个ID让坐席加入相应技能组。
- 创建坐席设置的AccountName之后会成为生成JWT Token的参数。
/** * 环境: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请求在response返回中得到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个字符,支持英文、数字。
- 授权类型:选择令牌交换模式。
- 授权范围:选择用户管理和热线,用于标识颁发的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、mobile、address等。
配置完成后,单击保存,即配置成功。单击上一步,支持返回凭证信息页面。
- 设置基础信息。
- 获取access_token。
OAuth模式集成接入有两种方式获取access_token,一种为前端直接获取,另一种为客户后端post访问access_token置换接口获取access_token。
- 前端方式获取access_token。
以微软为例,通过调用微软的OAuth authorize endpoint,回调到智能联络中心端,继而获取访问智能联络中心API的access token。该次调用最终会将智能联络中心的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等信息。
- 请求头header和请求参数的封装。
- 前端方式获取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,选择号码,选择需要重置的热线号码,单击右侧编辑按钮,进入编辑热线号码页面。
- 查询热线号码
选择号码管理Tab,选择号码,支持查看所有的热线号码配置列表。页面,切换到
- 坐席管理