From 909765878253ea62b0be98d046b9562bff001630 Mon Sep 17 00:00:00 2001 From: jinhhanhan <1683105490@qq.com> Date: Thu, 18 Dec 2025 15:20:00 +0800 Subject: [PATCH] =?UTF-8?q?chore=EF=BC=9A=E5=BE=AE=E4=BF=A1=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=81=87=E7=9A=84=E6=B5=81=E8=81=8A=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/js/ai-service.js | 563 ++++++++++++++++++---------------------- 1 file changed, 254 insertions(+), 309 deletions(-) diff --git a/common/js/ai-service.js b/common/js/ai-service.js index 7b47481..58353d1 100644 --- a/common/js/ai-service.js +++ b/common/js/ai-service.js @@ -1,10 +1,11 @@ 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) { @@ -13,16 +14,14 @@ try { } catch (e) { console.warn('读取会话ID失败:', e); } + export default { /** - * 发送消息到Dify API - * @param {string} message 用户消息内容 - * @param {Object} options 配置选项 - * @returns {Promise} + * 发送普通消息(blocking 模式) */ async sendMessage(message, options = {}) { try { - const aiConfig = store.getters.globalAIKefuConfig + const aiConfig = store.getters.globalAIKefuConfig; const params = { url: '/api/kefu/chat', data: { @@ -30,225 +29,90 @@ export default { stream: false, inputs: {}, query: message, - response_mode: options.stream ? 'streaming' : 'blocking', + response_mode: 'blocking', // 强制 blocking user: store.state.memberInfo?.id || 'anonymous', - conversation_id: this.getConversationId() || '' + conversation_id: this.getConversationId() || '' }, header: { 'Content-Type': 'application/json' } - } - console.log('Sending request to:', params.url); + }; + + 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, options); - if (result.conversationId) { - this.setConversationId(result.conversationId); - } - return result; - } catch (error) { - console.error('Dify API请求失败:', error) - throw new Error('AI服务暂时不可用,请稍后重试') - } - }, - + }); + + 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 { - return this.sendHttpStream(message, onChunk, onComplete) - } catch (error) { - console.error('流式消息发送失败:', error) - throw error - } - }, + const result = await this.sendMessage(message); + const content = result.content || ''; + const conversationId = result.conversationId || ''; - /** - * EventSource 流式聊天 - */ - chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) { - 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 = `/api/kefu/chat?${params.toString()}`; - try { - const eventSource = new EventSource(url); - window.currentEventSource = eventSource; - let aiMessage = ''; - eventSource.addEventListener('message', (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 - }); - } - if (data.conversation_id) { - conversationId = data.conversation_id; - } - } catch (error) { - console.error('解析消息失败:', error); - } - }); - eventSource.addEventListener('done', (event) => { - try { - const data = JSON.parse(event.data); - if (onComplete) onComplete(data); - } catch (error) { - console.error('解析完成事件失败:', error); - } - }); - eventSource.addEventListener('close', (event) => { - try { - const data = JSON.parse(event.data); - console.log('连接正常结束:', data); - } catch (error) { - console.error('解析关闭事件失败:', error); - } - window.currentEventSource = null; - }); - eventSource.addEventListener('error', (error) => { - console.error('EventSource错误:', error); - if (onError) onError({ error: 'EventSource连接错误' }); - window.currentEventSource = null; - }); - return eventSource; - } catch (error) { - console.error('创建EventSource失败:', error); - if (onError) onError({ error: error.message }); - return null; - } - }, + // 保存会话ID(确保连续对话) + if (conversationId) { + this.setConversationId(conversationId); + } - /** - * Fetch API 流式聊天 - */ - async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) { - try { - let requestData; - let headers = { - 'Accept': 'text/event-stream' - }; - requestData = { - uniacid: store.state.uniacid || '1', - user_id: store.state.memberInfo?.id || '123456', - query: message, - conversation_id: conversationId || '', - stream: true, - inputs: {}, - response_mode: 'streaming', - user: store.state.memberInfo?.id || '123456' - }; - headers['Content-Type'] = 'application/json'; - const response = await fetch('/api/kefu/chat', { - method: 'POST', - headers: headers, - body: JSON.stringify(requestData) - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - if (!response.body) { - throw new Error('响应体不可用'); - } - const reader = response.body.getReader(); - const decoder = new TextDecoder('utf-8'); - let buffer = ''; - let aiMessage = ''; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - let lineEnd; - while ((lineEnd = buffer.indexOf('\n')) !== -1) { - const line = buffer.substring(0, lineEnd); - buffer = buffer.substring(lineEnd + 1); - if (line.startsWith('data: ')) { - try { - const data = JSON.parse(line.substring(6)); - if (data.event === 'message' || data.event === 'text_message') { - const textContent = data.answer || data.text || ''; - aiMessage += textContent; - if (onMessage) onMessage(textContent); - } else if (data.event === 'message_end' || data.event === 'done') { - if (onComplete) onComplete({ - conversation_id: data.conversation_id, - message: aiMessage, - ...data - }); - } else if (data.event === 'error' && onError) { - onError(data); - } - if (data.conversation_id) { - conversationId = data.conversation_id; - } - } catch (e) { - console.warn('解析流式数据失败:', e); + // 模拟打字效果 + 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 }); } - } - } - if (buffer.startsWith('data: ')) { - try { - const data = JSON.parse(buffer.substring(6)); - if ((data.event === 'message' || data.event === 'text_message') && onMessage) { - const textContent = data.answer || data.text || ''; - onMessage(textContent); - } else if ((data.event === 'message_end' || data.event === 'done') && onComplete) { - onComplete({ - conversation_id: data.conversation_id, - message: aiMessage, - ...data - }); - } - } catch (e) { - console.warn('解析剩余数据失败:', e); - } - } + }, 80); // 打字速度:80ms/次 + }); } catch (error) { - console.error('Fetch流式聊天请求失败:', error); - if (onError) onError({ error: error.message }); - } - }, - - /** - * HTTP流式请求(修复conversationId未定义) - */ - async sendHttpStream(message, onChunk, onComplete) { - const params = { - url: '/api/kefu/chat', - data: { - query: message, - conversation_id: '', - user_id: store.state.memberInfo?.id || 'anonymous', - stream: true, - uniacid: store.state.uniacid, - inputs: {}, - response_mode: 'streaming', - user: store.state.memberInfo?.id || 'anonymous' - }, - header: { - 'Content-Type': 'application/json' + 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`; @@ -263,105 +127,133 @@ export default { inputs: {}, query: message, response_mode: 'streaming', - user_id: store.state.memberInfo?.id || 'anonymous' + user_id: store.state.memberInfo?.id || 'anonymous', + conversation_id: this.getConversationId() || '' }) - }) - const reader = response.body.getReader() - const decoder = new TextDecoder() - let content = '' - let buffer = '' - // 修复:声明conversationId变量 - let conversationId = ''; - - function processStreamData(buffer, callback) { - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - lines.forEach(line => { - line = line.trim(); - if (!line) return; - if (line.startsWith('data:')) { - const dataPart = line.slice(5).trim(); - if (dataPart) { + }); + + 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(dataPart); + const data = JSON.parse(jsonStr); if (data.event === 'message') { - callback(data.answer || ''); + const text = data.answer || data.text || ''; + content += text; + callback(text); } if (data.conversation_id) { conversationId = data.conversation_id; } if (data.event === 'message_end') { - console.log('对话完成'); + // 可选:提前完成 } - } catch (error) { - console.error('解析流式数据失败:', error); + } catch (e) { + console.warn('解析流数据失败:', e); } } } - }); - return buffer; + } + return buf; } - + while (true) { - const { done, value } = await reader.read() - if (done) break + const { done, value } = await reader.read(); + if (done) break; buffer += decoder.decode(value, { stream: true }); - buffer = processStreamData(buffer, (newData) => { - if (newData) { - content += newData; - if (onChunk) onChunk(newData); - } + buffer = processBuffer(buffer, (chunk) => { + if (onChunk) onChunk(chunk); }); } - - if (onComplete) onComplete({ content, conversation_id: conversationId }); - return { content, conversation_id: conversationId }; + + if (onComplete) { + onComplete({ + content, + conversation_id: conversationId + }); + } + return { content, conversation_id: conversationId }; } catch (error) { - console.error('HTTP流式请求失败:', error) - throw error + console.error('H5 流式请求失败:', error); + throw error; } // #endif - // #ifndef H5 - const response = await http.sendRequest({ - ...params, - async: false - }) - if (response.success && response.data) { - const content = response.data - const chunkSize = 3 - let index = 0 - const streamInterval = setInterval(() => { - if (index < content.length) { - const chunk = content.substring(index, index + chunkSize) - index += chunkSize - if (onChunk) onChunk(chunk) - } else { - clearInterval(streamInterval) - if (onComplete) onComplete(content) - } - }, 100) - } + + // #ifdef MP-WEIXIN + // 理论上不会执行到这里,但防止 fallback + return this.sendStreamMessage(message, onChunk, onComplete); // #endif }, - + /** - * 处理API响应 + * 处理API响应(统一格式) */ - handleResponse(response, options) { + handleResponse(response) { if (response.code === 0 || response.success) { return { success: true, - content: response.data?.answer || response.data?.content || response.data?.reply|| response.data, + 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服务返回错误') + throw new Error(response.message || 'AI服务返回错误'); } }, - + /** - * 生成会话ID + * 获取当前会话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 = { @@ -374,16 +266,16 @@ export default { header: { 'Content-Type': 'application/json' } - } + }; const response = await http.sendRequest({ ...params, async: false - }) - return this.handleResponse(response) + }); + return this.handleResponse(response); }, - + /** - * 获取AI服务状态 + * 获取服务状态 */ async getServiceStatus() { try { @@ -391,22 +283,23 @@ export default { 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; + }); + 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 { @@ -414,36 +307,13 @@ export default { url: '/api/kefu/clear-conversation', data: { conversation_id: conversationId }, async: false - }) - return response.success + }); + return response.success; } catch (error) { - console.error('清除会话失败:', error) - return false + console.error('清除会话失败:', error); + return false; } }, - getConversationId() { - return currentConversationId; - }, - - setConversationId(id) { - if (id && id !== currentConversationId) { - currentConversationId = id; - try { - uni.setStorageSync(CONVERSATION_KEY, id); - } catch (e) { - console.error('保存会话ID失败:', e); - } - } - }, - - clearConversationId() { - currentConversationId = null; - try { - uni.removeStorageSync(CONVERSATION_KEY); - } catch (e) { - console.error('清除会话ID失败:', e); - } - }, /** * 获取会话历史 @@ -485,5 +355,80 @@ export default { 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 +}; \ No newline at end of file