update pay function

This commit is contained in:
2025-11-28 14:49:16 +08:00
parent 18f8f75116
commit 9b7a221315
11 changed files with 2917 additions and 46 deletions

View File

@@ -32,3 +32,9 @@ export type {
UseAgentChatParams,
UseAgentChatReturn,
} from './useAgentChat';
export { useInvestmentMeeting } from './useInvestmentMeeting';
export type {
UseInvestmentMeetingParams,
UseInvestmentMeetingReturn,
} from './useInvestmentMeeting';

View File

@@ -0,0 +1,403 @@
// src/views/AgentChat/hooks/useInvestmentMeeting.ts
// 投研会议室 Hook - 管理会议状态、发送消息、处理 SSE 流
import { useState, useCallback, useRef } from 'react';
import axios from 'axios';
import {
MeetingMessage,
MeetingStatus,
MeetingEvent,
MeetingResponse,
getRoleConfig,
} from '../constants/meetingRoles';
/**
* useInvestmentMeeting Hook 参数
*/
export interface UseInvestmentMeetingParams {
/** 当前用户 ID */
userId?: string;
/** 当前用户昵称 */
userNickname?: string;
/** Toast 通知函数 */
onToast?: (options: {
title: string;
description?: string;
status: 'success' | 'error' | 'warning' | 'info';
}) => void;
}
/**
* useInvestmentMeeting Hook 返回值
*/
export interface UseInvestmentMeetingReturn {
/** 会议消息列表 */
messages: MeetingMessage[];
/** 会议状态 */
status: MeetingStatus;
/** 当前正在发言的角色 ID */
speakingRoleId: string | null;
/** 当前会话 ID */
sessionId: string | null;
/** 当前轮次 */
currentRound: number;
/** 是否已得出结论 */
isConcluded: boolean;
/** 结论消息 */
conclusion: MeetingMessage | null;
/** 输入框内容 */
inputValue: string;
/** 设置输入框内容 */
setInputValue: (value: string) => void;
/** 开始会议(用户提出议题) */
startMeeting: (topic: string) => Promise<void>;
/** 继续会议(下一轮讨论) */
continueMeeting: (userMessage?: string) => Promise<void>;
/** 用户插话 */
sendUserMessage: (message: string) => Promise<void>;
/** 重置会议 */
resetMeeting: () => void;
/** 当前议题 */
currentTopic: string;
/** 是否正在加载 */
isLoading: boolean;
}
/**
* 投研会议室 Hook
*
* 管理投研会议的完整生命周期:
* 1. 启动会议(用户提出议题)
* 2. 处理角色发言(支持流式和非流式)
* 3. 用户插话
* 4. 继续讨论
* 5. 得出结论
*/
export const useInvestmentMeeting = ({
userId = 'anonymous',
userNickname = '匿名用户',
onToast,
}: UseInvestmentMeetingParams = {}): UseInvestmentMeetingReturn => {
// 会议状态
const [messages, setMessages] = useState<MeetingMessage[]>([]);
const [status, setStatus] = useState<MeetingStatus>(MeetingStatus.IDLE);
const [speakingRoleId, setSpeakingRoleId] = useState<string | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const [currentRound, setCurrentRound] = useState(0);
const [isConcluded, setIsConcluded] = useState(false);
const [conclusion, setConclusion] = useState<MeetingMessage | null>(null);
const [currentTopic, setCurrentTopic] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [inputValue, setInputValue] = useState('');
// 用于取消 SSE 连接
const eventSourceRef = useRef<EventSource | null>(null);
/**
* 添加消息到列表
*/
const addMessage = useCallback((message: MeetingMessage) => {
setMessages((prev) => [
...prev,
{
...message,
id: message.id || Date.now() + Math.random(),
},
]);
}, []);
/**
* 重置会议状态
*/
const resetMeeting = useCallback(() => {
// 关闭 SSE 连接
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
setMessages([]);
setStatus(MeetingStatus.IDLE);
setSpeakingRoleId(null);
setSessionId(null);
setCurrentRound(0);
setIsConcluded(false);
setConclusion(null);
setCurrentTopic('');
setIsLoading(false);
setInputValue('');
}, []);
/**
* 启动会议(使用流式 SSE
*/
const startMeetingStream = useCallback(
async (topic: string) => {
setCurrentTopic(topic);
setStatus(MeetingStatus.STARTING);
setIsLoading(true);
setMessages([]);
try {
// 使用 EventSource 进行 SSE 连接
const params = new URLSearchParams({
topic,
user_id: userId,
user_nickname: userNickname,
});
const eventSource = new EventSource(
`/mcp/agent/meeting/stream?${params.toString()}`
);
eventSourceRef.current = eventSource;
eventSource.onmessage = (event) => {
try {
const data: MeetingEvent = JSON.parse(event.data);
switch (data.type) {
case 'session_start':
setSessionId(data.session_id || null);
setStatus(MeetingStatus.DISCUSSING);
break;
case 'order_decided':
// 发言顺序已决定
break;
case 'speaking_start':
setSpeakingRoleId(data.role_id || null);
setStatus(MeetingStatus.SPEAKING);
break;
case 'message':
if (data.message) {
addMessage(data.message);
setSpeakingRoleId(null);
// 检查是否是结论
if (data.message.is_conclusion) {
setConclusion(data.message);
setIsConcluded(true);
}
}
break;
case 'meeting_end':
setCurrentRound(data.round_number || 1);
setIsConcluded(data.is_concluded || false);
setStatus(
data.is_concluded
? MeetingStatus.CONCLUDED
: MeetingStatus.WAITING_INPUT
);
setIsLoading(false);
eventSource.close();
break;
}
} catch (e) {
console.error('解析 SSE 事件失败:', e);
}
};
eventSource.onerror = (error) => {
console.error('SSE 连接错误:', error);
eventSource.close();
setStatus(MeetingStatus.ERROR);
setIsLoading(false);
onToast?.({
title: '连接失败',
description: '会议连接中断,请重试',
status: 'error',
});
};
} catch (error) {
console.error('启动会议失败:', error);
setStatus(MeetingStatus.ERROR);
setIsLoading(false);
onToast?.({
title: '启动会议失败',
description: '请稍后重试',
status: 'error',
});
}
},
[userId, userNickname, addMessage, onToast]
);
/**
* 启动会议(非流式,获取完整响应)
*/
const startMeeting = useCallback(
async (topic: string) => {
setCurrentTopic(topic);
setStatus(MeetingStatus.STARTING);
setIsLoading(true);
setMessages([]);
try {
const response = await axios.post<MeetingResponse>(
'/mcp/agent/meeting/start',
{
topic,
user_id: userId,
user_nickname: userNickname,
}
);
if (response.data.success) {
const data = response.data;
setSessionId(data.session_id);
setCurrentRound(data.round_number);
setIsConcluded(data.is_concluded);
// 添加所有消息
data.messages.forEach((msg) => {
addMessage(msg);
});
// 设置结论
if (data.conclusion) {
setConclusion(data.conclusion);
}
setStatus(
data.is_concluded
? MeetingStatus.CONCLUDED
: MeetingStatus.WAITING_INPUT
);
} else {
throw new Error('会议启动失败');
}
} catch (error: any) {
console.error('启动会议失败:', error);
setStatus(MeetingStatus.ERROR);
onToast?.({
title: '启动会议失败',
description: error.response?.data?.detail || error.message,
status: 'error',
});
} finally {
setIsLoading(false);
}
},
[userId, userNickname, addMessage, onToast]
);
/**
* 继续会议讨论
*/
const continueMeeting = useCallback(
async (userMessage?: string) => {
if (!currentTopic) {
onToast?.({
title: '无法继续',
description: '请先启动会议',
status: 'warning',
});
return;
}
setStatus(MeetingStatus.DISCUSSING);
setIsLoading(true);
try {
const response = await axios.post<MeetingResponse>(
'/mcp/agent/meeting/continue',
{
topic: currentTopic,
user_id: userId,
user_nickname: userNickname,
session_id: sessionId,
user_message: userMessage,
conversation_history: messages,
}
);
if (response.data.success) {
const data = response.data;
setCurrentRound(data.round_number);
setIsConcluded(data.is_concluded);
// 添加新的消息
data.messages.forEach((msg) => {
addMessage(msg);
});
// 设置结论
if (data.conclusion) {
setConclusion(data.conclusion);
}
setStatus(
data.is_concluded
? MeetingStatus.CONCLUDED
: MeetingStatus.WAITING_INPUT
);
}
} catch (error: any) {
console.error('继续会议失败:', error);
setStatus(MeetingStatus.ERROR);
onToast?.({
title: '继续会议失败',
description: error.response?.data?.detail || error.message,
status: 'error',
});
} finally {
setIsLoading(false);
}
},
[currentTopic, userId, userNickname, sessionId, messages, addMessage, onToast]
);
/**
* 用户发送消息(插话)
*/
const sendUserMessage = useCallback(
async (message: string) => {
if (!message.trim()) return;
// 先添加用户消息到列表
const userRole = getRoleConfig('user');
addMessage({
role_id: 'user',
role_name: '用户',
nickname: userNickname,
avatar: userRole?.avatar || '',
color: userRole?.color || '#6366F1',
content: message,
timestamp: new Date().toISOString(),
round_number: currentRound,
});
// 清空输入框
setInputValue('');
// 继续会议,带上用户消息
await continueMeeting(message);
},
[userNickname, currentRound, addMessage, continueMeeting]
);
return {
messages,
status,
speakingRoleId,
sessionId,
currentRound,
isConcluded,
conclusion,
inputValue,
setInputValue,
startMeeting,
continueMeeting,
sendUserMessage,
resetMeeting,
currentTopic,
isLoading,
};
};
export default useInvestmentMeeting;