Files
leaudit-platform-frontend/app/services/api.client.ts
T
2025-06-04 11:18:52 +08:00

580 lines
18 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.
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: 'include' as RequestCredentials,
headers: new Headers({
'Content-Type': ContentType.json,
}),
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,
) => {
if (!response.ok) {
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;
function read() {
let hasError = false;
reader?.read().then((result: any) => {
if (result.done) {
onCompleted && onCompleted();
return;
}
buffer += decoder.decode(result.value, { stream: true });
const lines = buffer.split('\n');
try {
lines.forEach((message) => {
if (message.startsWith('data: ')) {
try {
bufferObj = JSON.parse(message.substring(6)) as Record<string, any>;
}
catch (e) {
// 处理消息截断
onData('', isFirstMessage, {
conversationId: bufferObj?.conversation_id,
messageId: bufferObj?.message_id || bufferObj?.id,
});
return;
}
if (bufferObj.status === 400 || !bufferObj.event) {
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: bufferObj?.message,
errorCode: bufferObj?.code,
});
hasError = true;
onCompleted?.(true);
return;
}
if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
onData(unicodeToChar(bufferObj.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) {
onThought(bufferObj as ThoughtItem);
} else if (bufferObj.event === 'message_file' && onFile) {
onFile(bufferObj as VisionFile);
} else if (bufferObj.event === 'message_end' && onMessageEnd) {
onMessageEnd(bufferObj as MessageEnd);
} else if (bufferObj.event === 'message_replace' && onMessageReplace) {
onMessageReplace(bufferObj as MessageReplace);
} else if (bufferObj.event === 'workflow_started' && onWorkflowStarted) {
onWorkflowStarted(bufferObj as WorkflowStartedResponse);
} else if (bufferObj.event === 'workflow_finished' && onWorkflowFinished) {
onWorkflowFinished(bufferObj as WorkflowFinishedResponse);
} else if (bufferObj.event === 'node_started' && onNodeStarted) {
onNodeStarted(bufferObj as NodeStartedResponse);
} else if (bufferObj.event === 'node_finished' && onNodeFinished) {
onNodeFinished(bufferObj as NodeFinishedResponse);
}
}
});
// 保留最后一行(可能是不完整的消息)
buffer = lines[lines.length - 1];
}
catch (err) {
console.error('解析响应时出错:', err);
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: `${err}`,
});
hasError = true;
onCompleted?.(true);
return;
}
if (!hasError)
read();
}).catch(err => {
console.error('读取流时出错:', err);
onError?.(err.message);
});
}
read();
};
// 基础Fetch函数
const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boolean = false) => {
const options = Object.assign({}, baseOptions, fetchOptions);
// 构建完整URL - 修复重复的/v1问题
// CHAT_CONFIG.API_URL 已经包含了 /v1,所以不需要再添加 API_PREFIX
let urlWithPrefix = `${CHAT_CONFIG.API_URL}/${url.replace(/^\//, '')}`;
const { method, params, body } = options;
// 处理GET请求的查询参数
if (method === 'GET' && params) {
const paramsArray: string[] = [];
Object.keys(params).forEach(key =>
paramsArray.push(`${key}=${encodeURIComponent(params[key])}`),
);
if (urlWithPrefix.search(/\?/) === -1)
urlWithPrefix += `?${paramsArray.join('&')}`;
else
urlWithPrefix += `&${paramsArray.join('&')}`;
delete options.params;
}
// 处理请求体
if (body && typeof body === 'object')
options.body = JSON.stringify(body);
// 添加认证头
if (!options.headers)
options.headers = {};
if (CHAT_CONFIG.API_KEY) {
options.headers['Authorization'] = `Bearer ${CHAT_CONFIG.API_KEY}`;
}
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, SSE_TIMEOUT);
}),
new Promise((resolve, reject) => {
fetch(urlWithPrefix, options)
.then((res: Response) => {
const resClone = res.clone();
('📥 API Response:', {
status: res.status,
statusText: res.statusText,
url: urlWithPrefix
});
// 错误处理
if (!/^(2|3)\d{2}$/.test(res.status.toString())) {
try {
const bodyJson = res.json();
switch (res.status) {
case 401:
console.error('❌ Invalid token');
break;
default:
bodyJson.then((data: any) => {
console.error('❌ API Error:', data.message);
});
}
}
catch (e) {
console.error('❌ Response Error:', e);
}
return Promise.reject(resClone);
}
// 处理删除API204状态码)
if (res.status === 204) {
resolve({ result: 'success' });
return;
}
// 返回数据
const contentType = res.headers.get('Content-Type') || '';
const data = contentType.includes('application/octet-stream') ? res.blob() : res.json();
resolve(needAllResponseContent ? resClone : data);
})
.catch((err) => {
console.error('❌ Fetch Error:', err);
reject(err);
});
}),
]);
};
// SSE POST 请求
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);
// 修复URL构建逻辑,与baseFetch保持一致
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,
};
if (CHAT_CONFIG.API_KEY) {
options.headers['Authorization'] = `Bearer ${CHAT_CONFIG.API_KEY}`;
}
options.signal = controller.signal;
const { body } = options;
if (body && typeof body === 'object')
options.body = JSON.stringify(body);
return fetch(urlWithPrefix, options)
.then((res: Response) => {
('📡 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 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
});
};
// 获取会话列表
export const fetchConversations = async () => {
return get('conversations', {
params: { limit: 100, first_id: '' },
});
};
// 获取聊天消息列表
export const fetchChatList = async (conversationId: string) => {
return get('messages', {
params: { conversation_id: conversationId, limit: 20, last_id: '' },
});
};
// 获取应用参数
export const fetchAppParams = async () => {
return get('parameters');
};
// 更新反馈
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
return post(url, { body });
};
// 生成会话名称
export const generateConversationName = async (id: string) => {
return post(`conversations/${id}/name`, {
body: { auto_generate: true },
});
};
// 重命名会话
export const renameConversation = async (id: string, name: string, autoGenerate: boolean = false) => {
return post(`conversations/${id}/name`, {
body: {
name: autoGenerate ? undefined : name,
auto_generate: autoGenerate
},
});
};
// 删除会话
export const deleteConversation = async (id: string) => {
return del(`conversations/${id}`);
};
// 文件上传
export const upload = (fetchOptions: any): Promise<any> => {
const urlPrefix = CHAT_CONFIG.API_PREFIX;
const urlWithPrefix = `${CHAT_CONFIG.API_URL}${urlPrefix}/file-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}`);
}
xhr.withCredentials = true;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200)
resolve({ id: xhr.response });
else
reject(xhr);
}
};
xhr.upload.onprogress = options.onprogress;
xhr.send(options.data);
});
};