From fed3962965b997da459ac0183bb266fded6c4874 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Thu, 14 May 2026 17:04:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai-service):=20WebSocket=E9=95=BF=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E9=87=8D=E6=9E=84=20+=20=E7=8E=AF=E5=A2=83=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=20+=20this=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改为长连接模式:首次建立WebSocket后保持不断开,后续复用 - 添加 #ifdef MP-WEIXIN / #ifdef H5 环境隔离 - 修复 ensureWebSocketConnected 中 .bind(this) 导致 this 为 undefined 的问题 - 组件 onUnload 中调用 disconnectWebSocket() 断开连接 --- common/js/ai-service.js | 328 +++++++++++++++---------- pages_tool/ai-chat/ai-chat-message.vue | 4 + 2 files changed, 208 insertions(+), 124 deletions(-) diff --git a/common/js/ai-service.js b/common/js/ai-service.js index 5cc86dd..5794acc 100644 --- a/common/js/ai-service.js +++ b/common/js/ai-service.js @@ -26,6 +26,169 @@ try { console.warn('读取会话ID失败:', e); } +// #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 = store.state.memberInfo?.id || 'anonymous'; + + 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 || ''; + wsMessageContent += text; + if (wsMessageOnChunk) wsMessageOnChunk(text); + } else if (data.event === 'message_end') { + var conversationId = data.conversation_id || ''; + // 保存会话ID(独立函数,不使用 this) + if (conversationId) { + if (conversationId !== currentConversationId) { + currentConversationId = conversationId; + try { + uni.setStorageSync(CONVERSATION_KEY, 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 }); + } + } 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 @@ -82,135 +245,38 @@ export default { /** * 流式消息入口(自动适配平台) + * 微信小程序:复用长连接 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) => { - const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:?#]+(?::\d+)?)/); - const domain = match ? match[1] : ''; + wsMessageResolve = resolve; + wsMessageReject = reject; - // 修复 5:Domain 提取失败时提前拒绝 - if (!domain) { - const errorMsg = 'WebSocket 域名配置无效'; - console.error(errorMsg); - reject(new Error(errorMsg)); - if (onComplete) onComplete({ error: errorMsg }); - return; - } - - const wssUrl = `wss://${domain}/ws/aikefu`; - const socketTask = wx.connectSocket({ - url: wssUrl, - header: {} + wsSocketTask.send({ + data: JSON.stringify({ + action: 'chat', + uniacid: this.getUniacid(), + query: message, + user_id: store.state.memberInfo?.id || 'anonymous', + conversation_id: this.getConversationId() || '', + ...getDomainParams() + }) }); - - let content = ''; - let conversationId = ''; - let isAuthenticated = false; - let closedNormally = false; - let settled = false; - let timeout = null; - - // 安全地拒绝(防止重复调用) - function safeReject(error) { - if (settled) return; - settled = true; - clearTimeout(timeout); - reject(error); - } - - // 安全地完成 - function safeResolve(result) { - if (settled) return; - settled = true; - clearTimeout(timeout); - resolve(result); - } - - // 安全地触发 onComplete - function safeComplete(result) { - if (onComplete) onComplete(result); - } - - socketTask.onOpen(() => { - socketTask.send({ - data: JSON.stringify({ - action: 'auth', - uniacid: this.getUniacid(), - token: store.state.token || 'test_token', - user_id: store.state.memberInfo?.id || 'anonymous' - }) - }); - }); - - socketTask.onMessage((res) => { - try { - const data = JSON.parse(res.data); - - if (data.type === 'auth_success') { - isAuthenticated = true; - socketTask.send({ - data: JSON.stringify({ - action: 'chat', - uniacid: this.getUniacid(), - query: message, - user_id: store.state.memberInfo?.id || 'anonymous', - conversation_id: this.getConversationId() || '', - ...getDomainParams() - }) - }); - } else if (data.type === 'auth_failed') { - const errorMsg = '认证失败,请重新登录'; - console.error(errorMsg, data); - safeComplete({ error: errorMsg }); - safeReject(new Error(errorMsg)); - socketTask.close(); - } - - // 处理流式消息块 - else if (data.event === 'message') { - const text = data.answer || data.content || data.text || ''; - content += text; - if (onChunk) onChunk(text); - } - - // 处理流结束 - else if (data.event === 'message_end') { - conversationId = data.conversation_id || ''; - if (conversationId) { - this.setConversationId(conversationId); - } - closedNormally = true; - safeComplete({ content, conversation_id: conversationId }); - safeResolve({ content, conversation_id: conversationId }); - socketTask.close(); - } - - // 可选:处理 done - else if (data.type === 'done') { - } - - } catch (e) { - console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data); - } - }); - - socketTask.onError((err) => { - const errorMsg = 'WebSocket 连接失败'; - console.error(errorMsg, err); - safeComplete({ error: errorMsg }); - safeReject(new Error(errorMsg)); - }); - - socketTask.onClose(() => { - if (!closedNormally && !settled) { - const errorMsg = 'WebSocket 连接意外关闭'; - console.error(errorMsg); - safeComplete({ error: errorMsg }); - safeReject(new Error(errorMsg)); - } - }); - }); // #endif }, @@ -553,5 +619,19 @@ export default { console.warn('chatWithFetchStream 不支持微信小程序'); return null; // #endif - } + }, + + // #ifdef MP-WEIXIN + /** + * [微信小程序] 断开 WebSocket 长连接(页面离开时调用) + */ + disconnectWebSocket() { + disconnectWebSocket(); + }, + // #endif + // #ifdef H5 + disconnectWebSocket() { + // H5 无 WebSocket 长连接,无需操作 + }, + // #endif }; diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue index 55ae522..1597589 100644 --- a/pages_tool/ai-chat/ai-chat-message.vue +++ b/pages_tool/ai-chat/ai-chat-message.vue @@ -596,6 +596,10 @@ export default { this.scrollToBottom() }) }, + onUnload() { + // 页面销毁时断开 WebSocket 长连接 + aiService.disconnectWebSocket(); + }, watch: { messages: { handler(newVal, oldVal) {