您好,欢迎接入Udesk WebIM网页插件, 以下提供基本用法、注意事项、参数说明、方法说明、事件说明、相关示例等。

基本用法

登录Udesk客服系统管理员账号,在 管理中心 -> 渠道管理 -> 即时通讯 -> 网页插件 -> (某一插件)管理,配置基本属性后复制代码到网页,即可启用Udesk IM。

注意事项

设备支持

WebIM目前支持PC/Android(含平板)/IOS(含平板)等设备。

浏览器兼容性

WebIM目前支持IE8+(含)的IE浏览器与基于它内核(360/qq/...)、其他主流浏览器(chrome/firefox/...)。

基本示例

内嵌代码示例

<body>
    <!-- 网页内容 具体代码请在管理员即时通讯网页插件处复制 -->

    <script>
    (function(a,h,c,b,f,g){a["UdeskApiObject"]=f;a[f]=a[f]||function(){(a[f].d=a[f].d||[]).push(arguments)};g=h.createElement(c);g.async=1;g.src=b;c=h.getElementsByTagName(c)[0];c.parentNode.insertBefore(g,c)})(window,document,"script","https://xxx.xxxx.cn/im_client/js/udeskApi.js?1487931175918","ud");
    ud({
        "code": "xxxx",
        "link": "https://xxx.xxxx.cn/im_client/?web_plugin_id=1",
        "isInvite": true,
        "mode": "inner",
        "color": "#307AE8",
        "pos_flag": "srb",
        "language": "en-us", //语言-英文
        "onlineText": "联系客服,在线咨询",
        "offlineText": "客服下班,请留言",
        "mobile": { //为响应式布局,提供pc、mobile自定制
            "mode": "blank",
            "color": "#307AE8",
            "pos_flag": "crb",
            "onlineText": "联系客服,在线咨询",
            "offlineText": "客服下班,请留言"
        }
    });
    </script>
</body>

链接示例

https://xxx.xxxx.cn/im_client/?web_plugin_id=xx&language=zh-cn&agent_id=xxx&group_id=xxx

自定义配置

开发者可以根据自己的需求自定义相关配置,所有属性、事件都在ud({...})里面进行配置,所有方法通过ud(methodName, ...arguments)的方式调用。

配置方式

1. ud({key: val, key2: val2, ...});
2. ud(key, val);

参数说明

全局参数

参数名称 类型 是否必需 说明
code String 系统生成 公司唯一标识
link String 系统生成 公司IM Client链接地址
isInvite Boolean true或false(默认) 是否开启访客邀请功能
管理台可配
agent_id String 指定客服ID,默认不指定 指定会话的客服人员
group_id String 指定客服组ID,默认不指定 指定会话的客服组
session_key String 6位随机字符串 同一个客户,不同的sesstion_key会触发新的会话.
仅支持字母、数字及下划线,禁用特殊字符
merchant_id String 商户ID 若传递了merchant_id,那么session_key、merchant_id均需要传输同一个商户ID
会话逻辑:传了存相关商户参数,不打乱现有会话顺序,机器人、询前表单、留言等,仅在人工会话时保存参数并进行显示替换
merchant_name String 商户名称 与该客服的会话中【客服介绍】会替换显示为【商户名称】
merchant_avatar String 商户头像 与该客服的会话中【客服头像】会替换显示为【商户头像】
merchant_list Boolean true或false(默认) 当网页插件链接中merchant_list=true时网页插件加载出当前客户历史会话中所有的商户,按照每个商户最新的会话时间排序
mode String inner:网页内嵌模式(默认)
blank:独立窗口模式
按钮打开窗口显示模式
管理台可配
color String 按钮颜色代码,默认为:#307AE8 配置IM按钮颜色
管理台可配
pos_flag String 按钮形状+位置
1)形状: h(横向) v(纵向) c(圆形) i(图片);
2)位置: lt(左上) mt(中上) rt(右上) lm(左中) mm(正中) rm(右中) lb(左下) mb(中下) rb(右下)
默认值:crb(pc),hrb(mobile)
按钮形状+位置
如:vrb(纵向右下角)
图片配置路径:管理中心-渠道管理-即时通讯-网页插件-管理-按钮设置
onlineText String 客服在线显示文本,默认为“联系客服,在线咨询” 管理台可配
offlineText String 客服离线显示文本,默认为“客服下班,请留言” 管理台可配
noBubble Boolean true或false(默认) 不显示消息弹出气泡提醒
manualInit Boolean true或false(默认) 手动初始化,设置为true时,嵌入代码不会自动初始化,需要主动调用ud('init')方法
language String "zh-cn"(表示中文)、"en-us"(表示英文)等 配置客户端多语言[详情]
channel String 自定义渠道 管理台可配
targetSelector String 支持#id,.class,tagName,...
默认系统生成 唯一
主目标按钮选择器
1) 用于触发聊天窗口
2) 用于代替系统默认提供的按钮,指定有效后默认按钮不再显示
3) 用于接收邀请
4) 用于接收新消息提醒
5) ...
selector String 支持#id,.class,tagName,...
默认无 可多个
辅助按钮选择器
1) 用于触发聊天窗口
css Object 按钮自定义样式,如:css:{top:10%},默认无
window Object 仅独立窗口模式 独立窗口默认大小为宽780,高560
例 window: {"width": "780", "height": "560"}
配置打开独立窗口大小
panel Object 仅网页内嵌模式 会话面板 [详情]
pop Object 消息提醒气泡 [详情]
customer Object 客户信息 [详情]
im_client_valid {v_nonce:string,v_timestamp:string,v_signature:string} 请求权限校验开通时必传 [详情]
product Object 咨询对象 [详情]
hide_product Boolean 参数为true时, 会隐藏客户端咨询对象的显示,客服端仍会显示
mobile Object 移动端相关配置 [详情]
当网站为响应式布局,提供pc、mobile自定制
robot_modelKey String 机器人常见问题模板ID 根据机器人常见问题模板ID调用常见问题模板,在‘Km-机器人-FAQ对话-知识库-常见问题-ID’可查看
c_cf_dialogueDesc String 自定义值,支持用逗号分隔多个值 此字段的作用是满足客户给机器人传任何自定义信息的需求
用法:首先要在客服系统里的客户自定义字段内,设置一个名字叫做c_cf_dialogueDesc的自定义字段。设置客户该字段值之后,打开web IM网页插件,将该字段添加到路径参数里,格式如下 https://xxx.udesk.cn/im_client/?web_plugin_id=41597&c_cf_dialogueDesc=* 这样系统会自动将c_cf_dialogueDesc的参数值传到机器人中,机器人的对话描述字段接收此值
uba {app_key: string} user behavior analysis 配置 例子:ud({uba:{app_key: 'xxxxxxx'}})
src_url String http URL地址 来源URL

