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;