fix(ai-service): WebSocket长连接重构 + 环境隔离 + this上下文修复
- 改为长连接模式:首次建立WebSocket后保持不断开,后续复用 - 添加 #ifdef MP-WEIXIN / #ifdef H5 环境隔离 - 修复 ensureWebSocketConnected 中 .bind(this) 导致 this 为 undefined 的问题 - 组件 onUnload 中调用 disconnectWebSocket() 断开连接
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -596,6 +596,10 @@ export default {
|
||||
this.scrollToBottom()
|
||||
})
|
||||
},
|
||||
onUnload() {
|
||||
// 页面销毁时断开 WebSocket 长连接
|
||||
aiService.disconnectWebSocket();
|
||||
},
|
||||
watch: {
|
||||
messages: {
|
||||
handler(newVal, oldVal) {
|
||||
|
||||
Reference in New Issue
Block a user