Files
AIEC-new/AIEC-server/services/api/chat-service.js
2025-10-17 09:31:28 +08:00

691 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 聊天API服务 - 对接新的 oneapi_replace 后端API
* 使用 /retrieve 接口进行问答
*/
class ChatService {
constructor() {
// 使用本地开发服务地址
// 生产环境: 'http://101.200.154.78:8081'
// 本地开发: 'http://localhost:8000'
this.baseURL = 'http://localhost:8000';
// 生成会话ID
this.sessionId = this.generateSessionId();
// 存储会话历史(本地管理)
this.conversations = new Map();
}
/**
* 生成会话ID
*/
generateSessionId() {
// 使用UUID格式
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0;
var v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 发送API请求的通用方法
*/
async request(endpoint, options = {}) {
// 保留token处理用于其他需要认证的服务
const token = window.TokenManager ? window.TokenManager.getToken() : null;
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
// 新后端不需要Authorization header但保留以备其他服务使用
// 'Authorization': token ? `Bearer ${token}` : ''
}
};
const config = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
};
try {
const response = await fetch(`${this.baseURL}${endpoint}`, config);
// 处理空响应如204 No Content
if (response.status === 204) {
return { success: true };
}
const data = await response.json();
if (!response.ok) {
// 保留401错误处理用于其他需要认证的服务
if (response.status === 401 && window.TokenManager) {
window.TokenManager.clearAll();
window.location.href = '/auth/pages/login.html';
throw new Error('认证失败,请重新登录');
}
throw new Error(data.message || data.detail || `请求失败: ${response.status}`);
}
return data;
} catch (error) {
console.error('Chat API request error:', error);
throw error;
}
}
/**
* 发送消息(同步方式)
* @param {object} messageData - 消息数据
* @returns {Promise} API响应
*/
async sendMessage(messageData) {
try {
// 转换为新后端的请求格式
const isDeepResearch = messageData.mode === 'research' || messageData.deepResearch;
// 生产环境使用 /query 接口,参数更简单
const response = await this.request('/query', {
method: 'POST',
body: JSON.stringify({
query: messageData.message,
mode: isDeepResearch ? 'complex' : '0', // 自动判断或复杂模式
save_output: false // 不保存到文件
})
});
// 保存到本地会话历史
const conversationId = messageData.conversationId || this.sessionId;
if (!this.conversations.has(conversationId)) {
this.conversations.set(conversationId, []);
}
const userMessage = {
role: 'user',
content: messageData.message,
timestamp: new Date().toISOString()
};
const assistantMessage = {
role: 'assistant',
content: response.answer,
timestamp: new Date().toISOString(),
metadata: response.metadata
};
this.conversations.get(conversationId).push(userMessage, assistantMessage);
// 转换响应格式以适配前端
return {
code: 200,
data: {
conversationId: conversationId,
messageId: Date.now().toString(),
content: response.answer,
reference: response.supporting_facts,
events: response.supporting_events,
metadata: response.metadata
},
message: '成功'
};
} catch (error) {
console.error('Send message failed:', error);
throw error;
}
}
/**
* 发送消息(真正的流式方式)
* @param {object} messageData - 消息数据
* @param {function} onMessage - 消息回调函数
* @param {function} onComplete - 完成回调函数
* @param {function} onError - 错误回调函数
* @param {function} onThinking - 思考过程回调函数
* @param {function} onStatus - 状态更新回调函数(新增)
* @returns {AbortController} 可用于取消请求的控制器
*/
sendMessageStream(messageData, onMessage, onComplete, onError, onThinking = null, onStatus = null) {
const abortController = new AbortController();
// 保存原始的abort方法
const originalAbort = abortController.abort.bind(abortController);
// 启动异步流处理但立即返回AbortController
this._startStream(abortController, originalAbort, messageData, onMessage, onComplete, onError, onThinking, onStatus);
// 立即返回AbortController不等待异步处理
return abortController;
}
async _startStream(abortController, originalAbort, messageData, onMessage, onComplete, onError, onThinking, onStatus) {
// 检查是否有流式端点
const streamEndpoint = '/retrieve/stream';
const hasStreamSupport = await this.checkStreamSupport();
if (!hasStreamSupport) {
// 降级到原有的伪流式实现
return this.sendMessageStreamLegacy(messageData, onMessage, onComplete, onError, onThinking);
}
try {
const isDeepResearch = messageData.deepResearch || false;
// 构建请求body
const requestBody = {
query: messageData.message,
mode: isDeepResearch ? 'complex' : '0',
use_cache: true,
save_output: false
};
// 使用fetch处理SSE
const response = await fetch(`${this.baseURL}${streamEndpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify(requestBody),
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 读取流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let accumulatedAnswer = '';
let supportingFacts = [];
let supportingEvents = [];
let metadata = {};
let taskId = null; // 任务ID
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留未完成的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
continue;
}
try {
const event = JSON.parse(data);
switch (event.type) {
case 'task_created':
// 保存任务ID
taskId = event.task_id;
console.log('[ChatService] Task created:', taskId);
// 重写abort方法添加取消任务的逻辑
abortController.abort = () => {
// 发送取消请求到后端
if (taskId) {
fetch(`${this.baseURL}/task/cancel/${taskId}`, {
method: 'POST'
}).catch(err => {
console.log('[ChatService] Cancel request failed:', err);
});
}
// 执行原始的abort
originalAbort();
};
break;
case 'cache_hit':
if (onStatus) {
onStatus({
type: 'cache',
message: event.message,
cached: true
});
}
break;
case 'cache_miss':
if (onStatus) {
onStatus({
type: 'cache',
message: event.message,
cached: false
});
}
break;
case 'starting':
case 'complexity_check':
case 'documents':
case 'sufficiency_check':
case 'sub_queries':
case 'iteration':
case 'generating':
if (onStatus) {
onStatus(event);
}
if (onThinking && event.message) {
onThinking(event.message + '\n');
}
break;
case 'answer_chunk':
const chunk = event.data?.text || '';
accumulatedAnswer += chunk;
if (onMessage) {
onMessage(chunk);
}
break;
case 'supporting_info':
supportingFacts = event.data?.supporting_facts || [];
supportingEvents = event.data?.supporting_events || [];
break;
case 'answer': // rag_api_server_production.py 使用这个类型
// 处理最终答案
if (event.data?.content) {
accumulatedAnswer = event.data.content;
}
supportingFacts = event.data?.supporting_facts || supportingFacts;
supportingEvents = event.data?.supporting_events || supportingEvents;
metadata = event.data || {};
// 触发完成状态
if (onStatus) {
onStatus({
type: 'complete',
message: event.message || '✅ 答案生成完成'
});
}
break;
case 'complete':
const finalAnswer = event.data?.answer || accumulatedAnswer;
metadata = event.data || {};
// 触发完成状态
if (onStatus) {
onStatus({
type: 'complete',
message: '✅ 答案生成完成'
});
}
break;
case 'cached':
if (onStatus) {
onStatus({
type: 'cached',
message: event.message
});
}
break;
case 'cancelled':
// 任务被取消
if (onStatus) {
onStatus({
type: 'cancelled',
message: event.message || '任务已被取消'
});
}
if (onError) {
onError(new Error(event.message || '任务已被取消'));
}
return abortController; // 提前返回
break;
case 'error':
throw new Error(event.message || '检索过程出错');
}
} catch (e) {
console.warn('Failed to parse SSE event:', e);
}
}
}
}
// 保存到会话历史
const conversationId = messageData.conversationId || this.sessionId;
if (!this.conversations.has(conversationId)) {
this.conversations.set(conversationId, []);
}
const userMessage = {
role: 'user',
content: messageData.message,
timestamp: new Date().toISOString()
};
const assistantMessage = {
role: 'assistant',
content: accumulatedAnswer,
timestamp: new Date().toISOString(),
metadata: metadata,
supporting_facts: supportingFacts,
supporting_events: supportingEvents
};
this.conversations.get(conversationId).push(userMessage, assistantMessage);
// 完成回调
const result = {
conversationId: conversationId,
messageId: Date.now().toString(),
content: accumulatedAnswer,
reference: supportingFacts,
events: supportingEvents,
metadata: metadata
};
if (onComplete) {
onComplete(result);
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Stream aborted');
} else {
console.error('Stream error:', error);
onError(error);
}
}
}
/**
* 原有的伪流式实现(作为降级方案)
*/
async sendMessageStreamLegacy(messageData, onMessage, onComplete, onError, onThinking = null) {
const abortController = new AbortController();
try {
const isDeepResearch = messageData.deepResearch || false;
const showThinking = messageData.showThinking || false;
if (showThinking && onThinking) {
onThinking('正在分析问题复杂度...\n');
}
const response = await this.request('/retrieve', {
method: 'POST',
signal: abortController.signal,
body: JSON.stringify({
query: messageData.message,
mode: isDeepResearch ? 'complex' : '0',
save_output: false
})
});
// 显示思考过程
if (showThinking && onThinking && response.metadata) {
const metadata = response.metadata;
let thinkingText = '';
if (metadata.complexity_level) {
thinkingText += `**问题复杂度**: ${metadata.complexity_level}\n`;
thinkingText += `**置信度**: ${(metadata.complexity_confidence * 100).toFixed(1)}%\n`;
}
if (thinkingText) {
onThinking(thinkingText);
}
}
// 保存到会话历史
const conversationId = messageData.conversationId || this.sessionId;
if (!this.conversations.has(conversationId)) {
this.conversations.set(conversationId, []);
}
this.conversations.get(conversationId).push(
{ role: 'user', content: messageData.message, timestamp: new Date().toISOString() },
{ role: 'assistant', content: response.answer, timestamp: new Date().toISOString() }
);
// 模拟流式输出
const chunks = this.splitIntoChunks(response.answer, 20);
for (const chunk of chunks) {
if (abortController.signal.aborted) break;
onMessage(chunk);
await new Promise(resolve => setTimeout(resolve, 30));
}
onComplete({
conversationId: conversationId,
messageId: Date.now().toString(),
content: response.answer,
reference: response.supporting_facts,
events: response.supporting_events,
metadata: response.metadata
});
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Legacy stream error:', error);
onError(error);
}
}
return abortController;
}
/**
* 检查是否支持流式端点
*/
async checkStreamSupport() {
try {
// 直接返回true因为我们知道端点存在
// 避免OPTIONS请求导致的405错误
return true;
} catch (error) {
return false;
}
}
/**
* 将文本分割成小块以模拟流式输出
*/
splitIntoChunks(text, chunkSize = 20) {
const chunks = [];
for (let i = 0; i < text.length; i += chunkSize) {
chunks.push(text.slice(i, i + chunkSize));
}
return chunks;
}
/**
* 获取用户的会话列表
* @returns {Promise} API响应
*/
async getConversations() {
// 从localStorage和内存中获取会话列表
try {
const conversations = [];
const chatManager = window.chatManager;
// 从chatManager获取
if (chatManager && chatManager.chats) {
for (const [id, chat] of chatManager.chats) {
conversations.push({
id: id,
title: chat.title || '新对话',
createdAt: chat.createdAt || new Date().toISOString(),
updatedAt: chat.updatedAt || new Date().toISOString()
});
}
}
// 也包含当前服务中的会话
for (const [id, messages] of this.conversations) {
if (!conversations.find(c => c.id === id)) {
const firstMessage = messages.find(m => m.role === 'user');
conversations.push({
id: id,
title: firstMessage ? firstMessage.content.substring(0, 30) + '...' : '新对话',
createdAt: messages[0]?.timestamp || new Date().toISOString(),
updatedAt: messages[messages.length - 1]?.timestamp || new Date().toISOString()
});
}
}
return {
success: true,
conversations: conversations
};
} catch (error) {
console.error('Get conversations failed:', error);
return {
success: true,
conversations: []
};
}
}
/**
* 获取会话的消息历史
* @param {string|number} conversationId - 会话ID
* @returns {Promise} API响应
*/
async getConversationMessages(conversationId) {
// 从localStorage和内存中获取消息历史
try {
// 首先尝试从内存中获取
if (this.conversations.has(conversationId)) {
const messages = this.conversations.get(conversationId);
return {
success: true,
messages: messages.map((msg, index) => ({
id: msg.id || index.toString(),
role: msg.role,
content: msg.content,
timestamp: msg.timestamp
}))
};
}
// 然后尝试从chatManager获取
const chatManager = window.chatManager;
if (chatManager && chatManager.chats && chatManager.chats.has(conversationId)) {
const chat = chatManager.chats.get(conversationId);
const messages = chat.messages || [];
return {
success: true,
messages: messages.map((msg, index) => ({
id: msg.id || index.toString(),
role: msg.role,
content: msg.content,
timestamp: msg.timestamp
}))
};
}
return {
success: true,
messages: []
};
} catch (error) {
console.error('Get conversation messages failed:', error);
return {
success: true,
messages: []
};
}
}
/**
* 创建新会话(通过发送第一条消息)
* @param {string} message - 第一条消息
* @param {string} mode - 聊天模式
* @returns {Promise} API响应
*/
async createConversation(message, mode = 'chat') {
// 生成新的会话ID
const newSessionId = this.generateSessionId();
// 使用新会话ID发送消息
return this.sendMessage({
message: message,
conversationId: newSessionId,
mode: mode
});
}
/**
* 删除会话
* @param {string|number} conversationId - 会话ID
* @returns {Promise} API响应
*/
async deleteConversation(conversationId) {
// 从内存中删除
this.conversations.delete(conversationId);
// 如果chatManager存在也从中删除
if (window.chatManager && window.chatManager.chats) {
window.chatManager.chats.delete(conversationId);
}
return {
success: true,
message: '会话已删除'
};
}
/**
* 检查服务状态
* @returns {Promise<boolean>} 服务是否可用
*/
async checkServiceStatus() {
try {
// 新后端使用 /health 接口进行健康检查
const response = await fetch(`${this.baseURL}/health`);
return response.ok;
} catch (error) {
console.error('Service check failed:', error);
return false;
}
}
/**
* 清除会话历史
* @param {string} sessionId - 会话ID
* @returns {Promise} API响应
*/
async clearConversation(sessionId) {
try {
// 清除本地会话历史
if (this.conversations.has(sessionId)) {
this.conversations.set(sessionId, []);
}
return {
success: true,
message: '会话已清除'
};
} catch (error) {
console.error('Clear conversation failed:', error);
throw error;
}
}
}
// 创建并导出服务实例
try {
window.ChatAPIService = new ChatService();
console.log('ChatAPIService 创建成功 (新后端):', window.ChatAPIService);
console.log('后端地址:', window.ChatAPIService.baseURL);
} catch (error) {
console.error('ChatAPIService 创建失败:', error);
}