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);
|
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 {
|
export default {
|
||||||
/**
|
/**
|
||||||
* 获取uniacid
|
* 获取uniacid
|
||||||
@@ -82,135 +245,38 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 流式消息入口(自动适配平台)
|
* 流式消息入口(自动适配平台)
|
||||||
|
* 微信小程序:复用长连接 WebSocket
|
||||||
*/
|
*/
|
||||||
async sendStreamMessage(message, onChunk, onComplete) {
|
async sendStreamMessage(message, onChunk, onComplete) {
|
||||||
// #ifdef MP-WEIXIN
|
// #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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:?#]+(?::\d+)?)/);
|
wsMessageResolve = resolve;
|
||||||
const domain = match ? match[1] : '';
|
wsMessageReject = reject;
|
||||||
|
|
||||||
// 修复 5:Domain 提取失败时提前拒绝
|
wsSocketTask.send({
|
||||||
if (!domain) {
|
data: JSON.stringify({
|
||||||
const errorMsg = 'WebSocket 域名配置无效';
|
action: 'chat',
|
||||||
console.error(errorMsg);
|
uniacid: this.getUniacid(),
|
||||||
reject(new Error(errorMsg));
|
query: message,
|
||||||
if (onComplete) onComplete({ error: errorMsg });
|
user_id: store.state.memberInfo?.id || 'anonymous',
|
||||||
return;
|
conversation_id: this.getConversationId() || '',
|
||||||
}
|
...getDomainParams()
|
||||||
|
})
|
||||||
const wssUrl = `wss://${domain}/ws/aikefu`;
|
|
||||||
const socketTask = wx.connectSocket({
|
|
||||||
url: wssUrl,
|
|
||||||
header: {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
// #endif
|
||||||
},
|
},
|
||||||
@@ -553,5 +619,19 @@ export default {
|
|||||||
console.warn('chatWithFetchStream 不支持微信小程序');
|
console.warn('chatWithFetchStream 不支持微信小程序');
|
||||||
return null;
|
return null;
|
||||||
// #endif
|
// #endif
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
/**
|
||||||
|
* [微信小程序] 断开 WebSocket 长连接(页面离开时调用)
|
||||||
|
*/
|
||||||
|
disconnectWebSocket() {
|
||||||
|
disconnectWebSocket();
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
disconnectWebSocket() {
|
||||||
|
// H5 无 WebSocket 长连接,无需操作
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -596,6 +596,10 @@ export default {
|
|||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onUnload() {
|
||||||
|
// 页面销毁时断开 WebSocket 长连接
|
||||||
|
aiService.disconnectWebSocket();
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
messages: {
|
messages: {
|
||||||
handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
|
|||||||
Reference in New Issue
Block a user