5 Commits

3 changed files with 237 additions and 119 deletions

View File

@@ -60,55 +60,107 @@ export default {
/** /**
* 流式消息入口(自动适配平台) * 流式消息入口(自动适配平台)
*/ */
async sendStreamMessage(message, onChunk, onComplete) { async sendStreamMessage(message, onChunk, onComplete) {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// 微信小程序:降级为普通请求 + 前端打字模拟 return new Promise((resolve, reject) => {
try { const socketTask = wx.connectSocket({
const result = await this.sendMessage(message); url: 'wss://dev.aigc-quickapp.com/ws/aikefu',
const content = result.content || ''; header: {}
const conversationId = result.conversationId || ''; });
// 保存会话ID确保连续对话 let content = '';
if (conversationId) { let conversationId = '';
this.setConversationId(conversationId); let isAuthenticated = false;
}
socketTask.onOpen(() => {
// 模拟打字效果 console.log('WebSocket 连接成功,开始认证...');
let index = 0; socketTask.send({
const chunkSize = 2; // 每次显示2个字符 data: JSON.stringify({
return new Promise((resolve) => { action: 'auth',
const timer = setInterval(() => { uniacid: store.state.uniacid || '1',
if (index < content.length) { token: store.state.token || 'test_token',
const chunk = content.substring(index, index + chunkSize); user_id: store.state.memberInfo?.id || 'anonymous'
index += chunkSize; })
if (onChunk) onChunk(chunk); });
} else { });
clearInterval(timer);
if (onComplete) { socketTask.onMessage((res) => {
onComplete({ try {
content: content, const data = JSON.parse(res.data);
conversation_id: conversationId console.log('收到 WebSocket 消息:', data);
});
} if (data.type === 'auth_success') {
resolve({ content, conversation_id: conversationId }); console.log('认证成功,发送聊天消息...');
} isAuthenticated = true;
}, 80); // 打字速度80ms/次 socketTask.send({
}); data: JSON.stringify({
} catch (error) { action: 'chat',
console.error('小程序流式消息降级失败:', error); uniacid: store.state.uniacid || '1',
if (onComplete) { query: message,
onComplete({ error: error.message || '发送失败' }); user_id: store.state.memberInfo?.id || 'anonymous',
} conversation_id: this.getConversationId() || ''
throw error; })
} });
} else if (data.type === 'auth_failed') {
const errorMsg = '认证失败,请重新登录';
console.error(errorMsg, data);
reject(new Error(errorMsg));
if (onComplete) onComplete({ error: errorMsg });
socketTask.close();
}
// 处理流式消息块
else if (data.type === 'message' || data.event === 'message') {
const text = data.answer || data.content || data.text || '';
content += text;
if (onChunk) onChunk(text);
}
// 处理流结束
else if (data.event === 'message_end' || data.type === 'message_end') {
conversationId = data.conversation_id || '';
if (conversationId) {
this.setConversationId(conversationId);
}
if (onComplete) {
onComplete({ content, conversation_id: conversationId });
}
resolve({ content, conversation_id: conversationId });
socketTask.close();
}
// 可选:处理 done
else if (data.type === 'done') {
console.log('对话完成:', data);
}
} catch (e) {
console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data);
}
});
socketTask.onError((err) => {
const errorMsg = 'WebSocket 连接失败';
console.error(errorMsg, err);
reject(new Error(errorMsg));
if (onComplete) onComplete({ error: errorMsg });
});
socketTask.onClose(() => {
console.log('WebSocket 连接已关闭');
});
const timeout = setTimeout(() => {
if (!isAuthenticated || content === '') {
console.warn('WebSocket 超时,强制关闭');
socketTask.close();
reject(new Error('AI服务响应超时'));
if (onComplete) onComplete({ error: 'AI服务响应超时' });
}
}, 10000);
});
// #endif // #endif
},
// #ifdef H5
// H5使用真实流式EventSource / Fetch
return this.sendHttpStream(message, onChunk, onComplete);
// #endif
},
/** /**
* HTTP 流式请求(仅 H5 使用) * HTTP 流式请求(仅 H5 使用)
*/ */
@@ -131,64 +183,75 @@ export default {
conversation_id: this.getConversationId() || '' conversation_id: this.getConversationId() || ''
}) })
}); });
if (!response.ok) { if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}`); throw new Error('无效响应');
} }
if (!response.body) {
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 = '';
function processBuffer(buf, callback) { while (true) {
const lines = buf.split('\n'); const { done, value } = await reader.read();
buf = lines.pop() || ''; if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按行分割,保留不完整的最后一行
const lines = buffer.split('\n');
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:')) {
const jsonStr = trimmed.slice(5).trim(); try {
if (jsonStr) { const jsonStr = trimmed.slice(5).trim();
try { if (jsonStr && jsonStr !== '[DONE]') {
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
if (data.event === 'message') { if (data.event === 'message') {
const text = data.answer || data.text || ''; const text = data.answer || data.text || '';
content += text; content += text;
callback(text); if (onChunk) onChunk(text);
} }
if (data.conversation_id) { if (data.conversation_id) {
conversationId = data.conversation_id; conversationId = data.conversation_id;
} }
if (data.event === 'message_end') { if (data.event === 'message_end') {
// 可选:提前完成 // 可提前结束
} }
} catch (e) {
console.warn('解析流数据失败:', e);
} }
} catch (e) {
console.warn('解析失败:', e, line);
} }
} }
} }
return buf;
} }
while (true) { // 处理最后残留的 buffer如果有
const { done, value } = await reader.read(); if (buffer.trim().startsWith('data:')) {
if (done) break; try {
buffer += decoder.decode(value, { stream: true }); const jsonStr = buffer.trim().slice(5);
buffer = processBuffer(buffer, (chunk) => { if (jsonStr) {
if (onChunk) onChunk(chunk); const data = JSON.parse(jsonStr);
}); if (data.event === 'message') {
const text = data.answer || '';
content += text;
if (onChunk) onChunk(text);
}
if (data.conversation_id) {
conversationId = data.conversation_id;
}
}
} catch (e) {
console.warn('最后 buffer 解析失败:', e);
}
} }
if (onComplete) { if (onComplete) {
onComplete({ onComplete({ content, conversation_id: conversationId });
content,
conversation_id: conversationId
});
} }
return { content, conversation_id: conversationId }; return { content, conversation_id: conversationId };
} catch (error) { } catch (error) {
@@ -196,11 +259,6 @@ export default {
throw error; throw error;
} }
// #endif // #endif
// #ifdef MP-WEIXIN
// 理论上不会执行到这里,但防止 fallback
return this.sendStreamMessage(message, onChunk, onComplete);
// #endif
}, },
/** /**

View File

@@ -65,6 +65,10 @@ export default {
componentRefresh() { componentRefresh() {
return this.$store.state.componentRefresh; return this.$store.state.componentRefresh;
}, },
// AI客服配置
globalAIKefuConfig() {
return this.$store.state.globalAIKefuConfig;
},
// 客服配置 // 客服配置
servicerConfig() { servicerConfig() {
return this.$store.state.servicerConfig; return this.$store.state.servicerConfig;

View File

@@ -810,65 +810,121 @@ export default {
}, },
// 发送流式消息 // 发送流式消息
async sendStreamMessage(userMessage) { // 发送流式消息(自动适配 H5 / 微信小程序)
// 创建流式消息对象 async sendStreamMessage(userMessage) {
const streamMessage = { // 创建流式消息对象
id: ++this.messageId, const streamMessage = {
role: 'ai', id: ++this.messageId,
type: 'text', role: 'ai',
content: '', type: 'text',
timestamp: Date.now(), content: '',
isStreaming: true timestamp: Date.now(),
} isStreaming: true
};
// 移除加载状态,添加流式消息
this.messages = this.messages.filter(msg => msg.type !== 'loading') // 移除加载状态,添加流式消息
this.shouldScrollToBottom = true this.messages = this.messages.filter(msg => msg.type !== 'loading');
this.messages.push(streamMessage) this.shouldScrollToBottom = true;
this.messages.push(streamMessage);
// 开始流式响应
await aiService.sendStreamMessage( try {
// #ifdef H5
// ===== H5: 使用 POST + 流式 (fetch readable stream) =====
await aiService.sendHttpStream(
userMessage, userMessage,
// 流式数据回调
(chunk) => { (chunk) => {
streamMessage.content += chunk // 实时更新内容
this.$forceUpdate() // 或 this.$nextTick() streamMessage.content += chunk;
this.$forceUpdate(); // 强制更新视图
}, },
// 完成回调:处理对象或字符串
(completeResult) => { (completeResult) => {
// 流结束回调
let finalContent = ''; let finalContent = '';
let convId = ''; let convId = '';
// 判断是对象还是字符串
if (typeof completeResult === 'string') { if (typeof completeResult === 'string') {
finalContent = completeResult; finalContent = completeResult;
} else { } else {
finalContent = completeResult.content || ''; finalContent = completeResult.content || '';
convId = completeResult.conversation_id || ''; // 👈 关键:提取 conversation_id convId = completeResult.conversation_id || '';
} }
// 更新消息状态 // 更新最终内容
streamMessage.isStreaming = false; streamMessage.isStreaming = false;
streamMessage.content = finalContent; streamMessage.content = finalContent;
// 添加操作按钮 // 添加操作按钮
streamMessage.actions = [ streamMessage.actions = [
{ id: 1, text: '有帮助', type: 'like' }, { id: 1, text: '有帮助', type: 'like' },
{ id: 2, text: '没帮助', type: 'dislike' } { id: 2, text: '没帮助', type: 'dislike' }
]; ];
// 保存 conversation_id 到本地 // 保存会话 ID
if (convId) { if (convId) {
this.currentConversationId = convId;
aiService.setConversationId(convId); aiService.setConversationId(convId);
this.saveConversationIdToLocal(convId);
} }
// 触发事件 // 触发事件
this.$emit('ai-response', streamMessage); this.$emit('ai-response', streamMessage);
this.saveConversationIdToLocal(convId);
this.currentConversationId = convId;
} }
); );
}, // #endif
// #ifdef MP-WEIXIN
// ===== 微信小程序: 使用 WebSocket =====
await aiService.sendStreamMessage(
userMessage,
(chunk) => {
// 实时更新内容
streamMessage.content += chunk;
this.$forceUpdate();
},
(completeResult) => {
// 流结束回调
let finalContent = completeResult?.content || '';
let convId = completeResult?.conversation_id || '';
// 更新最终内容
streamMessage.isStreaming = false;
streamMessage.content = finalContent;
// 添加操作按钮
streamMessage.actions = [
{ id: 1, text: '有帮助', type: 'like' },
{ id: 2, text: '没帮助', type: 'dislike' }
];
// 保存会话 ID
if (convId) {
this.currentConversationId = convId;
aiService.setConversationId(convId);
this.saveConversationIdToLocal(convId);
}
// 触发事件
this.$emit('ai-response', streamMessage);
}
);
// #endif
} catch (error) {
console.error('流式请求失败:', error);
// 移除流式消息
this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
// 显示错误
const errorMsg = {
id: ++this.messageId,
role: 'ai',
type: 'text',
content: '抱歉,服务暂时不可用,请稍后重试。',
timestamp: Date.now(),
actions: [{ id: 1, text: '重试', type: 'retry' }]
};
this.messages.push(errorMsg);
this.$emit('ai-response', errorMsg);
}
},
generateAIResponse(userMessage) { generateAIResponse(userMessage) {
const responses = { const responses = {
'你好': '您好我是AI智能客服很高兴为您服务有什么可以帮助您的吗', '你好': '您好我是AI智能客服很高兴为您服务有什么可以帮助您的吗',