/** * Agent 审计任务创建对话框 * 专门用于 Agent Audit 页面,UI 风格与终端界面保持一致 */ import { useState, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Search, ChevronRight, GitBranch, Package, Globe, Loader2, Bot, Settings2, Play, Upload, FolderOpen, } from "lucide-react"; import { toast } from "sonner"; import { api } from "@/shared/config/database"; import { createAgentTask } from "@/shared/api/agentTasks"; import { isRepositoryProject, isZipProject } from "@/shared/utils/projectUtils"; import { getZipFileInfo, type ZipFileMeta } from "@/shared/utils/zipStorage"; import { validateZipFile } from "@/features/projects/services/repoZipScan"; import type { Project } from "@/shared/types"; import FileSelectionDialog from "@/components/audit/FileSelectionDialog"; interface CreateAgentTaskDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } const DEFAULT_EXCLUDES = [ "node_modules/**", ".git/**", "dist/**", "build/**", "*.log", ]; export default function CreateAgentTaskDialog({ open, onOpenChange, }: CreateAgentTaskDialogProps) { const navigate = useNavigate(); // 状态 const [projects, setProjects] = useState([]); const [loadingProjects, setLoadingProjects] = useState(true); const [selectedProjectId, setSelectedProjectId] = useState(""); const [searchTerm, setSearchTerm] = useState(""); const [branch, setBranch] = useState("main"); const [branches, setBranches] = useState([]); const [loadingBranches, setLoadingBranches] = useState(false); const [excludePatterns, setExcludePatterns] = useState(DEFAULT_EXCLUDES); const [showAdvanced, setShowAdvanced] = useState(false); const [creating, setCreating] = useState(false); // ZIP 文件状态 const [zipFile, setZipFile] = useState(null); const [storedZipInfo, setStoredZipInfo] = useState(null); const [useStoredZip, setUseStoredZip] = useState(true); // 文件选择状态 const [selectedFiles, setSelectedFiles] = useState(); const [showFileSelection, setShowFileSelection] = useState(false); const selectedProject = projects.find((p) => p.id === selectedProjectId); // 加载项目列表 useEffect(() => { if (open) { setLoadingProjects(true); api.getProjects() .then((data) => { setProjects(data.filter((p: Project) => p.is_active)); }) .catch(() => { toast.error("加载项目列表失败"); }) .finally(() => setLoadingProjects(false)); // 重置状态 setSelectedProjectId(""); setSearchTerm(""); setBranch("main"); setExcludePatterns(DEFAULT_EXCLUDES); setShowAdvanced(false); setZipFile(null); setStoredZipInfo(null); setSelectedFiles(undefined); } }, [open]); // 加载分支列表 useEffect(() => { const loadBranches = async () => { // 使用 selectedProjectId 从 projects 中获取最新的 project 对象 const project = projects.find((p) => p.id === selectedProjectId); if (!project || !isRepositoryProject(project)) { setBranches([]); return; } setLoadingBranches(true); try { const result = await api.getProjectBranches(project.id); console.log("[Branch] 加载分支结果:", result); if (result.error) { console.warn("[Branch] 加载分支警告:", result.error); toast.error(`加载分支失败: ${result.error}`); } setBranches(result.branches); if (result.default_branch) { setBranch(result.default_branch); } } catch (err) { const msg = err instanceof Error ? err.message : "未知错误"; console.error("[Branch] 加载分支失败:", msg); toast.error(`加载分支失败: ${msg}`); setBranches([project.default_branch || "main"]); } finally { setLoadingBranches(false); } }; loadBranches(); }, [selectedProjectId, projects]); // 加载 ZIP 文件信息 useEffect(() => { const loadZipInfo = async () => { if (!selectedProject || !isZipProject(selectedProject)) { setStoredZipInfo(null); return; } try { const info = await getZipFileInfo(selectedProject.id); setStoredZipInfo(info); setUseStoredZip(info.has_file); } catch { setStoredZipInfo(null); } }; loadZipInfo(); }, [selectedProject?.id]); // 过滤项目 const filteredProjects = useMemo(() => { if (!searchTerm) return projects; const term = searchTerm.toLowerCase(); return projects.filter( (p) => p.name.toLowerCase().includes(term) || p.description?.toLowerCase().includes(term) ); }, [projects, searchTerm]); // 是否可以开始 const canStart = useMemo(() => { if (!selectedProject) return false; if (isZipProject(selectedProject)) { return (useStoredZip && storedZipInfo?.has_file) || !!zipFile; } return !!selectedProject.repository_url && !!branch.trim(); }, [selectedProject, useStoredZip, storedZipInfo, zipFile, branch]); // 创建任务 const handleCreate = async () => { if (!selectedProject) return; setCreating(true); try { const agentTask = await createAgentTask({ project_id: selectedProject.id, name: `Agent审计-${selectedProject.name}`, branch_name: isRepositoryProject(selectedProject) ? branch : undefined, exclude_patterns: excludePatterns, target_files: selectedFiles, verification_level: "sandbox", }); onOpenChange(false); toast.success("Agent 审计任务已创建"); navigate(`/agent-audit/${agentTask.id}`); } catch (err) { const msg = err instanceof Error ? err.message : "创建失败"; toast.error(msg); } finally { setCreating(false); } }; // 处理文件上传 const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const validation = validateZipFile(file); if (!validation.valid) { toast.error(validation.error || "文件无效"); e.target.value = ""; return; } setZipFile(file); setUseStoredZip(false); } }; return ( {/* Header */}
New Agent Audit

AI-Powered Security Analysis

{/* 项目选择 */}
Select Project {filteredProjects.length} available
{/* 搜索框 */}
setSearchTerm(e.target.value)} className="pl-9 h-10 bg-gray-900/50 border-gray-800 text-white font-mono placeholder:text-gray-600 focus:border-primary focus:ring-0" />
{/* 项目列表 */} {loadingProjects ? (
) : filteredProjects.length === 0 ? (
{searchTerm ? "No matches" : "No projects"}
) : (
{filteredProjects.map((project) => ( setSelectedProjectId(project.id)} /> ))}
)}
{/* 配置区域 */} {selectedProject && (
{/* 仓库项目:分支选择 */} {isRepositoryProject(selectedProject) && (
Branch {loadingBranches ? (
Loading...
) : ( )}
)} {/* ZIP 项目:文件选择 */} {isZipProject(selectedProject) && (
ZIP File
{storedZipInfo?.has_file && (
setUseStoredZip(true)} >
{storedZipInfo.original_filename} Stored
)}
)} {/* 高级选项 */} Advanced Options {/* 文件选择 */} {(() => { const isRepo = isRepositoryProject(selectedProject); const isZip = isZipProject(selectedProject); const hasStoredZip = storedZipInfo?.has_file; // 可以选择文件的条件:仓库项目 或 ZIP项目使用已存储文件 const canSelectFiles = isRepo || (isZip && useStoredZip && hasStoredZip); return (

Scan Scope

{selectedFiles ? `${selectedFiles.length} files selected` : "All files"}

{selectedFiles && canSelectFiles && ( )}
); })()} {/* 排除模式 */}
Exclude Patterns
{excludePatterns.map((p) => ( setExcludePatterns((prev) => prev.filter((x) => x !== p))} > {p} × ))}
{ if (e.key === "Enter" && e.currentTarget.value) { const val = e.currentTarget.value.trim(); if (val && !excludePatterns.includes(val)) { setExcludePatterns((prev) => [...prev, val]); } e.currentTarget.value = ""; } }} />
)}
{/* Footer */}
{/* 文件选择对话框 */}
); } // 项目列表项 function ProjectItem({ project, selected, onSelect, }: { project: Project; selected: boolean; onSelect: () => void; }) { const isRepo = isRepositoryProject(project); return (
{isRepo ? ( ) : ( )}
{project.name} {isRepo ? "REPO" : "ZIP"}
{project.description && (

{project.description}

)}
{selected && (
)}
); }