504 lines
15 KiB
JavaScript
504 lines
15 KiB
JavaScript
import Config from './config.js'
|
||
import http from './http.js'
|
||
import store from '@/store/index.js'
|
||
|
||
export default {
|
||
/**
|
||
* 发送消息到Dify API
|
||
* @param {string} message 用户消息内容
|
||
* @param {Object} options 配置选项
|
||
* @returns {Promise}
|
||
*/
|
||
async sendMessage(message, options = {}) {
|
||
try {
|
||
// 获取AI配置
|
||
const aiConfig = store.getters.globalAIKefuConfig
|
||
|
||
// const new_conversationId = await this.generateConversationId()
|
||
|
||
// 构建Dify API请求参数
|
||
const params = {
|
||
url: '/api/kefu/chat', // 后端代理接口
|
||
data: {
|
||
// conversation_id: options.conversationId ?? new_conversationId,
|
||
user_id: store.state.memberInfo?.id || 'anonymous',
|
||
stream: false,
|
||
// Dify API参数
|
||
inputs: {},
|
||
query: message,
|
||
response_mode: options.stream ? 'streaming' : 'blocking',
|
||
user: store.state.memberInfo?.id || 'anonymous'
|
||
},
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
// ✅ 发送请求前打印日志,用于调试
|
||
console.log('Sending request to:', params.url);
|
||
console.log('Request data:', params.data);
|
||
|
||
// 发送请求
|
||
const response = await http.sendRequest({
|
||
...params,
|
||
async: false // 使用Promise方式
|
||
})
|
||
|
||
return this.handleResponse(response, options)
|
||
|
||
} catch (error) {
|
||
console.error('Dify API请求失败:', error)
|
||
throw new Error('AI服务暂时不可用,请稍后重试')
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 流式消息处理
|
||
* @param {string} message 用户消息
|
||
* @param {Function} onChunk 流式数据回调
|
||
* @param {Function} onComplete 完成回调
|
||
*/
|
||
async sendStreamMessage(message, onChunk, onComplete) {
|
||
try {
|
||
// 使用HTTP流式请求
|
||
return this.sendHttpStream(message, onChunk, onComplete)
|
||
|
||
} catch (error) {
|
||
console.error('流式消息发送失败:', error)
|
||
throw error
|
||
}
|
||
},
|
||
|
||
/**
|
||
* EventSource 流式聊天
|
||
* @param {string} message 用户消息
|
||
* @param {string} conversationId 会话ID(可选)
|
||
* @param {Function} onMessage 消息回调
|
||
* @param {Function} onComplete 完成回调
|
||
* @param {Function} onError 错误回调
|
||
* @returns {EventSource|null} EventSource实例
|
||
*/
|
||
chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) {
|
||
// 关闭之前的连接
|
||
if (window.currentEventSource) {
|
||
window.currentEventSource.close();
|
||
}
|
||
|
||
// 构建请求参数
|
||
const params = new URLSearchParams({
|
||
uniacid: store.state.uniacid || '1',
|
||
user_id: store.state.memberInfo?.id || '123456',
|
||
query: message,
|
||
conversation_id: conversationId || '',
|
||
stream: 'true'
|
||
});
|
||
|
||
const url = `/api/kefu/chat?${params.toString()}`;
|
||
|
||
try {
|
||
const eventSource = new EventSource(url);
|
||
window.currentEventSource = eventSource;
|
||
|
||
let aiMessage = '';
|
||
|
||
// 监听消息事件
|
||
eventSource.addEventListener('message', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.event === 'message') {
|
||
// 更新 AI 消息
|
||
aiMessage += data.answer || '';
|
||
if (onMessage) onMessage(data.answer || '');
|
||
}
|
||
|
||
if (data.event === 'message_end') {
|
||
// 对话完成
|
||
if (onComplete) onComplete({
|
||
conversation_id: data.conversation_id,
|
||
message: aiMessage
|
||
});
|
||
}
|
||
|
||
if (data.conversation_id) {
|
||
conversationId = data.conversation_id;
|
||
}
|
||
} catch (error) {
|
||
console.error('解析消息失败:', error);
|
||
}
|
||
});
|
||
|
||
// 监听完成事件
|
||
eventSource.addEventListener('done', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
if (onComplete) onComplete(data);
|
||
} catch (error) {
|
||
console.error('解析完成事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 监听关闭事件
|
||
eventSource.addEventListener('close', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
console.log('连接正常结束:', data);
|
||
} catch (error) {
|
||
console.error('解析关闭事件失败:', error);
|
||
}
|
||
window.currentEventSource = null;
|
||
});
|
||
|
||
// 监听错误事件
|
||
eventSource.addEventListener('error', (error) => {
|
||
console.error('EventSource错误:', error);
|
||
if (onError) onError({ error: 'EventSource连接错误' });
|
||
window.currentEventSource = null;
|
||
});
|
||
|
||
return eventSource;
|
||
|
||
} catch (error) {
|
||
console.error('创建EventSource失败:', error);
|
||
if (onError) onError({ error: error.message });
|
||
return null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Fetch API 流式聊天
|
||
* @param {string} message 用户消息
|
||
* @param {string} conversationId 会话ID(可选)
|
||
* @param {Function} onMessage 消息回调
|
||
* @param {Function} onComplete 完成回调
|
||
* @param {Function} onError 错误回调
|
||
*/
|
||
async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
|
||
try {
|
||
// 构建请求体 - 支持JSON和FormData两种格式
|
||
let requestData;
|
||
let headers = {
|
||
'Accept': 'text/event-stream'
|
||
};
|
||
|
||
// 优先使用JSON格式
|
||
requestData = {
|
||
uniacid: store.state.uniacid || '1',
|
||
user_id: store.state.memberInfo?.id || '123456',
|
||
query: message,
|
||
conversation_id: conversationId || '',
|
||
stream: true,
|
||
inputs: {},
|
||
response_mode: 'streaming',
|
||
user: store.state.memberInfo?.id || '123456'
|
||
};
|
||
|
||
headers['Content-Type'] = 'application/json';
|
||
|
||
const response = await fetch('/api/kefu/chat', {
|
||
method: 'POST',
|
||
headers: headers,
|
||
body: JSON.stringify(requestData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('响应体不可用');
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder('utf-8');
|
||
let buffer = '';
|
||
let aiMessage = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
// 解码新接收的数据
|
||
buffer += decoder.decode(value, { stream: true });
|
||
|
||
// 处理缓冲的数据,按行分割
|
||
let lineEnd;
|
||
while ((lineEnd = buffer.indexOf('\n')) !== -1) {
|
||
const line = buffer.substring(0, lineEnd);
|
||
buffer = buffer.substring(lineEnd + 1);
|
||
|
||
if (line.startsWith('data: ')) {
|
||
try {
|
||
const data = JSON.parse(line.substring(6));
|
||
|
||
if (data.event === 'message' || data.event === 'text_message') {
|
||
const textContent = data.answer || data.text || '';
|
||
aiMessage += textContent;
|
||
if (onMessage) onMessage(textContent);
|
||
} else if (data.event === 'message_end' || data.event === 'done') {
|
||
if (onComplete) onComplete({
|
||
conversation_id: data.conversation_id,
|
||
message: aiMessage,
|
||
...data
|
||
});
|
||
} else if (data.event === 'error' && onError) {
|
||
onError(data);
|
||
}
|
||
|
||
if (data.conversation_id) {
|
||
conversationId = data.conversation_id;
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析流式数据失败:', e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理剩余的缓冲数据
|
||
if (buffer.startsWith('data: ')) {
|
||
try {
|
||
const data = JSON.parse(buffer.substring(6));
|
||
if ((data.event === 'message' || data.event === 'text_message') && onMessage) {
|
||
const textContent = data.answer || data.text || '';
|
||
onMessage(textContent);
|
||
} else if ((data.event === 'message_end' || data.event === 'done') && onComplete) {
|
||
onComplete({
|
||
conversation_id: data.conversation_id,
|
||
message: aiMessage,
|
||
...data
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析剩余数据失败:', e);
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Fetch流式聊天请求失败:', error);
|
||
if (onError) onError({ error: error.message });
|
||
}
|
||
},
|
||
|
||
/**
|
||
* HTTP流式请求(现有方法保持不变)
|
||
*/
|
||
async sendHttpStream(message, onChunk, onComplete) {
|
||
const params = {
|
||
url: '/api/kefu/chat',
|
||
data: {
|
||
query: message,
|
||
conversation_id: '', // 可以在外部传入或保持为空让服务器生成
|
||
user_id: store.state.memberInfo?.id || 'anonymous',
|
||
stream: true,
|
||
uniacid: store.state.uniacid, // 保留必填参数
|
||
inputs: {},
|
||
response_mode: 'streaming',
|
||
user: store.state.memberInfo?.id || 'anonymous'
|
||
},
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
|
||
// 使用fetch API进行流式请求(H5环境)
|
||
// #ifdef H5
|
||
try {
|
||
const url = Config.baseUrl + `/api/kefu/chat`;
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'text/event-stream',
|
||
},
|
||
body: JSON.stringify({
|
||
uniacid: store.state.uniacid || '1',
|
||
inputs: {},
|
||
query: message,
|
||
response_mode: 'streaming',
|
||
user_id: store.state.memberInfo?.id || 'anonymous'
|
||
})
|
||
})
|
||
|
||
const reader = response.body.getReader()
|
||
const decoder = new TextDecoder()
|
||
let content = ''
|
||
let buffer = ''
|
||
|
||
// 处理流式数据
|
||
function processStreamData(buffer, callback) {
|
||
const lines = buffer.split('\n');
|
||
buffer = lines.pop() || ''; // 最后一行可能不完整
|
||
|
||
lines.forEach(line => {
|
||
line = line.trim();
|
||
if (!line) return;
|
||
|
||
// 解析 SSE 格式
|
||
if (line.startsWith('data:')) {
|
||
const dataPart = line.slice(5).trim();
|
||
if (dataPart) {
|
||
try {
|
||
const data = JSON.parse(dataPart);
|
||
|
||
if (data.event === 'message') {
|
||
callback(data.answer || '');
|
||
}
|
||
|
||
if (data.conversation_id) {
|
||
conversationId = data.conversation_id;
|
||
}
|
||
|
||
if (data.event === 'message_end') {
|
||
// 对话完成
|
||
console.log('对话完成');
|
||
}
|
||
} catch (error) {
|
||
console.error('解析流式数据失败:', error);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
return buffer;
|
||
}
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
|
||
// 处理接收到的数据
|
||
buffer = processStreamData(buffer, (newData) => {
|
||
if (newData) {
|
||
// 更新 AI 消息
|
||
content += newData;
|
||
if (onChunk) onChunk(newData);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (onComplete) onComplete(content)
|
||
return content
|
||
|
||
} catch (error) {
|
||
console.error('HTTP流式请求失败:', error)
|
||
throw error
|
||
}
|
||
// #endif
|
||
|
||
// 非H5环境使用普通请求模拟流式效果
|
||
// #ifndef H5
|
||
const response = await http.sendRequest({
|
||
...params,
|
||
async: false
|
||
})
|
||
|
||
// 模拟流式效果
|
||
if (response.success && response.data) {
|
||
const content = response.data
|
||
const chunkSize = 3
|
||
let index = 0
|
||
|
||
const streamInterval = setInterval(() => {
|
||
if (index < content.length) {
|
||
const chunk = content.substring(index, index + chunkSize)
|
||
index += chunkSize
|
||
if (onChunk) onChunk(chunk)
|
||
} else {
|
||
clearInterval(streamInterval)
|
||
if (onComplete) onComplete(content)
|
||
}
|
||
}, 100)
|
||
}
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 处理API响应
|
||
*/
|
||
handleResponse(response, options) {
|
||
if (response.code === 0 || response.success) {
|
||
return {
|
||
success: true,
|
||
content: response.data?.answer || response.data?.content || response.data?.reply|| response.data,
|
||
conversationId: response.data?.conversation_id,
|
||
messageId: response.data?.message_id
|
||
}
|
||
} else {
|
||
throw new Error(response.message || 'AI服务返回错误')
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 生成会话ID
|
||
*/
|
||
async generateConversationId() {
|
||
// 构建Dify API请求参数
|
||
const params = {
|
||
url: '/api/kefu/createConversation', // 后端代理接口
|
||
data: {
|
||
uniacid: store.state.uniacid,
|
||
user_id: store.state.memberInfo?.id || 'anonymous',
|
||
member_id: store.state.memberInfo?.id || '',
|
||
},
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
|
||
// 发送请求
|
||
const response = await http.sendRequest({
|
||
...params,
|
||
async: false // 使用Promise方式
|
||
})
|
||
|
||
return this.handleResponse(response, options)
|
||
},
|
||
|
||
/**
|
||
* 获取AI服务状态
|
||
*/
|
||
async getServiceStatus() {
|
||
try {
|
||
// 简单的健康检查
|
||
const response = await http.sendRequest({
|
||
url: '/api/kefu/health',
|
||
async: false,
|
||
data: {}
|
||
})
|
||
|
||
const available = [response?.data?.status, response?.data?.components?.ai_service_config?.status].filter(item => item === 'healthy').length > 0;
|
||
|
||
return {
|
||
available,
|
||
reason: available ? '服务正常' : '服务异常'
|
||
}
|
||
|
||
} catch (error) {
|
||
return {
|
||
available: false,
|
||
reason: '服务检查失败'
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 清除会话历史
|
||
*/
|
||
async clearConversation(conversationId) {
|
||
try {
|
||
const response = await http.sendRequest({
|
||
url: '/api/kefu/clear-conversation',
|
||
data: { conversation_id: conversationId },
|
||
async: false
|
||
})
|
||
|
||
return response.success
|
||
|
||
} catch (error) {
|
||
console.error('清除会话失败:', error)
|
||
return false
|
||
}
|
||
}
|
||
} |