From a5a609e9ba282482af0d10b611036214fbac5bb2 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Wed, 17 Dec 2025 12:20:45 +0800 Subject: [PATCH] update pay ui --- src/views/AgentChat/hooks/useAgentChat.ts | 315 ++++++++++++++++++---- 1 file changed, 266 insertions(+), 49 deletions(-) diff --git a/src/views/AgentChat/hooks/useAgentChat.ts b/src/views/AgentChat/hooks/useAgentChat.ts index 0e9c76ee..3fecb672 100644 --- a/src/views/AgentChat/hooks/useAgentChat.ts +++ b/src/views/AgentChat/hooks/useAgentChat.ts @@ -1,9 +1,9 @@ // src/views/AgentChat/hooks/useAgentChat.ts // 消息处理 Hook - 发送消息、处理响应、错误处理 +// 支持 SSE 流式输出 -import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef } from 'react'; import type { Dispatch, SetStateAction, KeyboardEvent } from 'react'; -import axios from 'axios'; import { MessageTypes, type Message } from '../constants/messageTypes'; import { getApiBase } from '@utils/apiConfig'; import type { UploadedFile } from './useFileUpload'; @@ -118,6 +118,23 @@ export const useAgentChat = ({ const [inputValue, setInputValue] = useState(''); const [isProcessing, setIsProcessing] = useState(false); + // 用于追踪流式响应中的状态 + const streamStateRef = useRef<{ + thinkingContent: string; + summaryContent: string; + plan: any; + stepResults: any[]; + sessionId: string | null; + sessionTitle: string | null; + }>({ + thinkingContent: '', + summaryContent: '', + plan: null, + stepResults: [], + sessionId: null, + sessionTitle: null, + }); + /** * 添加消息到列表 */ @@ -130,10 +147,23 @@ export const useAgentChat = ({ ...message, } as Message, ]); - }, []); + }, [setMessages]); /** - * 发送消息到后端 API + * 更新最后一条指定类型的消息 + */ + const updateLastMessage = useCallback((type: MessageTypes, updates: Partial) => { + setMessages((prev) => { + const lastIndex = prev.map(m => m.type).lastIndexOf(type); + if (lastIndex === -1) return prev; + const newMessages = [...prev]; + newMessages[lastIndex] = { ...newMessages[lastIndex], ...updates }; + return newMessages; + }); + }, [setMessages]); + + /** + * 发送消息到后端 API(SSE 流式) */ const handleSendMessage = useCallback(async () => { if (!inputValue.trim() || isProcessing) return; @@ -154,6 +184,16 @@ export const useAgentChat = ({ clearFiles(); setIsProcessing(true); + // 重置流式状态 + streamStateRef.current = { + thinkingContent: '', + summaryContent: '', + plan: null, + stepResults: [], + sessionId: null, + sessionTitle: null, + }; + try { // 显示 "思考中" 状态 addMessage({ @@ -161,8 +201,8 @@ export const useAgentChat = ({ content: '正在分析你的问题...', }); - // 调用后端 API - const response = await axios.post(`${getApiBase()}/mcp/agent/chat`, { + // 构建请求体 + const requestBody = { message: userInput, conversation_history: messages .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) @@ -178,56 +218,94 @@ export const useAgentChat = ({ model: selectedModel, tools: selectedTools, files: uploadedFiles.length > 0 ? uploadedFiles : undefined, + }; + + // 使用 fetch 发起 SSE 流式请求 + const response = await fetch(`${getApiBase()}/mcp/agent/chat/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream', + }, + body: JSON.stringify(requestBody), + credentials: 'include', // 携带 cookies }); - // 移除 "思考中" 消息 + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || errorData.error || `HTTP ${response.status}`); + } + + // 处理 SSE 流 + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + throw new Error('无法获取响应流'); + } + + let buffer = ''; + let currentEvent = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // 按双换行分割 SSE 事件块 + const eventBlocks = buffer.split('\n\n'); + buffer = eventBlocks.pop() || ''; // 保留不完整的块 + + for (const block of eventBlocks) { + if (!block.trim()) continue; + + const lines = block.split('\n'); + let eventType = ''; + let eventData = ''; + + for (const line of lines) { + if (line.startsWith('event: ')) { + eventType = line.slice(7).trim(); + } else if (line.startsWith('data: ')) { + eventData = line.slice(6); + } + } + + if (eventData && eventData !== '[DONE]') { + try { + const data = JSON.parse(eventData); + // 使用 event 类型,如果没有则使用 data 中的 type + const type = eventType || data.type; + await handleSSEEvent({ type, data }); + } catch (e) { + console.warn('SSE parse error:', e, eventData); + } + } + } + } + + // 流结束后,移除思考中消息,显示最终结果 setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); - if (response.data.success) { - const data = response.data; - - // 更新会话 ID(如果是新会话) - if (data.session_id && !currentSessionId) { - setCurrentSessionId(data.session_id); - } - - // 获取执行步骤(后端返回 step_results 字段) - const stepResults = data.step_results || data.steps || []; - - // 显示执行计划(如果有) - if (data.plan) { - addMessage({ - type: MessageTypes.AGENT_PLAN, - content: '已制定执行计划', - plan: data.plan, - }); - } - - // 显示执行步骤(如果有) - if (stepResults.length > 0) { - addMessage({ - type: MessageTypes.AGENT_EXECUTING, - content: '正在执行步骤...', - plan: data.plan, - stepResults: stepResults, - }); - } - - // 移除 "执行中" 消息 - setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); - - // 显示最终回复(使用 final_summary 或 final_answer 或 message) + // 显示最终回复 + if (streamStateRef.current.summaryContent) { addMessage({ type: MessageTypes.AGENT_RESPONSE, - content: data.final_summary || data.final_answer || data.message || '处理完成', - plan: data.plan, - stepResults: stepResults, - metadata: data.metadata, + content: streamStateRef.current.summaryContent, + plan: streamStateRef.current.plan, + stepResults: streamStateRef.current.stepResults, }); - - // 重新加载会话列表 - loadSessions(); } + + // 更新会话 ID + if (streamStateRef.current.sessionId && !currentSessionId) { + setCurrentSessionId(streamStateRef.current.sessionId); + } + + // 重新加载会话列表 + loadSessions(); + } catch (error: any) { console.error('Agent chat error:', error); @@ -239,7 +317,7 @@ export const useAgentChat = ({ ); // 显示错误消息 - const errorMessage = error.response?.data?.error || error.message || '处理失败'; + const errorMessage = error.message || '处理失败'; addMessage({ type: MessageTypes.ERROR, content: `处理失败:${errorMessage}`, @@ -269,8 +347,147 @@ export const useAgentChat = ({ setCurrentSessionId, loadSessions, toast, + setMessages, ]); + /** + * 处理 SSE 事件 + * 后端事件类型: status, thinking, reasoning, plan, step_start, step_complete, + * summary, summary_chunk, session_title, done, error + */ + const handleSSEEvent = useCallback(async (event: any) => { + const { type, data } = event; + + switch (type) { + case 'status': + // 状态更新(如 "制定计划中..."、"执行工具中...") + updateLastMessage(MessageTypes.AGENT_THINKING, { + content: data?.message || '处理中...', + }); + break; + + case 'thinking': + // 思考过程(计划制定阶段的流式输出) + streamStateRef.current.thinkingContent += data?.content || ''; + updateLastMessage(MessageTypes.AGENT_THINKING, { + content: streamStateRef.current.thinkingContent, + }); + break; + + case 'reasoning': + // 推理过程 + streamStateRef.current.thinkingContent += data?.content || ''; + updateLastMessage(MessageTypes.AGENT_THINKING, { + content: streamStateRef.current.thinkingContent, + }); + break; + + case 'plan': + // 执行计划完成 + streamStateRef.current.plan = data; + // 重置思考内容 + streamStateRef.current.thinkingContent = ''; + // 移除思考消息,显示计划 + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + type: MessageTypes.AGENT_PLAN, + content: '已制定执行计划', + plan: data, + }); + // 添加新的执行中消息 + addMessage({ + type: MessageTypes.AGENT_EXECUTING, + content: '正在执行工具调用...', + plan: data, + stepResults: [], + }); + break; + + case 'step_start': + // 步骤开始执行 + updateLastMessage(MessageTypes.AGENT_EXECUTING, { + content: `正在执行步骤 ${(data?.step_index || 0) + 1}: ${data?.tool || '工具'}...`, + }); + break; + + case 'step_complete': + // 步骤执行完成 + if (data) { + streamStateRef.current.stepResults.push({ + tool: data.tool, + status: data.status, + result: data.result, + execution_time: data.execution_time, + }); + updateLastMessage(MessageTypes.AGENT_EXECUTING, { + content: `已完成 ${streamStateRef.current.stepResults.length} 个步骤`, + stepResults: [...streamStateRef.current.stepResults], + }); + } + break; + + case 'summary_chunk': + // 总结内容流式输出 + // 如果还在执行中,先切换到思考状态 + setMessages((prev) => { + const hasExecuting = prev.some((m) => m.type === MessageTypes.AGENT_EXECUTING); + if (hasExecuting) { + return prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING); + } + return prev; + }); + // 如果没有思考消息,添加一个 + setMessages((prev) => { + const hasThinking = prev.some((m) => m.type === MessageTypes.AGENT_THINKING); + if (!hasThinking) { + return [ + ...prev, + { + id: Date.now() + Math.random(), + timestamp: new Date().toISOString(), + type: MessageTypes.AGENT_THINKING, + content: '', + } as Message, + ]; + } + return prev; + }); + streamStateRef.current.summaryContent += data?.content || ''; + updateLastMessage(MessageTypes.AGENT_THINKING, { + content: streamStateRef.current.summaryContent, + }); + break; + + case 'summary': + // 完整总结(如果是一次性发送的) + if (data?.content) { + streamStateRef.current.summaryContent = data.content; + } + break; + + case 'session_title': + // 会话标题 + if (data?.title) { + streamStateRef.current.sessionTitle = data.title; + } + break; + + case 'done': + // 完成 + if (data?.session_id) { + streamStateRef.current.sessionId = data.session_id; + } + break; + + case 'error': + // 错误 + throw new Error(data?.message || '处理失败'); + + default: + console.log('Unknown SSE event:', type, data); + } + }, [addMessage, setMessages, updateLastMessage]); + /** * 键盘事件处理(Enter 发送,Shift+Enter 换行) */