diff --git a/public/images/example2.png b/public/images/example2.png index 29f414e..a545744 100644 Binary files a/public/images/example2.png and b/public/images/example2.png differ diff --git a/src/components/audit/CreateTaskDialog.tsx b/src/components/audit/CreateTaskDialog.tsx index 6dee43f..65c7d1e 100644 --- a/src/components/audit/CreateTaskDialog.tsx +++ b/src/components/audit/CreateTaskDialog.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } 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"; @@ -30,6 +31,7 @@ interface CreateTaskDialogProps { } export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, preselectedProjectId }: CreateTaskDialogProps) { + const navigate = useNavigate(); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); @@ -98,13 +100,21 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr await api.createAuditTask({ ...taskForm, - created_by: "system" // 无登录场景下使用系统用户 + created_by: null // 无登录场景下设置为null } as any); - toast.success("审计任务创建成功"); + // 显示详细的提示信息 + toast.success("审计任务创建成功", { + description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', + duration: 5000 + }); + onOpenChange(false); resetForm(); onTaskCreated(); + + // 跳转到项目详情页面 + navigate(`/projects/${taskForm.project_id}`); } catch (error) { console.error('Failed to create task:', error); toast.error("创建任务失败"); diff --git a/src/features/projects/services/repoScan.ts b/src/features/projects/services/repoScan.ts index dde5cf3..5431a9e 100644 --- a/src/features/projects/services/repoScan.ts +++ b/src/features/projects/services/repoScan.ts @@ -45,74 +45,81 @@ export async function runRepositoryAudit(params: { created_by: params.createdBy } as any); - try { - const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i); - if (!m) throw new Error("仅支持 GitHub 仓库 URL,例如 https://github.com/owner/repo"); - const owner = m[1]; - const repo = m[2]; + const taskId = (task as any).id as string; - const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`; - const tree = await githubApi<{ tree: GithubTreeItem[] }>(treeUrl, params.githubToken); - let files = (tree.tree || []).filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes)); - // 采样限制,优先分析较小文件与常见语言 - files = files - .sort((a, b) => (a.path.length - b.path.length)) - .slice(0, MAX_ANALYZE_FILES); + // 启动后台审计任务,不阻塞返回 + (async () => { + try { + const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i); + if (!m) throw new Error("仅支持 GitHub 仓库 URL,例如 https://github.com/owner/repo"); + const owner = m[1]; + const repo = m[2]; - let totalFiles = 0, totalLines = 0, createdIssues = 0; - let index = 0; - const worker = async () => { - while (true) { - const current = index++; - if (current >= files.length) break; - const f = files[current]; - totalFiles++; - try { - const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${f.path}`; - const contentRes = await fetch(rawUrl); - if (!contentRes.ok) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; } - const content = await contentRes.text(); - if (content.length > MAX_FILE_SIZE_BYTES) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; } - totalLines += content.split(/\r?\n/).length; - const language = (f.path.split(".").pop() || "").toLowerCase(); - const analysis = await CodeAnalysisEngine.analyzeCode(content, language); - const issues = analysis.issues || []; - createdIssues += issues.length; - for (const issue of issues) { - await api.createAuditIssue({ - task_id: (task as any).id, - file_path: f.path, - line_number: issue.line || null, - column_number: issue.column || null, - issue_type: issue.type || "maintainability", - severity: issue.severity || "low", - title: issue.title || "Issue", - description: issue.description || null, - suggestion: issue.suggestion || null, - code_snippet: issue.code_snippet || null, - ai_explanation: issue.xai ? JSON.stringify(issue.xai) : (issue.ai_explanation || null), - status: "open", - resolved_by: null, - resolved_at: null - } as any); - } - if (totalFiles % 10 === 0) { - await api.updateAuditTask((task as any).id, { status: "running", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues } as any); - } - } catch {} - await new Promise(r=>setTimeout(r, LLM_GAP_MS)); - } - }; + const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`; + const tree = await githubApi<{ tree: GithubTreeItem[] }>(treeUrl, params.githubToken); + let files = (tree.tree || []).filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes)); + // 采样限制,优先分析较小文件与常见语言 + files = files + .sort((a, b) => (a.path.length - b.path.length)) + .slice(0, MAX_ANALYZE_FILES); - const pool = Array.from({ length: Math.min(LLM_CONCURRENCY, files.length) }, () => worker()); - await Promise.all(pool); + let totalFiles = 0, totalLines = 0, createdIssues = 0; + let index = 0; + const worker = async () => { + while (true) { + const current = index++; + if (current >= files.length) break; + const f = files[current]; + totalFiles++; + try { + const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${f.path}`; + const contentRes = await fetch(rawUrl); + if (!contentRes.ok) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; } + const content = await contentRes.text(); + if (content.length > MAX_FILE_SIZE_BYTES) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; } + totalLines += content.split(/\r?\n/).length; + const language = (f.path.split(".").pop() || "").toLowerCase(); + const analysis = await CodeAnalysisEngine.analyzeCode(content, language); + const issues = analysis.issues || []; + createdIssues += issues.length; + for (const issue of issues) { + await api.createAuditIssue({ + task_id: taskId, + file_path: f.path, + line_number: issue.line || null, + column_number: issue.column || null, + issue_type: issue.type || "maintainability", + severity: issue.severity || "low", + title: issue.title || "Issue", + description: issue.description || null, + suggestion: issue.suggestion || null, + code_snippet: issue.code_snippet || null, + ai_explanation: issue.xai ? JSON.stringify(issue.xai) : (issue.ai_explanation || null), + status: "open", + resolved_by: null, + resolved_at: null + } as any); + } + if (totalFiles % 10 === 0) { + await api.updateAuditTask(taskId, { status: "running", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues } as any); + } + } catch {} + await new Promise(r=>setTimeout(r, LLM_GAP_MS)); + } + }; - await api.updateAuditTask((task as any).id, { status: "completed", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues, quality_score: 0 } as any); - return (task as any).id as string; - } catch (e) { - await api.updateAuditTask((task as any).id, { status: "failed" } as any); - throw e; - } + const pool = Array.from({ length: Math.min(LLM_CONCURRENCY, files.length) }, () => worker()); + await Promise.all(pool); + + await api.updateAuditTask(taskId, { status: "completed", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues, quality_score: 0 } as any); + } catch (e) { + console.error('审计任务执行失败:', e); + await api.updateAuditTask(taskId, { status: "failed" } as any); + } + })(); + + // 立即返回任务ID,让用户可以跳转到任务详情页面 + return taskId; } diff --git a/src/pages/ProjectDetail.tsx b/src/pages/ProjectDetail.tsx index 1be09cc..72792f6 100644 --- a/src/pages/ProjectDetail.tsx +++ b/src/pages/ProjectDetail.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useParams, Link } from "react-router-dom"; +import { useParams, Link, useNavigate } from "react-router-dom"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -26,6 +26,7 @@ import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; export default function ProjectDetail() { const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); const [project, setProject] = useState(null); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); @@ -66,17 +67,28 @@ export default function ProjectDetail() { } try { setScanning(true); - await runRepositoryAudit({ + console.log('开始启动审计任务...'); + const taskId = await runRepositoryAudit({ projectId: id, repoUrl: project.repository_url, branch: project.default_branch || 'main', githubToken: undefined, createdBy: undefined }); - toast.success('已启动仓库审计'); - await loadProjectData(); + + console.log('审计任务创建成功,taskId:', taskId); + + // 显示详细的提示信息 + toast.success('审计任务已启动', { + description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', + duration: 5000 + }); + + // 跳转到任务详情页面 + console.log('准备跳转到:', `/tasks/${taskId}`); + navigate(`/tasks/${taskId}`); } catch (e: any) { - console.error(e); + console.error('启动审计失败:', e); toast.error(e?.message || '启动审计失败'); } finally { setScanning(false); @@ -116,7 +128,10 @@ export default function ProjectDetail() { }; const handleTaskCreated = () => { - toast.success("审计任务已创建"); + toast.success("审计任务已创建", { + description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', + duration: 5000 + }); loadProjectData(); // 重新加载项目数据以显示新任务 }; diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 10bdd72..826871c 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -9,13 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Progress } from "@/components/ui/progress"; -import { - Plus, - Search, - GitBranch, - Calendar, - Users, - Settings, +import { + Plus, + Search, + GitBranch, + Calendar, + Users, + Settings, ExternalLink, Code, Shield, @@ -29,12 +29,15 @@ import { scanZipFile, validateZipFile } from "@/features/projects/services"; import type { Project, CreateProjectForm } from "@/shared/types"; import { Link } from "react-router-dom"; import { toast } from "sonner"; +import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; export default function Projects() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [showCreateDialog, setShowCreateDialog] = useState(false); + const [showCreateTaskDialog, setShowCreateTaskDialog] = useState(false); + const [selectedProjectForTask, setSelectedProjectForTask] = useState(""); const [uploadProgress, setUploadProgress] = useState(0); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); @@ -79,7 +82,7 @@ export default function Projects() { ...createForm, // 无登录场景下不传 owner_id,由后端置为 null } as any); - + toast.success("项目创建成功"); setShowCreateDialog(false); resetCreateForm(); @@ -189,6 +192,19 @@ export default function Projects() { return new Date(dateString).toLocaleDateString('zh-CN'); }; + const handleCreateTask = (projectId: string) => { + setSelectedProjectForTask(projectId); + setShowCreateTaskDialog(true); + }; + + const handleTaskCreated = () => { + toast.success("审计任务已创建", { + description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', + duration: 5000 + }); + // 任务创建后会自动跳转到项目详情页面 + }; + if (loading) { return (
@@ -205,7 +221,7 @@ export default function Projects() {

项目管理

管理您的代码项目,配置审计规则和查看分析结果

- +