专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【早阅】滚动页面布局形式的探索 ·  昨天  
前端早读课  ·  【早阅】可能不知道的CSS交互属性 ·  2 天前  
前端早读课  ·  【早阅】@scope 与 HTML ... ·  3 天前  
前端早读课  ·  【早阅】改掉10个不好的TypeScript习惯 ·  4 天前  
前端早读课  ·  Flutter CTO 报告 2024.PDF ·  6 天前  
51好读  ›  专栏  ›  前端早读课

【第3397期】客服工作台的实践总结

前端早读课  · 公众号  · 前端  · 2024-10-22 08:00

正文

前言

大转转客服工作台的实践总结,涵盖了系统整体介绍、客服 IM 与第三屏数据通信、第三屏多页签、客服会话缓存、全埋点统计等方面的技术实现和设计思考。今日前端早读课文章由公号 @大转转 FE 授权分享。

正文从这开始~~

客服工作台是什么?它能做解决什么问题?系统设计很复杂吗?带着这些问题,我们一起揭开转转客服工作台的面纱。

  • 系统整体介绍

  • 客服 IM 与第三屏数据通信

  • 第三屏多页签

  • 客服会话缓存

  • 全埋点统计

  • 最后的思考

随着我司业务的拓展,用户咨询或反馈问题的场景和诉求也越来越多,客服团队不断壮大。客服工作台是客服团队用来解答和处理用户问题的操作平台。客服团队分为一线和二线,其中一线客服(后面统称在线客服)主要接待用户通过客服入口的进线咨询,二线客服主要通过信息查询、电话外呼等方式处理工单流转进一步解决用户问题。本文提到的客服工作台特指为在线客服服务的系统。

【第3160期】客服发送一条消息背后的技术和思考

系统整体介绍

在线客服包括两部分,图中的左侧两屏属于客服与用户的聊天区域(后面统称客服 IM),其中第一屏展示客服的部分重要服务指标(满意度、首解率和接待量)、客服基本信息(昵称 / 头像 / 状态 / 时长)、当前会话和历史会话列表,第二屏为具体某个会话的聊天内容,而图中的右侧为当前会话对应的所有重要信息(后面统称第三屏),包括用户信息、订单信息、工单信息、售后信息、知识库等。

客服 IM

客服 IM 采用 iframe 内嵌的独立系统,主要负责当前会话的实时通讯、历史会话的消息展示、与第三屏数据通信的能力,后面会介绍客服 IM 与第三屏数据通信的消息分类和实现。

【第2910期】前端视角下的转转客服通信过程

第三屏

第三屏是在线客服工作台至关重要的部分,因为它承载了大部分协助客服解答用户问题的信息。在系统实现层面,第三屏有两个核心的问题要解决 —— 多页签和会话缓存。多页签满足客服打开不同页面,并完成信息查阅和操作的需要,会话缓存是客服在同时处理多个进线且需要来回切换会话时,缓存不同会话状态的技术。

同时为了分析系统设计对于客服费力度(评价使用某个产品、服务来解决问题的困难程度)的影响,需要对在线客服工作台做用户行为的采集和分析。

客服 IM 与第三屏数据通信

上面提到第三屏需要提供解决问题的相关信息,但这些信息需要和进线的用户相关,比如用户信息、用户进线时咨询的订单或商品、用户命中的售后单或工单等。那么就需要 iframe(客服 IM)和外层系统(工单系统)通过 postMessage 进行数据通信。

以工单系统收到消息为例,看下第三屏的代码设计。

第三屏部分将数据通信和视图进行了隔离,Message 为通信层,IframeImpage 为视图层。通信层对 iframe 的 postMessage 消息进行监听收发,并将这些消息存储在 model 中共享和管理数据。

 // 在线客服页面的入口
import React from 'react'
import Message from './message'
import IframeImPage from './IframePage'
import type { IframeImPagePropsType } from './@types'

// Message为通信层,IframeImpage为视图层
const IframeIndex: React.FC<IframeImPagePropsType> = (props) => {
return (
<Message {...props}>
<IframeImPage location={props?.location} />
Message>
)
}
export default IframeIndex
 // Message
