diff --git a/app/config/chat.ts b/app/config/chat.ts index b5ebaf7..c80273a 100644 --- a/app/config/chat.ts +++ b/app/config/chat.ts @@ -36,12 +36,12 @@ const getDifyApiUrl = () => { const getAppId = () => { const rawAppId = getEnvVar('NEXT_PUBLIC_APP_ID', ''); const extractedAppId = extractAppId(rawAppId); - console.log('🔧 Chat Config Debug:', { - rawAppId, - extractedAppId, - difyApiUrl: getDifyApiUrl(), - hasApiKey: !!getEnvVar('NEXT_PUBLIC_APP_KEY', ''), - }); + // console.log('🔧 Chat Config Debug:', { + // rawAppId, + // extractedAppId, + // difyApiUrl: getDifyApiUrl(), + // hasApiKey: !!getEnvVar('NEXT_PUBLIC_APP_KEY', ''), + // }); return extractedAppId; }; diff --git a/app/services/api.client.ts b/app/services/api.client.ts index 5a46834..edbad92 100644 --- a/app/services/api.client.ts +++ b/app/services/api.client.ts @@ -108,7 +108,40 @@ export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) = 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, @@ -123,12 +156,6 @@ const handleStream = ( onNodeFinished?: IOnNodeFinished, onError?: IOnError, ) => { - // console.log('🌊 [handleStream] 开始处理流式响应:', { - // status: response.status, - // statusText: response.statusText, - // headers: Object.fromEntries(response.headers.entries()) - // }); - if (!response.ok) { console.error('❌ [handleStream] 响应错误:', response.status, response.statusText); onError?.('网络响应错误'); @@ -142,19 +169,12 @@ const handleStream = ( let isFirstMessage = true; let messageCount = 0; - // console.log('📖 [handleStream] 获取reader:', !!reader); function read() { let hasError = false; reader?.read().then((result: any) => { - // console.log('📨 [handleStream] 读取数据块:', { - // done: result.done, - // valueLength: result.value?.length, - // messageCount: ++messageCount - // }); if (result.done) { - // console.log('✅ [handleStream] 流式响应完成, 总消息数:', messageCount); onCompleted && onCompleted(); return; } @@ -163,31 +183,14 @@ const handleStream = ( buffer += chunk; const lines = buffer.split('\n'); - // console.log('🔍 [handleStream] 处理数据块:', { - // chunkLength: chunk.length, - // bufferLength: buffer.length, - // linesCount: lines.length, - // chunk: chunk.substring(0, 100) + (chunk.length > 100 ? '...' : '') - // }); try { lines.forEach((message, index) => { if (message.startsWith('data: ')) { const jsonStr = message.substring(6); - // console.log(`📋 [handleStream] 解析消息 ${index}:`, { - // jsonLength: jsonStr.length, - // preview: jsonStr.substring(0, 200) + (jsonStr.length > 200 ? '...' : '') - // }); try { bufferObj = JSON.parse(jsonStr) as Record; - // console.log('✨ [handleStream] JSON解析成功:', { - // event: bufferObj.event, - // hasAnswer: !!bufferObj.answer, - // answerLength: bufferObj.answer?.length || 0, - // conversationId: bufferObj.conversation_id, - // messageId: bufferObj.id || bufferObj.message_id - // }); } catch (e) { console.warn('⚠️ [handleStream] JSON解析失败:', e, 'JSON:', jsonStr); @@ -219,14 +222,7 @@ const handleStream = ( if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') { const answer = unicodeToChar(bufferObj.answer); - // console.log('💬 [handleStream] 处理消息事件:', { - // event: bufferObj.event, - // isFirstMessage, - // answerLength: answer.length, - // answer: answer.substring(0, 50) + (answer.length > 50 ? '...' : ''), - // conversationId: bufferObj.conversation_id, - // messageId: bufferObj.id || bufferObj.message_id - // }); + onData(answer, isFirstMessage, { conversationId: bufferObj.conversation_id, @@ -269,10 +265,6 @@ const handleStream = ( // 保留最后一行(可能是不完整的消息) const lastLine = lines[lines.length - 1]; buffer = lastLine; - // console.log('💾 [handleStream] 保留缓冲区:', { - // lastLineLength: lastLine.length, - // preview: lastLine.substring(0, 50) - // }); } catch (err) { console.error('❌ [handleStream] 解析响应时出错:', err); @@ -287,10 +279,8 @@ const handleStream = ( } if (!hasError) { - // console.log('🔄 [handleStream] 继续读取下一块...'); read(); } else { - // console.log('🛑 [handleStream] 因错误停止读取'); } }).catch(err => { console.error('❌ [handleStream] 读取流时出错:', err); @@ -301,7 +291,34 @@ const handleStream = ( read(); }; -// 基础请求函数 +/** + * 基础 HTTP 请求函数 + * + * 提供统一的请求配置和错误处理: + * - 自动添加认证头 + * - 统一的 URL 处理 + * - 错误状态码处理 + * - 自动添加用户 ID + * + * @param url - 请求的 URL 路径(相对于 API 基础 URL) + * @param fetchOptions - fetch API 的配置选项 + * @param needAllResponseContent - 是否需要返回完整的响应内容而不是 JSON + * @returns Promise - 返回解析后的响应数据 + * + * @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); @@ -365,7 +382,42 @@ const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boole }); }; -// SSE请求处理 +/** + * 发送 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, @@ -429,11 +481,6 @@ export const ssePost = ( return fetch(urlWithPrefix, options) .then((res: Response) => { - console.log('📡 SSE Response:', { - status: res.status, - statusText: res.statusText, - url: urlWithPrefix - }); if (!/^(2|3)\d{2}$/.test(res.status.toString())) { res.json().then((data: any) => { @@ -464,7 +511,28 @@ export const ssePost = ( }); }; -// 获取会话列表 +/** + * 获取用户的会话列表 + * + * 从 Dify API 获取当前用户的所有会话: + * - 自动分页获取(最多100条) + * - 包含会话 ID、名称、输入参数等信息 + * - 按时间倒序排列 + * + * @returns Promise - 包含会话列表的响应对象 + * @returns Promise.data - 会话数组 + * @returns Promise.has_more - 是否还有更多数据 + * @returns Promise.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({ @@ -486,7 +554,29 @@ export const fetchConversations = async () => { }); }; -// 获取聊天消息列表 +/** + * 获取指定会话的聊天消息列表 + * + * 从 Dify API 获取特定会话的消息历史: + * - 支持分页加载(最多20条) + * - 包含用户消息和 AI 回复 + * - 按时间顺序排列 + * + * @param conversationId - 会话 ID + * @returns Promise - 包含消息列表的响应对象 + * @returns Promise.data - 消息数组 + * @returns Promise.has_more - 是否还有更多历史消息 + * @returns Promise.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({ @@ -509,7 +599,29 @@ export const fetchChatList = async (conversationId: string) => { }); }; -// 获取应用参数 +/** + * 获取应用参数配置 + * + * 从 Dify API 获取应用的配置信息: + * - 用户输入表单配置 + * - 开场白设置 + * - 文件上传配置 + * - 其他应用级别设置 + * + * @returns Promise - 包含应用参数的响应对象 + * @returns Promise.user_input_form - 用户输入表单配置 + * @returns Promise.opening_statement - 开场白内容 + * @returns Promise.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', @@ -524,7 +636,31 @@ export const fetchAppParams = async () => { }); }; -// 更新反馈 +/** + * 更新消息反馈[未使用] + * + * 向 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 - 反馈提交结果 + * + * @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 @@ -546,7 +682,26 @@ export const updateFeedback = async ({ url, body }: { url: string; body: Feedbac }); }; -// 生成会话名称 +/** + * 生成会话名称[未使用] + * + * 让 AI 根据会话内容自动生成合适的会话名称: + * - 基于会话中的消息内容 + * - 生成简洁有意义的标题 + * - 用于替换默认的"新对话"名称 + * + * @param id - 会话 ID + * @returns Promise - 包含生成名称的响应对象 + * @returns Promise.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', @@ -566,7 +721,31 @@ export const generateConversationName = async (id: string) => { }); }; -// 重命名会话 +/** + * 重命名会话 + * + * 更新会话的显示名称: + * - 支持手动设置名称 + * - 支持 AI 自动生成名称 + * - 更新后在会话列表中显示新名称 + * + * @param id - 会话 ID + * @param name - 新的会话名称(当 autoGenerate 为 false 时使用) + * @param autoGenerate - 是否使用 AI 自动生成名称,默认为 false + * @returns Promise - 重命名结果 + * @returns Promise.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', @@ -587,7 +766,25 @@ export const renameConversation = async (id: string, name: string, autoGenerate: }); }; -// 删除会话 +/** + * 删除会话 + * + * 从用户的会话列表中永久删除指定会话: + * - 删除会话及其所有消息 + * - 操作不可逆 + * - 删除后从会话列表中移除 + * + * @param id - 要删除的会话 ID + * @returns Promise - 删除操作结果 + * + * @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', @@ -606,7 +803,43 @@ export const deleteConversation = async (id: string) => { }); }; -// 文件上传 +/** + * 上传文件到 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 - 返回包含文件 ID 的对象 + * @returns Promise.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 => { const urlWithPrefix = `${CHAT_CONFIG.API_URL}/files/upload`; @@ -652,32 +885,149 @@ export const upload = (fetchOptions: any): Promise => { }); }; -// 公共请求函数 +/** + * 通用 HTTP 请求函数 + * + * 基于 baseFetch 的通用请求封装: + * - 合并基础配置和自定义选项 + * - 统一的错误处理 + * - 支持所有 HTTP 方法 + * + * @param url - 请求 URL 路径 + * @param options - 请求配置选项,默认为空对象 + * @param needAllResponseContent - 是否返回完整响应内容,默认为 false + * @returns Promise - 请求响应数据 + * + * @example + * ```typescript + * const data = await request('conversations', { method: 'GET' }); + * ``` + */ export const request = (url: string, options = {}, needAllResponseContent = false) => { return baseFetch(url, { ...baseOptions, ...options }, needAllResponseContent); }; -// GET 请求 +/** + * 发送 GET 请求 + * + * @param url - 请求 URL 路径 + * @param options - 额外的请求配置选项,默认为空对象 + * @returns Promise - 请求响应数据 + * + * @example + * ```typescript + * const conversations = await get('conversations'); + * ``` + */ export const get = (url: string, options = {}) => { return request(url, { ...options, method: 'GET' }); }; -// POST 请求 +/** + * 发送 POST 请求 + * + * @param url - 请求 URL 路径 + * @param options - 额外的请求配置选项,默认为空对象 + * @returns Promise - 请求响应数据 + * + * @example + * ```typescript + * const result = await post('chat-messages', { + * body: { query: 'Hello' } + * }); + * ``` + */ export const post = (url: string, options = {}) => { return request(url, { ...options, method: 'POST' }); }; -// PUT 请求 +/** + * 发送 PUT 请求 + * + * @param url - 请求 URL 路径 + * @param options - 额外的请求配置选项,默认为空对象 + * @returns Promise - 请求响应数据 + * + * @example + * ```typescript + * const result = await put('conversations/123', { + * body: { name: '新名称' } + * }); + * ``` + */ export const put = (url: string, options = {}) => { return request(url, { ...options, method: 'PUT' }); }; -// DELETE 请求 +/** + * 发送 DELETE 请求 + * + * @param url - 请求 URL 路径 + * @param options - 额外的请求配置选项,默认为空对象 + * @returns Promise - 请求响应数据 + * + * @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 - 异步操作完成 + * + * @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, {