Files
leaudit-platform-frontend/app/services/api.client.ts
T

730 lines
25 KiB
TypeScript

import { CHAT_CONFIG, ContentType, SSE_TIMEOUT } from '../config/chat';
import type { Feedbacktype, ThoughtItem, VisionFile, MessageEnd, MessageReplace } from '../types/dify_chat';
import { unicodeToChar } from '../utils/chat-utils';
// 基础请求选项
const baseOptions = {
method: 'GET',
mode: 'cors' as RequestMode,
credentials: 'omit' as RequestCredentials,
headers: new Headers({
'Content-Type': ContentType.json,
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
}),
redirect: 'follow' as RequestRedirect,
};
// 回调接口定义
export type IOnDataMoreInfo = {
conversationId?: string;
taskId?: string;
messageId: string;
errorMessage?: string;
errorCode?: string;
}
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void;
export type IOnThought = (thought: ThoughtItem) => void;
export type IOnFile = (file: VisionFile) => void;
export type IOnMessageEnd = (messageEnd: MessageEnd) => void;
export type IOnMessageReplace = (messageReplace: MessageReplace) => void;
export type IOnCompleted = (hasError?: boolean) => void;
export type IOnError = (msg: string, code?: string) => void;
// 工作流相关类型
export type WorkflowStartedResponse = {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
workflow_id: string;
sequence_number: number;
created_at: number;
};
}
export type WorkflowFinishedResponse = {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
workflow_id: string;
status: string;
outputs: any;
error: string;
elapsed_time: number;
total_tokens: number;
total_steps: number;
created_at: number;
finished_at: number;
};
}
export type NodeStartedResponse = {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
node_id: string;
node_type: string;
index: number;
predecessor_node_id?: string;
inputs: any;
created_at: number;
extras?: any;
};
}
export type NodeFinishedResponse = {
task_id: string;
workflow_run_id: string;
event: string;
data: {
id: string;
node_id: string;
node_type: string;
index: number;
predecessor_node_id?: string;
inputs: any;
process_data: any;
outputs: any;
status: string;
error: string;
elapsed_time: number;
execution_metadata: {
total_tokens: number;
total_price: number;
currency: string;
};
created_at: number;
};
}
export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void;
export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void;
export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void;
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void;
// 处理流式响应
const handleStream = (
response: Response,
onData: IOnData,
onCompleted?: IOnCompleted,
onThought?: IOnThought,
onMessageEnd?: IOnMessageEnd,
onMessageReplace?: IOnMessageReplace,
onFile?: IOnFile,
onWorkflowStarted?: IOnWorkflowStarted,
onWorkflowFinished?: IOnWorkflowFinished,
onNodeStarted?: IOnNodeStarted,
onNodeFinished?: IOnNodeFinished,
onError?: IOnError,
) => {
// console.log('🌊 [handleStream] 开始处理流式响应:', {
// status: response.status,
// statusText: response.statusText,
// headers: Object.fromEntries(response.headers.entries())
// });
if (!response.ok) {
console.error('❌ [handleStream] 响应错误:', response.status, response.statusText);
onError?.('网络响应错误');
throw new Error('网络响应错误');
}
const reader = response.body?.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let bufferObj: Record<string, any>;
let isFirstMessage = true;
let messageCount = 0;
// console.log('📖 [handleStream] 获取reader:', !!reader);
function read() {
let hasError = false;
reader?.read().then((result: any) => {
// console.log('📨 [handleStream] 读取数据块:', {
// done: result.done,
// valueLength: result.value?.length,
// messageCount: ++messageCount
// });
if (result.done) {
// console.log('✅ [handleStream] 流式响应完成, 总消息数:', messageCount);
onCompleted && onCompleted();
return;
}
const chunk = decoder.decode(result.value, { stream: true });
buffer += chunk;
const lines = buffer.split('\n');
// console.log('🔍 [handleStream] 处理数据块:', {
// chunkLength: chunk.length,
// bufferLength: buffer.length,
// linesCount: lines.length,
// chunk: chunk.substring(0, 100) + (chunk.length > 100 ? '...' : '')
// });
try {
lines.forEach((message, index) => {
if (message.startsWith('data: ')) {
const jsonStr = message.substring(6);
// console.log(`📋 [handleStream] 解析消息 ${index}:`, {
// jsonLength: jsonStr.length,
// preview: jsonStr.substring(0, 200) + (jsonStr.length > 200 ? '...' : '')
// });
try {
bufferObj = JSON.parse(jsonStr) as Record<string, any>;
// console.log('✨ [handleStream] JSON解析成功:', {
// event: bufferObj.event,
// hasAnswer: !!bufferObj.answer,
// answerLength: bufferObj.answer?.length || 0,
// conversationId: bufferObj.conversation_id,
// messageId: bufferObj.id || bufferObj.message_id
// });
}
catch (e) {
console.warn('⚠️ [handleStream] JSON解析失败:', e, 'JSON:', jsonStr);
// 处理消息截断
onData('', isFirstMessage, {
conversationId: bufferObj?.conversation_id,
messageId: bufferObj?.message_id || bufferObj?.id,
});
return;
}
if (bufferObj.status === 400 || !bufferObj.event) {
console.error('❌ [handleStream] 错误响应:', {
status: bufferObj.status,
event: bufferObj.event,
message: bufferObj.message,
code: bufferObj.code
});
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: bufferObj?.message,
errorCode: bufferObj?.code,
});
hasError = true;
onCompleted?.(true);
return;
}
if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
const answer = unicodeToChar(bufferObj.answer);
// console.log('💬 [handleStream] 处理消息事件:', {
// event: bufferObj.event,
// isFirstMessage,
// answerLength: answer.length,
// answer: answer.substring(0, 50) + (answer.length > 50 ? '...' : ''),
// conversationId: bufferObj.conversation_id,
// messageId: bufferObj.id || bufferObj.message_id
// });
onData(answer, isFirstMessage, {
conversationId: bufferObj.conversation_id,
messageId: bufferObj.id || bufferObj.message_id,
taskId: bufferObj.task_id,
});
isFirstMessage = false;
} else if (bufferObj.event === 'agent_thought' && onThought) {
// console.log('🤔 [handleStream] 处理思考事件:', bufferObj.event);
onThought(bufferObj as ThoughtItem);
} else if (bufferObj.event === 'message_file' && onFile) {
// console.log('📁 [handleStream] 处理文件事件:', bufferObj.event);
onFile(bufferObj as VisionFile);
} else if (bufferObj.event === 'message_end' && onMessageEnd) {
// console.log('🏁 [handleStream] 处理消息结束事件:', bufferObj.event);
onMessageEnd(bufferObj as MessageEnd);
} else if (bufferObj.event === 'message_replace' && onMessageReplace) {
// console.log('🔄 [handleStream] 处理消息替换事件:', bufferObj.event);
onMessageReplace(bufferObj as MessageReplace);
} else if (bufferObj.event === 'workflow_started' && onWorkflowStarted) {
// console.log('🚀 [handleStream] 处理工作流开始事件:', bufferObj.event);
onWorkflowStarted(bufferObj as WorkflowStartedResponse);
} else if (bufferObj.event === 'workflow_finished' && onWorkflowFinished) {
// console.log('🎯 [handleStream] 处理工作流完成事件:', bufferObj.event);
onWorkflowFinished(bufferObj as WorkflowFinishedResponse);
} else if (bufferObj.event === 'node_started' && onNodeStarted) {
// console.log('🔗 [handleStream] 处理节点开始事件:', bufferObj.event);
onNodeStarted(bufferObj as NodeStartedResponse);
} else if (bufferObj.event === 'node_finished' && onNodeFinished) {
// console.log('✅ [handleStream] 处理节点完成事件:', bufferObj.event);
onNodeFinished(bufferObj as NodeFinishedResponse);
} else {
// console.log('❓ [handleStream] 未知事件类型:', bufferObj.event);
}
} else if (message.trim()) {
// console.log('📝 [handleStream] 非data消息:', message.substring(0, 100));
}
});
// 保留最后一行(可能是不完整的消息)
const lastLine = lines[lines.length - 1];
buffer = lastLine;
// console.log('💾 [handleStream] 保留缓冲区:', {
// lastLineLength: lastLine.length,
// preview: lastLine.substring(0, 50)
// });
}
catch (err) {
console.error('❌ [handleStream] 解析响应时出错:', err);
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: `${err}`,
});
hasError = true;
onCompleted?.(true);
return;
}
if (!hasError) {
// console.log('🔄 [handleStream] 继续读取下一块...');
read();
} else {
// console.log('🛑 [handleStream] 因错误停止读取');
}
}).catch(err => {
console.error('❌ [handleStream] 读取流时出错:', err);
onError?.(err.message);
});
}
read();
};
// 基础请求函数
const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boolean = false) => {
const options = Object.assign({}, baseOptions, fetchOptions);
// 直接构建Dify API URL
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/${url.replace(/^\//, '')}`;
// 确保Authorization头存在
if (CHAT_CONFIG.API_KEY && options.headers) {
options.headers['Authorization'] = `Bearer ${CHAT_CONFIG.API_KEY}`;
}
const { body } = options;
if (body && typeof body === 'object') {
// 为所有请求添加user参数
const bodyWithUser = {
...body,
user: CHAT_CONFIG.generateUserId(),
};
options.body = JSON.stringify(bodyWithUser);
}
return fetch(urlWithPrefix, options)
.then((res: Response) => {
if (!res.ok) {
console.error('❌ Request failed:', {
status: res.status,
statusText: res.statusText,
url: urlWithPrefix
});
if (res.status === 422) {
return res.text().then(text => {
let errorMessage = text;
try {
const data = JSON.parse(text);
errorMessage = data.message || data.error || text;
} catch (e) {
// 如果不是JSON,使用原始文本
}
throw new Error(errorMessage);
});
}
throw new Error(`${res.status}: ${res.statusText}`);
}
if (needAllResponseContent) {
return res.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
return text;
}
});
}
const data = res.json();
return data;
})
.catch((err) => {
console.error('❌ Request error:', err.message);
throw err;
});
};
// SSE请求处理
export const ssePost = (
url: string,
fetchOptions: any,
{
onData,
onCompleted,
onThought,
onFile,
onMessageEnd,
onMessageReplace,
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onError,
getAbortController,
}: {
onData: IOnData;
onCompleted?: IOnCompleted;
onThought?: IOnThought;
onFile?: IOnFile;
onMessageEnd?: IOnMessageEnd;
onMessageReplace?: IOnMessageReplace;
onError?: IOnError;
getAbortController?: (abortController: AbortController) => void;
onWorkflowStarted?: IOnWorkflowStarted;
onWorkflowFinished?: IOnWorkflowFinished;
onNodeStarted?: IOnNodeStarted;
onNodeFinished?: IOnNodeFinished;
},
) => {
const options = Object.assign({}, baseOptions, {
method: 'POST',
}, fetchOptions);
// 直接构建Dify API URL
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/${url.replace(/^\//, '')}`;
const controller = new AbortController();
if (getAbortController)
getAbortController(controller);
options.headers = {
...options.headers,
'Content-Type': 'application/json',
'Accept': ContentType.stream,
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
};
options.signal = controller.signal;
const { body } = options;
if (body && typeof body === 'object') {
// 为SSE请求添加user参数
const bodyWithUser = {
...body,
user: CHAT_CONFIG.generateUserId(),
};
options.body = JSON.stringify(bodyWithUser);
}
return fetch(urlWithPrefix, options)
.then((res: Response) => {
console.log('📡 SSE Response:', {
status: res.status,
statusText: res.statusText,
url: urlWithPrefix
});
if (!/^(2|3)\d{2}$/.test(res.status.toString())) {
res.json().then((data: any) => {
console.error('❌ SSE Error:', data.message || 'Server Error');
onError?.(data.message || 'Server Error');
});
return;
}
handleStream(
res,
onData,
onCompleted,
onThought,
onMessageEnd,
onMessageReplace,
onFile,
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onError
);
})
.catch((err) => {
console.error('❌ SSE Request Error:', err);
onError?.(err.message);
});
};
// 获取会话列表
export const fetchConversations = async () => {
const user = CHAT_CONFIG.generateUserId();
const params = new URLSearchParams({
user,
limit: '100',
first_id: '',
});
return fetch(`${CHAT_CONFIG.API_URL}/conversations?${params}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to fetch conversations: ${res.status}`);
}
return res.json();
});
};
// 获取聊天消息列表
export const fetchChatList = async (conversationId: string) => {
const user = CHAT_CONFIG.generateUserId();
const params = new URLSearchParams({
user,
conversation_id: conversationId,
limit: '20',
last_id: '',
});
return fetch(`${CHAT_CONFIG.API_URL}/messages?${params}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to fetch chat list: ${res.status}`);
}
return res.json();
});
};
// 获取应用参数
export const fetchAppParams = async () => {
return fetch(`${CHAT_CONFIG.API_URL}/parameters`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to fetch app params: ${res.status}`);
}
return res.json();
});
};
// 更新反馈
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
const messageId = url.split('/').pop(); // 从URL中提取messageId
return fetch(`${CHAT_CONFIG.API_URL}/messages/${messageId}/feedbacks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
body: JSON.stringify({
...body,
user: CHAT_CONFIG.generateUserId(),
}),
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to update feedback: ${res.status}`);
}
return res.json();
});
};
// 生成会话名称
export const generateConversationName = async (id: string) => {
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
body: JSON.stringify({
auto_generate: true,
user: CHAT_CONFIG.generateUserId(),
}),
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to generate conversation name: ${res.status}`);
}
return res.json();
});
};
// 重命名会话
export const renameConversation = async (id: string, name: string, autoGenerate: boolean = false) => {
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}/name`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
body: JSON.stringify({
name: autoGenerate ? undefined : name,
auto_generate: autoGenerate,
user: CHAT_CONFIG.generateUserId(),
}),
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to rename conversation: ${res.status}`);
}
return res.json();
});
};
// 删除会话
export const deleteConversation = async (id: string) => {
return fetch(`${CHAT_CONFIG.API_URL}/conversations/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHAT_CONFIG.API_KEY}`,
},
body: JSON.stringify({
user: CHAT_CONFIG.generateUserId(),
}),
}).then(res => {
if (!res.ok) {
throw new Error(`Failed to delete conversation: ${res.status}`);
}
return res.json();
});
};
// 文件上传
export const upload = (fetchOptions: any): Promise<any> => {
const urlWithPrefix = `${CHAT_CONFIG.API_URL}/files/upload`;
const defaultOptions = {
method: 'POST',
url: urlWithPrefix,
data: {},
};
const options = {
...defaultOptions,
...fetchOptions,
};
return new Promise((resolve, reject) => {
const xhr = options.xhr;
xhr.open(options.method, options.url);
for (const key in options.headers)
xhr.setRequestHeader(key, options.headers[key]);
if (CHAT_CONFIG.API_KEY) {
xhr.setRequestHeader('Authorization', `Bearer ${CHAT_CONFIG.API_KEY}`);
}
// 添加user参数到formData
if (options.data instanceof FormData) {
options.data.append('user', CHAT_CONFIG.generateUserId());
}
xhr.withCredentials = false; // 改为false,因为直接调用Dify API
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200)
resolve({ id: xhr.response });
else
reject(new Error(xhr.responseText || 'Upload failed'));
}
};
xhr.upload.onprogress = options.onprogress;
xhr.send(options.data);
});
};
// 公共请求函数
export const request = (url: string, options = {}, needAllResponseContent = false) => {
return baseFetch(url, { ...baseOptions, ...options }, needAllResponseContent);
};
// GET 请求
export const get = (url: string, options = {}) => {
return request(url, { ...options, method: 'GET' });
};
// POST 请求
export const post = (url: string, options = {}) => {
return request(url, { ...options, method: 'POST' });
};
// PUT 请求
export const put = (url: string, options = {}) => {
return request(url, { ...options, method: 'PUT' });
};
// DELETE 请求
export const del = (url: string, options = {}) => {
return request(url, { ...options, method: 'DELETE' });
};
// 发送聊天消息
export const sendChatMessage = async (
body: Record<string, any>,
{
onData,
onCompleted,
onThought,
onFile,
onError,
getAbortController,
onMessageEnd,
onMessageReplace,
onWorkflowStarted,
onNodeStarted,
onNodeFinished,
onWorkflowFinished,
}: {
onData: IOnData;
onCompleted: IOnCompleted;
onFile?: IOnFile;
onThought?: IOnThought;
onMessageEnd?: IOnMessageEnd;
onMessageReplace?: IOnMessageReplace;
onError?: IOnError;
getAbortController?: (abortController: AbortController) => void;
onWorkflowStarted?: IOnWorkflowStarted;
onNodeStarted?: IOnNodeStarted;
onNodeFinished?: IOnNodeFinished;
onWorkflowFinished?: IOnWorkflowFinished;
},
) => {
return ssePost('chat-messages', {
body: {
...body,
response_mode: 'streaming',
},
}, {
onData,
onCompleted,
onThought,
onFile,
onError,
getAbortController,
onMessageEnd,
onMessageReplace,
onNodeStarted,
onWorkflowStarted,
onWorkflowFinished,
onNodeFinished
});
};