Files
schoolNews/schoolNewsWeb/src/apis/ai/chat.ts
2025-12-01 14:56:14 +08:00

475 lines
15 KiB
TypeScript
Raw 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.

/**
* @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) => {
// 使用配置的baseURLSSE流式推送
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;
}
};