From 53a71d7afdb77eb48e7f5aa3b39e79d30bf48782 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Sat, 20 Dec 2025 15:50:04 +0800 Subject: [PATCH] =?UTF-8?q?chore(js):=20=E6=96=B0=E5=A2=9Eai=E3=80=81?= =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E3=80=81=E5=AE=89=E5=85=A8=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E3=80=81wxwork-jssdk.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/js/ai-service.js | 434 +++++++++++++++++++++++++++++ common/js/customer-service.js | 511 ++++++++++++++++++++++++++++++++++ common/js/event-safety.js | 112 ++++++++ common/js/wxwork-jssdk.js | 187 +++++++++++++ lang/zh-cn/ai/ai-chat.js | 4 + 5 files changed, 1248 insertions(+) create mode 100644 common/js/ai-service.js create mode 100644 common/js/customer-service.js create mode 100644 common/js/event-safety.js create mode 100644 common/js/wxwork-jssdk.js create mode 100644 lang/zh-cn/ai/ai-chat.js diff --git a/common/js/ai-service.js b/common/js/ai-service.js new file mode 100644 index 0000000..58353d1 --- /dev/null +++ b/common/js/ai-service.js @@ -0,0 +1,434 @@ +import Config from './config.js' +import http from './http.js' +import store from '@/store/index.js' + +let currentConversationId = null; +const CONVERSATION_KEY = 'ai_conversation_id'; + +// 初始化时从本地读取会话ID +try { + const saved = uni.getStorageSync(CONVERSATION_KEY); + if (saved) { + currentConversationId = saved; + } +} catch (e) { + console.warn('读取会话ID失败:', e); +} + +export default { + /** + * 发送普通消息(blocking 模式) + */ + async sendMessage(message, options = {}) { + try { + const aiConfig = store.getters.globalAIKefuConfig; + const params = { + url: '/api/kefu/chat', + data: { + user_id: store.state.memberInfo?.id || 'anonymous', + stream: false, + inputs: {}, + query: message, + response_mode: 'blocking', // 强制 blocking + user: store.state.memberInfo?.id || 'anonymous', + conversation_id: this.getConversationId() || '' + }, + header: { + 'Content-Type': 'application/json' + } + }; + + console.log('Sending blocking request to:', params.url); + console.log('Request data:', params.data); + + const response = await http.sendRequest({ + ...params, + async: false + }); + + const result = this.handleResponse(response); + if (result.conversationId) { + this.setConversationId(result.conversationId); + } + return result; + } catch (error) { + console.error('Dify API请求失败:', error); + throw new Error('AI服务暂时不可用,请稍后重试'); + } + }, + + /** + * 流式消息入口(自动适配平台) + */ + async sendStreamMessage(message, onChunk, onComplete) { + // #ifdef MP-WEIXIN + // 微信小程序:降级为普通请求 + 前端打字模拟 + try { + const result = await this.sendMessage(message); + const content = result.content || ''; + const conversationId = result.conversationId || ''; + + // 保存会话ID(确保连续对话) + if (conversationId) { + this.setConversationId(conversationId); + } + + // 模拟打字效果 + let index = 0; + const chunkSize = 2; // 每次显示2个字符 + return new Promise((resolve) => { + const timer = setInterval(() => { + if (index < content.length) { + const chunk = content.substring(index, index + chunkSize); + index += chunkSize; + if (onChunk) onChunk(chunk); + } else { + clearInterval(timer); + if (onComplete) { + onComplete({ + content: content, + conversation_id: conversationId + }); + } + resolve({ content, conversation_id: conversationId }); + } + }, 80); // 打字速度:80ms/次 + }); + } catch (error) { + console.error('小程序流式消息降级失败:', error); + if (onComplete) { + onComplete({ error: error.message || '发送失败' }); + } + throw error; + } + // #endif + + // #ifdef H5 + // H5:使用真实流式(EventSource / Fetch) + return this.sendHttpStream(message, onChunk, onComplete); + // #endif + }, + + /** + * HTTP 流式请求(仅 H5 使用) + */ + async sendHttpStream(message, onChunk, onComplete) { + // #ifdef H5 + try { + const url = Config.baseUrl + `/api/kefu/chat`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream', + }, + body: JSON.stringify({ + uniacid: store.state.uniacid || '1', + inputs: {}, + query: message, + response_mode: 'streaming', + user_id: store.state.memberInfo?.id || 'anonymous', + conversation_id: this.getConversationId() || '' + }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + if (!response.body) { + throw new Error('响应体不可用'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + let content = ''; + let conversationId = ''; + + function processBuffer(buf, callback) { + const lines = buf.split('\n'); + buf = lines.pop() || ''; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('data:')) { + const jsonStr = trimmed.slice(5).trim(); + if (jsonStr) { + try { + const data = JSON.parse(jsonStr); + if (data.event === 'message') { + const text = data.answer || data.text || ''; + content += text; + callback(text); + } + if (data.conversation_id) { + conversationId = data.conversation_id; + } + if (data.event === 'message_end') { + // 可选:提前完成 + } + } catch (e) { + console.warn('解析流数据失败:', e); + } + } + } + } + return buf; + } + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + buffer = processBuffer(buffer, (chunk) => { + if (onChunk) onChunk(chunk); + }); + } + + if (onComplete) { + onComplete({ + content, + conversation_id: conversationId + }); + } + return { content, conversation_id: conversationId }; + } catch (error) { + console.error('H5 流式请求失败:', error); + throw error; + } + // #endif + + // #ifdef MP-WEIXIN + // 理论上不会执行到这里,但防止 fallback + return this.sendStreamMessage(message, onChunk, onComplete); + // #endif + }, + + /** + * 处理API响应(统一格式) + */ + handleResponse(response) { + if (response.code === 0 || response.success) { + return { + success: true, + content: response.data?.answer || response.data?.content || response.data?.reply || response.data, + conversationId: response.data?.conversation_id, + messageId: response.data?.message_id + }; + } else { + throw new Error(response.message || 'AI服务返回错误'); + } + }, + + /** + * 获取当前会话ID + */ + getConversationId() { + return currentConversationId; + }, + + /** + * 设置并持久化会话ID + */ + setConversationId(id) { + if (id && id !== currentConversationId) { + currentConversationId = id; + try { + uni.setStorageSync(CONVERSATION_KEY, id); + } catch (e) { + console.error('保存会话ID失败:', e); + } + } + }, + + /** + * 清除会话ID + */ + clearConversationId() { + currentConversationId = null; + try { + uni.removeStorageSync(CONVERSATION_KEY); + } catch (e) { + console.error('清除会话ID失败:', e); + } + }, + + /** + * 生成新会话(如需) + */ + async generateConversationId() { + const params = { + url: '/api/kefu/createConversation', + data: { + uniacid: store.state.uniacid, + user_id: store.state.memberInfo?.id || 'anonymous', + member_id: store.state.memberInfo?.id || '', + }, + header: { + 'Content-Type': 'application/json' + } + }; + const response = await http.sendRequest({ + ...params, + async: false + }); + return this.handleResponse(response); + }, + + /** + * 获取服务状态 + */ + async getServiceStatus() { + try { + const response = await http.sendRequest({ + url: '/api/kefu/health', + async: false, + data: {} + }); + const available = [response?.data?.status, response?.data?.components?.ai_service_config?.status] + .filter(item => item === 'healthy').length > 0; + return { + available, + reason: available ? '服务正常' : '服务异常' + }; + } catch (error) { + return { + available: false, + reason: '服务检查失败' + }; + } + }, + + /** + * 清除会话历史(调用后端接口) + */ + async clearConversation(conversationId) { + try { + const response = await http.sendRequest({ + url: '/api/kefu/clear-conversation', + data: { conversation_id: conversationId }, + async: false + }); + return response.success; + } catch (error) { + console.error('清除会话失败:', error); + return false; + } + }, + + /** + * 获取会话历史 + */ + async getConversationHistory(params = {}) { + try { + if (!params.conversation_id) { + throw new Error('会话ID(conversation_id)是必填参数'); + } + const requestData = { + uniacid: params.uniacid || store.state.uniacid || '1', + conversation_id: params.conversation_id, + user_id: params.user_id || store.state.memberInfo?.id || 'anonymous', + limit: params.limit || 20, + offset: params.offset || 0, + member_id: params.member_id || store.state.memberInfo?.id || '', + token: params.token || '' + }; + const response = await http.sendRequest({ + url: '/api/kefu/getHistory', + data: requestData, + header: { + 'Content-Type': 'application/json' + }, + async: false + }); + if (response.code === 0 || response.success) { + return { + success: true, + data: response.data, + messages: response.data?.messages || [], + total: response.data?.total || 0, + page_info: response.data?.page_info || { limit: 20, offset: 0 } + }; + } else { + throw new Error(response.message || '获取会话历史失败'); + } + } catch (error) { + console.error('获取会话历史失败:', error); + throw new Error(error.message || '获取会话历史时发生错误,请稍后重试'); + } + }, + + // ================================ + // 以下方法仅用于 H5,小程序无效 + // ================================ + + /** + * [H5 Only] EventSource 流式聊天(备用) + */ + chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) { + // #ifdef H5 + if (window.currentEventSource) { + window.currentEventSource.close(); + } + const params = new URLSearchParams({ + uniacid: store.state.uniacid || '1', + user_id: store.state.memberInfo?.id || '123456', + query: message, + conversation_id: conversationId || '', + stream: 'true' + }); + const url = `${Config.baseUrl}/api/kefu/chat?${params.toString()}`; + try { + const eventSource = new EventSource(url); + window.currentEventSource = eventSource; + let aiMessage = ''; + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.event === 'message') { + aiMessage += data.answer || ''; + if (onMessage) onMessage(data.answer || ''); + } + if (data.event === 'message_end') { + if (onComplete) onComplete({ + conversation_id: data.conversation_id, + message: aiMessage + }); + } + } catch (error) { + console.error('解析消息失败:', error); + } + }; + eventSource.onerror = (error) => { + console.error('EventSource错误:', error); + if (onError) onError({ error: '连接失败' }); + window.currentEventSource = null; + }; + return eventSource; + } catch (error) { + console.error('创建EventSource失败:', error); + if (onError) onError({ error: error.message }); + return null; + } + // #endif + // #ifdef MP-WEIXIN + console.warn('chatWithEventSource 不支持微信小程序'); + return null; + // #endif + }, + + /** + * [H5 Only] Fetch 流式聊天(备用) + */ + async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) { + // #ifdef H5 + // 实际逻辑已在 sendHttpStream 中实现,此处可复用或留空 + return this.sendHttpStream(message, onMessage, (res) => { + onComplete?.({ message: res.content, conversation_id: res.conversation_id }); + }); + // #endif + // #ifdef MP-WEIXIN + console.warn('chatWithFetchStream 不支持微信小程序'); + return null; + // #endif + } +}; \ No newline at end of file diff --git a/common/js/customer-service.js b/common/js/customer-service.js new file mode 100644 index 0000000..4960919 --- /dev/null +++ b/common/js/customer-service.js @@ -0,0 +1,511 @@ +/** + * 客服统一处理服务 + * 整合各种客服方式,提供统一的调用接口 + */ +export class CustomerService { + constructor(vueInstance, externalConfig = null) { + this.vm = vueInstance; + this.externalConfig = externalConfig; // 外部传入的最新配置(优先级最高) + this.latestPlatformConfig = null; + } + + /** + * 强制刷新配置(支持传入外部配置) + * @param {Object} externalConfig 外部最新配置 + */ + refreshConfig(externalConfig = null) { + this.externalConfig = externalConfig || this.externalConfig; + this.latestPlatformConfig = null; + return this.getPlatformConfig(); + } + + /** + * 获取平台配置 + * @returns {Object} 平台对应的客服配置 + */ + getPlatformConfig() { + if (this.latestPlatformConfig) { + return this.latestPlatformConfig; + } + + // 优先级:外部传入的最新配置 > vuex配置 > 空对象 + const servicerConfig = this.externalConfig || this.vm.$store.state.servicerConfig || {}; + console.log(`【实时客服配置】`, servicerConfig); + + let platformConfig = null; + // #ifdef H5 + platformConfig = servicerConfig.h5 ? (typeof servicerConfig.h5 === 'object' ? servicerConfig.h5 : null) : null; + // #endif + + // #ifdef MP-WEIXIN + platformConfig = servicerConfig.weapp ? (typeof servicerConfig.weapp === 'object' ? servicerConfig.weapp : null) : null; + // #endif + + // #ifdef MP-ALIPAY + platformConfig = servicerConfig.aliapp ? (typeof servicerConfig.aliapp === 'object' ? servicerConfig.aliapp : null) : null; + // #endif + + // #ifdef PC + platformConfig = servicerConfig.pc ? (typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null) : null; + // #endif + + // 处理空数组情况(你的配置中pc/aliapp是空数组,转为null) + if (Array.isArray(platformConfig)) { + platformConfig = null; + } + + this.latestPlatformConfig = platformConfig; + return platformConfig; + } + + /** + * 获取企业微信配置 + * @returns {Object} 企业微信配置 + */ + getWxworkConfig() { + return this.vm.$store.state.wxworkConfig || {}; + } + + /** + * 检查客服配置是否可用 + * @returns {boolean} 是否有可用配置 + */ + isConfigAvailable() { + const config = this.getPlatformConfig(); + return config && typeof config === 'object' && config.type; + } + + /** + * 验证客服配置完整性 + * @returns {Object} 验证结果 + */ + validateConfig() { + const config = this.getPlatformConfig(); + const wxworkConfig = this.getWxworkConfig(); + + const result = { + isValid: true, + errors: [], + warnings: [] + }; + + if (!config) { + result.isValid = false; + result.errors.push('客服配置不存在'); + return result; + } + + if (config.type === 'aikefu') { + return result; + } + + if (!config.type) { + result.isValid = false; + result.errors.push('客服类型未配置'); + } + + if (config.type === 'wxwork') { + if (!wxworkConfig) { + result.isValid = false; + result.errors.push('企业微信配置不存在'); + } else { + if (!wxworkConfig.enable) { + result.warnings.push('企业微信功能未启用'); + } + if (!wxworkConfig.contact_url) { + result.warnings.push('企业微信活码链接未配置,将使用原有客服方式'); + } + } + } + + return result; + } + + /** + * 跳转到Dify客服页面 + */ + openDifyService() { + try { + if (this.vm.setAiUnreadCount) { + this.vm.setAiUnreadCount(0); + } + // 强制跳转,忽略框架层的封装 + uni.redirectTo({ + url: '/pages_tool/ai-chat/index', + fail: (err) => { + // 兜底:使用window.location跳转(H5) + // #ifdef H5 + window.location.href = '/pages_tool/ai-chat/index'; + // #endif + console.error('跳转Dify客服失败:', err); + uni.showToast({ + title: '跳转客服失败', + icon: 'none' + }); + } + }); + } catch (e) { + console.error('跳转Dify客服异常:', e); + uni.showToast({ + title: '跳转客服失败', + icon: 'none' + }); + } + } + + /** + * 处理客服点击事件 + * @param {Object} options 选项参数 + */ + handleCustomerClick(options = {}) { + const validation = this.validateConfig(); + if (!validation.isValid) { + console.error('客服配置验证失败:', validation.errors); + this.showConfigErrorPopup(validation.errors); + return; + } + + if (validation.warnings.length > 0) { + console.warn('客服配置警告:', validation.warnings); + } + + const config = this.getPlatformConfig(); + const { niushop = {}, sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options; + + if (config.type === 'none') { + this.showNoServicePopup(); + return; + } + + // 核心分支:根据最新的type处理 + switch (config.type) { + case 'aikefu': + this.openDifyService(); + break; + case 'wxwork': + this.openWxworkService(false, config, options); + break; + case 'third': + this.openThirdService(config); + break; + case 'niushop': + this.openNiushopService(niushop); + break; + case 'weapp': + this.openWeappService(config, options); + break; + case 'aliapp': + this.openAliappService(config); + break; + default: + this.makePhoneCall(); + } + } + + /** + * 打开企业微信客服 + * @param {boolean} useOriginalService 是否使用原有客服方式 + * @param {Object} servicerConfig 客服配置 + * @param {Object} options 选项参数 + */ + openWxworkService(useOriginalService = false, servicerConfig = null, options = {}) { + const config = servicerConfig || this.getPlatformConfig(); + const wxworkConfig = this.getWxworkConfig(); + const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options; + + // #ifdef MP-WEIXIN + if (wxworkConfig?.enable && wxworkConfig?.contact_url && !useOriginalService) { + wx.navigateToMiniProgram({ + appId: 'wxeb490c6f9b154ef9', + path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig.contact_url)}`, + success: () => console.log('跳转企业微信成功'), + fail: (err) => { + console.error('跳转企业微信失败:', err); + this.fallbackToOriginalService(); + } + }); + } else { + wx.openCustomerServiceChat({ + extInfo: { url: config.wxwork_url }, + corpId: config.corpid, + showMessageCard: true, + sendMessageTitle, + sendMessagePath, + sendMessageImg + }); + } + // #endif + + // #ifdef H5 + if (wxworkConfig?.enable && wxworkConfig?.contact_url) { + window.location.href = wxworkConfig.contact_url; + } else if (config.wxwork_url) { + location.href = config.wxwork_url; + } else { + this.fallbackToPhoneCall(); + } + // #endif + } + + /** + * 打开第三方客服 + * @param {Object} config 客服配置 + */ + openThirdService(config) { + if (config.third_url) { + window.location.href = config.third_url; + } else { + this.fallbackToPhoneCall(); + } + } + + /** + * 打开牛商客服 + * @param {Object} niushop 牛商参数 + */ + openNiushopService(niushop) { + if (Object.keys(niushop).length > 0) { + this.vm.$util.redirectTo('/pages_tool/chat/room', niushop); + } else { + this.makePhoneCall(); + } + } + + /** + * 打开微信小程序客服 + * @param {Object} config 客服配置 + * @param {Object} options 选项参数 + */ + openWeappService(config, options = {}) { + if (!this.shouldUseCustomService(config)) { + console.log('使用官方微信小程序客服'); + return; + } + + console.log('使用自定义微信小程序客服'); + this.handleCustomWeappService(config, options); + } + + /** + * 处理自定义微信小程序客服 + * @param {Object} config 客服配置 + * @param {Object} options 选项参数 + */ + handleCustomWeappService(config, options = {}) { + const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options; + + if (config.customServiceUrl) { + let url = config.customServiceUrl; + const params = []; + if (sendMessageTitle) params.push(`title=${encodeURIComponent(sendMessageTitle)}`); + if (sendMessagePath) params.push(`path=${encodeURIComponent(sendMessagePath)}`); + if (sendMessageImg) params.push(`img=${encodeURIComponent(sendMessageImg)}`); + + if (params.length > 0) { + url += (url.includes('?') ? '&' : '?') + params.join('&'); + } + + uni.navigateTo({ + url: url, + fail: (err) => { + console.error('跳转自定义客服页面失败:', err); + this.tryThirdPartyService(config, options); + } + }); + return; + } + + this.tryThirdPartyService(config, options); + } + + /** + * 尝试使用第三方客服 + * @param {Object} config 客服配置 + * @param {Object} options 选项参数 + */ + tryThirdPartyService(config, options = {}) { + if (config.thirdPartyServiceUrl) { + // #ifdef H5 + window.open(config.thirdPartyServiceUrl, '_blank'); + // #endif + + // #ifdef MP-WEIXIN + if (config.thirdPartyMiniAppId) { + wx.navigateToMiniProgram({ + appId: config.thirdPartyMiniAppId, + path: config.thirdPartyMiniAppPath || '', + fail: (err) => { + console.error('跳转第三方小程序失败:', err); + this.fallbackToPhoneCall(); + } + }); + } else { + uni.setClipboardData({ + data: config.thirdPartyServiceUrl, + success: () => { + uni.showModal({ + title: '客服链接已复制', + content: '客服链接已复制到剪贴板,请在浏览器中粘贴访问', + showCancel: false + }); + } + }); + } + // #endif + return; + } + + this.fallbackToPhoneCall(); + } + + /** + * 降级到电话客服 + */ + fallbackToPhoneCall() { + uni.showModal({ + title: '联系客服', + content: '在线客服暂时不可用,是否拨打电话联系客服?', + success: (res) => { + if (res.confirm) { + this.makePhoneCall(); + } + } + }); + } + + /** + * 打开支付宝小程序客服 + * @param {Object} config 客服配置 + */ + openAliappService(config) { + console.log('支付宝小程序客服', config); + switch (config.type) { + case 'aikefu': + this.openDifyService(); + break; + case 'third': + this.openThirdService(config); + break; + default: + console.log('使用支付宝官方客服'); + break; + } + } + + /** + * 拨打电话 + */ + makePhoneCall() { + this.vm.$api.sendRequest({ + url: '/api/site/shopcontact', + success: res => { + if (res.code === 0 && res.data?.mobile) { + uni.makePhoneCall({ + phoneNumber: res.data.mobile + }); + } else { + uni.showToast({ + title: '暂无客服电话', + icon: 'none' + }); + } + }, + fail: () => { + uni.showToast({ + title: '获取客服电话失败', + icon: 'none' + }); + } + }); + } + + /** + * 显示无客服弹窗 + */ + showNoServicePopup() { + const siteInfo = this.vm.$store.state.siteInfo || {}; + const message = siteInfo?.site_tel + ? `请联系客服,客服电话是${siteInfo.site_tel}` + : '抱歉,商家暂无客服,请线下联系'; + + uni.showModal({ + title: '联系客服', + content: message, + showCancel: false + }); + } + + /** + * 显示配置错误弹窗 + * @param {Array} errors 错误列表 + */ + showConfigErrorPopup(errors) { + const message = errors.join('\n'); + uni.showModal({ + title: '配置错误', + content: `客服配置有误:\n${message}`, + showCancel: false + }); + } + + /** + * 降级处理:使用原有客服方式 + */ + fallbackToOriginalService() { + uni.showModal({ + title: '提示', + content: '无法直接添加企业微信客服,是否使用其他方式联系客服?', + success: (res) => { + if (res.confirm) { + this.openWxworkService(true); + } + } + }); + } + + /** + * 获取客服按钮配置 + * @returns {Object} 按钮配置 + */ + getButtonConfig() { + const config = this.getPlatformConfig(); + if (!config) return { openType: '' }; + + let openType = ''; + // #ifdef MP-WEIXIN + if (config.type === 'weapp') { + openType = config.useOfficial !== false ? 'contact' : ''; + } + // #endif + + // #ifdef MP-ALIPAY + if (config.type === 'aliapp') openType = 'contact'; + // #endif + + return { ...config, openType }; + } + + /** + * 判断是否应该使用自定义客服处理 + * @param {Object} config 客服配置 + * @returns {boolean} 是否使用自定义客服 + */ + shouldUseCustomService(config) { + // #ifdef MP-WEIXIN + if (config?.type === 'weapp') { + return config.useOfficial === false; + } + // #endif + return true; + } +} + +/** + * 创建客服服务实例 + * @param {Object} vueInstance Vue实例 + * @param {Object} externalConfig 外部最新配置 + * @returns {CustomerService} 客服服务实例 + */ +export function createCustomerService(vueInstance, externalConfig = null) { + return new CustomerService(vueInstance, externalConfig); +} \ No newline at end of file diff --git a/common/js/event-safety.js b/common/js/event-safety.js new file mode 100644 index 0000000..7798f8e --- /dev/null +++ b/common/js/event-safety.js @@ -0,0 +1,112 @@ +// 事件安全处理工具 +export class EventSafety { + // 创建安全的事件对象 + static createSafeEvent(originalEvent = {}) { + const safeEvent = { + type: originalEvent.type || 'unknown', + timeStamp: originalEvent.timeStamp || Date.now(), + detail: originalEvent.detail || {}, + // 安全的目标对象 + get target() { + return EventSafety.createSafeTarget(originalEvent.target) + }, + get currentTarget() { + return EventSafety.createSafeTarget(originalEvent.currentTarget) + }, + // 安全的 matches 方法 + matches(selector) { + return EventSafety.safeMatches(originalEvent.target, selector) + } + } + + return new Proxy(safeEvent, { + get(obj, prop) { + // 防止访问不存在的属性 + if (prop in obj) { + return obj[prop] + } + return undefined + } + }) + } + + // 创建安全的目标对象 + static createSafeTarget(target) { + if (!target || typeof target !== 'object') { + return EventSafety.getFallbackTarget() + } + + const safeTarget = { + // 基础属性 + tagName: target.tagName || '', + id: target.id || '', + className: target.className || '', + // 安全的方法 + matches: (selector) => EventSafety.safeMatches(target, selector), + // 数据集 + dataset: target.dataset || {} + } + + return safeTarget + } + + // 安全的 matches 检查 + static safeMatches(element, selector) { + if (!element || typeof element.matches !== 'function') { + return false + } + + try { + return element.matches(selector) + } catch (error) { + console.warn('matches 检查失败:', error) + return false + } + } + + // 回退目标对象 + static getFallbackTarget() { + return { + tagName: '', + id: '', + className: '', + matches: () => false, + dataset: {} + } + } + + // 包装事件处理器 + static wrapEventHandler(handler, options = {}) { + return function(event) { + try { + // 创建安全的事件对象 + const safeEvent = EventSafety.createSafeEvent(event) + return handler.call(this, safeEvent) + } catch (error) { + console.error('事件处理错误:', error) + // 可选的错误处理 + if (options.onError) { + options.onError(error, event, this) + } + } + } + } + + // 验证事件类型 + static isValidEventType(event, expectedType) { + return event && event.type === expectedType + } + + // 提取安全的事件数据 + static extractEventData(event, fields = ['type', 'timeStamp', 'detail']) { + const result = {} + + fields.forEach(field => { + if (event && field in event) { + result[field] = event[field] + } + }) + + return result + } +} \ No newline at end of file diff --git a/common/js/wxwork-jssdk.js b/common/js/wxwork-jssdk.js new file mode 100644 index 0000000..bd2932b --- /dev/null +++ b/common/js/wxwork-jssdk.js @@ -0,0 +1,187 @@ +/** + * 企业微信JS-SDK调用 + */ +let WxWork = function () { + // 企业微信JS-SDK + this.wxwork = null; + + /** + * 初始化企业微信JS-SDK + * @param {Object} params - 初始化参数 + * @param {string} params.corpId - 企业ID + * @param {string} params.agentId - 应用ID + * @param {string} params.timestamp - 时间戳 + * @param {string} params.nonceStr - 随机字符串 + * @param {string} params.signature - 签名 + * @param {Array} params.jsApiList - 需要使用的JS接口列表 + */ + this.init = function (params) { + if (typeof wx !== 'undefined' && wx.config) { + // 小程序环境下的企业微信 + this.wxwork = wx; + } else if (typeof WWOpenData !== 'undefined') { + // H5环境下的企业微信 + this.wxwork = WWOpenData; + } else { + console.error('企业微信JS-SDK未加载'); + return false; + } + + this.wxwork.config({ + beta: true, // 必须这么写,否则wx.invoke调用形式的jsapi会有问题 + debug: false, // 开启调试模式 + corpId: params.corpId, // 必填,企业号的唯一标识 + agentId: params.agentId, // 必填,企业微信应用ID + timestamp: params.timestamp, // 必填,生成签名的时间戳 + nonceStr: params.nonceStr, // 必填,生成签名的随机串 + signature: params.signature, // 必填,签名 + jsApiList: params.jsApiList || [ + 'openUserProfile', + 'openEnterpriseChat', + 'getContext', + 'getCurExternalContact', + 'openExistedChatWithMsg' + ] // 必填,需要使用的JS接口列表 + }); + + return true; + }; + + /** + * 添加企业微信联系人 + * @param {Object} params - 参数 + * @param {string} params.userId - 用户ID + * @param {Function} success - 成功回调 + * @param {Function} fail - 失败回调 + */ + this.addContact = function (params, success, fail) { + if (!this.wxwork) { + console.error('企业微信JS-SDK未初始化'); + if (fail) fail('企业微信JS-SDK未初始化'); + return; + } + + this.wxwork.ready(() => { + this.wxwork.invoke('openUserProfile', { + type: 'external', // 外部联系人 + userId: params.userId // 用户ID + }, (res) => { + if (res.err_msg === 'openUserProfile:ok') { + if (success) success(res); + } else { + console.error('打开用户资料失败:', res); + if (fail) fail(res.err_msg); + } + }); + }); + }; + + /** + * 打开企业微信客服会话 + * @param {Object} params - 参数 + * @param {string} params.corpId - 企业ID + * @param {string} params.url - 客服URL + * @param {string} params.name - 会话名称 + * @param {Function} success - 成功回调 + * @param {Function} fail - 失败回调 + */ + this.openCustomerService = function (params, success, fail) { + if (!this.wxwork) { + console.error('企业微信JS-SDK未初始化'); + if (fail) fail('企业微信JS-SDK未初始化'); + return; + } + + this.wxwork.ready(() => { + // #ifdef MP-WEIXIN + if (typeof wx !== 'undefined' && wx.openCustomerServiceChat) { + // 微信小程序环境 + wx.openCustomerServiceChat({ + extInfo: { + url: params.url + }, + corpId: params.corpId, + showMessageCard: true, + sendMessageTitle: params.sendMessageTitle || '', + sendMessagePath: params.sendMessagePath || '', + sendMessageImg: params.sendMessageImg || '' + }); + if (success) success(); + } + // #endif + // #ifdef H5 + else if (typeof WWOpenData !== 'undefined') { + // H5环境 + window.location.href = params.url; + if (success) success(); + } + // #endif + else { + // 直接跳转链接 + window.location.href = params.url; + if (success) success(); + } + }); + }; + + /** + * 生成企业微信活码链接 + * @param {Object} params - 参数 + * @param {string} params.configId - 活码配置ID + * @param {string} params.userId - 用户ID + * @returns {string} 活码链接 + */ + this.generateContactUrl = function (params) { + // 企业微信活码链接格式 + const baseUrl = 'https://work.weixin.qq.com/kfid'; + if (params.configId) { + return `${baseUrl}/${params.configId}`; + } + return null; + }; + + /** + * 检查环境是否支持企业微信 + * @returns {boolean} 是否支持 + */ + this.isSupported = function () { + // #ifdef MP-WEIXIN + return typeof wx !== 'undefined' && wx.openCustomerServiceChat; + // #endif + // #ifdef H5 + return typeof WWOpenData !== 'undefined' || /wxwork/i.test(navigator.userAgent); + // #endif + return false; + }; + + /** + * 获取当前环境信息 + * @returns {Object} 环境信息 + */ + this.getEnvironment = function () { + // #ifdef MP-WEIXIN + return { + platform: 'miniprogram', + isWxWork: false, + supportContact: typeof wx !== 'undefined' && wx.openCustomerServiceChat + }; + // #endif + // #ifdef H5 + const isWxWork = /wxwork/i.test(navigator.userAgent); + return { + platform: 'h5', + isWxWork: isWxWork, + supportContact: isWxWork || typeof WWOpenData !== 'undefined' + }; + // #endif + return { + platform: 'unknown', + isWxWork: false, + supportContact: false + }; + }; +} + +export { + WxWork +} \ No newline at end of file diff --git a/lang/zh-cn/ai/ai-chat.js b/lang/zh-cn/ai/ai-chat.js new file mode 100644 index 0000000..0f589fe --- /dev/null +++ b/lang/zh-cn/ai/ai-chat.js @@ -0,0 +1,4 @@ +export const lang = { + //title为每个页面的标题 + title: 'AI智能客服' +} \ No newline at end of file