示例

ud({
    "code": "xxxx",
    "link": "https://xxx.xxxx.cn/im_client/",
    "mode": "inner",
    "color": "#307AE8",
    "pos_flag": "srb",
    "onlineText": "联系客服,在线咨询",
    "offlineText": "客服下班,请留言",
    "targetSelector": "#btn_udesk_im",
    "selector": ".udesk_im",
    "pop": {},
    "panel": {},
    "mobile": {},
    "window": {}
});

> 移动端 - 参数说明

参数名称 类型 是否必选 说明
mode String inner:网页内嵌模式(默认)
blank:独立窗口模式
按钮打开窗口显示模式
管理台可配
color String 按钮颜色代码,默认为:#307AE8 配置IM按钮颜色
管理台可配
pos_flag String 按钮形状+位置
1)形状: h(横向) v(纵向) c(圆形) i(图片);
2)位置: lt(左上) mt(中上) rt(右上) lm(左中) mm(正中) rm(右中) lb(左下) mb(中下) rb(右下)
默认值:crb(pc),hrb(mobile)
按钮形状+位置
如:vrb(纵向右下角)
图片配置路径:管理中心-渠道管理-即时通讯-网页插件-管理-按钮设置
onlineText String 客服在线显示文本,默认为“联系客服,在线咨询” 管理台可配
offlineText String 客服离线显示文本,默认为“客服下班,请留言” 管理台可配
noBubble Boolean true或false(默认) 不显示消息弹出气泡提醒
language String "zh-cn"(中文) 或 "en-us"(英文) 配置客户端多语言
targetSelector String 支持#id,.class,tagName,...
默认系统生成 唯一
主目标按钮选择器
1) 用于触发聊天窗口
2) 用于代替系统默认提供的按钮,指定有效后默认按钮不再显示
3) 用于接收邀请
4) 用于接收新消息提醒
5) ...
selector String 支持#id,.class,tagName,...
默认无 可多个
辅助按钮选择器
1) 用于触发聊天窗口
css Object 按钮自定义样式,如:css:{top:10%},默认无
pop Object 消息提醒气泡 [详情]

示例

ud({
    "mobile", {
        "mode": "blank",
        "color": "#307AE8",
        "pos_flag": "crb",
        "onlineText": "联系客服,在线咨询",
        "offlineText": "客服下班,请留言",
        "pop": {
            "direction": "top",
            "arrow": {
                "top": 0,
                "left": "70%"
            }
        }
    }
});

> 会话面板 - 参数说明

参数名称 类型 是否必选 说明
css Object 自定义样式,如:css:{top:10%},默认无 面板位置只支持 top、left、right
onToggle Function 自定义方法
参数:data.visible(true或false)},
检测当前会话面板是否打开,visible为true时则是打开状态

示例

ud({
    "panel": {
        "css": {
            "top": "0",
            "left": "0"
        },
        "onToggle": function(data) {  //面板开关时间
            if (data.visible) {
                //console.log('窗口打开');
            }
            else{
                //console.log('窗口关闭');
            }
        }
    }
});

> 消息提醒气泡 - 参数说明

参数名称 类型 是否必选 说明
css Object 自定义样式,如:css:{top:10%},默认无
direction String 气泡弹出方向,"top"(默认);"right";"left";"bottom"
offset Object 气泡位移量,{top:0,left:0},默认相对居中显示
arrow Object 气泡箭头配置,{top:0,left:0},默认相对居中显示

示例

ud({
    "pop": {
        "direction": "top",
        "arrow": {
            "top": 0,
            "left": "70%"
        }
    }
});

> 客户信息 - 参数说明

概述

网站登录用户通过web im进行聊天对话时,可以将客户信息通过链接转给Udesk,这样客服在与客户对话时,就可以直接查看客户信息。通过客户信息辅助组件也可以进一步了解此客户的订单等其他信息。

使用方法