import React, { useRef, useEffect, useState } from 'react'
import { useModel } from 'umi'
import { PostMsg } from '@/utils/PostMsg'
import { useDebounceFn } from 'ahooks'
import type { CovCoreInfoType } from '@/models/@types'
import type {
IframeImPagePropsType,
PcimPostMsgContent
} from './@types'
const IframeImPage: React.FC<IframeImPagePropsType> = ({ children, location, history }) => {
const {
...
setCovCoreInfo,
...
} = useModel<'imWorkPlateform'>('imWorkPlateform')
// 监听postMessage消息
const chatWrap = useRef<PostMsg>(new PostMsg(handleOpenChatWrap, 'createWrap'))
// 避免客服连续快速切换--防抖200ms
const { run: handleOpenChatWrap } = useDebounceFn(handleOpenChatWrapFn, {
wait: 200
})
// 点击用户会话,打开对应的第三屏
const handleOpenChatWrapFn = async(content: PcimPostMsgContent): Promise<void> => {
const {
convId,
contactUid,
...
} = content
... ...
const targetUrl: string = `/home?uid=${contactUid}***`
// 在第三屏打开指定路由页面
handleAddChangeTab({
url: targetUrl,
covCoreInfoData: {
convId,
contactUid,
...
}
})
// model保存会话信息
setCovCoreInfo({
convId,
contactUid,
...
})
}

useEffect(() => {
...
return () => {
// 注销postMessage监听
chatWrap.current?.destroyPostMsg && chatWrap.current?.destroyPostMsg()
}
})

return <div>{children ? children : null}div>
}

第三屏多页签

针对 umi 多页签的实现,我们采用的是 antd 的 Tabs 组件,而具体需要渲染哪些页面,则需要通过自定义路由实现。目前多页签实现的功能如下:

从下面代码实现中可以看到,我们仿照 umi 写了一份路由对象和解析渲染的方法,把即将打开的页面地址,与路由中的 path 进行匹配,拿到对应的 component,交由 TabPannel 进行组件渲染。

 //自定义路由
const baseConfig = [
{
title: '首页',
component: (params: UserInfoParamsType) => import('../components/home/index'),
path: '/home',
keepAlivePermanentTab: true
},
{
title: '推荐信息',
component: (params: UserInfoParamsType) => import('../components/NewHome/index'),
path: '/newhome',
keepAlivePermanentTab: true
},
...
]

