import Config from './config.js' import http from './http.js' import store from '@/store/index.js' import lang from './lang.js' let currentConversationId = null; // ====== AI Agent Generalization (customer service is the first agent) ====== // setAgentId updates currentAgentId; default is agent_aikefu. // Any sendMessage / sendStreamMessage / getServiceConfig call passes agent_id automatically. // // PR-7: 按 agent_id 路由 endpoint // - agent_aikefu (默认智能客服) -> 走 aikefu addon (/api/kefu/* + /ws/aikefu) // - 其他 agent (knowledge / quick_reply / sales / hr) -> 走 ai-agent addon (/api/aiagent/* + /ws/aiagent) // - 前端不关心后端两个 addon 之间的差异,只关心 agent_id 路由 // 注意: 媒体上传端点按 addon 名注册 (aikefu = /api/aikefu/upload/*), 与普通接口 // /api/kefu/* 不同名, 该差异由 common/js/media-upload.js 内部的 uploadPath() 处理。 export const DEFAULT_AGENT_ID = 'agent_aikefu'; let currentAgentId = DEFAULT_AGENT_ID; // 多端/多 agent 会话隔离: 不同 agent 的 conversation_id 单独存 export const CONVERSATION_KEY_PREFIX = 'ai_conversation_id_'; export function getConversationKey(agentId) { return CONVERSATION_KEY_PREFIX + (agentId || DEFAULT_AGENT_ID); } // 兼容旧代码: 单 key 别名(指向默认 agent) export const CONVERSATION_KEY = getConversationKey(DEFAULT_AGENT_ID); // 统一 SSE/WS 事件分发器 (透出所有事件类型) let eventHandlers = { onEvent: null }; function dispatchEvent(onEvent, event, data) { if (typeof onEvent === 'function') { try { onEvent(event, data); } catch (e) { console.error('[AI Service] onEvent handler threw:', e); } } } /** * PR-7: agent_id -> 后端 addon 路由映射 * * 规则: * - agent_aikefu (默认智能客服) -> aikefu addon (老路径 /api/kefu/*) * - 其他 agent -> aiagent addon (新路径 /api/aiagent/*) * * 后端两个 addon 完全独立: * - aikefu: lucky_aikefu_config / conversation / message * - aiagent: lucky_ai_agent_config / conversation / message */ function resolveAddonName(agentId) { if (!agentId || agentId === 'agent_aikefu') return 'aikefu'; return 'aiagent'; } /** * PR-7: agent_id -> API endpoint URL * agent_aikefu -> '/api/kefu/...' (兼容老路径) * 其他 -> '/api/aiagent/...' (ai-agent 新路径) */ function resolveApiUrl(apiName) { var addon = resolveAddonName(currentAgentId); if (addon === 'aikefu') { return '/api/kefu/' + apiName; } return '/api/aiagent/' + apiName; } /** * PR-7: WebSocket URL 路径(/ws/{addonName}) */ function resolveWsPath() { return '/ws/' + resolveAddonName(currentAgentId); } /** * 获取域名配置参数(避免重复代码) */ function getDomainParams() { return { imgDomain: Config.imgDomain, h5Domain: Config.h5Domain, baseUrl: Config.baseUrl }; } // 初始化时从本地读取会话ID(按当前 agent 隔离) try { const saved = uni.getStorageSync(getConversationKey(currentAgentId)); 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; } /** * 获取当前 AI Agent。 */ function getAgentId() { return currentAgentId || DEFAULT_AGENT_ID; } /** * 设置当前 AI Agent。 */ function setAgentId(agentId) { if (agentId && typeof agentId === 'string') currentAgentId = agentId; } // #ifdef MP // ================================ // 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(lang.lang('aiService.wsDomainInvalid'))); return; } // PR-7: WebSocket 路径按当前 agent_id 路由 — agent_aikefu 走 /ws/aikefu,其他走 /ws/aiagent var wsPath = resolveWsPath(); var wssUrl = 'wss://' + domain + wsPath; 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', agent_id: getAgentId(), 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(lang.lang('aiService.wsAuthFailed'))); return; } // 以下为聊天消息响应,只在有活跃消息时处理 if (!wsMessageActive) return; // 优先走 onEvent 事件分发(透出 thought/tool_call/question 等) var onEvt = eventHandlers.onEvent; switch (data.event) { case 'message': { var text = data.answer || data.content || data.text || ''; // 兼容嵌套 data.data.content 格式 if (!text && data.data) { text = data.data.content || data.data.answer || ''; } // B 路 Dify 把思考内容混在 answer 里, 以 '> ' 前缀标识 if (text && text.startsWith('> ')) { dispatchEvent(onEvt, 'thought', { content: text, message_id: data.message_id }); break; } wsMessageContent += text; if (wsMessageOnChunk) wsMessageOnChunk(text); dispatchEvent(onEvt, 'message', { content: text, message_id: data.message_id }); break; } case 'thought': case 'thinking': case 'reasoning': { // 兼容字段: thought / thinking / reasoning / content / answer var thoughtText = data.thought || data.thinking || data.reasoning || data.content || data.answer || ''; console.log('[AI WS] thought event:', data.event, 'length:', thoughtText.length, 'msgId:', data.message_id); dispatchEvent(onEvt, 'thought', { content: thoughtText, message_id: data.message_id }); break; } case 'tool_call': dispatchEvent(onEvt, 'tool_call', data.tool_call || {}); break; case 'question': dispatchEvent(onEvt, 'question', { pending_question: data.pending_question || (data.data && data.data.pending_question), message_id: data.message_id, conversation_id: data.conversation_id }); break; case 'message_end': case 'done': { // 兼容两种结束信号:message_end(流结束)或 done(服务端完成) 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(按当前 agent 隔离) if (conversationId) { if (conversationId !== currentConversationId) { currentConversationId = conversationId; try { uni.setStorageSync(getConversationKey(currentAgentId), conversationId); console.log('[AI WS] 会话ID已持久化到 storage:', conversationId); } catch (e) { console.error('保存会话ID失败:', e); } } } var content = wsMessageContent; var convId = conversationId; // 重置消息状态,但不断开连接 var endPayload = { conversation_id: convId, message_id: data.message_id, content: data.content || content, thoughts: data.thoughts || '', tool_calls: data.tool_calls || [], tokens: data.tokens || null, phase: data.phase || 'completed', stop_reason: data.stop_reason || '', effective_model: data.effective_model || '' }; wsMessageActive = false; wsMessageContent = ''; dispatchEvent(onEvt, 'message_end', endPayload); if (wsMessageOnComplete) wsMessageOnComplete(endPayload); if (wsMessageResolve) wsMessageResolve(endPayload); break; } case 'error': dispatchEvent(onEvt, 'error', { code: data.code || -1, message: data.message || 'unknown' }); break; case 'ping': // keep-alive, 忽略 break; default: // 兜底: 老格式 data.data.content if (data.data && data.data.content) { var defText = data.data.content; wsMessageContent += defText; if (wsMessageOnChunk) wsMessageOnChunk(defText); dispatchEvent(onEvt, 'message', { content: defText }); } 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(lang.lang('aiService.wsConnectionFailed'))); reject(new Error(lang.lang('aiService.wsConnectionFailed'))); }); wsSocketTask.onClose(function() { console.warn('WebSocket 连接已关闭'); var prevReject = wsMessageReject; resetWsState(); resetWsMessageState(); if (prevReject) prevReject(new Error(lang.lang('aiService.wsConnectionClosed'))); }); }); 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 }, /** * PR-7: 获取当前 agent 对应的 addon 名称 (aikefu / aiagent) */ getAddonName() { return resolveAddonName(currentAgentId); }, /** * PR-7: 获取当前 agent 对应的 API endpoint 前缀 */ getApiPrefix() { return resolveAddonName(currentAgentId) === 'aikefu' ? '/api/kefu' : '/api/aiagent'; }, /** * 发送普通消息(blocking 模式) * PR-7: URL 按 agent_id 路由 */ async sendMessage(message, options = {}) { try { const params = { url: resolveApiUrl('chat'), data: { agent_id: getAgentId(), user_id: getUserId(), stream: false, inputs: {}, query: message, response_mode: 'blocking', user: getUserId(), conversation_id: this.getConversationId() || '', attachments: (options && options.attachments) || [], ...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(lang.lang('aiService.serviceUnavailable')); } }, /** * 流式消息入口(自动适配平台) * 微信小程序:复用长连接 WebSocket */ async sendStreamMessage(message, onEvent, onComplete, opts = {}) { // #ifdef MP try { await ensureWebSocketConnected(); } catch (e) { // WebSocket 不可用(真机常见: socket 合法域名未配置 / 后端 WS 未部署) // 自动降级为 HTTP 阻塞式请求, 复用与 H5 相同的后端接口 console.warn('[AI Service] MP WebSocket 不可用,降级为 HTTP blocking 请求:', e && e.message); return this._fallbackToHttp(message, onEvent, onComplete, opts); } wsMessageContent = ''; wsMessageActive = true; eventHandlers.onEvent = (typeof onEvent === 'function') ? onEvent : null; // onEvent 模式: 走事件分发; 否则兼容老的 onChunk/onComplete wsMessageOnChunk = (typeof onEvent === 'function') ? null : (onEvent || null); wsMessageOnComplete = (typeof onEvent === 'function') ? null : (onComplete || null); return new Promise((resolve, reject) => { wsMessageResolve = resolve; wsMessageReject = reject; try { wsSocketTask.send({ data: JSON.stringify({ action: 'chat', agent_id: getAgentId(), uniacid: this.getUniacid(), query: message, user_id: getUserId(), conversation_id: this.getConversationId() || '', attachments: opts.attachments || [], ...getDomainParams() }) }); } catch (sendErr) { console.warn('[AI Service] MP WebSocket send 失败,降级为 HTTP blocking 请求:', sendErr && sendErr.message); // 用降级结果决定该 promise 的最终状态, 避免同时弹出错误气泡 this._fallbackToHttp(message, onEvent, onComplete, opts) .then((res) => resolve(res)) .catch((err) => reject(err)); } }); // #endif }, /** * [MP 降级] WebSocket 不可用时, 改用 HTTP 阻塞式请求完成一次完整回复 * 只要「request 合法域名」已配置即可工作, 不依赖 socket 合法域名 */ async _fallbackToHttp(message, onEvent, onComplete, opts = {}) { try { const result = await this.sendMessage(message, { attachments: (opts && opts.attachments) || [] }); const content = result && result.content ? result.content : ''; // 阻塞式降级也尝试把后端返回的思考过程/用量透出来, 让小程序在 WS 不可用时仍能显示"思考过程 (N 步)" const fallbackPayload = { content, conversation_id: result.conversationId, thoughts: result.thoughts || '', tokens: result.tokens || null }; if (content || result.thoughts) { if (typeof onEvent === 'function') { if (result.thoughts) onEvent('thought', { content: result.thoughts }); onEvent('message', { content }); onEvent('message_end', fallbackPayload); } else { if (onComplete) onComplete(fallbackPayload); } return fallbackPayload; } const emptyErr = new Error(lang.lang('aiService.serviceUnavailable')); if (onComplete) onComplete({ error: emptyErr.message }); throw emptyErr; } catch (err) { console.error('[AI Service] MP HTTP 降级请求也失败:', err); if (onComplete) onComplete({ error: (err && err.message) || lang.lang('aiService.serviceUnavailable') }); throw err; } }, /** * HTTP 流式请求(仅 H5 使用) * PR-7: URL 按 agent_id 路由 */ async sendHttpStream(message, onEvent, onComplete, opts = {}) { // #ifdef H5 try { var chatUrl = resolveApiUrl('chat'); var url = Config.baseUrl + chatUrl; 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', agent_id: getAgentId(), user_id: getUserId(), conversation_id: this.getConversationId() || '', attachments: (opts && opts.attachments) || [], ...getDomainParams() }) }); if (!response.ok || !response.body) { throw new Error(lang.lang('aiService.invalidResponse')); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let content = ''; let conversationId = ''; const fire = (evt, payload) => { if (typeof onEvent === 'function') onEvent(evt, payload); }; 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:')) continue; const jsonStr = trimmed.slice(5).trim(); if (!jsonStr || jsonStr === '[DONE]') continue; try { const data = JSON.parse(jsonStr); switch (data.event) { case 'message': { var text = data.answer || data.text || data.content || ''; if (!text && data.data) text = data.data.content || data.data.answer || ''; // B 路 Dify 把思考内容混在 answer 里, 以 '> ' 前缀标识 if (text && text.startsWith('> ')) { fire('thought', { content: text, message_id: data.message_id }); break; } content += text; fire('message', { content: text, message_id: data.message_id }); break; } case 'thought': case 'thinking': case 'reasoning': { // 兼容字段: thought / thinking / reasoning / content / answer var thoughtText2 = data.thought || data.thinking || data.reasoning || data.content || data.answer || ''; fire('thought', { content: thoughtText2, message_id: data.message_id }); break; } case 'tool_call': fire('tool_call', data.tool_call || {}); break; case 'question': fire('question', { pending_question: data.pending_question || (data.data && data.data.pending_question), message_id: data.message_id, conversation_id: data.conversation_id }); break; case 'message_end': case 'done': { const cid = data.conversation_id || (data.data && data.data.conversation_id) || ''; if (cid) conversationId = cid; console.log('[AI SSE] 收到完成信号:', data.event, 'conversationId:', cid); fire('message_end', { conversation_id: cid, message_id: data.message_id, content: data.content || content, thoughts: data.thoughts || '', tool_calls: data.tool_calls || [], tokens: data.tokens || null, phase: data.phase || 'completed', stop_reason: data.stop_reason || '', effective_model: data.effective_model || '' }); break; } case 'error': fire('error', { code: data.code || -1, message: data.message || 'unknown' }); break; default: // B 路 Dify 把思考塞 answer 里 (> 前缀) if (data.event === 'message' && data.answer && data.answer.startsWith('> ')) { fire('thought', { content: data.answer }); } else if (data.event === 'ping') { // keep-alive } else { console.log('[AI SSE] 未识别事件:', data.event, JSON.stringify(data).substring(0, 200)); } } } catch (e) { console.warn('解析失败:', e, line); } } } 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, thoughts: response.data?.thoughts || '', tokens: response.data?.tokens || null }; } else { throw new Error(response.message || lang.lang('aiService.serviceResponseError')); } }, /** * 获取当前会话ID */ getConversationId() { return currentConversationId; }, /** * 设置并持久化会话ID */ setConversationId(id) { if (id && id !== currentConversationId) { currentConversationId = id; try { uni.setStorageSync(getConversationKey(currentAgentId), id); } catch (e) { console.error('保存会话ID失败:', e); } } }, /** * 清除会话ID */ clearConversationId() { currentConversationId = null; try { uni.removeStorageSync(getConversationKey(currentAgentId)); } catch (e) { console.error('清除会话ID失败:', e); } }, /** * 生成新会话(如需) * PR-7: URL 按 agent_id 路由 */ async generateConversationId() { const params = { url: resolveApiUrl('createConversation'), data: { agent_id: getAgentId(), 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); }, /** * 获取服务状态 * PR-7: URL 按 agent_id 路由 */ async getServiceStatus() { try { const response = await http.sendRequest({ url: resolveApiUrl('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 ? lang.lang('aiService.statusNormal') : lang.lang('aiService.statusAbnormal') }; } catch (error) { return { available: false, reason: lang.lang('aiService.statusCheckFailed') }; } }, /** * 获取服务配置信息 * PR-7: URL 按 agent_id 路由 */ async getServiceConfig() { try { const params = { url: resolveApiUrl('info'), data: { agent_id: getAgentId(), 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)); // 媒体上传需要 config_uuid (后端校验 config_uid/config_uuid 不能为空)。 // 此前从未写入过该缓存, 导致上传表单里 config_uuid 恒为空。 // 这里从 info 响应中提取并持久化, 兼容多种字段名。 const cfg = response && response.data; if (cfg && typeof cfg === 'object') { const uuid = cfg.config_uuid || cfg.config_uid || cfg.uuid || (cfg.info && (cfg.info.config_uuid || cfg.info.uuid)) || ''; if (uuid) uni.setStorageSync('current_config_uuid', uuid); } return response.data; } catch (error) { console.error('[AI Service] getServiceConfig error:', error); return null; } }, /** * 清除会话历史(调用后端接口) * PR-7: URL 按 agent_id 路由 */ async clearConversation(conversationId) { try { const response = await http.sendRequest({ url: resolveApiUrl('clear-conversation'), data: { agent_id: getAgentId(), conversation_id: conversationId, ...getDomainParams() }, async: false }); return response.success; } catch (error) { console.error('清除会话失败:', error); return false; } }, /** * 向 WorkBuddy Agent 回传 AskUserQuestion 的用户选项 * @param {Object} params { conversation_id, message_id, tool_call_id, rpc_id, answers } * @returns {Promise} */ async elicitResponse({ conversation_id, message_id, tool_call_id, rpc_id, answers }) { // #ifdef MP-WEIXIN || APP-PLUS || MP-HARMONY try { await ensureWebSocketConnected(); } catch (e) { console.warn('[ai-service] elicitResponse WS 不可用:', e && e.message); return false; } wsSocketTask.send({ data: JSON.stringify({ action: 'elicit_response', conversation_id, message_id, tool_call_id, rpc_id, answers, uniacid: this.getUniacid(), token: store.state.token || 'test_token' }) }); return true; // #endif // #ifdef H5 // H5 无原生 WS 长连接, HTTP SSE 也无 elicit_response 端点 // 兜底: 仅展示用户选项, 30s 超时由后端 pending_stale 兜底 console.warn('[ai-service] H5 elicitResponse 无可用通道, 仅展示用户选项 + 超时由 pending_stale 兜底'); return false; // #endif }, /** * 获取会话历史 * PR-7: URL 按 agent_id 路由 */ async getConversationHistory(params = {}) { try { if (!params.conversation_id) { throw new Error(lang.lang('aiService.conversationIdRequired')); } console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit); const requestData = { agent_id: params.agent_id || getAgentId(), 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: resolveApiUrl('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 || lang.lang('aiService.getHistoryFailed')); } } catch (error) { console.error('获取会话历史失败:', error); throw new Error(error.message || lang.lang('aiService.getHistoryError')); } }, // ================================ // 以下方法仅用于 H5,小程序无效 // ================================ /** * [H5 Only] EventSource 流式聊天(备用) * PR-7: URL 按 agent_id 路由 */ chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) { // #ifdef H5 if (window.currentEventSource) { window.currentEventSource.close(); } const paramsObj = { agent_id: getAgentId(), 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); var chatUrl = resolveApiUrl('chat'); const url = `${Config.baseUrl}${chatUrl}?${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: lang.lang('aiService.connectionFailed') }); window.currentEventSource = null; }; return eventSource; } catch (error) { console.error('创建EventSource失败:', error); if (onError) onError({ error: error.message }); return null; } // #endif // #ifdef MP console.warn('chatWithEventSource 不支持微信小程序'); return null; // #endif }, /** * [H5 Only] Fetch 流式聊天(备用) */ async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) { // #ifdef H5 return this.sendHttpStream(message, onMessage, (res) => { onComplete?.({ message: res.content, conversation_id: res.conversation_id }); }); // #endif // #ifdef MP console.warn('chatWithFetchStream 不支持微信小程序'); return null; // #endif }, // #ifdef MP /** * [微信小程序] 断开 WebSocket 长连接(页面离开时调用) */ disconnectWebSocket() { disconnectWebSocket(); }, // #endif // #ifdef H5 disconnectWebSocket() { // H5 无 WebSocket 长连接,无需操作 }, // #endif /** * Set current AI Agent (default aikefu; link system passes agent_id). * PR-7: 切换 agent 时, 如果跨 addon 则断开 WebSocket 重连 (路径变了) * @param {string} agentId */ setAgentId(agentId) { var oldAddon = resolveAddonName(currentAgentId); setAgentId(agentId); var newAddon = resolveAddonName(currentAgentId); if (oldAddon !== newAddon) { try { disconnectWebSocket(); } catch (e) { /* ignore */ } } // 切 agent 后恢复该 agent 的会话 ID (per-agent 隔离) currentConversationId = null; try { const saved = uni.getStorageSync(getConversationKey(currentAgentId)); if (saved) currentConversationId = saved; } catch (e) { /* ignore */ } }, /** * 获取当前 agent 的会话存储 key (per-agent 隔离) */ getConversationKey(agentId) { return getConversationKey(agentId); }, /** * 获取全部 agent 的会话 ID 映射 (用于会话列表, 可选) */ getAllConversations() { const out = {}; try { const info = uni.getStorageInfoSync(); (info.keys || []).filter(k => k.indexOf(CONVERSATION_KEY_PREFIX) === 0) .forEach(k => { out[k.substring(CONVERSATION_KEY_PREFIX.length)] = uni.getStorageSync(k); }); } catch (e) { /* ignore */ } return out; }, /** * 获取当前 config_uuid (媒体上传路由用) */ getCurrentConfigUuid() { return uni.getStorageSync('current_config_uuid') || ''; }, /** * 获取用户 ID (媒体上传字段用) */ getUserId() { return getUserId(); }, /** * Get current AI Agent. * @returns {string} */ getAgentId() { return getAgentId(); }, };