| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import {getApiUrl} from "@api/config";
- /**
- * SaleSmartly 聊天插件封装(JSSDK: help-en.salesmartly.com)
- * 集成模式:hideIcon 隐藏官方气泡;入口/红点/主动消息提示条由 WSupportCenter 自建,
- * Live Chat 用 SaleSmartly 官方窗口(chatOpen 唤起,外观/欢迎语/快捷问题在其后台配置)。
- * 主动消息触达仅网页内:依赖 onUnRead / onReceiveMessage 回调,登录后须 ensureReady() 预载。
- */
- class SsqChat{
- constructor() {
- this.src = "https://plugin-code.salesmartly.com/js/project_607414_626816_1768878382.js";
- this.apiUrl = getApiUrl();
- this.prefix=this.apiUrl.split(".")[0].split("//")[1]+"_";
- this.prefix="uk_";
- this.gameId=null;
- this.channel=null;
- this.labels=[]; // 会话来源标签(建议/投诉),经 setLoginInfo label_names 展示在客服端
- this.chatWindowOpen=false; // SaleSmartly 官方窗口开关态(onOpenChat/onCloseChat 维护)
- this.unread={num:0,list:[]};
- // 已读指纹持久化:SaleSmartly 服务端不因"看过"清 unread,onUnRead 会一直推旧值(刷新后又提示)。
- // 故由客户端记账:已读时把当前未读列表的内容指纹存 localStorage;此后 onUnRead 的列表指纹
- // 与之相同=还是那批已读消息(拦),不同=真有新消息(亮)。跨刷新有效,不依赖 onReceiveMessage。
- this._readSig="";
- this._loadPromise=null;
- this._listeners={unread:new Set(),message:new Set(),chatOpen:new Set(),chatClose:new Set()};
- }
- _readSigKey(){ return "SSQ_READ_SIG_"+(this.gameId||"g"); }
- /** 由未读列表算内容指纹:优先用消息 id/时间戳,退化到文本+条数 */
- _sigOf(list){
- try{
- const arr=Array.isArray(list)?list:[];
- if (!arr.length) return "";
- return arr.map(m=>String(
- (m&&(m.id||m.message_id||m.msg_id||m.create_time||m.time||m.content||m.text||m.msg))||""
- )).join("|")+"#"+arr.length;
- }catch(e){ return String((list&&list.length)||0); }
- }
- _loadReadSig(){
- try{ this._readSig=window.localStorage.getItem(this._readSigKey())||""; }catch(e){ this._readSig=""; }
- }
- _saveReadSig(sig){
- this._readSig=sig||"";
- try{ window.localStorage.setItem(this._readSigKey(), this._readSig); }catch(e){}
- }
- setGameId(gameId, channel) {
- this.channel=channel;
- this.gameId=gameId;
- this.gameId==null && window.ssq && window.ssq.push('clearUser');
- }
- setServicejs(url) {
- this.src=url;
- }
- /** 订阅事件:unread({num,list}) / message(msg) / chatOpen / chatClose;返回取消函数 */
- on(type,cb){
- this._listeners[type]?.add(cb);
- return ()=>this._listeners[type]?.delete(cb);
- }
- _emit(type,payload){
- this._listeners[type]?.forEach(cb=>{ try{ cb(payload); }catch(e){} });
- }
- async loadScript(){
- return new Promise((resolve, reject) => {
- const script = document.createElement('script');
- script.src = this.src;
- script.onload = () => {
- window.__ssc = window.__ssc || {};
- window.__ssc.setting = { hideIcon: true }; //true为隐藏,false为不隐藏
- window.ssq && window.ssq.push('onReady', () => {
- this._attachEvents();
- resolve();
- });
- };
- script.onerror = () => {
- this._loadPromise=null; // 失败允许下次重试
- reject(new Error(`Failed to load script`));
- };
- document.body.appendChild(script);
- })
- }
- /** 幂等预载:登录后即调,否则收不到 onUnRead/onReceiveMessage(主动消息触达失效) */
- ensureReady(){
- if (this._loadPromise) return this._loadPromise;
- this._loadPromise=this.loadScript();
- return this._loadPromise;
- }
- preLoadScript(){
- this.ensureReady().then(()=>{}).catch(()=>{});
- }
- _attachEvents(){
- this._loadReadSig(); // 从上次会话恢复已读指纹(跨刷新)
- // 未读数与未读内容:自建入口红点 + Tab 角标数据源。
- // 指纹守门:SaleSmartly 服务端不因"看过"清 unread,onUnRead 会持续推同一批旧消息
- //(表现为快速开关角标复现、刷新后又提示同一批旧消息)。本次列表指纹 == 已存已读指纹
- // → 还是那批已读消息,忽略;指纹不同 → 真有新消息,放行点亮。
- window.ssq.push('onUnRead',(obj)=>{
- const num=Number(obj?.num)||0;
- const list=obj?.list||[];
- const sig=this._sigOf(list);
- if (num>0 && sig && sig===this._readSig) return; // 与已读批次相同=陈旧,忽略
- this.unread={num,list};
- this._emit('unread',this.unread);
- });
- // 实时收到消息(含客服主动消息):供自建顶部提示条触达
- window.ssq.push('onReceiveMessage',(obj)=>this._emit('message',obj));
- window.ssq.push('onOpenChat',()=>{
- this.chatWindowOpen=true; this._emit('chatOpen');
- // 打开窗口即视为把当前未读全部读完:落已读指纹 + 清零广播
- this.clearUnread();
- });
- window.ssq.push('onCloseChat',()=>{ this.chatWindowOpen=false; this._emit('chatClose'); });
- }
- packUserInfo(){
- let info={
- language: global.i18n.getCurrLangKey(),
- }
- if (this.gameId!=null){
- Object.assign(info,{
- user_id: this.prefix + this.channel + "_" + this.gameId,
- user_name: this.prefix + this.channel + "_" + this.gameId,
- });
- // 客服工作台右侧用户面板:VIP/品牌/渠道(静态字段;动态深数据走内部 admin)
- const u=global.user||{};
- const ext={};
- if (u.vip!=null) ext["VIP"]=String(u.vip);
- if (window.themeConfig?.title) ext["Brand"]=window.themeConfig.title;
- ext["Site"]=window.location.hostname;
- if (u.Channel!=null) ext["Channel"]=String(u.Channel);
- info.custom_fields_ext=ext;
- if (this.labels.length) info.label_names=this.labels;
- }
- return info
- }
- /** 本地未读清零并广播(打开聊天窗即已读;SaleSmartly 的 onUnRead 不会主动推 0) */
- clearUnread(){
- // 把"当前这批未读"的内容指纹落为已读水位并持久化:此后 onUnRead 再推同一批即被拦(跨刷新)。
- // 打开窗口时当前 unread 可能已被清空,故指纹回退到上次 onUnRead 缓存的 list。
- const sig=this._sigOf(this.unread?.list);
- if (sig) this._saveReadSig(sig);
- this.unread={num:0,list:[]};
- this._emit('unread',this.unread);
- }
- /** 绑定/刷新登录信息(幂等);extraLabels 会并入会话标签 */
- bindUser(extraLabels=[]){
- extraLabels.forEach(l=>{ if(!this.labels.includes(l)) this.labels.push(l); });
- try{ window.ssq && window.ssq.push('setLoginInfo', this.packUserInfo()); }catch(e){ console.error(e); }
- }
- onOpenChat(callback=null){
- try {
- window.ssq.push('setLoginInfo', this.packUserInfo());
- window.ssq.push('chatOpen');
- if (callback!=null)callback();
- }
- catch (err){
- console.error(err);
- }
- }
- /** 是否已登录且拿到 GameID(仅用于控制"登录后才预加载",不再阻止打开会话) */
- hasUser(){
- return this.gameId != null && this.gameId !== "" && String(this.gameId) !== "0";
- }
- openChat(callback=null){
- // 未登录也可进客服(用 SaleSmartly 默认访客名);已登录则 onOpenChat 里 setLoginInfo 带真实身份
- if (window.ssq==null){
- let self=this;
- this.ensureReady().then(()=>{
- self.onOpenChat(callback);
- }).catch((error) => {
- if (callback!=null)callback();
- })
- }
- else this.onOpenChat(callback);
- return true;
- }
- closeChat(){
- try{ window.ssq && window.ssq.push('chatClose'); }catch(e){}
- }
- /**
- * 以访客身份发结构化消息(建议/投诉表单桥接进客服会话队列)并打来源标签。
- * 就绪等待带 8s 超时:脚本加载失败/域名未白名单时 onReady 可能永不回调,不能悬挂调用方。
- */
- async sendSystemContext(text,label=null){
- try{
- await Promise.race([
- this.ensureReady(),
- new Promise((_,rej)=>setTimeout(()=>rej(new Error('ssq ready timeout')),8000))
- ]);
- this.bindUser(label?[label]:[]);
- window.ssq.push('sendTextMessage',text);
- return true;
- }catch(e){
- return false;
- }
- }
- }
- const SsqHelp = new SsqChat();
- export default SsqHelp;
|