475 lines
15 KiB
TypeScript
475 lines
15 KiB
TypeScript
/**
|
||
* @description AI对话相关API
|
||
* @author AI Assistant
|
||
* @since 2025-11-04
|
||
*/
|
||
|
||
import { api } from '@/apis/index';
|
||
import { API_BASE_URL } from '@/config';
|
||
import type {
|
||
AiConversation,
|
||
AiMessage,
|
||
ChatRequest,
|
||
ResultDomain,
|
||
StreamCallback
|
||
} from '@/types';
|
||
|
||
/**
|
||
* AI对话API服务
|
||
*/
|
||
export const chatApi = {
|
||
/**
|
||
* 流式对话(SSE)- 两步法:POST准备 + GET建立SSE
|
||
* @param request 对话请求
|
||
* @param callback 流式回调
|
||
* @returns Promise<ResultDomain<AiMessage>>
|
||
*/
|
||
async streamChat(request: ChatRequest, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
||
return new Promise((resolve, reject) => {
|
||
// 使用IIFE包装async逻辑,避免Promise executor是async的警告
|
||
(async () => {
|
||
try {
|
||
const token = localStorage.getItem('token');
|
||
const tokenData = token ? JSON.parse(token).value : '';
|
||
// 第1步:POST准备会话,获取sessionId
|
||
const prepareResponse = await api.post<string>('/ai/chat/stream/prepare', {
|
||
agentId: request.agentId,
|
||
conversationId: request.conversationId || '',
|
||
query: request.query,
|
||
files: request.files || []
|
||
}, {
|
||
showLoading: false
|
||
});
|
||
|
||
if (!prepareResponse.data.success || !prepareResponse.data.data) {
|
||
throw new Error(prepareResponse.data.message || '准备会话失败');
|
||
}
|
||
|
||
const sessionId = prepareResponse.data.data;
|
||
console.log('[会话创建成功] sessionId:', sessionId);
|
||
|
||
// 第2步:GET建立SSE连接
|
||
const eventSource = new EventSource(
|
||
`${API_BASE_URL}/ai/chat/stream?sessionId=${sessionId}&token=${tokenData}`
|
||
);
|
||
|
||
// 通知外部EventSource已创建
|
||
callback?.onStart?.(eventSource);
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
let fullMessage = ''; // 累积完整消息内容
|
||
|
||
// 监听初始化事件(包含messageId和conversationId)
|
||
eventSource.addEventListener('init', (event) => {
|
||
try {
|
||
const initData = JSON.parse(event.data);
|
||
console.log('[初始化数据]', initData);
|
||
// 通知外部保存messageId(用于停止生成)
|
||
if (callback?.onInit) {
|
||
callback.onInit(initData);
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析init事件失败:', event.data);
|
||
}
|
||
});
|
||
|
||
// 监听标准消息事件
|
||
eventSource.addEventListener('message', (event) => {
|
||
try {
|
||
// 解析JSON字符串,处理Unicode转义
|
||
const data = JSON.parse(event.data);
|
||
fullMessage += data;
|
||
callback?.onMessage?.(data);
|
||
} catch (e) {
|
||
// 如果不是JSON,直接使用原始数据
|
||
const data = event.data;
|
||
fullMessage += data;
|
||
callback?.onMessage?.(data);
|
||
}
|
||
});
|
||
|
||
// 监听结束事件
|
||
eventSource.addEventListener('end', (event) => {
|
||
const metadata = JSON.parse(event.data);
|
||
callback?.onMessageEnd?.(metadata);
|
||
eventSource.close();
|
||
|
||
resolve({
|
||
code: 200,
|
||
success: true,
|
||
login: true,
|
||
auth: true,
|
||
data: metadata as AiMessage,
|
||
message: '对话成功'
|
||
});
|
||
});
|
||
|
||
// 监听所有Dify原始事件(workflow_started, node_started等)
|
||
const difyEventTypes = [
|
||
'dify_workflow_started',
|
||
'dify_node_started',
|
||
'dify_node_finished',
|
||
'dify_workflow_finished',
|
||
'dify_message',
|
||
'dify_agent_message',
|
||
'dify_message_end',
|
||
'dify_message_file',
|
||
'dify_agent_thought',
|
||
'dify_ping'
|
||
];
|
||
|
||
difyEventTypes.forEach(eventType => {
|
||
eventSource.addEventListener(eventType, (event: any) => {
|
||
try {
|
||
const eventData = JSON.parse(event.data);
|
||
|
||
// 调用自定义的Dify事件回调
|
||
if (callback?.onDifyEvent) {
|
||
const cleanEventType = eventType.replace('dify_', '');
|
||
callback.onDifyEvent(cleanEventType, eventData);
|
||
}
|
||
} catch (e) {
|
||
console.warn(`解析Dify事件失败 ${eventType}:`, event.data);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 监听错误事件
|
||
eventSource.addEventListener('error', (event: any) => {
|
||
// 如果 data 为空,说明是正常的流结束信号(有些工作流不发送end事件)
|
||
if (!event.data || event.data.trim() === '') {
|
||
console.log('[SSE流结束] 收到空error事件,当作正常结束处理');
|
||
|
||
// 触发结束回调(如果有的话)
|
||
callback?.onMessageEnd?.({
|
||
conversationId: request.conversationId,
|
||
messageId: '',
|
||
answer: fullMessage
|
||
} as any);
|
||
|
||
eventSource.close();
|
||
|
||
// 正常结束
|
||
resolve({
|
||
code: 200,
|
||
success: true,
|
||
login: true,
|
||
auth: true,
|
||
data: {
|
||
conversationId: request.conversationId,
|
||
answer: fullMessage
|
||
} as any,
|
||
message: '对话已结束'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const error = new Error(event.data);
|
||
callback?.onError?.(error);
|
||
eventSource.close();
|
||
reject(error);
|
||
});
|
||
|
||
eventSource.onerror = (error) => {
|
||
callback?.onError?.(error as unknown as Error);
|
||
eventSource.close();
|
||
reject(error);
|
||
};
|
||
} catch (error) {
|
||
console.error('流式对话失败:', error);
|
||
callback?.onError?.(error as Error);
|
||
reject(error);
|
||
}
|
||
})(); // 立即执行IIFE
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 阻塞式对话(非流式)
|
||
* @param request 对话请求
|
||
* @returns Promise<ResultDomain<AiMessage>>
|
||
*/
|
||
async blockingChat(request: ChatRequest): Promise<ResultDomain<AiMessage>> {
|
||
const response = await api.post<AiMessage>('/ai/chat/blocking', request);
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 停止对话生成(通过消息ID)
|
||
* @param messageId 消息ID
|
||
* @returns Promise<ResultDomain<boolean>>
|
||
*/
|
||
async stopChat(messageId: string): Promise<ResultDomain<boolean>> {
|
||
const response = await api.post<boolean>(`/ai/chat/stop/${messageId}`);
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 停止对话生成(通过Dify TaskID)
|
||
* @param taskId Dify任务ID
|
||
* @param agentId 智能体ID
|
||
* @returns Promise<ResultDomain<boolean>>
|
||
*/
|
||
async stopChatByTaskId(taskId: string, agentId: string): Promise<ResultDomain<boolean>> {
|
||
const response = await api.post<boolean>('/ai/chat/stop-by-taskid', {
|
||
taskId,
|
||
agentId
|
||
});
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 创建新会话
|
||
* @param agentId 智能体ID
|
||
* @param title 会话标题(可选)
|
||
* @returns Promise<ResultDomain<AiConversation>>
|
||
*/
|
||
async createConversation(agentId: string, title?: string): Promise<ResultDomain<AiConversation>> {
|
||
const response = await api.post<AiConversation>('/ai/chat/conversation', {
|
||
agentId,
|
||
title
|
||
}, {
|
||
showLoading: false
|
||
});
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 获取会话信息
|
||
* @param conversationId 会话ID
|
||
* @returns Promise<ResultDomain<AiConversation>>
|
||
*/
|
||
async getConversation(conversationId: string): Promise<ResultDomain<AiConversation>> {
|
||
const response = await api.get<AiConversation>(`/ai/chat/conversation/${conversationId}`);
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 更新会话
|
||
* @param conversation 会话信息
|
||
* @returns Promise<ResultDomain<AiConversation>>
|
||
*/
|
||
async updateConversation(conversation: AiConversation): Promise<ResultDomain<AiConversation>> {
|
||
const response = await api.put<AiConversation>('/ai/chat/conversation', conversation, {
|
||
showLoading: false
|
||
});
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 删除会话
|
||
* @param conversationId 会话ID
|
||
* @returns Promise<ResultDomain<boolean>>
|
||
*/
|
||
async deleteConversation(conversationId: string): Promise<ResultDomain<boolean>> {
|
||
const response = await api.delete<boolean>(`/ai/chat/conversation/${conversationId}`,{}, {
|
||
showLoading: false
|
||
});
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 获取用户的会话列表
|
||
* @param agentId 智能体ID(可选)
|
||
* @returns Promise<ResultDomain<AiConversation[]>>
|
||
*/
|
||
async listUserConversations(agentId?: string): Promise<ResultDomain<AiConversation[]>> {
|
||
const response = await api.get<AiConversation[]>('/ai/chat/conversations', {
|
||
params: { agentId }
|
||
});
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 获取会话的消息列表
|
||
* @param conversationId 会话ID
|
||
* @returns Promise<ResultDomain<AiMessage[]>>
|
||
*/
|
||
async listMessages(conversationId: string): Promise<ResultDomain<AiMessage>> {
|
||
const response = await api.get<AiMessage>(`/ai/chat/conversation/${conversationId}/messages`, {},{
|
||
showLoading: false
|
||
});
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 获取单条消息
|
||
* @param messageId 消息ID
|
||
* @returns Promise<ResultDomain<AiMessage>>
|
||
*/
|
||
async getMessage(messageId: string): Promise<ResultDomain<AiMessage>> {
|
||
const response = await api.get<AiMessage>(`/ai/chat/message/${messageId}`);
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 重新生成回答(SSE流式)
|
||
* @param messageId 原消息ID
|
||
* @param callback 流式回调
|
||
* @returns Promise<ResultDomain<AiMessage>>
|
||
*/
|
||
async regenerateAnswer(messageId: string, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
||
const token = localStorage.getItem('token');
|
||
const tokenData = token ? JSON.parse(token) : null;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// 使用配置的baseURL,SSE流式推送
|
||
const eventSource = new EventSource(
|
||
`${API_BASE_URL}/ai/chat/regenerate/${messageId}?` +
|
||
new URLSearchParams({
|
||
token: tokenData?.value || ''
|
||
})
|
||
);
|
||
|
||
// 通知外部EventSource已创建
|
||
callback?.onStart?.(eventSource);
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
let fullMessage = ''; // 累积完整消息内容
|
||
|
||
// 监听初始化事件(包含messageId和conversationId)
|
||
eventSource.addEventListener('init', (event) => {
|
||
try {
|
||
const initData = JSON.parse(event.data);
|
||
console.log('[初始化数据-重新生成]', initData);
|
||
// 通知外部保存messageId(用于停止生成)
|
||
if (callback?.onInit) {
|
||
callback.onInit(initData);
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析init事件失败:', event.data);
|
||
}
|
||
});
|
||
|
||
// 监听标准消息事件
|
||
eventSource.addEventListener('message', (event) => {
|
||
try {
|
||
// 解析JSON字符串,处理Unicode转义
|
||
const data = JSON.parse(event.data);
|
||
fullMessage += data;
|
||
callback?.onMessage?.(data);
|
||
} catch (e) {
|
||
// 如果不是JSON,直接使用原始数据
|
||
const data = event.data;
|
||
fullMessage += data;
|
||
callback?.onMessage?.(data);
|
||
}
|
||
});
|
||
|
||
// 监听结束事件
|
||
eventSource.addEventListener('end', (event) => {
|
||
const metadata = JSON.parse(event.data);
|
||
callback?.onMessageEnd?.(metadata);
|
||
eventSource.close();
|
||
|
||
resolve({
|
||
code: 200,
|
||
success: true,
|
||
login: true,
|
||
auth: true,
|
||
data: metadata as AiMessage,
|
||
message: '重新生成成功'
|
||
});
|
||
});
|
||
|
||
// 监听所有Dify原始事件(workflow_started, node_started等)
|
||
const difyEventTypes = [
|
||
'dify_workflow_started',
|
||
'dify_node_started',
|
||
'dify_node_finished',
|
||
'dify_workflow_finished',
|
||
'dify_message',
|
||
'dify_agent_message',
|
||
'dify_message_end',
|
||
'dify_message_file',
|
||
'dify_agent_thought',
|
||
'dify_ping'
|
||
];
|
||
|
||
difyEventTypes.forEach(eventType => {
|
||
eventSource.addEventListener(eventType, (event: any) => {
|
||
try {
|
||
const eventData = JSON.parse(event.data);
|
||
// 调用自定义的Dify事件回调
|
||
if (callback?.onDifyEvent) {
|
||
const cleanEventType = eventType.replace('dify_', '');
|
||
callback.onDifyEvent(cleanEventType, eventData);
|
||
}
|
||
} catch (e) {
|
||
console.warn(`解析Dify事件失败 ${eventType}:`, event.data);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 监听错误事件
|
||
eventSource.addEventListener('error', (event: any) => {
|
||
// 如果 data 为空,说明是正常的流结束信号(有些工作流不发送end事件)
|
||
if (!event.data || event.data.trim() === '') {
|
||
console.log('[SSE流结束-重新生成] 收到空error事件,当作正常结束处理');
|
||
|
||
// 触发结束回调(如果有的话)
|
||
callback?.onMessageEnd?.({
|
||
messageId: messageId,
|
||
answer: fullMessage
|
||
} as any);
|
||
|
||
eventSource.close();
|
||
|
||
// 正常结束
|
||
resolve({
|
||
code: 200,
|
||
success: true,
|
||
login: true,
|
||
auth: true,
|
||
data: {
|
||
messageId: messageId,
|
||
answer: fullMessage
|
||
} as any,
|
||
message: '重新生成已结束'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const error = new Error(event.data);
|
||
callback?.onError?.(error);
|
||
eventSource.close();
|
||
reject(error);
|
||
});
|
||
|
||
eventSource.onerror = (error) => {
|
||
callback?.onError?.(error as unknown as Error);
|
||
eventSource.close();
|
||
reject(error);
|
||
};
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 异步生成会话摘要
|
||
* @param conversationId 会话ID
|
||
* @returns Promise<ResultDomain<boolean>>
|
||
*/
|
||
async generateSummary(conversationId: string): Promise<ResultDomain<boolean>> {
|
||
const response = await api.post<boolean>(`/ai/chat/conversation/${conversationId}/summary`);
|
||
return response.data;
|
||
},
|
||
|
||
/**
|
||
* 评价消息
|
||
* @param messageId 消息ID
|
||
* @param rating 评分(1=好评,-1=差评,0=取消评价)
|
||
* @param feedback 反馈内容(可选)
|
||
* @returns Promise<ResultDomain<boolean>>
|
||
*/
|
||
async rateMessage(messageId: string, rating: number, feedback?: string): Promise<ResultDomain<boolean>> {
|
||
const response = await api.post<boolean>(`/ai/chat/message/${messageId}/rate`, {
|
||
rating,
|
||
feedback
|
||
}, {
|
||
showLoading: false
|
||
});
|
||
return response.data;
|
||
}
|
||
};
|
||
|