在web im对话窗口的链接(https://xxxx.udesk.cn/im_client/)按照以下规则增加参数和加密信息,即可传输客户信息。

其中邮箱和电话号码用于客户识别,即与Udesk系统内CRM中已有数据进行比对,如果为已有客户会更新信息,如果没有匹配则会新建客户。

> 客户身份认证 - 请求参数

传递客户信息、业务记录需要做身份认证 signature加密算法,如果不需要传入这些数据,就不需要做身份认证;
客户身份识别有效时间: 4天;
客户身份的识别顺序: web_token > customer_token > c_email > c_phone;
如果传入的web_token或customer_token在系统中没有;但是传入的c_email或c_phone已存在,系统仍然会识别为原有客户。 如果只传入web_token,不传入customer_token、c_email、c_phone等身份数据,则web_token只匹配客户的web_token一项数据,如未匹配到则创建新客户。

参数名称 类型 是否必选 说明
nonce String 随机数 随机数,动态的比静态随机数安全系数更高
timestamp String 时间戳 当前时间戳(13位毫秒)
web_token String 客户ID 客户唯一标示,推荐使用邮箱、手机号等
仅支持字母、数字及下划线,禁用特殊字符
customer_token String 客户外部ID 客户外部唯一标识 等同于open_api_token
仅支持字母、数字及下划线,禁用特殊字符
signature String 加密签名 [signature加密算法]
encryption_algorithm String SHA1或者SHA256 加密算法,不填写默认为SHA1 推荐使用SHA256

signature加密算法(三步)

1、按后面参数及顺序拼接字符,以key=value&形式: noncetimestampweb_tokenim_user_key
im_user_key获取位置【管理中心-即时通讯-网页插件-管理/添加客户信息中的KEY】

sign_str = nonce=value&timestamp=value&web_token=value&im_user_key

2、 使用加密算法计算出签名字符串

sign_str = sha1(sign_str)
或者 sign_str = sha256(sign_str)

3、 将字符串转换为大写

sign_str = sign_str.toUpperCase()

4、易错提醒

1、 im_user_key无需参数仅需要值即可 2、 signature字符串要转换为大写

SHA1示例:
    sign_str = "nonce=9ca6fff5a509fb887ac72cf5c92010e7&timestamp=1455675719000&web_token=test@udesk.cn&b476f9f8-5309-4d0a-a2d4-af08c4507a15";
    sign_str = sha1(sign_str);
    sign_str = sign_str.toUpperCase();

示例

// SHA1示例
ud({
    "customer": {
        "nonce": "9ca6fff5a509fb887ac72cf5c92010e7",
        "signature": "9B2619225AA6476DC1EB80DBB8801E1575EBE39C",
        "timestamp": "1455675719000",
        "web_token": "test@udesk.cn"
    }
});

// SHA256示例
ud({
    "customer": {
        "nonce": "9ca6fff5a509fb887ac72cf5c92010e7",
        "signature": "4CC40FA2D762DAB1D4509750A7135123743D26F2552B7E23611AB8CB5D35825B",
        "timestamp": "1455675719000",
        "web_token": "test@udesk.cn",
        "encryption_algorithm": "SHA256"
    }
});

客户参数

参数名称 类型 是否必选 说明
c_name String 客户姓名
c_email String 客户邮箱唯一
c_phone String 客户电话唯一
customer_token String 客户外部标识唯一 open_api_token同一字段
c_desc String 客户描述
c_org String 公司名称 若开通多公司,则支持传多个,用英文逗号分隔 如:"帅气,漂亮";未开通则仅取第一个
c_tags String 客户标签 传入客户标签,用逗号分隔 如:"帅气,漂亮"
c_owner String 客户负责人ID
c_vip String 'vip'(vip客户) 或者 'normal'(普通客户) 客户vip识别
c_owner_group String 客户负责组ID
c_other_emails String 客户其他邮箱列表 逗号分隔 如:"a@xxx.cn,b@xxx.cn"
c_cf_<自定义字段名称> String 客户自定义字段 客户自定义字段 如: c_cf_名字、c_cf_age、...

> 业务记录 - 请求参数

参数名称 类型 是否必选 说明
c_cn_title String 业务记录主题 传入业务记录主题
c_cn_<自定义字段名称> String 业务记录自定义字段 业务记录自定义字段 如: c_cn_名字、c_cn_age、...

示例

ud({
    "customer": {
        "c_name": "客户姓名kimi",
        "c_email": "newest3@udesk.cn",
        "c_other_emails": '11@udesk.cn,22@udesk.cn',
        "c_desc": "意向客户,潜力巨大",
        "c_org": "desc",
        "c_phone": "1100110012",
        "c_tags": "英俊,新加的",
        "c_owner_group": "62",
        "c_owner": "3",
        "c_vip": "vip",
        "c_qq": "123123",
        "c_cf_年龄": 10,
        "c_cf_爱好": "极限挑战",
        "c_cn_title":"业务记录主题",
        "c_cn_名字":"业务记录的名字",
        "nonce": "694db2645b3f69a8",
        "signature": "315345C77C73A128CF9850EAD777F7A71D423A36",
        "timestamp": "1465878579000",
        "web_token": "newest3@udesk.cn"
    }
});

链接示例

所传的参数请编码处理,否则会出现乱码!

https://xxx.udesk.cn/im_client/?c_desc=test123456789&c_email=jiangst%40udesio.com&c_phone=15888888888&echostr=hello&nonce=cdebd5d13f4d331e&signature=693140534BDDDAF002A46F0027E06244DF0B21AB&timestamp=1457696747000&web_token=42f6607e-8892-4fcf-889b-f9babf627060

> 工单信息 - 请求参数

参数名称 类型 是否必选 说明
t_subject String 工单主题 工单主题
t_content String 工单描述 传值后会带入页面,覆盖留言内容模板
t_priority_id String 工单优先级 t_priority_id取值范围 1(紧急),2(高), 3(标准), 4(低) 详情
t_status_id String 工单状态 t_status_id取值范围 1(开启),2(已解决), 3(已关闭), 4(解决中) 详情
t_tags String 工单标签 传入工单标签,用逗号分隔 如:"tag1,tag2,tag3"
t_cf_<自定义字段名称> String 工单自定义字段 工单自定义字段,通过api获取字段详情列表 如: t_cf_TextField_213、t_cf_SelectField_225 详情

示例

ud({
    "ticket": {
        "t_subject": "工单主题",
        "t_content": "工单描述",
        "t_priority_id": '3',//标准
        "t_status_id": "1",//开启
        "t_tags": "隐约雷鸣,但盼风雨来,能留你在此",
        "t_cf_TextField_213": "文本",
        "t_cf_TextField_214": "12:00:00",//时间
        "t_cf_TextField_215": "2018-10-26",//日期
        "t_cf_TextField_215": "2018-10-20 12:00",//时间日期
        "t_cf_SelectField_225": "3",//单选或下拉
        "t_cf_SelectField_226": "1,2",//复选
        "t_cf_SelectField_227": "0,0",//级联
    }
});

链接示例

所传的参数请编码处理,否则会出现乱码!

https://xxx.udesk.cn/im_client/?t_priority_id=3&t_status_id=1&t_cf_TextField_124=%E6%96%87%E6%9C%AC%E6%A1%86&t_cf_TextField_135=2018-10-20&t_cf_TextField_136=12:00:00&t_cf_TextField_137=2018-10-20%2012:00&t_cf_TextField_138=3.14&t_cf_TextField_139=250&t_cf_TextField_140=https://www.baidu.com&t_cf_TextField_141=abc&t_cf_TextField_142=%E9%80%9A%E5%8E%BF&t_cf_SelectField_134=1&t_cf_SelectField_135=0,0&t_cf_SelectField_136=0&t_cf_SelectField_137=1,2&t_tags=%E6%B5%8B%E8%AF%951,%E5%BC%A0%E4%B8%89,%E6%9D%8E%E5%9B%9B,%E7%8E%8B%E4%BA%94&c_name=im_client%E6%B5%8B%E8%AF%95%E5%AE%A2%E6%88%B7&c_email=t_im_client@163.com&c_phone=13520361010

> 咨询对象 - 参数说明

提供两种接入方式:url传参方式、javascript接入方式

参数说明

名称 说明
title String 标题
title_style String 标题的样式,例如:color:rgb(50,50,50);font-weight:bold
url String 跳转页的链接地址
image String 显示图片地址
send true或false 为true时,咨询对象下方会出现发送按钮,点击发送会将当前咨询对象作为商品消息发送给客服
<自定义参数> 可定义多个自定义参数,可为中文
<自定义参数>_style String 自定义参数的样式

url传参方式

在所有的参数前面加前缀product_,所传的参数请编码处理,否则会出现乱码!

如:https://udeskdemo.udesk.cn/im_client/?product_send=true&product_title=Apple%20iPhone%207&product_url=http%3A%2F%2Fitem.jd.com%2F3133829.html%3Fcu%3Dtrue%26utm_source%E2%80%A6erm%3D9457752645_0_11333d2bdbd545f1839f020ae9b27f14&product_image=http%3A%2F%2Fimg14.360buyimg.com%2Fn1%2Fs450x450_jfs%2Ft3157%2F63%2F1645131029%2F112074%2Ff4f79169%2F57d0d44dN8cddf5c5.jpg%3Fv%3D1474635884816&product_%E4%BB%B7%E6%A0%BC=%EF%BF%A56189.00&product_%E4%BF%83%E9%94%80%E4%BF%A1%E6%81%AF=%E8%B4%AD%E4%B9%B0%E4%BA%AC%E4%B8%9C%E8%87%AA%E8%90%A5%E7%94%B5%E8%84%91%2C%E6%89%8B%E6%9C%BA%2C%E6%95%B0%E7%A0%81%E5%93%81%E7%B1%BB%E6%8C%87%E5%AE%9A%E4%BA%A7%E5%93%81%2C%E5%AE%9E%E9%99%85%E4%B8%8B%E5%8D%95%E7%BB%93%E7%AE%97%E9%87%91%E9%A2%9D%E6%BB%A1199%E5%85%83%2C%E8%BF%94%E4%BA%AC%E4%B8%9C%E8%87%AA%E8%90%A5%E5%A4%A7%E9%97%B8%E8%9F%B9%E4%B8%9C%E5%88%B8%E4%B8%80%E5%BC%A0.

javascript接入方式

ud({
    product: {
        title: "Apple iPhone 7",
        url: "http://item.jd.com/3133829.html?cu=true&utm_source…erm=9457752645_0_11333d2bdbd545f1839f020ae9b27f14",
        image: "http://img14.360buyimg.com/n1/s450x450_jfs/t3157/63/1645131029/112074/f4f79169/57d0d44dN8cddf5c5.jpg",
        "价格": "¥6189.00",
        "促销价": "¥6188.00"
   }
});

> 配置客户端多语言 -详情

从多语言配置中查找已经配置的多语言语言码

多语言功能请单独联系Udesk开通

管理 -> 账户设置-> 多语言支持

语言名称 文件名称 语言码 更新日期 操作
English reocar_language_translation(en-us).xlsx en-us 2017/08/23 14:30 下载文件 上传选择文件 更新

选取配置的语言码 en-us 作为 language的参数。如果需要使用法语,需要先配置法语的多语言,然后使用多语言支持列表中的法语配置记录的语言码

zh-cn(简体中文)已经是系统默认,可以直接使用,不需要在多语言支持中配置

如果language传了一个多语言支持列表中没有配置的语言码参数,会使用默认的简体中文

前端的静态资源中的显示内容现在支持简体中文和English,不支持自定义语言翻译。如果需要支持更多的语言,请联系我们

多语言语言码配置规定

语言码只能从以下列表中选择:

如果想配置的多语言语言码不在下面列表中, 请联系Udesk

语言名称 语言码
阿拉伯语 ar
English en-us
西班牙语 es
法语 fr
日语 ja
朝鲜语/韩语 ko
泰语 th
印度尼西亚语 id
繁体中文 zh-TW
葡萄牙语 pt
俄罗斯语 ru

方法说明

方法名称 参数 说明
init 手动初始化,但manualInit设置为true有效 如:ud("init")
hidePanel 手动隐藏会话面板,如:ud("hidePanel")
showPanel 手动打开会话面板,如:ud("showPanel")
dataTrace type(数据类型), data(数据) 数据跟踪:包括浏览轨迹、事件等

> 商品浏览轨迹上传

方法说明

ud("dataTrace", type, data);

参数说明

参数名称 类型 是否必选 说明
type String product 跟踪类型
data Object 跟踪数据
data.name String 商品名称
data.url String 商品跳转链接(新页显示),如果值为空,则不能点击
data.imgUrl String 如果值为空,则不显示
data.params Object 参数列表
data.params.text String 参数文本
data.params.color String 参数颜色值,规定为十六进制值的颜色(比如 #ff0000)
data.params.fold Boolean 是否粗体
data.params.break Boolean 是否换行
data.params.size Number 默认12 字体大小

示例

// 商品浏览轨迹
ud("dataTrace", "product", {
    "name": " Apple iPhone X (A1903) 64GB 深空灰色 移动联通4G手机",
    "url": "https://item.jd.com/6748052.html",
    "imgUrl": "http://img12.360buyimg.com/n1/s450x450_jfs/t10675/253/1344769770/66891/92d54ca4/59df2e7fN86c99a27.jpg",
    "params": [
        {
            "text": "¥6999.00",
            "color": "#FF0000",
            "fold": false,
            "break": false,  //break为关键字,ie8下需要引号包裹
            "size": 12
        }
    ]
});

> 订单事件上传

订单事件上传需要先进行客户身份认证,再通过以下方法调用

方法说明

ud("dataTrace", type, data);

参数说明

参数名称 类型 是否必选 说明
type String order 跟踪类型
data Object 跟踪数据
data.order_no String 订单编号
data.name String 订单名称
data.url String 订单跳转链接
data.price Number 订单价格
data.order_at Date 下单时间
data.pay_at Date 付款时间
data.status String 订单状态: wait_pay(待付款)、paid(已付款)、closed(已关闭)
data.remark String 备注, 最大长度为1000字节(中文2个字节)
data.consignee_name String 收货人姓名
data.consignee_phone String 收货人电话
data.commodit_num String 商品总数量
commodities Array 商品信息Array(Ojbect)

commodities商品信息对象

参数名 类型 必填 说明
commodit_name string 商品名称
commodit_no string 商品编号
commodit_count numeric 商品数量
commodit_fee string 商品价格

示例

ud("dataTrace", "order", {
    order_no: '1',
    name: '第一个订单',
    url: 'http://xxx.xxxx.com/订单链接',
    price: 16.8,
    order_at: new Date(),
    pay_at: new Date(),
    status: 'wait_pay',
    remark: '备注',
    consignee_name: '张三',
    consignee_phone: '01012345',
    consignee_address: '北京市大兴区',
    commodit_num: 88,
    commodities: [
      {
        commodit_name: '牛奶A',
        commodit_no: 'NO123456',
        commodit_count: 1,
        commodit_fee: '46.5'
      }
    ]
});

> 营销会话事件跟踪

方法说明

ud("dataTrace", type, data);

参数说明

参数名称 类型 是否必选 说明
type String marketing 跟踪类型
data Object 跟踪数据
data.name String 事件名称

示例

// 商品浏览轨迹
ud("dataTrace", "marketing", {
    "name": "取消订单"
});

事件说明

参数名称 类型 是否必需 说明
onReady Function 监听事件,无参数 Udesk IM插件初始化完成事件回调
onUnread Function 监听事件,参数:data:{count:1 //未读消息数} 未读消息事件回调

示例

ud({
    "onReady": function() {
        console.log('初始化完成');
    },
    "onUnread": function(data) {
        console.log('未读消息数'+data.count);
    }
});

> 按钮位置修改(拖拽)

概述

主要用于第三方修改按钮位置来实现自定义拖拽事件。 修改class为udesk-client-btn的css(top,right,bottom,left)

注意事项

为防止拖拽点击触发展开会话模版,接受邀请等事件,需要: 在document.onmousedown事件中调用ud('noDrag'); 在document.onmousemove事件中调用ud('isDrag')

示例

  1. 修改按钮位置: 客服系统后台->渠道管理->即时通讯->某网页插件(点击管理)->按钮设置(右上角)
  2. 拖拽修改方式: 用户自己实现,可使用原生的mousemove和mouseup方法,根据自身的实际业务逻辑实现

窗口设置 - 信息区自定义URL

当使用udesk网页插件,并且在网页插件的窗口设置中配置了自定义URL时,可以在配置的页面内给Udesk客服/机器人发送消息.

基本用法

自定义URL 页面通过 postMessage 和父级页面通信, 需遵循特定的消息格式

客服聊天界面: 可以发送文本消息和商品消息

机器人聊天界面: 只支持发送文本消息

当前窗口网页链接传入自定义的iframe

窗口会将当前网页链接的参数传入自定义URL的iframe.(其中这四个参数cur_url/cur_title/pre_url/src_url不传).

参数说明

参数名称 类型 是否必填 说明
method String 'sendMsg' 方法名称,目前只支持sendMsg
type String 'text' or 'product' 消息类型: 文本消息商品消息
content String Object 消息内容

示例

1. 文本消息

    var data = JSON.stringify({
            method: "sendMsg",   // 固定method
            type: "text",        // 固定type
            content:  “xxx”      // 需要发送的消息
        });
    var origin = "*";
    window.parent.postMessage(data, origin);

2. 商品消息 参数详情

    var data = JSON.stringify({
            method: "sendMsg",      // 固定method
            type: "product",        // 固定type
            content: {              // 商品消息格式
                name: "Apple iPhone X (A1903) 64GB 深空灰色 移动联通4G手机",
                url: "https://item.jd.com/6748052.html",
                imgUrl: "http://img12.360buyimg.com/n1/s450x450_jfs/t10675/253/1344769770/66891/92d54ca4/59df2e7fN86c99a27.jpg",
                params: [{
                    text: "¥6999.00",
                    color: "#FF0000",
                    fold: false,
                    break: false,
                    size: 12
                }, {
                    text: "满1999元另加30元"
                }]
            }
        });
    var origin = "*";
    window.parent.postMessage(data, origin);

注意事项

  1. 请限制发送频率
  2. 注意消息格式

自定义搜索关键词

参数名称 类型 说明
udesk_wd String 搜索关键词,由第三方跳转过来如:百度推广

第三方网站链接示例(如:公司官网)

https://www.公司域名.com?udesk_wd=xxxx

网页插件链接示例

https://公司域名.udesk.cn/im_client/?udesk_wd=xxxx

向外部传递事件

网页插件通过window.postMessage向外部传递事件

第三方页面通过window.addEventListener('message',function (event) { //event })接收

示例

    window.addEventListener('message',function (event) {
        /*
            以会话结束事件举例
            通过接收到的type区分事件类型,具体事件类型详见参数说明
        */
        let receiveEvent = JSON.parse(event.data)
        if (receiveEvent.type === 'chatDone') {
            //自定义事件
        }
    })
说明
chatting 会话开始事件
chatDone 会话结束事件
queuing 进入排队事件
hidePanel 隐藏窗口事件

开通 "web IM请求权限校验"以后的校验方法

如果想要强制网页插件链接必须校验以后才允许访问,
可以在管理中心->网页插件->其他设置中打开“web IM请求权限校验”,
打开以后插件链接里必须传 v_signature、v_timestamp和v_nonce

v_signature加密算法(三步)

1、按后面参数及顺序拼接字符,以key=value&形式: v_noncev_timestampim_user_key
im_user_key获取位置【管理中心-即时通讯-网页插件-管理/添加客户信息中的KEY】

sign_str = v_nonce=value&v_timestamp=value&im_user_key

2、 使用加密算法计算出签名字符串

sign_str = sha1(sign_str)

3、 将字符串转换为大写

sign_str = sign_str.toUpperCase()

Java代码示例

1.安装依赖

commons-codec用于计算 sha1

<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.17.0</version>
</dependency>

2.代码实现

import org.apache.commons.codec.digest.DigestUtils;

public class App {
    public static void main(String[] args) {
        // 插件专用链接
        String baseURL = "[这里替换为您的插件专用链接]";
        // 插件的身份认证Key
        String im_user_key = "[这里替换为您的插件的 im_user_key]";
        // 时间戳,单位是毫秒
        long timestamp = System.currentTimeMillis();
        // 随机字符串
        String nonce = Long.toString(Double.valueOf(Math.ceil(Math.random() * Long.MAX_VALUE)).longValue(), 36);
        // 合并为需要签名的字符串
        String s = String.format("v_nonce=%s&v_timestamp=%s&%s", nonce, timestamp, im_user_key);
        // 计算sha1,并将结果大写
        String signature = DigestUtils.sha1Hex(s).toUpperCase();
        // 拼接最终的网页插件链接
        String url = String.format(
                "%s&v_nonce=%s&v_timestamp=%s&v_signature=%s",
                baseURL,
                nonce,
                timestamp,
                signature
        );

        System.out.println(url);

        // 如果是用代码对接方式
        System.out.printf(
                "ud({\n  code: '%s',\n  link: '%s',\n  im_client_valid:{\n    v_nonce:'%s',\n    v_timestamp:'%s',\n    v_signature:'%s'\n  }\n})\n",
                "[替换为您的 code]",
                baseURL,
                nonce,
                timestamp,
                signature
        );
    }
}

如何加密网页插件参数

如果使用 url 的方式对接网页插件,会把客户信息全部拼接到 url 里,而且是明文的,
为解决此问题,udesk 提供了一种方式,可以通过公钥加密 url 里所有 c_ 开头的参数,
然后将加密后的密文作为参数 customer_encrypt 的值。

下面是一个用 java 实现这个功能的例子

1.安装依赖

commons-lang3 是一个java里常用的工具类
commons-codec 用于计算签名
jackson-databind 用于转换 object 为 json 字符串

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.16.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.2</version>
</dependency>

2.代码实现

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomStringUtils;

import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;

public class DemoUdeskImClientApplication {

    // 用于加密所有c_开头的参数
    private static final String PUBLIC_KEY = "[替换为网页插件的public key]";
    // 用于参数签名
    private static final String IM_USER_KEY = "[替换为网页插件用于身份认证的 key]";
    private static final String URL_BASE = "https://[这里替换为客服系统域名]/im_client/?web_plugin_id=[替换为网页插件id]";

    public static void main(String[] args) throws Exception {
        Customer customer = new Customer(
                "李四",
                "lisi@udesk.cn",
                "18518518518",
                "李四是一个测试客户",
                "客户标签,客户标签2",
                "normal",
                "AAJD1710397772205@udesktest.com"
        );

        String params = String.format(
                "nonce=%s&timestamp=%s&web_token=%s",
                RandomStringUtils.random(5, true, true),
                System.currentTimeMillis(),
                customer.getWeb_token()
        );

        String url = URL_BASE
                + "&customer_encrypt=" + encode(customer)
                + "&" + params
                + "&encryption_algorithm=SHA256"
                + "&signature=" + getSignature(params);

        System.out.println(url);
    }

    public static String encode(Customer customer) throws Exception {
        // 将 customer 转成 json 字符串
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
        String message = objectMapper.writer().writeValueAsString(customer);

        // 获取 RSAPublicKey 对象
        byte[] encoded = Base64.getDecoder().decode(PUBLIC_KEY);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
        RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);

        // 初始化 Cipher
        Cipher encryptCipher = Cipher.getInstance("RSA");
        encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);

        // 对密文分段
        int dataCount = (int) Math.ceil((double) message.getBytes().length / 117);
        byte[][] datas = new byte[dataCount][];
        for (int i = 0; i < dataCount; i++) {
            int length = (i == dataCount - 1) ? (message.getBytes().length % 117) : 117;
            byte[] item = Arrays.copyOfRange(message.getBytes(), i * 117, i * 117 + length);
            datas[i] = item;
        }

        // 对分好段的密文加密,然后合成一个大的 byte 数组,最后将这个大数组使用 base64 编码
        byte[] mi = new byte[]{};
        for (byte[] s : datas) {
            mi = ArrayUtils.addAll(mi, encryptCipher.doFinal(s));
        }
        return Base64.getEncoder().encodeToString(mi);
    }

    public static String getSignature(String paramString) {
        return DigestUtils.sha256Hex(paramString + "&" + IM_USER_KEY).toUpperCase();
    }

    @JsonRootName("customer")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    static class Customer {
        String c_name;
        String c_email;
        String c_phone;
        String c_desc;
        String c_tags;
        String c_vip;

        String web_token;

        public Customer(String c_name, String c_email, String c_phone, String c_desc, String c_tags, String c_vip, String web_token) {
            this.c_name = c_name;
            this.c_email = c_email;
            this.c_phone = c_phone;
            this.c_desc = c_desc;
            this.c_tags = c_tags;
            this.c_vip = c_vip;
            this.web_token = web_token;
        }

        public Customer(String c_name) {
            this.c_name = c_name;
        }

        public String getC_name() {
            return c_name;
        }

        public void setC_name(String c_name) {
            this.c_name = c_name;
        }

        public String getWeb_token() {
            return web_token;
        }

        public void setWeb_token(String web_token) {
            this.web_token = web_token;
        }

        public String getC_email() {
            return c_email;
        }

        public void setC_email(String c_email) {
            this.c_email = c_email;
        }

        public String getC_phone() {
            return c_phone;
        }

        public void setC_phone(String c_phone) {
            this.c_phone = c_phone;
        }

        public String getC_desc() {
            return c_desc;
        }

        public void setC_desc(String c_desc) {
            this.c_desc = c_desc;
        }

        public String getC_tags() {
            return c_tags;
        }

        public void setC_tags(String c_tags) {
            this.c_tags = c_tags;
        }

        public String getC_vip() {
            return c_vip;
        }

        public void setC_vip(String c_vip) {
            this.c_vip = c_vip;
        }
    }
}

常见问题

企业微信H5无法拍照?

请联系 udesk 客服配置企业微信代开发应用的可信域名

安卓webview中无法拍照或者录像?

网页插件的文件、拍照、录像都是普通的<input type='file'>标签
在安卓 webview 中需要写一些代码适配,
下面的代码实现了以下功能,打开视频查看效果
1. 点击拍照时直接打开相机,点击录像时直接打开相机录像,点击文件时可以选择所有类型的文件
2. 未授权过的 app 拍照或者录屏时申请权限
3. 拒绝相机权限的时候,弹出“去设置”的提示框

import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.Settings;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainActivity extends AppCompatActivity {

    private static final int FILE_CHOOSER_REQUEST_CODE = 1;
    private static final int CAMERA_PERMISSION_REQUEST_CODE = 2;
    private static final int CAMERA_VIDEO_PERMISSION_REQUEST_CODE = 3;

    private WebView webView;
    private ValueCallback<Uri[]> filePathCallback;
    private String cameraPhotoPath;
    private String cameraVideoPath;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webview);
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);

        webView.setWebViewClient(new WebViewClient());
        webView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
                MainActivity.this.filePathCallback = filePathCallback;
                String[] acceptTypes = fileChooserParams.getAcceptTypes();
                if (acceptTypes.length > 0) {
                    String acceptType = acceptTypes[0];

                    if (acceptType.equals("video/*") || acceptType.equals("image/*")) {
                        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                            ActivityCompat.requestPermissions(
                                    MainActivity.this,
                                    new String[]{Manifest.permission.CAMERA},
                                    acceptType.equals("video/*") ? CAMERA_VIDEO_PERMISSION_REQUEST_CODE : CAMERA_PERMISSION_REQUEST_CODE
                            );
                            return true;
                        } else {
                            if (acceptType.equals("image/*")) {
                                openImageChooser();
                                return true;
                            } else if (acceptType.equals("video/*")) {
                                openVideoChooser();
                                return true;
                            }
                        }
                    }
                }

                openFileChooser();
                return true;
            }
        });

        webView.loadUrl("https://[替换为您的专属域名]/im_client/");
    }

    private void openImageChooser() {
        // 创建用于调用相机的Intent
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            // 创建用于存储拍摄图片的文件
            File photoFile = null;
            try {
                photoFile = createImageFile();
                takePictureIntent.putExtra("PhotoPath", cameraPhotoPath);
            } catch (IOException ex) {
                // 错误处理
                ex.printStackTrace();
            }

            if (photoFile != null) {
                cameraPhotoPath = "file:" + photoFile.getAbsolutePath();
                Uri photoURI = FileProvider.getUriForFile(this, "udesk.im.client.webview.demo.provider", photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
            } else {
                takePictureIntent = null;
            }
        }

        startActivityForResult(takePictureIntent, FILE_CHOOSER_REQUEST_CODE);
    }

    private File createImageFile() throws IOException {
        // 创建图片文件名
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "JPEG_" + timeStamp + "_";
        File storageDir = getExternalFilesDir(null);
        return File.createTempFile(imageFileName, ".jpg", storageDir);
    }

    private void openVideoChooser() {
        // 创建用于调用相机录像的Intent
        Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
        if (takeVideoIntent.resolveActivity(getPackageManager()) != null) {
            // 创建用于存储录像文件
            File videoFile = null;
            try {
                videoFile = createVideoFile();
                takeVideoIntent.putExtra("VideoPath", cameraVideoPath);
            } catch (IOException ex) {
                // 错误处理
                ex.printStackTrace();
            }

            if (videoFile != null) {
                cameraVideoPath = "file:" + videoFile.getAbsolutePath();
                Uri videoURI = FileProvider.getUriForFile(this, "udesk.im.client.webview.demo.provider", videoFile);
                takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI);
            } else {
                takeVideoIntent = null;
            }
        }

        startActivityForResult(takeVideoIntent, FILE_CHOOSER_REQUEST_CODE);
    }

    private File createVideoFile() throws IOException {
        // 创建视频文件名
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String videoFileName = "MP4_" + timeStamp + "_";
        File storageDir = getExternalFilesDir(null);
        return File.createTempFile(videoFileName, ".mp4", storageDir);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILE_CHOOSER_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri[] results = null;
            if (resultCode == Activity.RESULT_OK) {
                if (data == null || data.getData() == null) {
                    if (cameraPhotoPath != null) {
                        results = new Uri[]{Uri.parse(cameraPhotoPath)};
                    }
                } else {
                    String dataString = data.getDataString();
                    if (dataString != null) {
                        results = new Uri[]{Uri.parse(dataString)};
                    }
                }
            }
            filePathCallback.onReceiveValue(results);
            filePathCallback = null;
        } else {
            if (filePathCallback != null) {
                filePathCallback.onReceiveValue(null);
                filePathCallback = null;
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE || requestCode == CAMERA_VIDEO_PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                if (requestCode == CAMERA_VIDEO_PERMISSION_REQUEST_CODE) {
                    openVideoChooser(); // 点击录像申请权限时同意授权
                } else if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
                    openImageChooser(); // 点击拍照申请权限时同意授权
                }
            } else {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
                    showPermissionDeniedDialog();  // 权限被拒绝,并且勾选了“不再询问”。
                } else {
                    Toast.makeText(this, "需要相机权限才能拍照和录屏。", Toast.LENGTH_SHORT).show();
                }
                if (filePathCallback != null) {
                    filePathCallback.onReceiveValue(null);
                    filePathCallback = null;
                }
            }
        }
    }


    private void openFileChooser() {
        Intent takePictureIntent = new Intent(Intent.ACTION_GET_CONTENT);
        takePictureIntent.setType("*/*");
        startActivityForResult(takePictureIntent, FILE_CHOOSER_REQUEST_CODE);
    }

    private void showPermissionDeniedDialog() {
        new AlertDialog.Builder(this)
                .setTitle("需要权限")
                .setMessage("拍照需要相机权限。请在设置中启用该权限。")
                .setPositiveButton("去设置", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        openAppSettings();
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }

    private void openAppSettings() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        startActivity(intent);
    }
}

