fix(ai-service): 修复WebSocket域名正则丢弃端口、timeout未定义问题

- 正则 /^(?:https?:\/\/|wss?:\/\/)([^\/\s:?#]+(?::\d+)?)/ 正确捕获host:port
- 添加 let timeout = null 声明,修复 ReferenceError
This commit is contained in:
2026-05-14 15:21:45 +08:00
parent a4f2823e75
commit 6de74a3baf

View File

@@ -83,136 +83,137 @@ export default {
/** /**
* 流式消息入口(自动适配平台) * 流式消息入口(自动适配平台)
*/ */
async sendStreamMessage(message, onChunk, onComplete) { async sendStreamMessage(message, onChunk, onComplete) {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:]+)/); const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:?#]+(?::\d+)?)/);
const domain = match ? match[1] : ''; const domain = match ? match[1] : '';
// 修复 5:Domain 提取失败时提前拒绝 // 修复 5:Domain 提取失败时提前拒绝
if (!domain) { if (!domain) {
const errorMsg = 'WebSocket 域名配置无效'; const errorMsg = 'WebSocket 域名配置无效';
console.error(errorMsg); console.error(errorMsg);
reject(new Error(errorMsg)); reject(new Error(errorMsg));
if (onComplete) onComplete({ error: errorMsg }); if (onComplete) onComplete({ error: errorMsg });
return; return;
} }
const wssUrl = `wss://${domain}/ws/aikefu`; const wssUrl = `wss://${domain}/ws/aikefu`;
const socketTask = wx.connectSocket({ const socketTask = wx.connectSocket({
url: wssUrl, url: wssUrl,
header: {} header: {}
}); });
let content = '';
let conversationId = '';
let isAuthenticated = false;
let closedNormally = false;
let settled = false;
// 安全地拒绝(防止重复调用) let content = '';
function safeReject(error) { let conversationId = '';
if (settled) return; let isAuthenticated = false;
settled = true; let closedNormally = false;
clearTimeout(timeout); let settled = false;
reject(error); let timeout = null;
}
// 安全地完成 // 安全地拒绝(防止重复调用)
function safeResolve(result) { function safeReject(error) {
if (settled) return; if (settled) return;
settled = true; settled = true;
clearTimeout(timeout); clearTimeout(timeout);
resolve(result); reject(error);
} }
// 安全地触发 onComplete // 安全地完成
function safeComplete(result) { function safeResolve(result) {
if (onComplete) onComplete(result); if (settled) return;
} settled = true;
clearTimeout(timeout);
socketTask.onOpen(() => { resolve(result);
socketTask.send({ }
data: JSON.stringify({
action: 'auth', // 安全地触发 onComplete
uniacid: this.getUniacid(), function safeComplete(result) {
token: store.state.token || 'test_token', if (onComplete) onComplete(result);
user_id: store.state.memberInfo?.id || 'anonymous' }
})
}); socketTask.onOpen(() => {
}); socketTask.send({
data: JSON.stringify({
socketTask.onMessage((res) => { action: 'auth',
try { uniacid: this.getUniacid(),
const data = JSON.parse(res.data); token: store.state.token || 'test_token',
user_id: store.state.memberInfo?.id || 'anonymous'
if (data.type === 'auth_success') { })
isAuthenticated = true; });
socketTask.send({ });
data: JSON.stringify({
action: 'chat', socketTask.onMessage((res) => {
uniacid: this.getUniacid(), try {
query: message, const data = JSON.parse(res.data);
user_id: store.state.memberInfo?.id || 'anonymous',
conversation_id: this.getConversationId() || '', if (data.type === 'auth_success') {
...getDomainParams() isAuthenticated = true;
}) socketTask.send({
}); data: JSON.stringify({
} else if (data.type === 'auth_failed') { action: 'chat',
const errorMsg = '认证失败,请重新登录'; uniacid: this.getUniacid(),
console.error(errorMsg, data); query: message,
safeComplete({ error: errorMsg }); user_id: store.state.memberInfo?.id || 'anonymous',
safeReject(new Error(errorMsg)); conversation_id: this.getConversationId() || '',
socketTask.close(); ...getDomainParams()
} })
});
// 处理流式消息块 } else if (data.type === 'auth_failed') {
else if (data.event === 'message') { const errorMsg = '认证失败,请重新登录';
const text = data.answer || data.content || data.text || ''; console.error(errorMsg, data);
content += text; safeComplete({ error: errorMsg });
if (onChunk) onChunk(text); safeReject(new Error(errorMsg));
} socketTask.close();
}
// 处理流结束
else if (data.event === 'message_end') { // 处理流式消息块
conversationId = data.conversation_id || ''; else if (data.event === 'message') {
if (conversationId) { const text = data.answer || data.content || data.text || '';
this.setConversationId(conversationId); content += text;
} if (onChunk) onChunk(text);
closedNormally = true; }
safeComplete({ content, conversation_id: conversationId });
safeResolve({ content, conversation_id: conversationId }); // 处理流结束
socketTask.close(); else if (data.event === 'message_end') {
} conversationId = data.conversation_id || '';
if (conversationId) {
// 可选:处理 done this.setConversationId(conversationId);
else if (data.type === 'done') { }
} closedNormally = true;
safeComplete({ content, conversation_id: conversationId });
} catch (e) { safeResolve({ content, conversation_id: conversationId });
console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data); socketTask.close();
} }
});
// 可选:处理 done
socketTask.onError((err) => { else if (data.type === 'done') {
const errorMsg = 'WebSocket 连接失败'; }
console.error(errorMsg, err);
safeComplete({ error: errorMsg }); } catch (e) {
safeReject(new Error(errorMsg)); console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data);
}); }
});
socketTask.onClose(() => {
if (!closedNormally && !settled) { socketTask.onError((err) => {
const errorMsg = 'WebSocket 连接意外关闭'; const errorMsg = 'WebSocket 连接失败';
console.error(errorMsg); console.error(errorMsg, err);
safeComplete({ error: errorMsg }); safeComplete({ error: errorMsg });
safeReject(new 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
}, },
/** /**
* HTTP 流式请求(仅 H5 使用) * HTTP 流式请求(仅 H5 使用)
*/ */
@@ -237,27 +238,27 @@ export default {
...getDomainParams() ...getDomainParams()
}) })
}); });
if (!response.ok || !response.body) { if (!response.ok || !response.body) {
throw new Error('无效响应'); throw new Error('无效响应');
} }
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder('utf-8');
let buffer = ''; let buffer = '';
let content = ''; let content = '';
let conversationId = ''; let conversationId = '';
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true });
// 按行分割,保留不完整的最后一行 // 按行分割,保留不完整的最后一行
const lines = buffer.split('\n'); const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 未完成的行留到下次 buffer = lines.pop() || ''; // 未完成的行留到下次
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (trimmed.startsWith('data:')) { if (trimmed.startsWith('data:')) {
@@ -283,7 +284,7 @@ export default {
} }
} }
} }
// 处理最后残留的 buffer(如果有) // 处理最后残留的 buffer(如果有)
if (buffer.trim().startsWith('data:')) { if (buffer.trim().startsWith('data:')) {
try { try {
@@ -303,7 +304,7 @@ export default {
console.warn('最后 buffer 解析失败:', e); console.warn('最后 buffer 解析失败:', e);
} }
} }
if (onComplete) { if (onComplete) {
onComplete({ content, conversation_id: conversationId }); onComplete({ content, conversation_id: conversationId });
} }
@@ -368,19 +369,19 @@ export default {
* 生成新会话(如需) * 生成新会话(如需)
*/ */
async generateConversationId() { async generateConversationId() {
const params = { const params = {
url: '/api/kefu/createConversation', url: '/api/kefu/createConversation',
data: { data: {
uniacid: this.getUniacid(), uniacid: this.getUniacid(),
token: store.state.token || 'test_token', token: store.state.token || 'test_token',
user_id: store.state.memberInfo?.id || 'anonymous', user_id: store.state.memberInfo?.id || 'anonymous',
member_id: store.state.memberInfo?.id || 'anonymous', member_id: store.state.memberInfo?.id || 'anonymous',
...getDomainParams() ...getDomainParams()
}, },
header: { header: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}; };
const response = await http.sendRequest({ const response = await http.sendRequest({
...params, ...params,
async: false async: false
@@ -419,7 +420,7 @@ export default {
try { try {
const response = await http.sendRequest({ const response = await http.sendRequest({
url: '/api/kefu/clear-conversation', url: '/api/kefu/clear-conversation',
data: { data: {
conversation_id: conversationId, conversation_id: conversationId,
...getDomainParams() ...getDomainParams()
}, },
@@ -511,9 +512,9 @@ export default {
if (onMessage) onMessage(data.answer || ''); if (onMessage) onMessage(data.answer || '');
} }
if (data.event === 'message_end') { if (data.event === 'message_end') {
if (onComplete) onComplete({ if (onComplete) onComplete({
conversation_id: data.conversation_id, conversation_id: data.conversation_id,
message: aiMessage message: aiMessage
}); });
} }
} catch (error) { } catch (error) {