Compare commits
5 Commits
custom/281
...
xindeznkf
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ac1ab06be | |||
| 6f83bd8f4a | |||
| 042df8c1c6 | |||
| c63a7de1c4 | |||
| 43ac988fc9 |
@@ -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(() => {
|
||||||
let index = 0;
|
console.log('WebSocket 连接成功,开始认证...');
|
||||||
const chunkSize = 2; // 每次显示2个字符
|
socketTask.send({
|
||||||
return new Promise((resolve) => {
|
data: JSON.stringify({
|
||||||
const timer = setInterval(() => {
|
action: 'auth',
|
||||||
if (index < content.length) {
|
uniacid: store.state.uniacid || '1',
|
||||||
const chunk = content.substring(index, index + chunkSize);
|
token: store.state.token || 'test_token',
|
||||||
index += chunkSize;
|
user_id: store.state.memberInfo?.id || 'anonymous'
|
||||||
if (onChunk) onChunk(chunk);
|
})
|
||||||
} else {
|
});
|
||||||
clearInterval(timer);
|
});
|
||||||
if (onComplete) {
|
|
||||||
onComplete({
|
socketTask.onMessage((res) => {
|
||||||
content: content,
|
try {
|
||||||
conversation_id: conversationId
|
const data = JSON.parse(res.data);
|
||||||
});
|
console.log('收到 WebSocket 消息:', data);
|
||||||
}
|
|
||||||
resolve({ content, conversation_id: conversationId });
|
if (data.type === 'auth_success') {
|
||||||
}
|
console.log('认证成功,发送聊天消息...');
|
||||||
}, 80); // 打字速度:80ms/次
|
isAuthenticated = true;
|
||||||
});
|
socketTask.send({
|
||||||
} catch (error) {
|
data: JSON.stringify({
|
||||||
console.error('小程序流式消息降级失败:', error);
|
action: 'chat',
|
||||||
if (onComplete) {
|
uniacid: store.state.uniacid || '1',
|
||||||
onComplete({ error: error.message || '发送失败' });
|
query: message,
|
||||||
}
|
user_id: store.state.memberInfo?.id || 'anonymous',
|
||||||
throw error;
|
conversation_id: this.getConversationId() || ''
|
||||||
}
|
})
|
||||||
|
});
|
||||||
|
} 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 使用)
|
||||||
*/
|
*/
|
||||||
@@ -132,11 +184,8 @@ export default {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
||||||
@@ -145,50 +194,64 @@ export default {
|
|||||||
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
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -810,44 +810,46 @@ 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.messages = this.messages.filter(msg => msg.type !== 'loading');
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true;
|
||||||
this.messages.push(streamMessage)
|
this.messages.push(streamMessage);
|
||||||
|
|
||||||
// 开始流式响应
|
try {
|
||||||
await aiService.sendStreamMessage(
|
// #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;
|
||||||
|
|
||||||
@@ -857,18 +859,72 @@ export default {
|
|||||||
{ 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智能客服,很高兴为您服务!有什么可以帮助您的吗?',
|
||||||
|
|||||||
Reference in New Issue
Block a user