/** * Dify 客户端 API 模块 * * 提供浏览器端调用 Dify API 的函数,通过 Remix API Routes 代理请求 * * 调用链路: * 客户端 → Remix /api/* → FastAPI /dify/* → Dify * * @module api/dify/client */ import axios from 'axios'; import { CHAT_CONFIG, ContentType } from '~/config/chat'; import { handleSSEStream, handleStream } from './sse-handler'; import type { SSECallbacks, Feedbacktype, SendMessageParams, ConversationsResponse, MessagesResponse, AppParametersResponse, } from './types'; // ============================================================================ // 基础配置 // ============================================================================ /** * API 基础 URL * 指向 Remix API Routes(/api/*) */ const API_URL = CHAT_CONFIG.API_URL; /** * 基础请求选项 */ const baseOptions: RequestInit = { method: 'GET', mode: 'cors', credentials: 'include', // 携带 cookie headers: { 'Content-Type': ContentType.json, }, redirect: 'follow', }; // ============================================================================ // 会话管理 API // ============================================================================ /** * 获取用户的会话列表 * * @returns 包含会话列表的响应对象 * @throws {Error} 当获取会话列表失败时抛出错误 * * @example * ```typescript * const response = await fetchConversations(); * const conversations = response.data; * console.log('会话数量:', conversations.length); * ``` */ export async function fetchConversations(): Promise { const params = new URLSearchParams({ limit: '100', }); const url = `${API_URL}/conversations?${params}`; console.log('📋 [Dify Client] 获取会话列表:', { url }); try { const response = await axios.get(url, { withCredentials: true, }); console.log('📋 [Dify Client] 会话列表响应:', { status: response.status }); return response.data; } catch (err) { if (axios.isAxiosError(err)) { console.error('❌ [Dify Client] 获取会话列表失败:', { status: err.response?.status, body: err.response?.data }); throw new Error(`获取会话列表失败: ${err.response?.status}`); } throw err; } } /** * 获取指定会话的聊天消息列表 * * @param conversationId - 会话 ID * @returns 包含消息列表的响应对象 * @throws {Error} 当获取消息列表失败时抛出错误 * * @example * ```typescript * const response = await fetchChatList('conv-123'); * const messages = response.data; * console.log('消息数量:', messages.length); * ``` */ export async function fetchChatList(conversationId: string): Promise { const params = new URLSearchParams({ conversation_id: conversationId, }); try { const response = await axios.get( `${API_URL}/chat-messages?${params}`, { withCredentials: true } ); return response.data; } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`获取消息列表失败: ${err.response?.status}`); } throw err; } } /** * 重命名会话 * * @param id - 会话 ID * @param name - 新的会话名称 * @param autoGenerate - 是否使用 AI 自动生成名称,默认为 false * @returns 重命名结果 * @throws {Error} 当重命名失败时抛出错误 * * @example * ```typescript * // 手动设置名称 * await renameConversation('conv-123', '关于编程的讨论'); * * // AI 自动生成名称 * await renameConversation('conv-123', '', true); * ``` */ export async function renameConversation( id: string, name: string, autoGenerate: boolean = false ): Promise<{ name: string }> { try { const response = await axios.post( `${API_URL}/conversations/${id}/name`, { name: autoGenerate ? undefined : name, auto_generate: autoGenerate, }, { headers: { 'Content-Type': 'application/json' }, withCredentials: true, } ); return response.data; } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`重命名会话失败: ${err.response?.status}`); } throw err; } } /** * 生成会话名称 * * 让 AI 根据会话内容自动生成合适的会话名称 * * @param id - 会话 ID * @returns 包含生成名称的响应对象 * @throws {Error} 当生成名称失败时抛出错误 */ export async function generateConversationName(id: string): Promise<{ name: string }> { return renameConversation(id, '', true); } /** * 删除会话 * * @param id - 要删除的会话 ID * @returns 删除操作结果 * @throws {Error} 当删除会话失败时抛出错误 * * @example * ```typescript * await deleteConversation('conv-123'); * console.log('会话已删除'); * ``` */ export async function deleteConversation(id: string): Promise<{ result: string }> { console.log('🗑️ [Dify Client] 删除会话:', id); try { const response = await axios.delete(`${API_URL}/conversations/${id}`, { headers: { 'Content-Type': 'application/json' }, withCredentials: true, }); console.log('🗑️ [Dify Client] 删除会话响应:', { status: response.status, statusText: response.statusText }); return response.data; } catch (err) { if (axios.isAxiosError(err)) { console.error('❌ [Dify Client] 删除会话失败:', err.response?.data); throw new Error(`删除会话失败: ${err.response?.status}`); } throw err; } } // ============================================================================ // 应用参数 API // ============================================================================ /** * 获取应用参数配置 * * @returns 包含应用参数的响应对象 * @throws {Error} 当获取应用参数失败时抛出错误 * * @example * ```typescript * const params = await fetchAppParams(); * const { user_input_form, opening_statement } = params; * console.log('开场白:', opening_statement); * ``` */ export async function fetchAppParams(): Promise { const url = `${API_URL}/parameters`; console.log('⚙️ [Dify Client] 获取应用参数:', { url }); try { const response = await axios.get(url, { withCredentials: true, }); console.log('⚙️ [Dify Client] 应用参数响应:', { status: response.status }); return response.data; } catch (err) { if (axios.isAxiosError(err)) { console.error('❌ [Dify Client] 获取应用参数失败:', { status: err.response?.status, body: err.response?.data }); throw new Error(`获取应用参数失败: ${err.response?.status}`); } throw err; } } // ============================================================================ // 消息 API // ============================================================================ /** * 发送聊天消息(流式响应) * * @param params - 消息参数 * @param callbacks - SSE 回调配置 * * @example * ```typescript * await sendChatMessage( * { query: '你好', conversation_id: 'conv-123' }, * { * onData: (message, isFirst, info) => updateUI(message), * onCompleted: () => setLoading(false), * onError: (error) => showError(error), * } * ); * ``` */ export async function sendChatMessage( params: SendMessageParams, callbacks: SSECallbacks ): Promise { const body = { ...params, response_mode: 'streaming', }; const url = `${API_URL}/chat-messages`; const controller = new AbortController(); callbacks.getAbortController?.(controller); const options: RequestInit = { ...baseOptions, method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': ContentType.stream, }, body: JSON.stringify(body), signal: controller.signal, }; try { const response = await fetch(url, options); if (!/^(2|3)\d{2}$/.test(response.status.toString())) { const data = await response.json(); console.error('❌ [Dify Client] SSE 错误:', data.message || 'Server Error'); callbacks.onError?.(data.message || 'Server Error'); return; } handleSSEStream(response, callbacks); } catch (err: any) { if (err.name !== 'AbortError') { console.error('❌ [Dify Client] SSE 请求错误:', err); callbacks.onError?.(err.message); } } } /** * 更新消息反馈 * * @param messageId - 消息 ID * @param feedback - 反馈内容 * @returns 反馈提交结果 * @throws {Error} 当提交反馈失败时抛出错误 * * @example * ```typescript * await updateFeedback('msg-123', { rating: 'like' }); * ``` */ export async function updateFeedback( messageId: string, feedback: Feedbacktype ): Promise { try { const response = await axios.post( `${API_URL}/messages/${messageId}/feedbacks`, feedback, { headers: { 'Content-Type': 'application/json' }, withCredentials: true, } ); return response.data; } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`提交反馈失败: ${err.response?.status}`); } throw err; } } // ============================================================================ // 文件上传 API // ============================================================================ /** * 上传文件 * * @param options - 上传配置选项 * @returns 包含文件 ID 的对象 * @throws {Error} 当文件上传失败时抛出错误 * * @example * ```typescript * const formData = new FormData(); * formData.append('file', fileBlob); * * const xhr = new XMLHttpRequest(); * const result = await uploadFile({ * data: formData, * xhr: xhr, * onProgress: (event) => { * const progress = (event.loaded / event.total) * 100; * console.log('上传进度:', progress + '%'); * } * }); * console.log('文件ID:', result.id); * ``` */ export function uploadFile(options: { data: FormData; xhr: XMLHttpRequest; onProgress?: (event: ProgressEvent) => void; }): Promise<{ id: string }> { const url = `${API_URL}/files/upload`; return new Promise((resolve, reject) => { const { xhr, data, onProgress } = options; xhr.open('POST', url); xhr.withCredentials = true; xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve({ id: xhr.response }); } else { reject(new Error(xhr.responseText || '上传失败')); } } }; if (onProgress) { xhr.upload.onprogress = onProgress; } xhr.send(data); }); } // 重新导出 SSE 处理器 export { handleStream, handleSSEStream };