import Config from './config.js' import http from './http.js' import store from '@/store/index.js' import lang from './lang.js' let currentConversationId = null; export const CONVERSATION_KEY = 'ai_conversation_id'; /** * 获取域名配置参数(避免重复代码) */ function getDomainParams() { return { imgDomain: Config.imgDomain, h5Domain: Config.h5Domain, baseUrl: Config.baseUrl }; } // 初始化时从本地读取会话ID try { const saved = uni.getStorageSync(CONVERSATION_KEY); if (saved) { currentConversationId = saved; } } catch (e) { console.warn('读取会话ID失败:', e); } const GUEST_ID_KEY = 'ai_guest_user_id'; /** * 获取用户ID:已登录返回 memberId,未登录生成访客唯一标识并持久化 */ function getUserId() { const memberId = store.state.memberInfo?.id; if (memberId) return memberId; let guestId; try { guestId = uni.getStorageSync(GUEST_ID_KEY); } catch (e) { /* ignore */ } if (!guestId) { guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10); try { uni.setStorageSync(GUEST_ID_KEY, guestId); } catch (e) { /* ignore */ } } return guestId; } // #ifdef MP-WEIXIN // ================================ // WebSocket 长连接管理(微信小程序) // ================================ let wsSocketTask = null; let wsAuthenticated = false; let wsConnected = false; let wsConnectionPromise = null; function getWsDomain() { var match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:?#]+(?::\d+)?)/); return match ? match[1] : ''; } function resetWsState() { wsSocketTask = null; wsAuthenticated = false; wsConnected = false; wsConnectionPromise = null; } /** * 确保 WebSocket 已连接并认证,返回一个 Promise * 连接建立后保持不断开,后续可复用 */ function ensureWebSocketConnected() { if (wsConnected && wsAuthenticated) { return Promise.resolve(); } if (wsConnectionPromise) { return wsConnectionPromise; } wsConnectionPromise = new Promise(function(resolve, reject) { var domain = getWsDomain(); if (!domain) { wsConnectionPromise = null; reject(new Error('WebSocket 域名配置无效')); return; } var wssUrl = 'wss://' + domain + '/ws/aikefu'; wsConnected = false; wsAuthenticated = false; wsSocketTask = wx.connectSocket({ url: wssUrl, header: {} }); // 获取认证参数(独立函数,不使用 this) var _uniacid = uni.getStorageSync('uniacid') || Config.uniacid; var _token = store.state.token || 'test_token'; var _userId = getUserId(); wsSocketTask.onOpen(function() { wsConnected = true; wsSocketTask.send({ data: JSON.stringify({ action: 'auth', uniacid: _uniacid, token: _token, user_id: _userId }) }); }); wsSocketTask.onMessage(function(res) { try { var data = JSON.parse(res.data); // Auth 响应 if (data.type === 'auth_success') { wsAuthenticated = true; resolve(); return; } if (data.type === 'auth_failed') { wsAuthenticated = false; wsConnectionPromise = null; reject(new Error('WebSocket 认证失败')); return; } // 以下为聊天消息响应,只在有活跃消息时处理 if (!wsMessageActive) return; if (data.event === 'message') { var text = data.answer || data.content || data.text || ''; // 兼容嵌套 data.data.content 格式 if (!text && data.data) { text = data.data.content || data.data.answer || ''; } wsMessageContent += text; if (wsMessageOnChunk) wsMessageOnChunk(text); } else if (data.event === 'message_end' || data.event === 'done') { // 兼容两种结束信号:message_end(流结束)或 done(服务端完成) // 兼容两种数据层级:conversation_id 可能在顶层或 data.data 中 var conversationId = data.conversation_id || (data.data && data.data.conversation_id) || ''; console.log('[AI WS] 流结束信号, event:', data.event, 'type:', data.type, 'conversationId:', conversationId, '当前内存:', currentConversationId); // 保存会话ID(独立函数,不使用 this) if (conversationId) { if (conversationId !== currentConversationId) { currentConversationId = conversationId; try { uni.setStorageSync(CONVERSATION_KEY, conversationId); console.log('[AI WS] 会话ID已持久化到 storage:', conversationId); } catch (e) { console.error('保存会话ID失败:', e); } } } var content = wsMessageContent; var convId = conversationId; // 重置消息状态,但不断开连接 wsMessageActive = false; wsMessageContent = ''; if (wsMessageOnComplete) wsMessageOnComplete({ content: content, conversation_id: convId }); if (wsMessageResolve) wsMessageResolve({ content: content, conversation_id: convId }); } else { // 兜底日志:打印所有未识别的消息类型 console.log('[AI WS] 未识别的消息:', JSON.stringify(data).substring(0, 200)); } } catch (e) { console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data); } }); wsSocketTask.onError(function(err) { console.error('WebSocket 连接失败:', err); var prevReject = wsMessageReject; resetWsState(); resetWsMessageState(); if (prevReject) prevReject(new Error('WebSocket 连接失败')); reject(new Error('WebSocket 连接失败')); }); wsSocketTask.onClose(function() { console.warn('WebSocket 连接已关闭'); var prevReject = wsMessageReject; resetWsState(); resetWsMessageState(); if (prevReject) prevReject(new Error('WebSocket 连接意外关闭')); }); }); return wsConnectionPromise; } // 当前消息的回调状态(只支持串行) var wsMessageResolve = null; var wsMessageReject = null; var wsMessageOnChunk = null; var wsMessageOnComplete = null; var wsMessageContent = ''; var wsMessageActive = false; function resetWsMessageState() { wsMessageResolve = null; wsMessageReject = null; wsMessageOnChunk = null; wsMessageOnComplete = null; wsMessageContent = ''; wsMessageActive = false; } /** * 断开 WebSocket 长连接 */ function disconnectWebSocket() { if (wsSocketTask) { try { wsSocketTask.close(); } catch (e) { /* ignore */ } } resetWsState(); resetWsMessageState(); } // #endif export default { /** * 获取uniacid * @returns {number} uniacid */ getUniacid() { return uni.getStorageSync('uniacid') || Config.uniacid; // 从缓存中获取uniacid,或使用默认uniacid }, /** * 发送普通消息(blocking 模式) */ async sendMessage(message, options = {}) { try { const params = { url: '/api/kefu/chat', data: { user_id: getUserId(), stream: false, inputs: {}, query: message, response_mode: 'blocking', // 强制 blocking user: getUserId(), conversation_id: this.getConversationId() || '', ...getDomainParams() }, header: { 'Content-Type': 'application/json' } }; 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('[AI Service] AiChat API请求失败,详细错误:', error); console.error('[AI Service] 错误堆栈:', error.stack); // 打印更多信息帮助调试 console.error('[AI Service] store 状态:', { memberInfo: store.state.memberInfo, uniacid: this.getUniacid(), token: store.state.token }); throw new Error('AI服务暂时不可用,请稍后重试'); } }, /** * 流式消息入口(自动适配平台) * 微信小程序:复用长连接 WebSocket */ async sendStreamMessage(message, onChunk, onComplete) { // #ifdef MP-WEIXIN // 先确保连接已就绪 try { await ensureWebSocketConnected(); } catch (e) { if (onComplete) onComplete({ error: e.message }); throw e; } // 设置当前消息回调 wsMessageContent = ''; wsMessageActive = true; wsMessageOnChunk = onChunk || null; wsMessageOnComplete = onComplete || null; return new Promise((resolve, reject) => { wsMessageResolve = resolve; wsMessageReject = reject; wsSocketTask.send({ data: JSON.stringify({ action: 'chat', uniacid: this.getUniacid(), query: message, user_id: getUserId(), conversation_id: this.getConversationId() || '', ...getDomainParams() }) }); }); // #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: this.getUniacid(), token: store.state.token || 'test_token', inputs: {}, query: message, response_mode: 'streaming', user_id: getUserId(), conversation_id: this.getConversationId() || '', ...getDomainParams() }) }); if (!response.ok || !response.body) { throw new Error('无效响应'); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let content = ''; let conversationId = ''; 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:')) { try { const jsonStr = trimmed.slice(5).trim(); if (jsonStr && jsonStr !== '[DONE]') { const data = JSON.parse(jsonStr); if (data.event === 'message') { var text = data.answer || data.text || ''; // 兼容嵌套 data.data 格式 if (!text && data.data) { text = data.data.content || data.data.answer || ''; } content += text; if (onChunk) onChunk(text); } // 兼容两种层级:conversation_id 可能在顶层或 data.data 中 var cid = data.conversation_id || (data.data && data.data.conversation_id) || ''; if (cid) { conversationId = cid; } if (data.event === 'message_end' || data.event === 'done') { console.log('[AI SSE] 收到完成信号:', data.event, 'conversationId:', cid); } } } catch (e) { console.warn('解析失败:', e, line); } } } } // 处理最后残留的 buffer(如果有) if (buffer.trim().startsWith('data:')) { try { const jsonStr = buffer.trim().slice(5); if (jsonStr) { const data = JSON.parse(jsonStr); if (data.event === 'message') { var text = data.answer || data.text || ''; if (!text && data.data) { text = data.data.content || data.data.answer || ''; } content += text; if (onChunk) onChunk(text); } var cid = data.conversation_id || (data.data && data.data.conversation_id) || ''; if (cid) { conversationId = cid; } } } catch (e) { console.warn('最后 buffer 解析失败:', e); } } console.log('[AI SSE] 流完成, conversationId:', conversationId, 'content长度:', content.length); if (onComplete) { onComplete({ content, conversation_id: conversationId }); } return { content, conversation_id: conversationId }; } catch (error) { console.error('H5 流式请求失败:', error); throw error; } // #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: this.getUniacid(), token: store.state.token || 'test_token', user_id: getUserId(), member_id: getUserId(), ...getDomainParams() }, 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 getServiceConfig() { try { const params = { url: '/api/kefu/info', data: { uniacid: this.getUniacid(), token: store.state.token || 'test_token', user_id: getUserId(), member_id: getUserId(), locale: lang.getCurrentLocale() }, header: { 'Content-Type': 'application/json' } }; const response = await http.sendRequest({ ...params, async: false, }); console.log('[AI Service] getServiceConfig response:', JSON.stringify(response)); return response.data; } catch (error) { console.error('[AI Service] getServiceConfig error:', error); return null; } }, /** * 清除会话历史(调用后端接口) */ async clearConversation(conversationId) { try { const response = await http.sendRequest({ url: '/api/kefu/clear-conversation', data: { conversation_id: conversationId, ...getDomainParams() }, 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)是必填参数'); } console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit); const requestData = { uniacid: params.uniacid || this.getUniacid(), conversation_id: params.conversation_id, user_id: params.user_id || getUserId(), limit: params.limit || 20, offset: params.offset || 0, member_id: params.member_id || getUserId(), token: params.token || '', ...getDomainParams() }; 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 paramsObj = { uniacid: this.getUniacid(), user_id: getUserId(), member_id: getUserId(), token: store.state.token || 'test_token', query: message, conversation_id: conversationId || '', stream: 'true', ...getDomainParams() }; const params = new URLSearchParams(paramsObj); 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 }, // #ifdef MP-WEIXIN /** * [微信小程序] 断开 WebSocket 长连接(页面离开时调用) */ disconnectWebSocket() { disconnectWebSocket(); }, // #endif // #ifdef H5 disconnectWebSocket() { // H5 无 WebSocket 长连接,无需操作 }, // #endif };