diff --git a/.local.config.js b/.local.config.js index 6386cb0..cdc5e52 100644 --- a/.local.config.js +++ b/.local.config.js @@ -42,6 +42,6 @@ const localDevConfig = ({ uniacid: 2, domain: 'http://localhost:8050/', }, -})['2811']; // 选择要使用的环境配置 +})['1']; // 选择要使用的环境配置 export default localDevConfig; \ No newline at end of file diff --git a/common/js/ai-service.js b/common/js/ai-service.js index 88c4dff..f88c7a2 100644 --- a/common/js/ai-service.js +++ b/common/js/ai-service.js @@ -60,55 +60,107 @@ export default { /** * 流式消息入口(自动适配平台) */ - 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; - } + async sendStreamMessage(message, onChunk, onComplete) { + // #ifdef MP-WEIXIN + return new Promise((resolve, reject) => { + const socketTask = wx.connectSocket({ + url: 'wss://dev.aigc-quickapp.com/ws/aikefu', + header: {} + }); + + let content = ''; + let conversationId = ''; + let isAuthenticated = false; + + socketTask.onOpen(() => { + console.log('WebSocket 连接成功,开始认证...'); + socketTask.send({ + data: JSON.stringify({ + action: 'auth', + uniacid: store.state.uniacid || '1', + token: store.state.token || 'test_token', + user_id: store.state.memberInfo?.id || 'anonymous' + }) + }); + }); + + socketTask.onMessage((res) => { + try { + const data = JSON.parse(res.data); + console.log('收到 WebSocket 消息:', data); + + if (data.type === 'auth_success') { + console.log('认证成功,发送聊天消息...'); + isAuthenticated = true; + socketTask.send({ + data: JSON.stringify({ + action: 'chat', + uniacid: store.state.uniacid || '1', + query: message, + user_id: store.state.memberInfo?.id || 'anonymous', + conversation_id: this.getConversationId() || '' + }) + }); + } else if (data.type === 'auth_failed') { + const errorMsg = '认证失败,请重新登录'; + console.error(errorMsg, data); + reject(new Error(errorMsg)); + if (onComplete) onComplete({ error: errorMsg }); + socketTask.close(); + } + + // 处理流式消息块 + else if (data.type === 'message' || data.event === 'message') { + const text = data.answer || data.content || data.text || ''; + content += text; + if (onChunk) onChunk(text); + } + + // 处理流结束 + else if (data.event === 'message_end' || data.type === 'message_end') { + conversationId = data.conversation_id || ''; + if (conversationId) { + this.setConversationId(conversationId); + } + if (onComplete) { + onComplete({ content, conversation_id: conversationId }); + } + resolve({ content, conversation_id: conversationId }); + socketTask.close(); + } + + // 可选:处理 done + else if (data.type === 'done') { + console.log('对话完成:', data); + } + + } catch (e) { + console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data); + } + }); + + socketTask.onError((err) => { + const errorMsg = 'WebSocket 连接失败'; + console.error(errorMsg, err); + reject(new Error(errorMsg)); + if (onComplete) onComplete({ error: errorMsg }); + }); + + socketTask.onClose(() => { + console.log('WebSocket 连接已关闭'); + }); + + const timeout = setTimeout(() => { + if (!isAuthenticated || content === '') { + console.warn('WebSocket 超时,强制关闭'); + socketTask.close(); + reject(new Error('AI服务响应超时')); + if (onComplete) onComplete({ error: 'AI服务响应超时' }); + } + }, 10000); + }); // #endif - - // #ifdef H5 - // H5:使用真实流式(EventSource / Fetch) - return this.sendHttpStream(message, onChunk, onComplete); - // #endif - }, - +}, /** * HTTP 流式请求(仅 H5 使用) */ @@ -131,64 +183,75 @@ export default { conversation_id: this.getConversationId() || '' }) }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + + if (!response.ok || !response.body) { + throw new Error('无效响应'); } - 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() || ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // 按行分割,保留不完整的最后一行 + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // 未完成的行留到下次 + for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data:')) { - const jsonStr = trimmed.slice(5).trim(); - if (jsonStr) { - try { + try { + const jsonStr = trimmed.slice(5).trim(); + if (jsonStr && jsonStr !== '[DONE]') { const data = JSON.parse(jsonStr); if (data.event === 'message') { const text = data.answer || data.text || ''; content += text; - callback(text); + if (onChunk) onChunk(text); } if (data.conversation_id) { conversationId = data.conversation_id; } if (data.event === 'message_end') { - // 可选:提前完成 + // 可提前结束 } - } catch (e) { - console.warn('解析流数据失败:', e); } + } catch (e) { + console.warn('解析失败:', e, line); } } } - 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); - }); + + // 处理最后残留的 buffer(如果有) + if (buffer.trim().startsWith('data:')) { + try { + const jsonStr = buffer.trim().slice(5); + if (jsonStr) { + const data = JSON.parse(jsonStr); + if (data.event === 'message') { + const text = data.answer || ''; + content += text; + if (onChunk) onChunk(text); + } + if (data.conversation_id) { + conversationId = data.conversation_id; + } + } + } catch (e) { + console.warn('最后 buffer 解析失败:', e); + } } - + if (onComplete) { - onComplete({ - content, - conversation_id: conversationId - }); + onComplete({ content, conversation_id: conversationId }); } return { content, conversation_id: conversationId }; } catch (error) { @@ -196,11 +259,6 @@ export default { throw error; } // #endif - - // #ifdef MP-WEIXIN - // 理论上不会执行到这里,但防止 fallback - return this.sendStreamMessage(message, onChunk, onComplete); - // #endif }, /** diff --git a/common/js/customer-service.js b/common/js/customer-service.js index bd1b508..df71d59 100644 --- a/common/js/customer-service.js +++ b/common/js/customer-service.js @@ -2,68 +2,13 @@ * 客服统一处理服务 * 整合各种客服方式,提供统一的调用接口 */ -class CustomerService { - constructor(vueInstance, externalConfig = {}) { - if (!vueInstance.$lang) { - throw new Error('CustomerService 必须在 Vue 实例中初始化'); - } - +export class CustomerService { + constructor(vueInstance, externalConfig = null) { this.vm = vueInstance; this.externalConfig = externalConfig; // 外部传入的最新配置(优先级最高) this.latestPlatformConfig = null; } - getSupoortKeFuList() { - if (!this.vm) return []; - - const vm = this.vm; - - return [ - { - id: 'weixin-official', - name: vm.$lang('customer.weChatKefu'), - isOfficial: true, - type: 'weapp' - }, - { - id: 'custom-kefu', - name: vm.$lang('customer.systemKefu'), - isOfficial: false - }, - { - id: 'qyweixin-kefu', - name: vm.$lang('customer.weChatWorkKefu'), - isOfficial: false - }, - ] - } - - /** - * 打开客服选择弹窗 - */ - openCustomerSelectPopupDialog() { - const kefu_list = this.getSupoortKeFuList(); - const kefuNames = kefu_list.map(item => item.name); - - uni.showActionSheet({ - itemList: kefuNames, - success: (res) => { - const kefu = kefu_list[res.tapIndex]; - this.externalConfig = kefu ?? this.externalConfig ?? {}; - if (kefu.isOfficial) { - uni.openCustomerServiceConversation({ - sessionFrom: 'weapp', - showMessageCard: true - }); - } else if (kefu.id === 'custom-kefu') { - this.handleCustomerClick(); - } else if (kefu.id === 'qyweixin-kefu') { - this.handleQyWeixinKefuClick(); - } - } - }); - } - /** * 强制刷新配置(支持传入外部配置) * @param {Object} externalConfig 外部最新配置 @@ -83,28 +28,29 @@ class CustomerService { return this.latestPlatformConfig; } - // 优先级:外部传入的最新配置 > vuex配置 > 空对象 + // 优先级:外部传入 > vuex store > 空对象 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; + platformConfig = servicerConfig.h5 && typeof servicerConfig.h5 === 'object' ? servicerConfig.h5 : null; // #endif // #ifdef MP-WEIXIN - platformConfig = servicerConfig.weapp ? (typeof servicerConfig.weapp === 'object' ? servicerConfig.weapp : null) : null; + platformConfig = servicerConfig.weapp && typeof servicerConfig.weapp === 'object' ? servicerConfig.weapp : null; // #endif // #ifdef MP-ALIPAY - platformConfig = servicerConfig.aliapp ? (typeof servicerConfig.aliapp === 'object' ? servicerConfig.aliapp : null) : null; + platformConfig = servicerConfig.aliapp && typeof servicerConfig.aliapp === 'object' ? servicerConfig.aliapp : null; // #endif // #ifdef PC - platformConfig = servicerConfig.pc ? (typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null) : null; + platformConfig = servicerConfig.pc && typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null; // #endif - // 处理空数组情况(你的配置中pc/aliapp是空数组,转为null) + // 防止空数组被当作有效配置 if (Array.isArray(platformConfig)) { platformConfig = null; } @@ -144,32 +90,18 @@ class CustomerService { warnings: [] }; - if (!config) { - result.isValid = false; - result.errors.push('客服配置不存在'); - return result; - } - - if (config.type === 'aikefu') { - return result; - } - - if (!config.type) { + if (!config || !config.type) { result.isValid = false; result.errors.push('客服类型未配置'); + return result; } 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('企业微信活码链接未配置,将使用原有客服方式'); - } + if (!wxworkConfig || !wxworkConfig.enable) { + result.warnings.push('企业微信未启用'); + } + if (!wxworkConfig.contact_url) { + result.warnings.push('企业微信活码链接未配置'); } } @@ -177,16 +109,39 @@ class CustomerService { } /** - * 跳转到AI客服页面 + * 跳转到 AI 客服页面(Dify) */ - openAIKeFuService() { - const vm = this.vm; - vm.$util.redirectTo(vm.$util.AI_CHAT_PAGE_URL); + openDifyService() { + try { + // 清除未读数(如果存在) + if (typeof this.vm.setAiUnreadCount === 'function') { + this.vm.setAiUnreadCount(0); + } + + // ✅ 修正路径:必须与 pages.json 中注册的路径一致 + const aiChatUrl = '/pages_tool/ai-chat/index'; + + // ✅ 使用 navigateTo 保留返回栈(体验更好) + uni.navigateTo({ + url: aiChatUrl, + fail: (err) => { + console.error('跳转 AI 客服失败:', err); + // H5 兜底 + // #ifdef H5 + window.location.href = aiChatUrl; + // #endif + uni.showToast({ title: '打开客服失败', icon: 'none' }); + } + }); + } catch (e) { + console.error('跳转 AI 客服异常:', e); + uni.showToast({ title: '打开客服失败', icon: 'none' }); + } } /** - * 处理客服点击事件 - * @param {Object} options 选项参数 + * 处理客服点击事件(统一入口) + * @param {Object} options 选项参数(用于消息卡片等) */ handleCustomerClick(options = {}) { const validation = this.validateConfig(); @@ -201,51 +156,61 @@ class CustomerService { } const config = this.getPlatformConfig(); - const { niushop = {}, sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options || {}; + console.log('【当前客服配置】', config); + console.log('【客服类型】', config.type); + + const { niushop = {}, sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options; if (config.type === 'none') { this.showNoServicePopup(); return; } - // 核心分支:根据最新的type处理 + // 核心路由:根据 type 决定行为 switch (config.type) { case 'aikefu': - this.openAIKeFuService(); + console.log('【跳转 AI 客服】目标路径: /pages_tool/ai-chat/index'); + this.openDifyService(); break; case 'wxwork': + console.log('【跳转企业微信客服】'); this.openWxworkService(false, config, options); break; case 'third': + console.log('【跳转第三方客服】'); + this.openThirdService(config); + break; + case 'miniprogram': + console.log('【跳转第三方小程序客服】'); this.openThirdService(config); break; case 'niushop': + console.log('【跳转牛商客服】'); this.openNiushopService(niushop); break; case 'weapp': + console.log('【跳转微信官方客服】'); this.openWeappService(config, options); break; case 'aliapp': + console.log('【跳转支付宝客服】'); this.openAliappService(config); break; default: + console.error('【未知客服类型】', config.type); 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 || {}; + const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options; // #ifdef MP-WEIXIN - if (wxworkConfig?.enable && wxworkConfig?.contact_url && !useOriginalService) { + if (!useOriginalService && wxworkConfig?.enable && wxworkConfig?.contact_url) { wx.navigateToMiniProgram({ appId: 'wxeb490c6f9b154ef9', path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig.contact_url)}`, @@ -256,9 +221,16 @@ class CustomerService { } }); } else { + // 检查是否有企业微信配置 + if (!config.wxwork_url && !config.corpid) { + console.error('企业微信配置不完整,缺少 wxwork_url 或 corpid'); + uni.showToast({ title: '企业微信配置不完整', icon: 'none' }); + this.fallbackToPhoneCall(); + return; + } wx.openCustomerServiceChat({ - extInfo: { url: config.wxwork_url }, - corpId: config.corpid, + extInfo: { url: config.wxwork_url || '' }, + corpId: config.corpid || '', showMessageCard: true, sendMessageTitle, sendMessagePath, @@ -268,227 +240,181 @@ class CustomerService { // #endif // #ifdef H5 - if (wxworkConfig?.enable && wxworkConfig?.contact_url) { + if (!useOriginalService && wxworkConfig?.enable && wxworkConfig?.contact_url) { window.location.href = wxworkConfig.contact_url; } else if (config.wxwork_url) { - location.href = config.wxwork_url; + window.location.href = config.wxwork_url; } else { this.fallbackToPhoneCall(); } // #endif } - /** - * 打开第三方客服 - * @param {Object} config 客服配置 - */ openThirdService(config) { + console.log('【第三方客服配置】', config); + console.log('【配置字段】', Object.keys(config)); + + // 支持多种可能的字段名 + const miniAppId = config.mini_app_id || config.miniAppId || config.appid || config.appId || config.app_id; + const miniAppPath = config.mini_app_path || config.miniAppPath || config.path || config.page_path || ''; + + console.log('【解析后的小程序配置】AppID:', miniAppId, 'Path:', miniAppPath); + + // 优先处理第三方微信小程序客服 + if (miniAppId) { + console.log('【跳转第三方小程序】AppID:', miniAppId, 'Path:', miniAppPath); + // #ifdef MP-WEIXIN + wx.navigateToMiniProgram({ + appId: miniAppId, + path: miniAppPath, + success: () => { + console.log('【跳转第三方小程序成功】'); + }, + fail: (err) => { + console.error('【跳转第三方小程序失败】', err); + uni.showToast({ title: '跳转失败,请稍后重试', icon: 'none' }); + } + }); + // #endif + // #ifdef H5 + uni.showToast({ title: '第三方小程序客服仅在微信小程序中可用', icon: 'none' }); + // #endif + return; + } + + // 处理第三方链接客服 if (config.third_url) { + console.log('【跳转第三方链接】', config.third_url); + // #ifdef H5 window.location.href = config.third_url; + // #endif + // #ifdef MP-WEIXIN + uni.setClipboardData({ + data: config.third_url, + success: () => { + uni.showToast({ title: '链接已复制,请在浏览器打开', icon: 'none' }); + } + }); + // #endif } else { + console.error('【第三方客服配置不完整】缺少 mini_app_id 或 third_url'); this.fallbackToPhoneCall(); } } - /** - * 打开牛商客服 - * @param {Object} niushop 牛商参数 - */ openNiushopService(niushop) { - if (Object.keys(niushop).length > 0) { + if (Object.keys(niushop).length > 0 && this.vm.$util?.redirectTo) { 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('使用官方微信小程序客服'); + // 如果 useOfficial 为 true 或 undefined,则使用原生系统客服(由 button open-type="contact" 触发) + // 此方法仅用于自定义跳转(如 useOfficial: false) + if (config.useOfficial !== false) { + // 不做任何事,应由