// 多页签实现RouterView.tsx
const RouterView: React.FC<RouteViewParamsType> = (props) => {
return (
<div>
<Tabs>
{panels.map((curPathKey, idx) => {
return (
<Tabs.TabPane>
// View是动态获取的路由中的组件
<Bundle component={View} />
Tabs.TabPane>
)
})}
Tabs>
div>
}

// Bundle.tsx
import React from 'react'
import { useRef, useEffect, useState } from 'react'
import ErrorBoundary from './error-boundary'
import type { BundleParamsType } from '../../@types'

const Bundle: React.FC<BundleParamsType> = (props) => {
const [CompCache, setCompCache] = useState<any>(null)
const initCompCache = async() => {
if (typeof props.component === 'function' && props.component() instanceof Promise) {
const CurComp = await props.component()
setCompCache(
<ErrorBoundary type={2}>
<CurComp.default {...props} />
ErrorBoundary>
)
} else {
const CurComp = props.component
setCompCache(
<ErrorBoundary type={2}>
<CurComp {...props} />
ErrorBoundary>
)
}
}
useEffect(() => {
initCompCache()
}, [props.component])

return CompCache ? CompCache : null
}

export default Bundle

客服会话缓存

先解释下为什么需要缓存会话。根据客服的熟练程度,可能出现 1Vn 的服务场景,即 1 个客服同时与 n 个用户进行沟通会话,那么第三屏信息必然会频繁的切换。客服在与每个用户沟通中操作的页面交互状态,需要在切换中保持不变。否则,客服又得重新打开或查询重复的信息,极大增加了操作费力度。因此系统需要具备能将数据和交互(即会话数据)缓存下来的能力。

【第2904期】客服IM消息列表虚拟滚动技术实践

以下是会话缓存的示意图。

图中包括四部分:LRU 操作、即将更新的会话数据、第三屏的渲染区域、当前会话数据;

  • LRU 是我们常见的通过链表实现的最大数值为 n 的缓存算法;

  • 会话数据分为当前会话数据和即将更新的会话数据,包含以下四部分信息:

    • 包含一个基于会话 id 的缓存唯一值 cacheKey

    • 以及代表第三屏渲染的真实 dom 的 childern

    • 用于动态挂载第三屏真实 dom 的 $ref(也就是图中的最右侧 renderRef)

    • 用于挂载 ref 的容器 conRef

  • 第三屏的渲染区域是一个固定的 dom,它会随着会话的切换,替换为对应 cacheKey 的 dom 容器。

现在简单说下切换会话的时序逻辑。

  • 表示将第三屏渲染的 dom 挂载回到所属缓存的会话数据

  • 表示将当前会话重新更新到 LRU 中

  • 表示新建或切换会话时,基于新的会话 id,创建或从 LRU 中获取会话数据

  • 表示把即将更新的会话数据的挂载容器替换第三屏当前的容器,实现动态挂载真实 dom

  • 表示将新会话数据更新到 LRU 中。

最终的动态挂载真实 dom,在 KeepAliveScope 组件中完成(见下面代码)。第三屏的 dom 数据是从 LRU 缓冲中获取,并通过 createPortal 渲染在 renderRef 对应的实际节点上。

 // IframeImPage 第三屏代码
import { KeepAliveScope, KeepAlive } from '@/components/KeepAlive'
const IframeImPage: React.FC<IframeImPagePropsType> = (props) => {
return <KeepAliveScope>
<KeepAlive name="iframeIMNew" cacheKey={covCoreInfo.childConvId} maxLimit={5}>
<RootRouter
url={covCoreInfo.fullUrl}
needReplace={needReplace}
keepAliveCacheKey={covCoreInfo.childConvId}
/>
KeepAlive>
KeepAliveScope>
}

export default IframeImPage


// KeepAliveScope
const KeepAliveScope: React.FC<KeepAliveScopePropsType> = ({ dispatch, keepAlive, children }) => {
return return (
<KeepAliveContext.Provider value={contextState}>
<>
{/* 子内容渲染 */}
{children}

{/* 渲染每个缓存空间的内容 */}
{Object.keys(keepAlive.cache).map((namespace: string) => {
// 渲染缓存内容
return keepAlive.cache[namespace].map((data: LRUDataType) => {
// 每个缓存内容渲染到独立的容器中(第三个参数为key)
return ReactDOM.createPortal(
<div data-cache-namespace={namespace} data-cache-key={data.key}>
{data.val.children}
div>,
data.val.$ref,
`${namespace}_${data.key}`
)
})
})}
>
KeepAliveContext.Provider>
)
}

export default connect(({ keepAlive }: { keepAlive: KeepAliveStateType }) => ({
keepAlive
}))(KeepAliveScope)

全埋点统计

作为服务客服的产研团队,我们希望能提供更利于客服操作的系统,其中操作费力度就是我们参考的重要指标,基于此目标,也在不断的迭代和升级产品。

费力度的数据表现有很多维度,本篇文章以客服操作的次数来介绍具体的技术实现。比如客服从接待用户进线到最后创建工单的整个操作周期中,客服点击页面的次数。但是由于第三屏数据量过大,我们无法对每个点击事件都进行埋点上报,这无疑是一个很大的工作量,在当下这种场景,全埋点就是一个比较理想的选择。

大致流程:

  • 通过监听点击事件,导致 dom 变化、请求接口或打开新浏览器页签的行为数据,都需要埋点上报。

  • dom 变化、请求接口和开启浏览器页签,通过发布 - 订阅进行事件通信,触发埋点上报。


这套方案把客服点击操作的场景分为三部分:

  • 点击事件造成 dom 变化的,通过 mutationObserver 监听判断;

  • 点击事件重新开启新系统页签(工单系统中其他页签)或浏览器页签的,或者单纯的切到其他系统页签或浏览器页签的,通过监听 visibilitychange 判断;

  • 点击事件后造成了接口请求的,比如一些加密信息,如手机号、地址等,在客服点击后请求接口可以脱敏展示。

需要说明的一点是,埋点上报的数据中要携带点击区域的位置信息,具体规则是页面 - 区域 - 当前元素的文本,这样我们可以推测客服具体操作了什么动作。当然这种方案还是要做一些特殊处理的,比如 iframe 中的客服操作、统计的维度数据 —— 会话 id、弹窗中监听事件等等。

最后的思考

在线客服工作台作为一线客服最重要的系统,在收集用户反馈、处理用户问题方面起着举足轻重的作用。本篇文章主要从工作台的核心功能的角度做了介绍。基于目前现状,从系统开发成本、系统扩展灵活性、业务降本增效三个角度看,谈下未来有价值的事情。

1、由于历史原因,客服 IM 是通过 iframe 内嵌的方式存在于工作台系统中,虽然这种方式在目前没有带来明显的功能问题,但开发成本和性能方面,它仍然是一个重要的优化方向。

2、快速提供排查问题的信息、提供统一且便利的交互操作、适配转转各种复杂业务场景的诉求,是工作台扩展灵活性的目标。比如,针对售前售中售后、手机平板奢侈品等不同时机、不同品类的咨询,能提供灵活的 sop 流程操作,对系统的价值提升无疑是巨大的。

3、从业务角度出发,客服工作台除了要助力客服高质量的解决用户问题,还应该高效的解决问题。随着人工智能技术的普及,在客服领域的应用得到大力的施展,工作台目前已经接入 AI 辅助、智能推荐回复等功能,未来系统可以在更多方面拥抱人工智能,大幅提升客服的工作效率。

关于本文
作者:@大转转 FE
原文:https://mp.weixin.qq.com/s/XBLhn9q8M84lNWYw4kDZnA

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。