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 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 连接成功,开始认证...'); // 第一步:发送 auth 认证 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.action === 'chat_response') { if (data.type === 'chunk') { const text = data.content || ''; content += text; if (onChunk) onChunk(text); } else if (data.type === 'end') { conversationId = data.conversation_id || ''; if (conversationId) { this.setConversationId(conversationId); } if (onComplete) { onComplete({ content, conversation_id: conversationId }); } resolve({ content, conversation_id: conversationId }); socketTask.close(); } } } 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 }, /** * 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 } };