update pay ui
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
// src/views/AgentChat/hooks/useAgentChat.ts
|
// src/views/AgentChat/hooks/useAgentChat.ts
|
||||||
// 消息处理 Hook - 发送消息、处理响应、错误处理
|
// 消息处理 Hook - 发送消息、处理响应、错误处理
|
||||||
|
// 支持 SSE 流式输出
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import type { Dispatch, SetStateAction, KeyboardEvent } from 'react';
|
import type { Dispatch, SetStateAction, KeyboardEvent } from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import { MessageTypes, type Message } from '../constants/messageTypes';
|
import { MessageTypes, type Message } from '../constants/messageTypes';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
import type { UploadedFile } from './useFileUpload';
|
import type { UploadedFile } from './useFileUpload';
|
||||||
@@ -118,6 +118,23 @@ export const useAgentChat = ({
|
|||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
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,
|
...message,
|
||||||
} as Message,
|
} as Message,
|
||||||
]);
|
]);
|
||||||
}, []);
|
}, [setMessages]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息到后端 API
|
* 更新最后一条指定类型的消息
|
||||||
|
*/
|
||||||
|
const updateLastMessage = useCallback((type: MessageTypes, updates: Partial<Message>) => {
|
||||||
|
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 () => {
|
const handleSendMessage = useCallback(async () => {
|
||||||
if (!inputValue.trim() || isProcessing) return;
|
if (!inputValue.trim() || isProcessing) return;
|
||||||
@@ -154,6 +184,16 @@ export const useAgentChat = ({
|
|||||||
clearFiles();
|
clearFiles();
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
// 重置流式状态
|
||||||
|
streamStateRef.current = {
|
||||||
|
thinkingContent: '',
|
||||||
|
summaryContent: '',
|
||||||
|
plan: null,
|
||||||
|
stepResults: [],
|
||||||
|
sessionId: null,
|
||||||
|
sessionTitle: null,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 显示 "思考中" 状态
|
// 显示 "思考中" 状态
|
||||||
addMessage({
|
addMessage({
|
||||||
@@ -161,8 +201,8 @@ export const useAgentChat = ({
|
|||||||
content: '正在分析你的问题...',
|
content: '正在分析你的问题...',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 调用后端 API
|
// 构建请求体
|
||||||
const response = await axios.post(`${getApiBase()}/mcp/agent/chat`, {
|
const requestBody = {
|
||||||
message: userInput,
|
message: userInput,
|
||||||
conversation_history: messages
|
conversation_history: messages
|
||||||
.filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
|
.filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
|
||||||
@@ -178,56 +218,94 @@ export const useAgentChat = ({
|
|||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
tools: selectedTools,
|
tools: selectedTools,
|
||||||
files: uploadedFiles.length > 0 ? uploadedFiles : undefined,
|
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));
|
setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING));
|
||||||
|
|
||||||
if (response.data.success) {
|
// 显示最终回复
|
||||||
const data = response.data;
|
if (streamStateRef.current.summaryContent) {
|
||||||
|
|
||||||
// 更新会话 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)
|
|
||||||
addMessage({
|
addMessage({
|
||||||
type: MessageTypes.AGENT_RESPONSE,
|
type: MessageTypes.AGENT_RESPONSE,
|
||||||
content: data.final_summary || data.final_answer || data.message || '处理完成',
|
content: streamStateRef.current.summaryContent,
|
||||||
plan: data.plan,
|
plan: streamStateRef.current.plan,
|
||||||
stepResults: stepResults,
|
stepResults: streamStateRef.current.stepResults,
|
||||||
metadata: data.metadata,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重新加载会话列表
|
|
||||||
loadSessions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新会话 ID
|
||||||
|
if (streamStateRef.current.sessionId && !currentSessionId) {
|
||||||
|
setCurrentSessionId(streamStateRef.current.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载会话列表
|
||||||
|
loadSessions();
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Agent chat error:', error);
|
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({
|
addMessage({
|
||||||
type: MessageTypes.ERROR,
|
type: MessageTypes.ERROR,
|
||||||
content: `处理失败:${errorMessage}`,
|
content: `处理失败:${errorMessage}`,
|
||||||
@@ -269,8 +347,147 @@ export const useAgentChat = ({
|
|||||||
setCurrentSessionId,
|
setCurrentSessionId,
|
||||||
loadSessions,
|
loadSessions,
|
||||||
toast,
|
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 换行)
|
* 键盘事件处理(Enter 发送,Shift+Enter 换行)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user