fix(ai-service): WebSocket长连接重构 + 环境隔离 + this上下文修复

- 改为长连接模式:首次建立WebSocket后保持不断开,后续复用
- 添加 #ifdef MP-WEIXIN / #ifdef H5 环境隔离
- 修复 ensureWebSocketConnected 中 .bind(this) 导致 this 为 undefined 的问题
- 组件 onUnload 中调用 disconnectWebSocket() 断开连接
This commit is contained in:
2026-05-14 17:04:28 +08:00
parent 6de74a3baf
commit fed3962965
2 changed files with 208 additions and 124 deletions

View File

@@ -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
};

View File

@@ -596,6 +596,10 @@ export default {
this.scrollToBottom()
})
},
onUnload() {
// 页面销毁时断开 WebSocket 长连接
aiService.disconnectWebSocket();
},
watch: {
messages: {
handler(newVal, oldVal) {