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; 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; } 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); } // 处理删除API(204状态码) 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, { 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 => { 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); }); };