传统的客服平台常常面临着重复咨询量大,问题解决效率低等痛点。信也科技远程协助平台可以实现客服人员与用户的实时远程协助,直接指导用户在设备上进行操作,避免了繁琐的语言解释和误操作风险;电销人员也能够迅速而准确地了解客户需求,辅助用户在App中快速完成整个业务流程的操作,为用户答疑解惑,并提供个性化建议。
1、价值
- 远程协助实现了客服人员与用户之间的实时远程互动,直接引导用户在设备上完成操作,免去了沟通不当和误操作的困扰;
- 电销人员能够快速掌握客户需求,辅助用户在App中顺利完成整个业务流程的操作,为用户提供专业的咨询和建议;
直观、便捷的远程协助方式,不仅有效降低了用户的重复咨询量和操作失误率,提高了问题的一次性解决率,还增强了销售效果和客户满意度,为企业带来了显著的效益提升。这款远程协助平台是一款创新的客服解决方案,为用户提供一对一的定制化服务,并赋能电销和客服人员拓展业务,实现成本节省和效益提升,并打造优质的用户体验,为企业的长远发展奠定坚实的基础。
2、方案选型介绍
前端屏幕投影技术,从有线到无线,从局域网到互联网,对于前端开发者来说,屏幕投影技术是一项重要且具有挑战性的技术。前端开发者需要考虑如何在不同的浏览器和设备上实现屏幕投影功能,以及如何保证屏幕投影的清晰度、流畅度、稳定性和安全性。目前市面上已经出现了很多种前端屏幕投影技术和解决方案,各有千秋。
- 录制视频(MediaRecorder)技术:这种技术利用了HTML5中提供的MediaRecorder API,可以实现对浏览器中任意媒体源(如摄像头、麦克风、屏幕等)进行录制和编码,并生成可传输或存储的视频数据。这种技术可以实现高清晰度和流畅度的屏幕投影,但是需要较高的网络带宽和处理能力。它适合于需要实时互动和反馈的场景,如在线教学、会议、演示等。
- 截图(html2Canvas)技术:这种技术利用了HTML5中提供的Canvas API,可以实现对浏览器中任意HTML元素进行截图,并生成可传输或存储的图片数据。这种技术可以实现轻量级和快速的屏幕投影,但是不能捕捉动态变化和音频信息。它适合于需要简单展示和分享的场景,如报告、文档、图片等。
- Dom快照技术:这种技术利用了HTML5中提供的MutationObserver API,可以实现对浏览器中任意DOM元素的变化进行监听和记录,并生成可传输或存储的快照数据。这种技术可以实现精确且可回放的屏幕投影,但是需要额外的存储空间和解析工具。它适合于需要记录和分析的场景,如直播、测试、调试、审计等。
在选择屏幕投影技术时,需要根据不同的需求和场景进行权衡和比较。以下是一些通常需要考虑的因素:
- 清晰度
- 流畅度
- 稳定性
- 安全性
综合上述情况远程协助选择的是以Dom快照方式实现,以实现公司业务安全可靠、稳定可控的接入。
3、系统介绍及流程
- 用户只需打开App并同意授权协议,就可以进入远程协助模式,无需其他操作。在这种模式下,客服或电销人员可以实时看到用户的操作界面和行为轨迹,并通过语音或文字给予指导。这样可以提高用户的使用效率和满意度。
- 在与客服或电销人员建立基于socket技术的通信连接,客服可以通过事件分发机制,实时地接收和发送数据,对协助房间进行灵活的管理,比如创建、加入或退出房间等操作。同时,为了保障用户和客服、电销人员之间的数据安全,所有的数据传输都会采用加密算法进行加密处理。
- 中台的客服和电销系统集成了远程协助功能,可以与用户高效地沟通和解决问题。该功能不仅提供了开放式API和默认UI,还支持研发人员根据业务需求,自由地定制和集成远程协助功能,为用户提供更加便捷和可靠的业务服务。
4、疑难杂症
对于通过Dom快照的方式来实现远程协助屏幕投技术,已经是一个成熟的方案。如Sessionstack、Sentry、FullStory、LogRocket等都已经商业化了。但是如果把这项技术结合业务,难度就是指数级增长,因为需要考虑业务场景、数据质量、用户体验、业务需求、法律规范、用户信任等多个因素。
下面列举了结合业务所遇到的一些问题。
4.1 录制的数据脱敏
问题:对于rrweb中的maskAllInputs配置,虽然可以设置信息脱敏,但是应用到具体的场景后会出现,输入框内容闪烁,这也就暴露了用户的输入信息,对于合规要求来说这样的操作是不可取的。
方案:回放时可以通过修改rrweb-snapshot这个lib来做到数据的完全脱敏。
function buildNode(
n: serializedNodeWithId,
options: {
doc: Document;
hackCss: boolean;
cache: BuildCache;
},
): Node | null {
switch (n.type) {
case NodeType.Element: {
const tagName = getTagName(n);
if (tagName === 'input') {
if (n.attributes.value) {
n.attributes.value = `${n.attributes.value}`.replace(/./g, "*");
}
}
}
}
}
4.2 快照数据过大
问题:
- Nginx直接拦下数据(client_max_body_size),数据传输失败;
- 服务413request entity too large;
方案:上述的问题都是由于快照数据过于庞大导致。Nginx层面最简单的解决方案就是提高bodysize,并不是长久方案。对于服务来说也可通过分片上传来解决。但这并不是最终的解决方案。远程协助SDK采用的则是按需上传,当用户每次上线消息通知到后进行数据上报。在服务端进行数据拼接,再二次转发。
// 每次新页面上线上报之前的dom diff数据
this._socket.on(ONLINE_EVENT, ({id, url, time, userAgent}) => {
this.userAgentInfo = userAgent
this._observerHandler && this._observerHandler.online({id, url, time, userAgent});
this._pageInfoList.push({url, time})
this.reportGuidanceRecord()
});
// 服务端对每次页面数据进行拼接
async saveRoomSnapshotInfo(info: IRoomSnapshotInfo) {
const roomInfo = await this.snapshotModel
.findOne({ roomId: info.roomId })
.exec();
if (roomInfo) {
if (info.snapshot) {
info.snapshot.push(...roomInfo.snapshot);
} else {
info['snapshot'] = roomInfo.snapshot;
}
}
return await this.snapshotModel
.findOneAndUpdate({ roomId: info.roomId }, info, {
new: true,
upsert: true,
})
.exec();
}
// 当前协助结束进行数据存储和视频生成
async saveRoomSnapshotInfo(@Body() snapshotInfo: IRoomSnapshotInfo) {
if (snapshotInfo && snapshotInfo.roomId) {
const result = await this.snapshotService.saveRoomSnapshotInfo(
snapshotInfo
);
if (snapshotInfo.isEnd) {
const pageInfoList = snapshotInfo.roomInfo.sharePageInfoList;
for (const pageinfo of pageInfoList) {
const { url } = pageinfo;
const myURL = new URL(url);
await this.domainService.updateDomainRecord(myURL.host);
}
const result = await this.snapshotService.getRoomSnapshotByRoomId(
snapshotInfo.roomId
);
const event = result.snapshot.reverse().map(res => JSON.parse(res));
ConvertVideo(snapshotInfo.roomId, Flat(event)).then(
async res => {
const [res1, res2] = await Promise.all([
this.snapshotService.clearRoomSnapshotInfo({
roomId: result.roomId,
snapshot: [],
type: result.type,
isEnd: true,
roomInfo: result.roomInfo,
}),
this.covertVideoService.saveVoideInfo({
roomId: snapshotInfo.roomId,
url: `https://xxx.ppdaicdn.com/xxx/${snapshotInfo.roomId}.mp4`,
state: true,
type: snapshotInfo.type,
errMessage: '',
}),
]);
console.log('convert video success', res1, res2);
},
error => {
this.covertVideoService.saveVoideInfo({
roomId: snapshotInfo.roomId,
url: '',
state: false,
type: snapshotInfo.type,
errMessage: `${JSON.stringify(error)}`,
});
console.log('covert video fail', error);
}
);
}
return {
Result: 200,
CodeMsg: null,
ResultMessage: null,
Content: result,
};
} else {
return {
Result: 400,
CodeMsg: null,
ResultMessage: null,
Content: '错误参数',
};
}
}
4.3 App中Native/H5页面衔接
问题:用户在App中操作时,会遇到Native页面和H5页面,当用户在这两种页面中不停的反复操作时,如何保证H5页面能顺滑的衔接上。
方案:SDK在播放时需要通过对每个H5页面数据进行缓存,同时需要保持一个播放实例,App端则需要对页面的销毁显示隐藏进行处理。
this._socket.on(PLAY_EVENT, (data) => {
if (this.roomDisbanded) return;
const { event, target } = data;
this._videoInfo.push(event)
if (!this._observerTarget) this._observerTarget = target;
if (this._observerRecordMap) {
if (this._observerRecordMap.has(target)) {
let eventArray = this._observerRecordMap.get(target);
eventArray.push(event);
this._observerRecordMap.set(target, eventArray);
} else {
let eventArray = []
eventArray.push(event)
this._observerRecordMap.set(target, eventArray);
}
} else {
this._observerRecordMap = new Map();
let eventArray = []
eventArray.push(event)
this._observerRecordMap.set(target, eventArray);
}
})
window.PPDWebUI.ListenerService.resumeListener(() => {
this.startRecord()
this.resumeDispatchEvent()
});
window.PPDWebUI.ListenerService.pauseListener(() => {
this.pauseRecord()
});
if (window.PPDWebUI.H5VisibleService) {
window.PPDWebUI.H5VisibleService.isVisible(
res => {
if (!res.visible) {
this.pauseRecord()
} else {
setTimeout(() => {
window.PPDWebUI.H5VisibleService.isVisible(res => {
if (!res.visible) {
this.pauseRecord()
}
});
}, 200);
}
},
() => {}
);
}
4.4 Native页面Mock
问题:当用户在App中不停的切换页面时,如果中间遇到Native页面,会导致播放页面看似卡住,座席和客服则无法得知用户当前在操作场景。因此:
- App需要主动告知当前页面;
- SDK需要根据告知的页面进行页面Mock;
- 方案:
- 本着改动最小交互最小的方式实现App告知当前页面。直接页面切换后通过接口告知;
- 当服务收到消息后,对对应的房间分发通知。SDK收到消息进行页面Mock以解决中间断层的场景;
1. 预置Navite形式的html页面
//通过createHTMLDocument创建一个dom
document.implementation.createHTMLDocument(`${title}`);
// ...具体页面的dom...
2. 创建全量快照
// 全量快照第一步需要添加个Meta的Event
addEvent({
type: EventType.Meta,
timestamp: Date.now() - 100,
data: {
href,
width: 375,
height: 375 * 2,
},
})
//第二步需要把上述的dom全量快照后生成虚拟dom
function snapshot(
n: Document,
options?: {...params},
): serializedNodeWithId | null {
const {
mirror = new Mirror(),
blockClass = 'rr-block',
blockSelector = null,
maskTextClass = 'rr-mask',
maskTextSelector = null,
inlineStylesheet = true,
inlineImages = false,
recordCanvas = false,
maskAllInputs = false,
maskTextFn,
maskInputFn,
slimDOM = false,
dataURLOptions,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn = () => false,
} = options || {};
const maskInputOptions: MaskInputOptions =
maskAllInputs === true
? { ...parmas }
: maskAllInputs === false
? {
password: true,
}
: maskAllInputs;
const slimDOMOptions: SlimDOMOptions =
slimDOM === true || slimDOM === 'all'
? // if true: set of sensible options that should not throw away any information
{
script: true,
comment: true,
headFavicon: true,
headWhitespace: true,
headMetaDescKeywords: slimDOM === 'all', // destructive
headMetaSocial: true,
headMetaRobots: true,
headMetaHttpEquiv: true,
headMetaAuthorship: true,
headMetaVerification: true,
}
: slimDOM === false
? {}
: slimDOM;
return serializeNodeWithId(n, {
doc: n,
mirror,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
newlyAddedElement: false,
});
}
const node = snapshot(doc);
{
type: EventType.FullSnapshot,
timestamp: Date.now() + 100,
data: {
node,
initialOffset: getWindowScroll(window),
},
}
4.5 Node Socket 多台机器多个进程之间适配
问题:单机部署应用时,用socket.io监听消息事件就够了。业务扩大后,要考虑多机集群部署,客户端能向任一节点发消息。要做到多节点同时推送,要建立多节点间消息分发/订阅架构。
方案:客户端用socket.io namespace指定roomid,请求到nginx。nginx根据ip_hash反向代理到对应机器和端口的socket.io server进程。之后建立websocket连接,并通过redis订阅对应房间的channel。这样,一个订阅了某房间的websocket通道就建好了。用户发消息时,socket.io server收到该房间的消息后,redis通过publish消息到对应房间channel 。所有订阅了该房间channel的socket.io server都会收到订阅响应,然后找到对应房间的webscoket通道,并把消息推送到客户端。
5、未来规划
未来,远程协助将持续迭代,依据客户需求,构建基于远程协助和页面投影技术内容的知识图谱,为更多行业的业务系统赋能。并积极加入到社区开源项目建设中,推动企业向实时化、智能化、数字化转型高质量发展。
作者介绍
Cuieney,现任信也科技移动应用前端研发专家
CC,现任信也科技移动应用前端研发资深专家
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。