/** * Terminal Progress Dialog * Cyberpunk Terminal Aesthetic */ import { useEffect, useRef, useState, useCallback } from "react"; import { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { Terminal, X as XIcon, Activity, Cpu, HardDrive, AlertTriangle, CheckCircle2 } from "lucide-react"; import { cn, calculateTaskProgress } from "@/shared/utils/utils"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; import { taskControl } from "@/shared/services/taskControl"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; interface TerminalProgressDialogProps { open: boolean; onOpenChange: (open: boolean) => void; taskId: string | null; taskType: "repository" | "zip"; } interface LogEntry { id: string; timestamp: string; message: string; type: "info" | "success" | "error" | "warning"; } export default function TerminalProgressDialog({ open, onOpenChange, taskId, taskType }: TerminalProgressDialogProps) { const [logs, setLogs] = useState([]); const [isCompleted, setIsCompleted] = useState(false); const [isFailed, setIsFailed] = useState(false); const [isCancelled, setIsCancelled] = useState(false); const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })); const logsEndRef = useRef(null); const pollIntervalRef = useRef(null); const hasInitializedLogsRef = useRef(false); // Refs for state accessed in intervals/effects to avoid dependency cycles const logsRef = useRef([]); const isCompletedRef = useRef(false); const isFailedRef = useRef(false); const isCancelledRef = useRef(false); // Sync refs with state useEffect(() => { logsRef.current = logs; }, [logs]); useEffect(() => { isCompletedRef.current = isCompleted; }, [isCompleted]); useEffect(() => { isFailedRef.current = isFailed; }, [isFailed]); useEffect(() => { isCancelledRef.current = isCancelled; }, [isCancelled]); // 添加日志条目 const addLog = useCallback((message: string, type: LogEntry["type"] = "info") => { const timestamp = new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" }); const newLog = { id: Math.random().toString(36).substr(2, 9), timestamp, message, type }; setLogs(prev => [...prev, newLog]); }, []); // 取消任务处理 const handleCancel = async () => { if (!taskId) return; if (!confirm('确定要取消此任务吗?已分析的结果将被保留。')) { return; } // 1. 标记任务为取消状态 taskControl.cancelTask(taskId); setIsCancelled(true); addLog("[ERR] 用户取消任务,正在停止...", "error"); // 2. 立即更新数据库状态 try { const { api } = await import("@/shared/config/database"); // biome-ignore lint/suspicious/noExplicitAny: API type mismatch workaround await api.updateAuditTask(taskId, { status: 'cancelled' } as any); addLog("[WARN] 任务状态已更新为已取消", "warning"); toast.success("任务已取消"); } catch (error) { console.error('更新取消状态失败:', error); toast.warning("任务已标记取消,后台正在停止..."); } }; // 自动滚动到底部 // biome-ignore lint/correctness/useExhaustiveDependencies: We want to scroll when logs change useEffect(() => { logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [logs]); // 实时更新光标处的时间 useEffect(() => { if (!open || isCompleted || isFailed || isCancelled) { return; } const timeInterval = setInterval(() => { setCurrentTime(new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })); }, 1000); return () => { clearInterval(timeInterval); }; }, [open, isCompleted, isFailed, isCancelled]); // 轮询任务状态 useEffect(() => { if (!open || !taskId) { // 清理状态 setLogs([]); logsRef.current = []; setIsCompleted(false); setIsFailed(false); setIsCancelled(false); hasInitializedLogsRef.current = false; if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } return; } // 只初始化日志一次(防止React严格模式重复) if (!hasInitializedLogsRef.current) { hasInitializedLogsRef.current = true; // 初始化日志 addLog("[INFO] 审计任务已启动", "info"); addLog(`TASK_ID: ${taskId}`, "info"); addLog(`TYPE: ${taskType === "repository" ? "REPO_AUDIT" : "ZIP_AUDIT"}`, "info"); addLog("[WAIT] 正在初始化审计环境...", "info"); } let lastScannedFiles = 0; let lastIssuesCount = 0; let lastTotalLines = 0; let lastStatus = ""; let _pollCount = 0; let hasDataChange = false; let isFirstPoll = true; // 开始轮询 const pollTask = async () => { // 如果任务已完成或失败,停止轮询 if (isCompletedRef.current || isFailedRef.current) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } return; } try { _pollCount++; hasDataChange = false; const requestStartTime = Date.now(); // 使用 api.getAuditTaskById 获取任务状态 const { api } = await import("@/shared/config/database"); const task = await api.getAuditTaskById(taskId); const requestDuration = Date.now() - requestStartTime; if (!task) { addLog(`[ERR] 任务不存在 (${requestDuration}ms)`, "error"); throw new Error("任务不存在"); } // 检查是否有数据变化 const statusChanged = task.status !== lastStatus; const filesChanged = task.scanned_files !== lastScannedFiles; const issuesChanged = task.issues_count !== lastIssuesCount; const linesChanged = task.total_lines !== lastTotalLines; hasDataChange = statusChanged || filesChanged || issuesChanged || linesChanged; // 标记首次轮询已完成 if (isFirstPoll) { isFirstPoll = false; } // 只在有变化时显示请求/响应信息(跳过 pending 状态) if (hasDataChange && task.status !== "pending") { addLog(`[NET] 正在获取任务状态...`, "info"); addLog( `[OK] 状态: ${task.status} | 文件: ${task.scanned_files}/${task.total_files} | 问题: ${task.issues_count} (${requestDuration}ms)`, "success" ); } // 更新上次状态 if (statusChanged) { lastStatus = task.status; } // 检查任务状态 if (task.status === "pending") { // 静默跳过 pending 状态,不显示任何日志 } else if (task.status === "running") { // 首次进入运行状态 if (statusChanged && logsRef.current.filter(l => l.message.includes("开始扫描")).length === 0) { addLog("[SCAN] 开始扫描代码文件...", "info"); if (task.project) { addLog(`[PROJ] 项目: ${task.project.name}`, "info"); if (task.branch_name) { addLog(`[BRCH] 分支: ${task.branch_name}`, "info"); } } } // 显示进度更新(仅在有变化时) if (filesChanged && task.scanned_files > lastScannedFiles) { const progress = calculateTaskProgress(task.scanned_files, task.total_files); const filesProcessed = task.scanned_files - lastScannedFiles; addLog( `[PROG] 扫描进度: ${task.scanned_files || 0}/${task.total_files || 0} 文件 (${progress}%) [+${filesProcessed}]`, "info" ); lastScannedFiles = task.scanned_files; } // 显示问题发现(仅在有变化时) if (issuesChanged && task.issues_count > lastIssuesCount) { const newIssues = task.issues_count - lastIssuesCount; addLog(`[WARN] 发现 ${newIssues} 个新问题 (总计: ${task.issues_count})`, "warning"); lastIssuesCount = task.issues_count; } // 显示代码行数(仅在有变化时) if (linesChanged && task.total_lines > lastTotalLines) { const newLines = task.total_lines - lastTotalLines; addLog(`[STAT] 已分析 ${task.total_lines.toLocaleString()} 行代码 [+${newLines.toLocaleString()}]`, "info"); lastTotalLines = task.total_lines; } } else if (task.status === "completed") { // 任务完成 if (!isCompletedRef.current) { addLog("", "info"); // 空行分隔 addLog("[DONE] 代码扫描完成", "success"); addLog("----------------------------------", "info"); addLog(`[STAT] 总计扫描: ${task.total_files} 个文件`, "success"); addLog(`[STAT] 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success"); addLog(`[RSLT] 发现问题: ${task.issues_count} 个`, task.issues_count > 0 ? "warning" : "success"); // 解析问题类型分布 if (task.issues_count > 0) { try { const { api: apiImport } = await import("@/shared/config/database"); const issues = await apiImport.getAuditIssues(taskId); const severityCounts = { critical: issues.filter(i => i.severity === 'critical').length, high: issues.filter(i => i.severity === 'high').length, medium: issues.filter(i => i.severity === 'medium').length, low: issues.filter(i => i.severity === 'low').length }; if (severityCounts.critical > 0) { addLog(` [CRIT] 严重: ${severityCounts.critical} 个`, "error"); } if (severityCounts.high > 0) { addLog(` [HIGH] 高: ${severityCounts.high} 个`, "warning"); } if (severityCounts.medium > 0) { addLog(` [MED] 中等: ${severityCounts.medium} 个`, "warning"); } if (severityCounts.low > 0) { addLog(` [LOW] 低: ${severityCounts.low} 个`, "info"); } } catch (_e) { // 静默处理错误 } } addLog(`[SCOR] 质量评分: ${task.quality_score.toFixed(1)}/100`, "success"); addLog("----------------------------------", "info"); addLog("[FIN] 审计任务已完成!", "success"); if (task.completed_at) { const startTime = new Date(task.created_at).getTime(); const endTime = new Date(task.completed_at).getTime(); const duration = Math.round((endTime - startTime) / 1000); addLog(`[TIME] 总耗时: ${duration} 秒`, "info"); } setIsCompleted(true); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } } else if (task.status === "cancelled") { // 任务被取消 if (!isCancelledRef.current) { addLog("", "info"); // 空行分隔 addLog("[STOP] 任务已被用户取消", "warning"); addLog("----------------------------------", "warning"); addLog(`[STAT] 完成统计:`, "info"); addLog(` • 已分析文件: ${task.scanned_files}/${task.total_files}`, "info"); addLog(` • 发现问题: ${task.issues_count} 个`, "info"); addLog(` • 代码行数: ${task.total_lines.toLocaleString()} 行`, "info"); addLog("----------------------------------", "warning"); addLog("[SAVE] 已分析的结果已保存到数据库", "success"); setIsCancelled(true); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } } else if (task.status === "failed") { // 任务失败 if (!isFailedRef.current) { addLog("", "info"); // 空行分隔 addLog("[FAIL] 审计任务执行失败", "error"); addLog("----------------------------------", "error"); // 尝试从日志系统获取具体错误信息 try { const { logger } = await import("@/shared/utils/logger"); const recentLogs = logger.getLogs({ startTime: Date.now() - 60000, // 最近1分钟 }); // 查找与当前任务相关的错误 const taskErrors = recentLogs .filter(log => log.level === 'ERROR' && (log.message.includes(taskId) || log.message.includes('审计') || log.message.includes('API')) ) .slice(-3); // 最近3条错误 if (taskErrors.length > 0) { addLog("具体错误信息:", "error"); taskErrors.forEach(log => { addLog(` • ${log.message}`, "error"); if (log.data?.error) { const errorMsg = typeof log.data.error === 'string' ? log.data.error : log.data.error.message || JSON.stringify(log.data.error); addLog(` ${errorMsg}`, "error"); } }); } else { // 如果没有找到具体错误,显示常见原因 addLog("可能的原因:", "error"); addLog(" • 网络连接问题", "error"); addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error"); addLog(" • GitHub/GitLab API 限流", "error"); addLog(" • LLM API 配置错误或额度不足", "error"); } } catch (_e) { // 如果获取日志失败,显示常见原因 addLog("可能的原因:", "error"); addLog(" • 网络连接问题", "error"); addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error"); addLog(" • GitHub/GitLab API 限流", "error"); addLog(" • LLM API 配置错误或额度不足", "error"); } addLog("----------------------------------", "error"); addLog("[HINT] 建议: 检查系统配置和网络连接后重试", "warning"); addLog("[LOGS] 查看完整日志: 导航栏 -> 系统日志", "warning"); setIsFailed(true); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "未知错误"; addLog(`[ERR] ${errorMessage}`, "error"); // 不中断轮询,继续尝试 } }; // 立即执行一次 pollTask(); // 设置定时轮询(每2秒) pollIntervalRef.current = window.setInterval(pollTask, 2000); // 清理函数 return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } }; }, [open, taskId, taskType, addLog]); // 获取日志颜色 const getLogColor = (type: LogEntry["type"]) => { switch (type) { case "success": return "text-emerald-400"; case "error": return "text-rose-400"; case "warning": return "text-amber-400"; default: return "text-gray-400"; } }; return ( e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} > 审计进度监控 实时显示代码审计任务的执行进度和详细信息 {/* Header */}
AUDIT_TERMINAL v2.0
{/* 状态指示灯 */}
{/* Main Content */}
{/* Left Sidebar - Task Info */}
Task ID
{taskId?.slice(0, 8)}...
Type
{taskType === 'repository' ? : } {taskType}
{/* Status Badge */}
Status
{isCancelled ? ( CANCELLED ) : isCompleted ? ( COMPLETED ) : isFailed ? ( FAILED ) : ( RUNNING )}
{/* Terminal Screen */}
{/* Terminal Output */}
{/* Grid background */}
{logs.map((log) => (
{log.timestamp} {log.message}
))} {!isCompleted && !isFailed && !isCancelled && (
{currentTime} _
)}
{/* Bottom Controls */}
{isCompleted ? "任务已完成" : isFailed ? "任务失败" : isCancelled ? "任务已取消" : "正在执行..."}
{!isCompleted && !isFailed && !isCancelled && ( )} {isFailed && ( )} {(isCompleted || isFailed || isCancelled) && ( )}
); }