/** * Dify 服务端 API 模块 * * 提供 Node.js 服务端调用 FastAPI 后端的函数 * 用于 Remix loader/action 中调用 Dify API * * 调用链路: * Remix Server → FastAPI /dify/* → Dify * * @module api/dify/client.server */ import { API_BASE_URL } from '~/config/api-config'; // ============================================================================ // 配置 // ============================================================================ /** * 获取环境变量的服务端函数 */ const getServerEnvVar = (name: string, defaultValue: string = ''): string => { return process.env[name] || defaultValue; }; /** * Dify API 客户端配置 * 通过 FastAPI 后端的 /dify 路由代理访问 Dify */ const DIFY_CONFIG = { API_URL: `${API_BASE_URL}/dify`, API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''), APP_ID: (() => { const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', ''); const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/); return match ? match[1] : rawAppId; })(), }; console.log('🔧 [Dify Server] 配置:', { apiUrl: DIFY_CONFIG.API_URL, apiBaseUrl: API_BASE_URL, appId: DIFY_CONFIG.APP_ID, configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID) }); // ============================================================================ // 基础请求函数 // ============================================================================ /** * Dify API 基础请求函数 * * 使用 JWT 认证通过 FastAPI 代理访问 Dify * * @param endpoint - API 端点路径 * @param options - fetch 请求选项 * @param jwt - JWT 认证令牌 * @returns Response 对象 */ async function difyFetch( endpoint: string, options: RequestInit = {}, jwt?: string ): Promise { const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`; const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, }; if (jwt) { (headers as Record)['Authorization'] = `Bearer ${jwt}`; } else { console.warn('⚠️ [Dify Server] 没有提供 JWT,请求可能失败'); } const response = await fetch(url, { ...options, headers, }); if (!response.ok) { const errorText = await response.text(); console.error('❌ [Dify Server] Dify API 错误:', { status: response.status, statusText: response.statusText, error: errorText }); if (response.status === 401) { throw new Error('JWT认证失败,请重新登录'); } throw new Error(`Dify API Error: ${response.status} ${response.statusText}`); } return response; } // ============================================================================ // Dify API 客户端 // ============================================================================ /** * Dify 服务端 API 客户端 * * 所有方法都需要传入 JWT 进行认证 * user 参数由后端自动从 JWT 中提取 */ export const difyClient = { /** * 获取应用参数 */ async getApplicationParameters(jwt?: string): Promise { const response = await difyFetch('parameters', { method: 'GET', }, jwt); return response.json(); }, /** * 获取会话列表 */ async getConversations(jwt?: string): Promise { const params = new URLSearchParams({ limit: '100', first_id: '', }); const response = await difyFetch(`conversations?${params}`, { method: 'GET', }, jwt); return response.json(); }, /** * 获取会话消息 */ async getConversationMessages(conversationId: string, jwt?: string): Promise { const params = new URLSearchParams({ conversation_id: conversationId, limit: '20', last_id: '', }); const response = await difyFetch(`messages?${params}`, { method: 'GET', }, jwt); return response.json(); }, /** * 发送聊天消息 * * @param inputs - 输入参数 * @param query - 用户问题 * @param responseMode - 响应模式 ('streaming' | 'blocking') * @param conversationId - 会话 ID * @param files - 附件文件 * @param jwt - JWT 认证令牌 * @returns 对于流式响应返回 Response 对象,否则返回 JSON */ async createChatMessage( inputs: Record, query: string, responseMode: string = 'streaming', conversationId?: string, files?: any[], jwt?: string ): Promise { const body = { inputs, query, response_mode: responseMode, conversation_id: conversationId, files: files || [], }; const response = await difyFetch('chat-messages', { method: 'POST', body: JSON.stringify(body), }, jwt); // 对于流式响应,直接返回 Response 对象 if (responseMode === 'streaming') { return response; } console.log('[Dify Server] 解析 JSON 响应'); return response.json(); }, /** * 重命名会话 */ async renameConversation( conversationId: string, name: string, autoGenerate: boolean = false, jwt?: string ): Promise { const body = { name, auto_generate: autoGenerate, }; const response = await difyFetch(`conversations/${conversationId}/name`, { method: 'POST', body: JSON.stringify(body), }, jwt); return response.json(); }, /** * 删除会话 */ async deleteConversation(conversationId: string, jwt?: string): Promise { console.log('remix后端接收到删除请求,调用fastapi:', conversationId); try { const response = await difyFetch(`conversations/${conversationId}`, { method: 'DELETE', body: JSON.stringify({}), }, jwt); // 对于 204 No Content 响应,直接返回成功 if (response.status === 204) { console.log('删除会话' + conversationId + '成功'); return { result: 'success' }; } const contentType = response.headers.get('Content-Type'); if (contentType && contentType.includes('application/json')) { const data = await response.json(); console.log('🗑️ [Dify Server] 删除会话 JSON 响应:', data); return data; } const text = await response.text(); console.log('🗑️ [Dify Server] 删除会话文本响应:', text); return { result: 'success' }; } catch (error: any) { console.warn('⚠️ [Dify Server] 删除会话请求失败,但可能已成功删除:', error.message); return { result: 'success' }; } }, /** * 更新消息反馈 */ async updateMessageFeedback( messageId: string, rating: 'like' | 'dislike' | null, jwt?: string ): Promise { const body = { rating, }; const response = await difyFetch(`messages/${messageId}/feedbacks`, { method: 'POST', body: JSON.stringify(body), }, jwt); return response.json(); }, }; // ============================================================================ // 工具函数 // ============================================================================ /** * Dify 工具函数 */ export const difyUtils = { /** * 获取 Dify 配置 */ getConfig: () => DIFY_CONFIG, };