From c0ac7d0544e6c8fb8c9cd4f179a7bf2adccf6b87 Mon Sep 17 00:00:00 2001 From: lintsinghua Date: Thu, 18 Dec 2025 23:58:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8A=A5=E5=91=8A):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=8A=A5=E5=91=8A=E5=AF=BC=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 AuditTasks 页面添加快速扫描和 Agent 任务的报告导出功能 - 在 ReportExportDialog 中优化颜色样式以支持亮色/暗色模式 - 修复报告生成器中字段为空时的处理逻辑 --- backend/app/services/report_generator.py | 19 +++- .../components/ReportExportDialog.tsx | 59 ++++++------ frontend/src/pages/AuditTasks.tsx | 92 ++++++++++++++++++- 3 files changed, 132 insertions(+), 38 deletions(-) diff --git a/backend/app/services/report_generator.py b/backend/app/services/report_generator.py index e282dff..508de92 100644 --- a/backend/app/services/report_generator.py +++ b/backend/app/services/report_generator.py @@ -344,7 +344,9 @@ class ReportGenerator: {% endif %} + {% if issue.description %}
{{ issue.description }}
+ {% endif %} {% if issue.code_snippet %}
{{ issue.code_snippet }}
@@ -413,13 +415,24 @@ class ReportGenerator: item['severity'] = item.get('severity', 'low') item['severity_label'] = sev_labels.get(item['severity'], 'UNKNOWN') item['line'] = item.get('line_number') or item.get('line') - + # 确保代码片段存在 (处理可能的字段名差异) code = item.get('code_snippet') or item.get('code') or item.get('context') if isinstance(code, list): code = '\n'.join(code) - item['code_snippet'] = code - + item['code_snippet'] = code if code else None + + # 确保 description 不为 None + desc = item.get('description') + if not desc or desc == 'None': + desc = item.get('title', '') # 如果没有描述,使用标题 + item['description'] = desc + + # 确保 suggestion 不为 None + suggestion = item.get('suggestion') + if suggestion == 'None' or suggestion is None: + item['suggestion'] = None + processed.append(item) return processed diff --git a/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx b/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx index 7805417..c48ea84 100644 --- a/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx +++ b/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx @@ -91,8 +91,8 @@ const FORMAT_CONFIG: Record, extension: ".md", mime: "text/markdown", - color: "text-sky-400", - bgColor: "bg-sky-500/10 border-sky-500/30", + color: "text-sky-600 dark:text-sky-400", + bgColor: "bg-sky-100 dark:bg-sky-500/10 border-sky-300 dark:border-sky-500/30", }, json: { label: "JSON", @@ -100,8 +100,8 @@ const FORMAT_CONFIG: Record, extension: ".json", mime: "application/json", - color: "text-amber-400", - bgColor: "bg-amber-500/10 border-amber-500/30", + color: "text-amber-600 dark:text-amber-400", + bgColor: "bg-amber-100 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30", }, html: { label: "HTML", @@ -109,8 +109,8 @@ const FORMAT_CONFIG: Record, extension: ".html", mime: "text/html", - color: "text-emerald-400", - bgColor: "bg-emerald-500/10 border-emerald-500/30", + color: "text-emerald-600 dark:text-emerald-400", + bgColor: "bg-emerald-100 dark:bg-emerald-500/10 border-emerald-300 dark:border-emerald-500/30", }, }; @@ -126,10 +126,10 @@ const DEFAULT_EXPORT_OPTIONS: ExportOptions = { function getSeverityColor(severity: string): string { const colors: Record = { - critical: "text-rose-400", - high: "text-orange-400", - medium: "text-amber-400", - low: "text-sky-400", + critical: "text-rose-600 dark:text-rose-400", + high: "text-orange-600 dark:text-orange-400", + medium: "text-amber-600 dark:text-amber-400", + low: "text-sky-600 dark:text-sky-400", info: "text-muted-foreground", }; return colors[severity.toLowerCase()] || colors.info; @@ -145,10 +145,10 @@ function formatBytes(bytes: number): string { // 获取安全评分颜色 function getScoreColor(score: number): { text: string; bg: string; glow: string } { - if (score >= 80) return { text: "text-emerald-400", bg: "stroke-emerald-500", glow: "drop-shadow-[0_0_8px_rgba(16,185,129,0.5)]" }; - if (score >= 60) return { text: "text-amber-400", bg: "stroke-amber-500", glow: "drop-shadow-[0_0_8px_rgba(245,158,11,0.5)]" }; - if (score >= 40) return { text: "text-orange-400", bg: "stroke-orange-500", glow: "drop-shadow-[0_0_8px_rgba(249,115,22,0.5)]" }; - return { text: "text-rose-400", bg: "stroke-rose-500", glow: "drop-shadow-[0_0_8px_rgba(244,63,94,0.5)]" }; + if (score >= 80) return { text: "text-emerald-600 dark:text-emerald-400", bg: "stroke-emerald-500", glow: "" }; + if (score >= 60) return { text: "text-amber-600 dark:text-amber-400", bg: "stroke-amber-500", glow: "" }; + if (score >= 40) return { text: "text-orange-600 dark:text-orange-400", bg: "stroke-orange-500", glow: "" }; + return { text: "text-rose-600 dark:text-rose-400", bg: "stroke-rose-500", glow: "" }; } // ============ Sub Components ============ @@ -181,7 +181,7 @@ const CircularProgress = memo(function CircularProgress({ fill="none" stroke="currentColor" strokeWidth={strokeWidth} - className="text-foreground/50" + className="text-slate-300 dark:text-slate-700" /> {/* Progress circle */} 0 ? "up" : null, }, { icon: , label: "高危问题", value: criticalAndHigh, - color: criticalAndHigh > 0 ? "text-rose-400" : "text-muted-foreground", - iconColor: "text-orange-400", + color: criticalAndHigh > 0 ? "text-rose-600 dark:text-rose-400" : "text-muted-foreground", + iconColor: "text-orange-600 dark:text-orange-400", trend: criticalAndHigh > 0 ? "critical" : null, }, { icon: , label: "已验证", value: verified, - color: "text-emerald-400", - iconColor: "text-emerald-400", + color: "text-emerald-600 dark:text-emerald-400", + iconColor: "text-emerald-600 dark:text-emerald-400", trend: null, }, ]; @@ -272,7 +272,7 @@ const EnhancedStatsPanel = memo(function EnhancedStatsPanel({ {stat.value} {stat.trend === "critical" && stat.value > 0 && ( - + )} @@ -1622,21 +1622,14 @@ export const ReportExportDialog = memo(function ReportExportDialog({ return ( - - {/* Header - 增强设计 */} -
- {/* 装饰性背景元素 */} -
-
-
-
- + + {/* Header */} +
-
+
-
@@ -1824,7 +1817,7 @@ export const ReportExportDialog = memo(function ReportExportDialog({
{/* Footer - 增强设计 */} -
+
diff --git a/frontend/src/pages/AuditTasks.tsx b/frontend/src/pages/AuditTasks.tsx index 7675cf8..ef1ecef 100644 --- a/frontend/src/pages/AuditTasks.tsx +++ b/frontend/src/pages/AuditTasks.tsx @@ -24,16 +24,20 @@ import { Shield, Terminal, Bot, - Zap + Zap, + Download } from "lucide-react"; import { api } from "@/shared/config/database"; +import { apiClient } from "@/shared/api/serverClient"; import type { AuditTask } from "@/shared/types"; import { Link, useNavigate } from "react-router-dom"; import { toast } from "sonner"; import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog"; +import ExportReportDialog from "@/components/reports/ExportReportDialog"; import { calculateTaskProgress } from "@/shared/utils/utils"; -import { getAgentTasks, cancelAgentTask, type AgentTask } from "@/shared/api/agentTasks"; +import { getAgentTasks, cancelAgentTask, getAgentFindings, type AgentTask, type AgentFinding } from "@/shared/api/agentTasks"; +import ReportExportDialog from "@/pages/AgentAudit/components/ReportExportDialog"; // Zombie task detection config const ZOMBIE_TIMEOUT = 180000; // 3 minutes without progress is potentially stuck @@ -59,6 +63,14 @@ export default function AuditTasks() { const [agentTasks, setAgentTasks] = useState([]); const [agentLoading, setAgentLoading] = useState(true); const [cancellingAgentTaskId, setCancellingAgentTaskId] = useState(null); + const [exportingTaskId, setExportingTaskId] = useState(null); + const [showExportDialog, setShowExportDialog] = useState(false); + const [exportTask, setExportTask] = useState(null); + const [exportIssues, setExportIssues] = useState([]); + // Agent 任务导出对话框状态 + const [showAgentExportDialog, setShowAgentExportDialog] = useState(false); + const [exportAgentTask, setExportAgentTask] = useState(null); + const [exportAgentFindings, setExportAgentFindings] = useState([]); // Zombie task detection: track progress and time for each task const taskProgressRef = useRef>(new Map()); @@ -201,6 +213,40 @@ export default function AuditTasks() { } }; + // 打开快速扫描任务导出对话框 + const handleOpenExportDialog = async (task: AuditTask) => { + try { + setExportingTaskId(task.id); + // 获取任务的问题列表 + const issuesResponse = await apiClient.get(`/tasks/${task.id}/issues`); + setExportTask(task); + setExportIssues(issuesResponse.data || []); + setShowExportDialog(true); + } catch (error: any) { + console.error('获取问题列表失败:', error); + toast.error("获取问题列表失败"); + } finally { + setExportingTaskId(null); + } + }; + + // 打开 Agent 任务导出对话框 + const handleOpenAgentExportDialog = async (task: AgentTask) => { + try { + setExportingTaskId(task.id); + // 获取任务的 findings 列表 + const findings = await getAgentFindings(task.id); + setExportAgentTask(task); + setExportAgentFindings(findings); + setShowAgentExportDialog(true); + } catch (error: any) { + console.error('获取 findings 列表失败:', error); + toast.error("获取审计结果失败"); + } finally { + setExportingTaskId(null); + } + }; + const loadTasks = async () => { try { setLoading(true); @@ -711,6 +757,17 @@ export default function AuditTasks() { )} + {(task.status === 'completed' || (task.findings_count != null && task.findings_count > 0)) && ( + + )} {/* 任务详情按钮 */} )} + {(task.issues_count > 0 || task.status === 'completed') && ( + + )}
); }