UNPKG

@tencentcloud/ai-desk-customer-uniapp

Version:

uni-app Vue2/Vue3 UIKit for AI Desk

440 lines (400 loc) 13.6 kB
import TUIChatEngine, { TUITranslateService, TUIStore, StoreName, IMessageModel, TUIUserService, IConversationModel, TUIChatService, } from '../@aidesk/uikit-engine'; import { isApp, isH5, isWeChat } from "./env"; import { vueVersion } from "../adapter-vue-uniapp"; import { JSONToObject, isCustomerServiceMessage } from "./index"; import { CUSTOM_MESSAGE_SRC } from "../constant"; import Log from './logger'; import { marked } from './lib-marked'; export function deepCopy(data: any, hash = new WeakMap()) { if (typeof data !== 'object' || data === null || data === undefined) { return data; } if (hash.has(data)) { return hash.get(data); } const newData: any = Object.create(Object.getPrototypeOf(data)); const dataKeys = Object.keys(data); dataKeys.forEach((value) => { const currentDataValue = data[value]; if (typeof currentDataValue !== 'object' || currentDataValue === null) { newData[value] = currentDataValue; } else if (Array.isArray(currentDataValue)) { newData[value] = [...currentDataValue]; } else if (currentDataValue instanceof Set) { newData[value] = new Set([...currentDataValue]); } else if (currentDataValue instanceof Map) { newData[value] = new Map([...currentDataValue]); } else { hash.set(data, data); newData[value] = deepCopy(currentDataValue, hash); } }); return newData; } export const handleSkeletonSize = ( width: number, height: number, maxWidth: number, maxHeight: number, ): { width: number; height: number } => { const widthToHeight = width / height; const maxWidthToHeight = maxWidth / maxHeight; if (width <= maxWidth && height <= maxHeight) { return { width, height }; } if ( (width <= maxWidth && height > maxHeight) || (width > maxWidth && height > maxHeight && widthToHeight <= maxWidthToHeight) ) { return { width: width * (maxHeight / height), height: maxHeight }; } return { width: maxWidth, height: height * (maxWidth / width) }; }; // Image loading complete export function getImgLoad(container: any, className: string, callback: any) { const images = container?.querySelectorAll(`.${className}`) || []; const promiseList = Array.prototype.slice.call(images).map((node: any) => { return new Promise((resolve: any) => { node.onload = () => { resolve(node); }; node.onloadeddata = () => { resolve(node); }; node.onprogress = () => { resolve(node); }; if (node.complete) { resolve(node); } }); }); return Promise.all(promiseList) .then(() => { callback && callback(); }) .catch((e) => { console.error('网络异常', e); }); } export const isCreateGroupCustomMessage = (message: IMessageModel) => { return ( message.type === TUIChatEngine.TYPES.MSG_CUSTOM && message?.getMessageContent()?.businessID === 'group_create' ); }; /** * displayMessageReadReceipt: User-level control to display message read status * After turning off, the messages you send and receive will not have message read status * You will not be able to see whether the other party has read their messages, and they will also not be able to see whether you have read the messages they sent * * enabledMessageReadReceipt: App-level setting to enable read receipts * @return {boolean} - Returns a boolean value indicating if the message read receipt is enabled globally. */ export function isEnabledMessageReadReceiptGlobal(): boolean { return TUIStore.getData(StoreName.USER, 'displayMessageReadReceipt') && TUIStore.getData(StoreName.APP, 'enabledMessageReadReceipt'); } export function shallowCopyMessage(message: IMessageModel) { return Object.assign({}, message); } // calculate timestamp export function calculateTimestamp(timestamp: number): string { const todayZero = new Date().setHours(0, 0, 0, 0); const thisYear = new Date( new Date().getFullYear(), 0, 1, 0, 0, 0, 0, ).getTime(); const target = new Date(timestamp); const oneDay = 24 * 60 * 60 * 1000; const oneWeek = 7 * oneDay; const diff = todayZero - target.getTime(); function formatNum(num: number): string { return num < 10 ? '0' + num : num.toString(); } if (diff <= 0) { // today, only display hour:minute return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`; } else if (diff <= oneDay) { // yesterday, display yesterday:hour:minute return `${TUITranslateService.t('time.昨天')} ${formatNum( target.getHours(), )}:${formatNum(target.getMinutes())}`; } else if (diff <= oneWeek - oneDay) { // Within a week, display weekday hour:minute const weekdays = [ '星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', ]; const weekday = weekdays[target.getDay()]; return `${TUITranslateService.t('time.' + weekday)} ${formatNum( target.getHours(), )}:${formatNum(target.getMinutes())}`; } else if (target.getTime() >= thisYear) { // Over a week, within this year, display mouth/day hour:minute return `${target.getMonth() + 1}/${target.getDate()} ${formatNum( target.getHours(), )}:${formatNum(target.getMinutes())}`; } else { // Not within this year, display year/mouth/day hour:minute return `${target.getFullYear()}/${ target.getMonth() + 1 }/${target.getDate()} ${formatNum(target.getHours())}:${formatNum( target.getMinutes(), )}`; } } export const isVue2App = isApp && vueVersion === 2; export const isVue3App = isApp && vueVersion === 3; // 是否 vue2 编译的微信小程序 export const isVue2ToWechat = isWeChat && vueVersion === 2; // uniapp vue2 uikit 打包 app ,流式消息的兼容逻辑, // 通过自实现响应式更新,解决 vue 追踪不到属性变更问题 export const needHackForStreamText = (data: string) => { if (isVue2App && JSONToObject(data).src === CUSTOM_MESSAGE_SRC.STREAM_TEXT) { return true; } return false; } export function getSafeUrl(url) { try { const decodedUrl = decodeURIComponent(url); // uni-app 打包 App 后,URL 不可用 if (typeof URL !== 'undefined') { const parsedUrl = new URL(decodedUrl); if (!['http:', 'https:'].includes(parsedUrl.protocol)) { return null; } // 清除 username 和 password parsedUrl.username = ''; parsedUrl.password = ''; return parsedUrl.href; } return decodedUrl; } catch (e) { return null; } } export function openSafeUrl(content: string) { const safeUrl = getSafeUrl(content); if (safeUrl) { if (isApp) { // #ifdef APP-PLUS // @ts-ignore plus.runtime.openURL(safeUrl); // #endif } else if (isH5) { window.open(safeUrl, '_blank', 'noopener,noreferrer'); } } else { Log.w(`Invalid URL provided:${content}`); } } // call after chat engine is ready export function switchReadStatus(value: number) { if (value !== 1) { TUIUserService.switchMessageReadStatus(false); } else { TUIUserService.switchMessageReadStatus(true); } } export function transferToTaskFlow(conversationID: string, taskFlowID: number, description?: string) { if (!taskFlowID) { Log.w(`taskFlowID is required`); return; } TUIChatService.sendCustomMessage({ to: conversationID, conversationType: TUIChatEngine.TYPES.CONV_C2C, payload: { data: JSON.stringify({ src: CUSTOM_MESSAGE_SRC.TRANSFER_TO_TASK_FLOW, customerServicePlugin: 0, taskId: taskFlowID, env: 'production', description: description || '', }) } },{ onlineUserOnly: description ? false : true }); } export function transferToHuman(conversationID: string, groupID?: number, specificMemberList?: Array<string>, description?: string) { if (!groupID && (!specificMemberList || specificMemberList.length === 0)) { Log.w(`groupID or specificMemberList is required`); return; } TUIChatService.sendCustomMessage({ to: conversationID, conversationType: TUIChatEngine.TYPES.CONV_C2C, payload: { data: JSON.stringify({ src: CUSTOM_MESSAGE_SRC.TRANSFER_TO_HUMAN, customerServicePlugin: 0, groupId: groupID || 0, specificMemberList: specificMemberList || [], description: description || '', }) } },{ onlineUserOnly: description ? false : true }); } export function getTo(conversation: IConversationModel): string { return conversation?.groupProfile?.groupID || conversation?.userProfile?.userID; } export function isNonEmptyObject(obj: any): boolean { if (obj && Object.getPrototypeOf(obj) === Object.prototype && Object.keys(obj).length > 0) { return true; } return false; } export function getQuoteContentForDesk(message: IMessageModel): string { let result = TUITranslateService.t('TUIChat.自定义'); if (isCustomerServiceMessage(message)) { const payload = JSONToObject(message.payload.data); const src = payload.src; if (src === CUSTOM_MESSAGE_SRC.BRANCH || src === CUSTOM_MESSAGE_SRC.BRANCH_NUMBER) { result = payload.content.header; } else if (src === CUSTOM_MESSAGE_SRC.RICH_TEXT) { result = parseQuoteMarkdown(payload.content); } else if (src === CUSTOM_MESSAGE_SRC.STREAM_TEXT) { const chunks = Array.isArray(payload.chunks) ? payload.chunks.join('') : payload.chunks; result = parseQuoteMarkdown(chunks); } else if (src === CUSTOM_MESSAGE_SRC.MULTI_BRANCH) { result = payload.content.header; } else if (src === CUSTOM_MESSAGE_SRC.MULTI_FORM) { result = payload.content.tip; } else if (src === CUSTOM_MESSAGE_SRC.ROBOT_MSG) { result = payload.content.title; } } return result; } const quoteMarkedRenderer = new marked.Renderer(); quoteMarkedRenderer.image = (href, title, text) => { return TUITranslateService.t('TUIChat.图片'); } quoteMarkedRenderer.link = (href, title, text) => { if (href) { return TUITranslateService.t('TUIChat.链接'); } return text; } function parseQuoteMarkdown(content: string): string { let ret = marked.parse(content, { renderer: quoteMarkedRenderer }); // 去掉换行符、markdown 格式符号、html 标签 return ret.replace(/\n+/g, ' ').trim() .replace(/\*\*|\*|_|`|#/g, '') .replace(/<[^>]+>/g, ''); } // 自实现简化版 throttle 函数,代替 lodash 的 throttle,解决打包到抖音小程序的运行时错误 export function throttle(func: Function, wait: number) { let timer = null; let lastCallTime = 0; return function(...args) { const now = Date.now(); // 检查是否超过等待时间 if (now - lastCallTime >= wait) { // 立即执行(模拟 leading=true) func.apply(this, args); lastCallTime = now; } else { // 清除之前的定时器,重新计时 clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); lastCallTime = Date.now(); }, wait - (now - lastCallTime)); } }; } const MessageTypeList = [ 'standardTaskFlowBranchOption', // 标准任务流分支选项。 'standardTaskFlowInformationCollection', // 标准任务流信息收集。 'standardTaskFlowReplyMessage', // 标准任务流回复消息。 'fallback', // 兜底消息。 'faq', // 机器人问答库回复消息(命中问答库)。 'clarify', // 机器人澄清消息(引导提问)。 'chitchat', // 机器人闲聊(欢迎卡片及命中寒暄库)。 'rag', // 机器人文档检索回复消息(命中文档问答)。 'aiTaskFlowInformationCollection', // 机器人智能任务流信息收集。 'aiTaskFlowEndReply', // 机器人智能任务流结束回复。 'aiReply', // ai回复(大模型模式下命中大模型回答)。 ]; const AINoteMessageTypeList = [ 'fallback', 'faq', 'aiReply', ]; export function canShowFeedbackButton(cloudCustomData: string): boolean { try { const jsonObj = JSONToObject(cloudCustomData); return MessageTypeList.includes(jsonObj.messageType) && jsonObj.role === "robot"; } catch (e) { return false; } } export function canShowAINote(cloudCustomData: string): boolean { try { const jsonObj = JSONToObject(cloudCustomData); return AINoteMessageTypeList.includes(jsonObj.messageType) && jsonObj.role === "robot"; } catch (e) { return false; } } export function validateUserID(userID: string) { let ret = 0; if (!/^[\x20-\x7E]+$/.test(userID)) { ret = 1; } else if (userID.toLowerCase().includes('administrator')) { ret = 2; } else if (userID.length > 45) { ret = 3; } return ret; } export function updateCustomStore(key: string, data: any) { TUIStore.update(StoreName.CUSTOM, key, data); } export function getCustomStoreData(key: string) { return TUIStore.getData(StoreName.CUSTOM, key); } export function closeBottomPopup(instance: any) { if (instance) { instance.close(); updateCustomStore('formPopup', false); } } export function showBottomPopup(instance: any) { if (instance) { instance.open('bottom'); updateCustomStore('formPopup', true); } } export function debounce(func, delay) { let timeoutID; return function (this: any, ...args) { const context = this; if (timeoutID) { clearTimeout(timeoutID); } timeoutID = setTimeout(() => { func.apply(context, args); }, delay); } }