Files
leaudit-platform-frontend/app/services/api.client.ts
T
2025-07-02 10:28:47 +08:00

1080 lines
34 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: '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;
/**
* 处理服务器发送事件 (SSE) 流式响应
*
* 这是核心的流式响应处理函数,负责:
* - 解析 SSE 数据流
* - 处理各种事件类型(消息、思考、文件、工作流等)
* - 错误处理和状态管理
* - 实时更新 UI
*
* @param response - fetch API 返回的 Response 对象
* @param onData - 处理消息数据的回调函数
* @param onCompleted - 流式响应完成时的回调函数
* @param onThought - 处理 Agent 思考过程的回调函数
* @param onMessageEnd - 处理消息结束事件的回调函数
* @param onMessageReplace - 处理消息替换事件的回调函数
* @param onFile - 处理文件事件的回调函数
* @param onWorkflowStarted - 处理工作流开始事件的回调函数
* @param onWorkflowFinished - 处理工作流完成事件的回调函数
* @param onNodeStarted - 处理节点开始事件的回调函数
* @param onNodeFinished - 处理节点完成事件的回调函数
* @param onError - 处理错误的回调函数
*
* @example
* ```typescript
* const response = await fetch('/api/chat-messages', options);
* handleStream(
* response,
* (message, isFirst, info) => console.log('收到消息:', message),
* () => console.log('流式响应完成'),
* (thought) => console.log('AI思考:', thought),
* // ... 其他回调
* );
* ```
*/
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) {
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;
function read() {
let hasError = false;
reader?.read().then((result: any) => {
if (result.done) {
onCompleted && onCompleted();
return;
}
const chunk = decoder.decode(result.value, { stream: true });
buffer += chunk;
const lines = buffer.split('\n');
try {
lines.forEach((message, index) => {
if (message.startsWith('data: ')) {
const jsonStr = message.substring(6);
try {
bufferObj = JSON.parse(jsonStr) as Record<string, any>;
}
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);
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;
}
catch (err) {
console.error('❌ [handleStream] 解析响应时出错:', err);
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: `${err}`,
});
hasError = true;
onCompleted?.(true);
return;
}
if (!hasError) {
read();
} else {
}
}).catch(err => {
console.error('❌ [handleStream] 读取流时出错:', err);
onError?.(err.message);
});
}
read();
};
/**
* 基础 HTTP 请求函数
*
* 提供统一的请求配置和错误处理:
* - 自动添加认证头
* - 统一的 URL 处理
* - 错误状态码处理
* - 自动添加用户 ID
*
* @param url - 请求的 URL 路径(相对于 API 基础 URL)
* @param fetchOptions - fetch API 的配置选项
* @param needAllResponseContent - 是否需要返回完整的响应内容而不是 JSON
* @returns Promise<any> - 返回解析后的响应数据
*
* @throws {Error} 当请求失败时抛出错误
*
* @example
* ```typescript
* // 发送 GET 请求
* const data = await baseFetch('conversations', { method: 'GET' });
*
* // 发送 POST 请求
* const result = await baseFetch('chat-messages', {
* method: 'POST',
* body: { query: 'Hello' }
* });
* ```
*/
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 (Server-Sent Events) POST 请求
*
* 专门用于处理流式响应的 POST 请求:
* - 配置 SSE 相关的请求头
* - 设置 AbortController 用于取消请求
* - 调用 handleStream 处理流式响应
* - 自动添加用户 ID 到请求体
*
* @param url - 请求的 URL 路径
* @param fetchOptions - fetch 配置选项
* @param callbacks - 包含各种事件回调的对象
* @param callbacks.onData - 处理消息数据的回调
* @param callbacks.onCompleted - 流式响应完成时的回调
* @param callbacks.onThought - 处理思考过程的回调
* @param callbacks.onFile - 处理文件的回调
* @param callbacks.onMessageEnd - 处理消息结束的回调
* @param callbacks.onMessageReplace - 处理消息替换的回调
* @param callbacks.onError - 处理错误的回调
* @param callbacks.getAbortController - 获取中止控制器的回调
* @param callbacks.onWorkflowStarted - 处理工作流开始的回调
* @param callbacks.onWorkflowFinished - 处理工作流完成的回调
* @param callbacks.onNodeStarted - 处理节点开始的回调
* @param callbacks.onNodeFinished - 处理节点完成的回调
*
* @example
* ```typescript
* ssePost('chat-messages', {
* body: { query: 'Hello', response_mode: 'streaming' }
* }, {
* onData: (message, isFirst, info) => updateUI(message),
* onCompleted: () => setLoading(false),
* onError: (error) => showError(error)
* });
* ```
*/
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) => {
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);
});
};
/**
* 获取用户的会话列表
*
* 从 Dify API 获取当前用户的所有会话:
* - 自动分页获取(最多100条)
* - 包含会话 ID、名称、输入参数等信息
* - 按时间倒序排列
*
* @returns Promise<any> - 包含会话列表的响应对象
* @returns Promise<any>.data - 会话数组
* @returns Promise<any>.has_more - 是否还有更多数据
* @returns Promise<any>.limit - 每页限制数量
*
* @throws {Error} 当获取会话列表失败时抛出错误
*
* @example
* ```typescript
* const response = await fetchConversations();
* const conversations = response.data;
* console.log('会话数量:', conversations.length);
* ```
*/
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();
});
};
/**
* 获取指定会话的聊天消息列表
*
* 从 Dify API 获取特定会话的消息历史:
* - 支持分页加载(最多20条)
* - 包含用户消息和 AI 回复
* - 按时间顺序排列
*
* @param conversationId - 会话 ID
* @returns Promise<any> - 包含消息列表的响应对象
* @returns Promise<any>.data - 消息数组
* @returns Promise<any>.has_more - 是否还有更多历史消息
* @returns Promise<any>.limit - 每页限制数量
*
* @throws {Error} 当获取消息列表失败时抛出错误
*
* @example
* ```typescript
* const response = await fetchChatList('conv-123');
* const messages = response.data;
* console.log('消息数量:', messages.length);
* ```
*/
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();
});
};
/**
* 获取应用参数配置
*
* 从 Dify API 获取应用的配置信息:
* - 用户输入表单配置
* - 开场白设置
* - 文件上传配置
* - 其他应用级别设置
*
* @returns Promise<any> - 包含应用参数的响应对象
* @returns Promise<any>.user_input_form - 用户输入表单配置
* @returns Promise<any>.opening_statement - 开场白内容
* @returns Promise<any>.file_upload - 文件上传配置
*
* @throws {Error} 当获取应用参数失败时抛出错误
*
* @example
* ```typescript
* const params = await fetchAppParams();
* const { user_input_form, opening_statement } = params.data;
* console.log('开场白:', opening_statement);
* ```
*/
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();
});
};
/**
* 更新消息反馈[未使用]
*
* 向 Dify API 提交用户对 AI 回复的反馈:
* - 支持点赞/点踩评价
* - 可添加文字反馈内容
* - 用于改进 AI 回复质量
*
* @param params - 反馈参数对象
* @param params.url - 包含消息 ID 的 URL
* @param params.body - 反馈内容
* @param params.body.rating - 评分:'like' | 'dislike' | null
* @param params.body.content - 文字反馈内容(可选)
* @returns Promise<any> - 反馈提交结果
*
* @throws {Error} 当提交反馈失败时抛出错误
*
* @example
* ```typescript
* await updateFeedback({
* url: '/messages/msg-123/feedbacks',
* body: { rating: 'like', content: '回答很好' }
* });
* ```
*/
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();
});
};
/**
* 生成会话名称[未使用]
*
* 让 AI 根据会话内容自动生成合适的会话名称:
* - 基于会话中的消息内容
* - 生成简洁有意义的标题
* - 用于替换默认的"新对话"名称
*
* @param id - 会话 ID
* @returns Promise<any> - 包含生成名称的响应对象
* @returns Promise<any>.name - 生成的会话名称
*
* @throws {Error} 当生成名称失败时抛出错误
*
* @example
* ```typescript
* const result = await generateConversationName('conv-123');
* console.log('生成的名称:', result.name);
* ```
*/
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();
});
};
/**
* 重命名会话
*
* 更新会话的显示名称:
* - 支持手动设置名称
* - 支持 AI 自动生成名称
* - 更新后在会话列表中显示新名称
*
* @param id - 会话 ID
* @param name - 新的会话名称(当 autoGenerate 为 false 时使用)
* @param autoGenerate - 是否使用 AI 自动生成名称,默认为 false
* @returns Promise<any> - 重命名结果
* @returns Promise<any>.name - 最终的会话名称
*
* @throws {Error} 当重命名失败时抛出错误
*
* @example
* ```typescript
* // 手动设置名称
* await renameConversation('conv-123', '关于编程的讨论');
*
* // AI 自动生成名称
* await renameConversation('conv-123', '', true);
* ```
*/
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();
});
};
/**
* 删除会话
*
* 从用户的会话列表中永久删除指定会话:
* - 删除会话及其所有消息
* - 操作不可逆
* - 删除后从会话列表中移除
*
* @param id - 要删除的会话 ID
* @returns Promise<any> - 删除操作结果
*
* @throws {Error} 当删除会话失败时抛出错误
*
* @example
* ```typescript
* await deleteConversation('conv-123');
* console.log('会话已删除');
* ```
*/
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();
});
};
/**
* 上传文件到 Dify API[未使用]
*
* 使用 XMLHttpRequest 上传文件:
* - 支持文件上传进度监控
* - 自动添加认证头和用户 ID
* - 返回文件 ID 用于后续引用
*
* @param fetchOptions - 上传配置选项
* @param fetchOptions.method - HTTP 方法(通常为 'POST'
* @param fetchOptions.url - 上传 URL(可选,会自动构建)
* @param fetchOptions.data - FormData 对象,包含要上传的文件
* @param fetchOptions.headers - 额外的请求头
* @param fetchOptions.xhr - XMLHttpRequest 实例
* @param fetchOptions.onprogress - 上传进度回调函数
* @returns Promise<any> - 返回包含文件 ID 的对象
* @returns Promise<any>.id - 上传后的文件 ID
*
* @throws {Error} 当文件上传失败时抛出错误
*
* @example
* ```typescript
* const formData = new FormData();
* formData.append('file', fileBlob);
*
* const xhr = new XMLHttpRequest();
* const result = await upload({
* data: formData,
* xhr: xhr,
* onprogress: (event) => {
* const progress = (event.loaded / event.total) * 100;
* console.log('上传进度:', progress + '%');
* }
* });
* console.log('文件ID:', result.id);
* ```
*/
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);
});
};
/**
* 通用 HTTP 请求函数
*
* 基于 baseFetch 的通用请求封装:
* - 合并基础配置和自定义选项
* - 统一的错误处理
* - 支持所有 HTTP 方法
*
* @param url - 请求 URL 路径
* @param options - 请求配置选项,默认为空对象
* @param needAllResponseContent - 是否返回完整响应内容,默认为 false
* @returns Promise<any> - 请求响应数据
*
* @example
* ```typescript
* const data = await request('conversations', { method: 'GET' });
* ```
*/
export const request = (url: string, options = {}, needAllResponseContent = false) => {
return baseFetch(url, { ...baseOptions, ...options }, needAllResponseContent);
};
/**
* 发送 GET 请求
*
* @param url - 请求 URL 路径
* @param options - 额外的请求配置选项,默认为空对象
* @returns Promise<any> - 请求响应数据
*
* @example
* ```typescript
* const conversations = await get('conversations');
* ```
*/
export const get = (url: string, options = {}) => {
return request(url, { ...options, method: 'GET' });
};
/**
* 发送 POST 请求
*
* @param url - 请求 URL 路径
* @param options - 额外的请求配置选项,默认为空对象
* @returns Promise<any> - 请求响应数据
*
* @example
* ```typescript
* const result = await post('chat-messages', {
* body: { query: 'Hello' }
* });
* ```
*/
export const post = (url: string, options = {}) => {
return request(url, { ...options, method: 'POST' });
};
/**
* 发送 PUT 请求
*
* @param url - 请求 URL 路径
* @param options - 额外的请求配置选项,默认为空对象
* @returns Promise<any> - 请求响应数据
*
* @example
* ```typescript
* const result = await put('conversations/123', {
* body: { name: '新名称' }
* });
* ```
*/
export const put = (url: string, options = {}) => {
return request(url, { ...options, method: 'PUT' });
};
/**
* 发送 DELETE 请求
*
* @param url - 请求 URL 路径
* @param options - 额外的请求配置选项,默认为空对象
* @returns Promise<any> - 请求响应数据
*
* @example
* ```typescript
* await del('conversations/123');
* ```
*/
export const del = (url: string, options = {}) => {
return request(url, { ...options, method: 'DELETE' });
};
/**
* 发送聊天消息
*
* 向 Dify API 发送聊天消息并处理流式响应:
* - 自动设置流式响应模式
* - 支持文件附件
* - 支持会话输入参数
* - 处理各种类型的响应事件
*
* @param body - 消息请求体
* @param body.query - 用户的问题文本
* @param body.conversation_id - 会话 ID(可选,用于继续现有会话)
* @param body.files - 附件文件列表(可选)
* @param body.inputs - 会话输入参数(可选)
* @param callbacks - 事件回调函数集合
* @param callbacks.onData - 处理消息数据的回调,必需
* @param callbacks.onCompleted - 流式响应完成时的回调,必需
* @param callbacks.onFile - 处理文件的回调(可选)
* @param callbacks.onThought - 处理思考过程的回调(可选)
* @param callbacks.onMessageEnd - 处理消息结束的回调(可选)
* @param callbacks.onMessageReplace - 处理消息替换的回调(可选)
* @param callbacks.onError - 处理错误的回调(可选)
* @param callbacks.getAbortController - 获取中止控制器的回调(可选)
* @param callbacks.onWorkflowStarted - 处理工作流开始的回调(可选)
* @param callbacks.onNodeStarted - 处理节点开始的回调(可选)
* @param callbacks.onNodeFinished - 处理节点完成的回调(可选)
* @param callbacks.onWorkflowFinished - 处理工作流完成的回调(可选)
* @returns Promise<void> - 异步操作完成
*
* @example
* ```typescript
* await sendChatMessage({
* query: '你好,请介绍一下自己',
* conversation_id: 'conv-123'
* }, {
* onData: (message, isFirst, info) => {
* console.log('收到消息:', message);
* updateChatUI(message, info.messageId);
* },
* onCompleted: (hasError) => {
* console.log('对话完成', hasError ? '有错误' : '成功');
* setLoading(false);
* },
* onThought: (thought) => {
* console.log('AI思考:', thought.thought);
* },
* onError: (error) => {
* console.error('发送失败:', error);
* showErrorMessage(error);
* }
* });
* ```
*/
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
});
};