fix(ai-service): 修复WebSocket域名正则丢弃端口、timeout未定义问题
- 正则 /^(?:https?:\/\/|wss?:\/\/)([^\/\s:?#]+(?::\d+)?)/ 正确捕获host:port - 添加 let timeout = null 声明,修复 ReferenceError
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user