/** * Agent Audit Page - Modular Implementation * Main entry point for the Agent Audit feature * Cassette Futurism / Terminal Retro aesthetic */ import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { useParams } from "react-router-dom"; import { Terminal, Bot, Loader2, Radio, Filter, Maximize2, ArrowDown } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; import { useAgentStream } from "@/hooks/useAgentStream"; import { getAgentTask, getAgentFindings, cancelAgentTask, getAgentTree, getAgentEvents, AgentEvent, } from "@/shared/api/agentTasks"; import CreateAgentTaskDialog from "@/components/agent/CreateAgentTaskDialog"; // Local imports import { SplashScreen, Header, LogEntry, AgentTreeNodeItem, AgentDetailPanel, StatsPanel, AgentErrorBoundary, } from "./components"; import ReportExportDialog from "./components/ReportExportDialog"; import { useAgentAuditState } from "./hooks"; import { ACTION_VERBS, POLLING_INTERVALS } from "./constants"; import { cleanThinkingContent, truncateOutput, createLogItem } from "./utils"; import type { LogItem } from "./types"; function AgentAuditPageContent() { const { taskId } = useParams<{ taskId: string }>(); const { task, findings, agentTree, logs, selectedAgentId, showAllLogs, isLoading, connectionStatus, isAutoScroll, expandedLogIds, treeNodes, filteredLogs, isRunning, isComplete, setTask, setFindings, setAgentTree, addLog, updateLog, removeLog, selectAgent, setLoading, setConnectionStatus, setAutoScroll, toggleLogExpanded, setCurrentAgentName, getCurrentAgentName, setCurrentThinkingId, getCurrentThinkingId, dispatch, reset, } = useAgentAuditState(); // Local state const [showSplash, setShowSplash] = useState(!taskId); const [showCreateDialog, setShowCreateDialog] = useState(false); const [showExportDialog, setShowExportDialog] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const [statusVerb, setStatusVerb] = useState(ACTION_VERBS[0]); const [statusDots, setStatusDots] = useState(0); const logEndRef = useRef(null); const agentTreeRefreshTimer = useRef | null>(null); const lastAgentTreeRefreshTime = useRef(0); const previousTaskIdRef = useRef(undefined); const disconnectStreamRef = useRef<(() => void) | null>(null); const lastEventSequenceRef = useRef(0); const historicalEventsLoadedRef = useRef(false); // 🔥 当 taskId 变化时立即重置状态(新建任务时清理旧日志) useEffect(() => { // 如果 taskId 发生变化,立即重置 if (taskId !== previousTaskIdRef.current) { // 1. 先断开旧的 SSE 流连接 if (disconnectStreamRef.current) { disconnectStreamRef.current(); disconnectStreamRef.current = null; } // 2. 重置所有状态 reset(); setShowSplash(!taskId); // 3. 重置事件序列号和加载状态 lastEventSequenceRef.current = 0; historicalEventsLoadedRef.current = false; } previousTaskIdRef.current = taskId; }, [taskId, reset]); // ============ Data Loading ============ const loadTask = useCallback(async () => { if (!taskId) return; try { const data = await getAgentTask(taskId); setTask(data); } catch { toast.error("Failed to load task"); } }, [taskId, setTask]); const loadFindings = useCallback(async () => { if (!taskId) return; try { const data = await getAgentFindings(taskId); setFindings(data); } catch (err) { console.error(err); } }, [taskId, setFindings]); const loadAgentTree = useCallback(async () => { if (!taskId) return; try { const data = await getAgentTree(taskId); setAgentTree(data); } catch (err) { console.error(err); } }, [taskId, setAgentTree]); const debouncedLoadAgentTree = useCallback(() => { const now = Date.now(); const minInterval = POLLING_INTERVALS.AGENT_TREE_DEBOUNCE; if (agentTreeRefreshTimer.current) { clearTimeout(agentTreeRefreshTimer.current); } const timeSinceLastRefresh = now - lastAgentTreeRefreshTime.current; if (timeSinceLastRefresh < minInterval) { agentTreeRefreshTimer.current = setTimeout(() => { lastAgentTreeRefreshTime.current = Date.now(); loadAgentTree(); }, minInterval - timeSinceLastRefresh); } else { agentTreeRefreshTimer.current = setTimeout(() => { lastAgentTreeRefreshTime.current = Date.now(); loadAgentTree(); }, POLLING_INTERVALS.AGENT_TREE_MIN_DELAY); } }, [loadAgentTree]); // 🔥 NEW: 加载历史事件并转换为日志项 const loadHistoricalEvents = useCallback(async () => { if (!taskId) return 0; try { console.log(`[AgentAudit] Fetching historical events for task ${taskId}...`); const events = await getAgentEvents(taskId, { limit: 500 }); console.log(`[AgentAudit] Received ${events.length} events from API`); if (events.length === 0) { console.log('[AgentAudit] No historical events found'); return 0; } // 按 sequence 排序确保顺序正确 events.sort((a, b) => a.sequence - b.sequence); // 转换事件为日志项 let processedCount = 0; events.forEach((event: AgentEvent) => { // 更新最后的事件序列号 if (event.sequence > lastEventSequenceRef.current) { lastEventSequenceRef.current = event.sequence; } // 提取 agent_name const agentName = (event.metadata?.agent_name as string) || (event.metadata?.agent as string) || undefined; // 根据事件类型创建日志项 switch (event.event_type) { // LLM 思考相关 case 'thinking': case 'llm_thought': case 'llm_decision': case 'llm_start': case 'llm_complete': case 'llm_action': case 'llm_observation': dispatch({ type: 'ADD_LOG', payload: { type: 'thinking', title: event.message?.slice(0, 100) + (event.message && event.message.length > 100 ? '...' : '') || 'Thinking...', content: event.message || (event.metadata?.thought as string) || '', agentName, } }); processedCount++; break; // 工具调用相关 case 'tool_call': dispatch({ type: 'ADD_LOG', payload: { type: 'tool', title: `Tool: ${event.tool_name || 'unknown'}`, content: event.tool_input ? `Input:\n${JSON.stringify(event.tool_input, null, 2)}` : '', tool: { name: event.tool_name || 'unknown', status: 'running' as const, }, agentName, } }); processedCount++; break; case 'tool_result': dispatch({ type: 'ADD_LOG', payload: { type: 'tool', title: `Completed: ${event.tool_name || 'unknown'}`, content: event.tool_output ? `Output:\n${truncateOutput(typeof event.tool_output === 'string' ? event.tool_output : JSON.stringify(event.tool_output, null, 2))}` : '', tool: { name: event.tool_name || 'unknown', duration: event.tool_duration_ms || 0, status: 'completed' as const, }, agentName, } }); processedCount++; break; // 发现漏洞 - 🔥 包含所有 finding 相关事件类型 case 'finding': case 'finding_new': case 'finding_verified': dispatch({ type: 'ADD_LOG', payload: { type: 'finding', title: event.message || (event.metadata?.title as string) || 'Vulnerability found', severity: (event.metadata?.severity as string) || 'medium', agentName, } }); processedCount++; break; // 调度和阶段相关 case 'dispatch': case 'dispatch_complete': case 'phase_start': case 'phase_complete': case 'node_start': case 'node_complete': dispatch({ type: 'ADD_LOG', payload: { type: 'dispatch', title: event.message || `Event: ${event.event_type}`, agentName, } }); processedCount++; break; // 任务完成 case 'task_complete': dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: event.message || 'Task completed', agentName, } }); processedCount++; break; // 任务错误 case 'task_error': dispatch({ type: 'ADD_LOG', payload: { type: 'error', title: event.message || 'Task error', agentName, } }); processedCount++; break; // 任务取消 case 'task_cancel': dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: event.message || 'Task cancelled', agentName, } }); processedCount++; break; // 进度事件 case 'progress': // 进度事件可以选择显示或跳过 if (event.message) { dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: event.message, agentName, } }); processedCount++; } break; // 信息和错误 case 'info': case 'complete': case 'error': case 'warning': dispatch({ type: 'ADD_LOG', payload: { type: event.event_type === 'error' ? 'error' : 'info', title: event.message || `${event.event_type}`, agentName, } }); processedCount++; break; // 跳过 thinking_token 等高频事件(它们不会被保存到数据库) case 'thinking_token': case 'thinking_start': case 'thinking_end': // 这些事件是流式传输用的,不保存到数据库 break; default: // 其他事件类型也显示为 info(如果有消息) if (event.message) { dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: event.message, agentName, } }); processedCount++; } } }); console.log(`[AgentAudit] Processed ${processedCount} events into logs, last sequence: ${lastEventSequenceRef.current}`); return events.length; } catch (err) { console.error('[AgentAudit] Failed to load historical events:', err); return 0; } }, [taskId, dispatch]); // ============ Stream Event Handling ============ const streamOptions = useMemo(() => ({ includeThinking: true, includeToolCalls: true, // 🔥 使用最后的事件序列号,避免重复接收历史事件 afterSequence: lastEventSequenceRef.current, onEvent: (event: { type: string; message?: string; metadata?: { agent_name?: string; agent?: string } }) => { if (event.metadata?.agent_name) { setCurrentAgentName(event.metadata.agent_name); } const dispatchEvents = ['dispatch', 'dispatch_complete', 'node_start', 'phase_start']; if (dispatchEvents.includes(event.type)) { if (event.type === 'dispatch' || event.type === 'dispatch_complete') { dispatch({ type: 'ADD_LOG', payload: { type: 'dispatch', title: event.message || `Agent dispatch: ${event.metadata?.agent || 'unknown'}`, agentName: getCurrentAgentName() || undefined, } }); } debouncedLoadAgentTree(); } }, onThinkingStart: () => { const currentId = getCurrentThinkingId(); if (currentId) { updateLog(currentId, { isStreaming: false }); } setCurrentThinkingId(null); }, onThinkingToken: (_token: string, accumulated: string) => { if (!accumulated?.trim()) return; const cleanContent = cleanThinkingContent(accumulated); if (!cleanContent) return; const currentId = getCurrentThinkingId(); if (!currentId) { // 预生成 ID,这样我们可以跟踪这个日志 const newLogId = `thinking-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; dispatch({ type: 'ADD_LOG', payload: { id: newLogId, type: 'thinking', title: 'Thinking...', content: cleanContent, isStreaming: true, agentName: getCurrentAgentName() || undefined, }}); setCurrentThinkingId(newLogId); } else { updateLog(currentId, { content: cleanContent }); } }, onThinkingEnd: (response: string) => { const cleanResponse = cleanThinkingContent(response || ""); const currentId = getCurrentThinkingId(); if (!cleanResponse) { if (currentId) { removeLog(currentId); } setCurrentThinkingId(null); return; } if (currentId) { updateLog(currentId, { title: cleanResponse.slice(0, 100) + (cleanResponse.length > 100 ? '...' : ''), content: cleanResponse, isStreaming: false }); setCurrentThinkingId(null); } }, onToolStart: (name: string, input: Record) => { const currentId = getCurrentThinkingId(); if (currentId) { updateLog(currentId, { isStreaming: false }); setCurrentThinkingId(null); } dispatch({ type: 'ADD_LOG', payload: { type: 'tool', title: `Tool: ${name}`, content: `Input:\n${JSON.stringify(input, null, 2)}`, tool: { name, status: 'running' }, agentName: getCurrentAgentName() || undefined, } }); }, onToolEnd: (name: string, output: unknown, duration: number) => { const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2); dispatch({ type: 'COMPLETE_TOOL_LOG', payload: { toolName: name, output: truncateOutput(outputStr), duration, } }); }, onFinding: (finding: Record) => { dispatch({ type: 'ADD_LOG', payload: { type: 'finding', title: (finding.title as string) || 'Vulnerability found', severity: (finding.severity as string) || 'medium', agentName: getCurrentAgentName() || undefined, } }); loadFindings(); }, onComplete: () => { dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: 'Audit completed successfully' } }); loadTask(); loadFindings(); loadAgentTree(); }, onError: (err: string) => { dispatch({ type: 'ADD_LOG', payload: { type: 'error', title: `Error: ${err}` } }); }, }), [dispatch, loadTask, loadFindings, loadAgentTree, debouncedLoadAgentTree, updateLog, removeLog, getCurrentAgentName, getCurrentThinkingId, setCurrentAgentName, setCurrentThinkingId]); const { connect: connectStream, disconnect: disconnectStream, isConnected } = useAgentStream(taskId || null, streamOptions); // 保存 disconnect 函数到 ref,以便在 taskId 变化时使用 useEffect(() => { disconnectStreamRef.current = disconnectStream; }, [disconnectStream]); // ============ Effects ============ // Status animation useEffect(() => { if (!isRunning) return; const dotTimer = setInterval(() => setStatusDots(d => (d + 1) % 4), 500); const verbTimer = setInterval(() => { setStatusVerb(ACTION_VERBS[Math.floor(Math.random() * ACTION_VERBS.length)]); }, 5000); return () => { clearInterval(dotTimer); clearInterval(verbTimer); }; }, [isRunning]); // Initial load - 🔥 加载任务数据和历史事件 useEffect(() => { if (!taskId) { setShowSplash(true); return; } setShowSplash(false); setLoading(true); historicalEventsLoadedRef.current = false; const loadAllData = async () => { try { // 先加载任务基本信息 await Promise.all([loadTask(), loadFindings(), loadAgentTree()]); // 🔥 加载历史事件 - 无论任务是否运行都需要加载 const eventsLoaded = await loadHistoricalEvents(); console.log(`[AgentAudit] Loaded ${eventsLoaded} historical events for task ${taskId}`); // 标记历史事件已加载完成 historicalEventsLoadedRef.current = true; } catch (error) { console.error('[AgentAudit] Failed to load data:', error); historicalEventsLoadedRef.current = true; // 即使出错也标记为完成,避免无限等待 } finally { setLoading(false); } }; loadAllData(); }, [taskId, loadTask, loadFindings, loadAgentTree, loadHistoricalEvents, setLoading]); // Stream connection - 🔥 在历史事件加载完成后连接 useEffect(() => { // 等待历史事件加载完成,且任务正在运行 if (!taskId || !task?.status || task.status !== 'running') return; // 如果历史事件尚未加载完成,等待一下 const checkAndConnect = () => { if (historicalEventsLoadedRef.current) { connectStream(); dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: 'Connected to audit stream' } }); } else { // 延迟重试 setTimeout(checkAndConnect, 100); } }; checkAndConnect(); return () => disconnectStream(); }, [taskId, task?.status, connectStream, disconnectStream, dispatch]); // Polling useEffect(() => { if (!taskId || !isRunning) return; const interval = setInterval(loadAgentTree, POLLING_INTERVALS.AGENT_TREE); return () => clearInterval(interval); }, [taskId, isRunning, loadAgentTree]); useEffect(() => { if (!taskId || !isRunning) return; const interval = setInterval(loadTask, POLLING_INTERVALS.TASK_STATS); return () => clearInterval(interval); }, [taskId, isRunning, loadTask]); // Auto scroll useEffect(() => { if (isAutoScroll && logEndRef.current) { logEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [logs, isAutoScroll]); // ============ Handlers ============ const handleAgentSelect = useCallback((agentId: string) => { if (selectedAgentId === agentId) { selectAgent(null); } else { selectAgent(agentId); } }, [selectedAgentId, selectAgent]); const handleCancel = async () => { if (!taskId || isCancelling) return; setIsCancelling(true); dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: 'Requesting task cancellation...' } }); try { await cancelAgentTask(taskId); toast.success("Task cancellation requested"); dispatch({ type: 'ADD_LOG', payload: { type: 'info', title: 'Task cancellation confirmed' } }); await loadTask(); disconnectStream(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; toast.error(`Failed to cancel task: ${errorMessage}`); dispatch({ type: 'ADD_LOG', payload: { type: 'error', title: `Failed to cancel: ${errorMessage}` } }); } finally { setIsCancelling(false); } }; const handleExportReport = () => { if (!task) return; setShowExportDialog(true); }; // ============ Render ============ if (showSplash && !taskId) { return ( <> setShowCreateDialog(true)} /> ); } if (isLoading && !task) { return (
{/* Grid background */}
Loading audit task...
); } return (
{/* Subtle grid background */}
{/* Header */}
setShowCreateDialog(true)} /> {/* Main content */}
{/* Left Panel - Activity Log */}
{/* Log header */}
Activity Log
{isConnected && (
Live
)} {filteredLogs.length}{!showAllLogs && logs.length !== filteredLogs.length ? ` / ${logs.length}` : ''}
{/* Log content */}
{/* Filter indicator */} {selectedAgentId && !showAllLogs && (
Filtering logs for selected agent
)} {/* Logs */} {filteredLogs.length === 0 ? (
{isRunning ? (
{selectedAgentId && !showAllLogs ? 'Waiting for activity from selected agent...' : 'Waiting for agent activity...'}
) : ( {selectedAgentId && !showAllLogs ? 'No activity from selected agent' : 'No activity yet'} )}
) : (
{filteredLogs.map(item => ( toggleLogExpanded(item.id)} /> ))}
)}
{/* Status bar */} {task && (
{isRunning ? ( {statusVerb}{'.'.repeat(statusDots)} ) : isComplete ? ( Audit {task.status} ) : ( Ready )}
{task.progress_percentage?.toFixed(0) || 0} % | {task.analyzed_files} /{task.total_files} files | {task.tool_calls_count || 0} tools
)}
{/* Right Panel - Agent Tree + Stats */}
{/* Agent Tree section */}
{/* Tree header */}
Agent Tree {agentTree && ( {agentTree.total_agents} )}
{selectedAgentId && !showAllLogs && ( )} {agentTree && agentTree.running_agents > 0 && (
{agentTree.running_agents}
)}
{/* Tree content */}
{treeNodes.length > 0 ? ( treeNodes.map(node => ( )) ) : (
{isRunning ? (
Initializing agents...
) : ( 'No agents yet' )}
)}
{/* Bottom section - Details + Stats */}
{/* Agent detail panel */} {selectedAgentId && !showAllLogs && ( selectAgent(null)} /> )} {/* Stats panel */}
{/* Create dialog */} {/* Export dialog */}
); } // Wrapped export with Error Boundary export default function AgentAuditPage() { const { taskId } = useParams<{ taskId: string }>(); return ( window.location.reload()} > ); }