安卓webview中如何下载图片?

网页插件点击图片会显示专门的图片查看界面,界面的顶部有一个下载的按钮,
这个按钮是一个普通的 a 标签,当我们点击这个 a 标签的时候会在新的页面打开这个图片,
所有我们要在安卓 app 里拦截这个行为,改为下载。

下面是具体的代码实现

import android.app.DownloadManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private WebView myWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myWebView = findViewById(R.id.webview);
        myWebView.getSettings().setJavaScriptEnabled(true);
        myWebView.setWebViewClient(new MyWebViewClient());
        myWebView.loadUrl("https://[替换为您的专属域名]/im_client/");
    }

    private class MyWebViewClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (isImageUrl(url)) {
                // 如果是图片链接,调用下载方法
                downloadImage(MainActivity.this, url);
                return true;
            } else {
                // 如果不是图片链接,继续加载URL
                return false;
            }
        }

        private boolean isImageUrl(String url) {
            // 检查URL是否为图片格式
            return url.endsWith(".jpg")
                    || url.endsWith(".jpeg")
                    || url.endsWith(".png")
                    || url.endsWith(".gif")
                    || url.contains(".jpg?")
                    || url.contains(".jpeg?")
                    || url.contains(".png?")
                    || url.contains(".gif?");
        }

        private void downloadImage(Context context, String url) {
            DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
            request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, url.substring(url.lastIndexOf("/") + 1));
            request.allowScanningByMediaScanner();
            request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
            DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
            if (downloadManager != null) {
                downloadManager.enqueue(request);
                Toast.makeText(context, "正在下载...", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context, "下载管理器不可用", Toast.LENGTH_SHORT).show();
            }
        }
    }
}