diff --git a/backend/app/api/v1/endpoints/prompts.py b/backend/app/api/v1/endpoints/prompts.py index 2dd8cc6..9045c5b 100644 --- a/backend/app/api/v1/endpoints/prompts.py +++ b/backend/app/api/v1/endpoints/prompts.py @@ -280,18 +280,42 @@ async def test_prompt_template( ) -> Any: """测试提示词效果""" from app.services.llm.service import LLMService + from app.models.user_config import UserConfig + from app.core.encryption import decrypt_sensitive_data start_time = time.time() try: - # 创建LLM服务实例 - llm_service = LLMService() + # 获取用户配置 + user_config = {} + result_config = await db.execute( + select(UserConfig).where(UserConfig.user_id == current_user.id) + ) + config = result_config.scalar_one_or_none() + if config: + # 需要解密的敏感字段 + SENSITIVE_LLM_FIELDS = [ + 'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey', + 'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey', + 'baiduApiKey', 'minimaxApiKey', 'doubaoApiKey' + ] + + llm_config = json.loads(config.llm_config) if config.llm_config else {} + for field in SENSITIVE_LLM_FIELDS: + if field in llm_config and llm_config[field]: + llm_config[field] = decrypt_sensitive_data(llm_config[field]) + + user_config = {'llmConfig': llm_config} + + # 创建使用用户配置的LLM服务实例 + llm_service = LLMService(user_config=user_config) # 使用自定义提示词进行分析 result = await llm_service.analyze_code_with_custom_prompt( code=request.code, language=request.language, custom_prompt=request.content, + output_language=request.output_language, ) execution_time = time.time() - start_time @@ -303,6 +327,9 @@ async def test_prompt_template( ) except Exception as e: execution_time = time.time() - start_time + import traceback + print(f"❌ 提示词测试失败: {e}") + print(traceback.format_exc()) return PromptTestResponse( success=False, error=str(e), diff --git a/backend/app/api/v1/endpoints/scan.py b/backend/app/api/v1/endpoints/scan.py index 661fa7f..4640502 100644 --- a/backend/app/api/v1/endpoints/scan.py +++ b/backend/app/api/v1/endpoints/scan.py @@ -126,7 +126,21 @@ async def process_zip_task(task_id: str, file_path: str, db_session_factory, use total_lines += content.count('\n') + 1 language = get_language_from_path(file_info['path']) - result = await llm_service.analyze_code(content, language) + # 获取规则集和提示词模板ID + scan_config = (user_config or {}).get('scan_config', {}) + rule_set_id = scan_config.get('rule_set_id') + prompt_template_id = scan_config.get('prompt_template_id') + + # 使用规则集和提示词模板进行分析 + if rule_set_id or prompt_template_id: + result = await llm_service.analyze_code_with_rules( + content, language, + rule_set_id=rule_set_id, + prompt_template_id=prompt_template_id, + db_session=db + ) + else: + result = await llm_service.analyze_code(content, language) issues = result.get("issues", []) for i in issues: @@ -267,9 +281,13 @@ async def scan_zip( # 获取用户配置 user_config = await get_user_config_dict(db, current_user.id) - # 将扫描配置注入到 user_config 中 - if parsed_scan_config and 'file_paths' in parsed_scan_config: - user_config['scan_config'] = {'file_paths': parsed_scan_config['file_paths']} + # 将扫描配置注入到 user_config 中(包括规则集和提示词模板) + if parsed_scan_config: + user_config['scan_config'] = { + 'file_paths': parsed_scan_config.get('file_paths', []), + 'rule_set_id': parsed_scan_config.get('rule_set_id'), + 'prompt_template_id': parsed_scan_config.get('prompt_template_id'), + } # Trigger Background Task - 使用持久化存储的文件路径 stored_zip_path = await load_project_zip(project_id) @@ -281,6 +299,8 @@ async def scan_zip( class ScanRequest(BaseModel): file_paths: Optional[List[str]] = None full_scan: bool = True + rule_set_id: Optional[str] = None + prompt_template_id: Optional[str] = None @router.post("/scan-stored-zip") @@ -323,9 +343,13 @@ async def scan_stored_zip( # 获取用户配置 user_config = await get_user_config_dict(db, current_user.id) - # 将扫描配置注入到 user_config 中,以便 process_zip_task 使用 - if scan_request and scan_request.file_paths: - user_config['scan_config'] = {'file_paths': scan_request.file_paths} + # 将扫描配置注入到 user_config 中(包括规则集和提示词模板) + if scan_request: + user_config['scan_config'] = { + 'file_paths': scan_request.file_paths or [], + 'rule_set_id': scan_request.rule_set_id, + 'prompt_template_id': scan_request.prompt_template_id, + } # Trigger Background Task background_tasks.add_task(process_zip_task, task.id, stored_zip_path, AsyncSessionLocal, user_config) @@ -336,6 +360,7 @@ async def scan_stored_zip( class InstantAnalysisRequest(BaseModel): code: str language: str + prompt_template_id: Optional[str] = None class InstantAnalysisResponse(BaseModel): @@ -411,7 +436,15 @@ async def instant_analysis( start_time = datetime.now(timezone.utc) try: - result = await llm_service.analyze_code(req.code, req.language) + # 如果指定了提示词模板,使用自定义分析 + if req.prompt_template_id: + result = await llm_service.analyze_code_with_rules( + req.code, req.language, + prompt_template_id=req.prompt_template_id, + db_session=db + ) + else: + result = await llm_service.analyze_code(req.code, req.language) except Exception as e: # 分析失败,返回错误信息 error_msg = str(e) diff --git a/backend/app/schemas/prompt_template.py b/backend/app/schemas/prompt_template.py index 306240b..6e93d1a 100644 --- a/backend/app/schemas/prompt_template.py +++ b/backend/app/schemas/prompt_template.py @@ -61,6 +61,7 @@ class PromptTestRequest(BaseModel): content: str = Field(..., description="提示词内容") language: str = Field("python", description="编程语言") code: str = Field(..., description="测试代码") + output_language: str = Field("zh", description="输出语言: zh/en") class PromptTestResponse(BaseModel): diff --git a/backend/app/services/llm/service.py b/backend/app/services/llm/service.py index 81d5fdb..f6f33a8 100644 --- a/backend/app/services/llm/service.py +++ b/backend/app/services/llm/service.py @@ -646,7 +646,8 @@ Please analyze the following code: code: str, language: str, custom_prompt: str, - rules: Optional[list] = None + rules: Optional[list] = None, + output_language: Optional[str] = None ) -> Dict[str, Any]: """ 使用自定义提示词分析代码 @@ -656,9 +657,13 @@ Please analyze the following code: language: 编程语言 custom_prompt: 自定义系统提示词 rules: 可选的审计规则列表 + output_language: 输出语言 (zh/en),如果不指定则使用系统配置 """ - output_language = self._get_output_language() - is_chinese = output_language == 'zh-CN' + if output_language: + is_chinese = output_language == 'zh' + else: + system_output_language = self._get_output_language() + is_chinese = system_output_language == 'zh-CN' # 添加行号 code_with_lines = '\n'.join( @@ -701,12 +706,25 @@ Please analyze the following code: }""" # 构建完整的系统提示词 - format_instruction = f""" + if is_chinese: + format_instruction = f""" 【输出格式要求】 1. 必须只输出纯JSON对象 2. 禁止在JSON前后添加任何文字、说明、markdown标记 -3. 输出格式必须符合以下 JSON Schema: +3. 所有文本字段(title, description, suggestion等)必须使用中文输出 +4. 输出格式必须符合以下 JSON Schema: + +{schema} +{rules_prompt}""" + else: + format_instruction = f""" + +【Output Format Requirements】 +1. Must output pure JSON object only +2. Do not add any text, explanation, or markdown markers before or after JSON +3. All text fields (title, description, suggestion, etc.) must be in English +4. Output format must conform to the following JSON Schema: {schema} {rules_prompt}""" diff --git a/backend/app/services/scanner.py b/backend/app/services/scanner.py index cc7af98..0a4b573 100644 --- a/backend/app/services/scanner.py +++ b/backend/app/services/scanner.py @@ -371,8 +371,20 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N language = get_language_from_path(file_info["path"]) print(f"🤖 正在调用 LLM 分析: {file_info['path']} ({language}, {len(content)} bytes)") - # LLM分析 - analysis = await llm_service.analyze_code(content, language) + # LLM分析 - 支持规则集和提示词模板 + scan_config = (user_config or {}).get('scan_config', {}) + rule_set_id = scan_config.get('rule_set_id') + prompt_template_id = scan_config.get('prompt_template_id') + + if rule_set_id or prompt_template_id: + analysis = await llm_service.analyze_code_with_rules( + content, language, + rule_set_id=rule_set_id, + prompt_template_id=prompt_template_id, + db_session=db + ) + else: + analysis = await llm_service.analyze_code(content, language) print(f"✅ LLM 分析完成: {file_info['path']}") # 再次检查是否取消(LLM分析后) diff --git a/frontend/src/components/audit/CreateTaskDialog.tsx b/frontend/src/components/audit/CreateTaskDialog.tsx index 314e2f7..2a0e69b 100644 --- a/frontend/src/components/audit/CreateTaskDialog.tsx +++ b/frontend/src/components/audit/CreateTaskDialog.tsx @@ -34,9 +34,12 @@ import { Globe, Shield, Loader2, + Zap, } from "lucide-react"; import { toast } from "sonner"; import { api } from "@/shared/config/database"; +import { getRuleSets, type AuditRuleSet } from "@/shared/api/rules"; +import { getPromptTemplates, type PromptTemplate } from "@/shared/api/prompts"; import { useProjects } from "./hooks/useTaskForm"; import { useZipFile, formatFileSize } from "./hooks/useZipFile"; @@ -86,6 +89,12 @@ export default function CreateTaskDialog({ const [uploading, setUploading] = useState(false); const [showTerminal, setShowTerminal] = useState(false); const [currentTaskId, setCurrentTaskId] = useState(null); + + // 规则集和提示词模板 + const [ruleSets, setRuleSets] = useState([]); + const [promptTemplates, setPromptTemplates] = useState([]); + const [selectedRuleSetId, setSelectedRuleSetId] = useState(""); + const [selectedPromptTemplateId, setSelectedPromptTemplateId] = useState(""); const { projects, loading, loadProjects } = useProjects(); const selectedProject = projects.find((p) => p.id === selectedProjectId); @@ -127,6 +136,23 @@ export default function CreateTaskDialog({ ); }, [projects, searchTerm]); + // 加载规则集和提示词模板 + useEffect(() => { + const loadRulesAndPrompts = async () => { + try { + const [rulesRes, promptsRes] = await Promise.all([ + getRuleSets({ is_active: true }), + getPromptTemplates({ is_active: true }), + ]); + setRuleSets(rulesRes.items); + setPromptTemplates(promptsRes.items); + } catch (error) { + console.error("加载规则集和提示词失败:", error); + } + }; + loadRulesAndPrompts(); + }, []); + useEffect(() => { if (open) { loadProjects(); @@ -135,6 +161,8 @@ export default function CreateTaskDialog({ } setSearchTerm(""); setShowAdvanced(false); + setSelectedRuleSetId(""); + setSelectedPromptTemplateId(""); zipState.reset(); } }, [open, preselectedProjectId]); @@ -158,6 +186,8 @@ export default function CreateTaskDialog({ excludePatterns, createdBy: "local-user", filePaths: selectedFiles, + ruleSetId: selectedRuleSetId || undefined, + promptTemplateId: selectedPromptTemplateId || undefined, }); } else if (zipState.zipFile) { taskId = await scanZipFile({ @@ -165,6 +195,8 @@ export default function CreateTaskDialog({ zipFile: zipState.zipFile, excludePatterns, createdBy: "local-user", + ruleSetId: selectedRuleSetId || undefined, + promptTemplateId: selectedPromptTemplateId || undefined, }); } else { toast.error("请上传 ZIP 文件"); @@ -182,6 +214,8 @@ export default function CreateTaskDialog({ exclude: excludePatterns, createdBy: "local-user", filePaths: selectedFiles, + ruleSetId: selectedRuleSetId || undefined, + promptTemplateId: selectedPromptTemplateId || undefined, }); } @@ -335,6 +369,48 @@ export default function CreateTaskDialog({ )} {/* 高级选项 */} + {/* 规则集和提示词选择 */} +
+
+ + 审计配置 +
+
+
+ + +
+
+ + +
+
+
+ { + static async analyzeCode(code: string, language: string, promptTemplateId?: string): Promise { try { - const response = await apiClient.post('/scan/instant', { code, language }); + const response = await apiClient.post('/scan/instant', { + code, + language, + prompt_template_id: promptTemplateId || undefined, + }); return response.data; } catch (error: any) { console.error('Analysis failed:', error); diff --git a/frontend/src/features/projects/services/repoScan.ts b/frontend/src/features/projects/services/repoScan.ts index d264b8c..eb351e1 100644 --- a/frontend/src/features/projects/services/repoScan.ts +++ b/frontend/src/features/projects/services/repoScan.ts @@ -7,6 +7,8 @@ export async function runRepositoryAudit(params: { exclude?: string[]; createdBy?: string; filePaths?: string[]; + ruleSetId?: string; + promptTemplateId?: string; }) { // 后端会从用户配置中读取 GitHub/GitLab Token,前端不需要传递 // The backend handles everything now. @@ -22,7 +24,9 @@ export async function runRepositoryAudit(params: { branch_name: params.branch || "main", exclude_patterns: params.exclude || [], scan_config: { - file_paths: params.filePaths + file_paths: params.filePaths, + rule_set_id: params.ruleSetId, + prompt_template_id: params.promptTemplateId, }, created_by: params.createdBy || "unknown" } as any); diff --git a/frontend/src/features/projects/services/repoZipScan.ts b/frontend/src/features/projects/services/repoZipScan.ts index a6927a8..ef9ebee 100644 --- a/frontend/src/features/projects/services/repoZipScan.ts +++ b/frontend/src/features/projects/services/repoZipScan.ts @@ -9,6 +9,8 @@ export async function scanZipFile(params: { excludePatterns?: string[]; createdBy?: string; filePaths?: string[]; + ruleSetId?: string; + promptTemplateId?: string; }): Promise { const formData = new FormData(); formData.append("file", params.zipFile); @@ -16,7 +18,9 @@ export async function scanZipFile(params: { const scanConfig = { file_paths: params.filePaths, - full_scan: !params.filePaths || params.filePaths.length === 0 + full_scan: !params.filePaths || params.filePaths.length === 0, + rule_set_id: params.ruleSetId, + prompt_template_id: params.promptTemplateId, }; formData.append("scan_config", JSON.stringify(scanConfig)); @@ -37,10 +41,14 @@ export async function scanStoredZipFile(params: { excludePatterns?: string[]; createdBy?: string; filePaths?: string[]; + ruleSetId?: string; + promptTemplateId?: string; }): Promise { const scanRequest = { file_paths: params.filePaths, - full_scan: !params.filePaths || params.filePaths.length === 0 + full_scan: !params.filePaths || params.filePaths.length === 0, + rule_set_id: params.ruleSetId, + prompt_template_id: params.promptTemplateId, }; const res = await apiClient.post(`/scan/scan-stored-zip`, scanRequest, { params: { project_id: params.projectId }, diff --git a/frontend/src/pages/AuditRules.tsx b/frontend/src/pages/AuditRules.tsx index a2b871c..9cc27a6 100644 --- a/frontend/src/pages/AuditRules.tsx +++ b/frontend/src/pages/AuditRules.tsx @@ -1,10 +1,9 @@ /** - * 审计规则管理页面 + * 审计规则管理页面 - Retro Terminal 风格 */ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; @@ -13,7 +12,7 @@ import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { useToast } from '@/shared/hooks/use-toast'; +import { toast } from 'sonner'; import { Plus, Trash2, @@ -28,6 +27,9 @@ import { ChevronDown, ChevronRight, ExternalLink, + Activity, + CheckCircle, + Terminal, } from 'lucide-react'; import { getRuleSets, @@ -47,18 +49,18 @@ import { } from '@/shared/api/rules'; const CATEGORIES = [ - { value: 'security', label: '安全', icon: Shield, color: 'text-red-500' }, - { value: 'bug', label: 'Bug', icon: Bug, color: 'text-orange-500' }, - { value: 'performance', label: '性能', icon: Zap, color: 'text-yellow-500' }, - { value: 'style', label: '代码风格', icon: Code, color: 'text-blue-500' }, - { value: 'maintainability', label: '可维护性', icon: Settings, color: 'text-purple-500' }, + { value: 'security', label: '安全', icon: Shield, color: 'text-red-600', bg: 'bg-red-100' }, + { value: 'bug', label: 'Bug', icon: Bug, color: 'text-orange-600', bg: 'bg-orange-100' }, + { value: 'performance', label: '性能', icon: Zap, color: 'text-yellow-600', bg: 'bg-yellow-100' }, + { value: 'style', label: '代码风格', icon: Code, color: 'text-blue-600', bg: 'bg-blue-100' }, + { value: 'maintainability', label: '可维护性', icon: Settings, color: 'text-purple-600', bg: 'bg-purple-100' }, ]; const SEVERITIES = [ - { value: 'critical', label: '严重', color: 'bg-red-500' }, - { value: 'high', label: '高', color: 'bg-orange-500' }, - { value: 'medium', label: '中', color: 'bg-yellow-500' }, - { value: 'low', label: '低', color: 'bg-blue-500' }, + { value: 'critical', label: '严重', color: 'bg-red-600 text-white' }, + { value: 'high', label: '高', color: 'bg-orange-500 text-white' }, + { value: 'medium', label: '中', color: 'bg-yellow-500 text-black' }, + { value: 'low', label: '低', color: 'bg-blue-500 text-white' }, ]; const LANGUAGES = [ @@ -68,8 +70,6 @@ const LANGUAGES = [ { value: 'typescript', label: 'TypeScript' }, { value: 'java', label: 'Java' }, { value: 'go', label: 'Go' }, - { value: 'rust', label: 'Rust' }, - { value: 'cpp', label: 'C/C++' }, ]; const RULE_TYPES = [ @@ -80,45 +80,26 @@ const RULE_TYPES = [ ]; export default function AuditRules() { - const { toast } = useToast(); const [ruleSets, setRuleSets] = useState([]); const [loading, setLoading] = useState(true); const [expandedSets, setExpandedSets] = useState>(new Set()); - - // 对话框状态 const [showCreateDialog, setShowCreateDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [showRuleDialog, setShowRuleDialog] = useState(false); const [showImportDialog, setShowImportDialog] = useState(false); - const [selectedRuleSet, setSelectedRuleSet] = useState(null); const [selectedRule, setSelectedRule] = useState(null); - - // 表单状态 + const [ruleSetForm, setRuleSetForm] = useState({ - name: '', - description: '', - language: 'all', - rule_type: 'custom', + name: '', description: '', language: 'all', rule_type: 'custom', }); - const [ruleForm, setRuleForm] = useState({ - rule_code: '', - name: '', - description: '', - category: 'security', - severity: 'medium', - custom_prompt: '', - fix_suggestion: '', - reference_url: '', - enabled: true, + rule_code: '', name: '', description: '', category: 'security', + severity: 'medium', custom_prompt: '', fix_suggestion: '', reference_url: '', enabled: true, }); - const [importJson, setImportJson] = useState(''); - useEffect(() => { - loadRuleSets(); - }, []); + useEffect(() => { loadRuleSets(); }, []); const loadRuleSets = async () => { try { @@ -126,7 +107,7 @@ export default function AuditRules() { const response = await getRuleSets(); setRuleSets(response.items); } catch (error) { - toast({ title: '加载失败', description: '无法加载规则集', variant: 'destructive' }); + toast.error('加载规则集失败'); } finally { setLoading(false); } @@ -134,47 +115,38 @@ export default function AuditRules() { const toggleExpand = (id: string) => { const newExpanded = new Set(expandedSets); - if (newExpanded.has(id)) { - newExpanded.delete(id); - } else { - newExpanded.add(id); - } + if (newExpanded.has(id)) newExpanded.delete(id); + else newExpanded.add(id); setExpandedSets(newExpanded); }; const handleCreateRuleSet = async () => { try { await createRuleSet(ruleSetForm); - toast({ title: '创建成功', description: '规则集已创建' }); + toast.success('规则集已创建'); setShowCreateDialog(false); setRuleSetForm({ name: '', description: '', language: 'all', rule_type: 'custom' }); loadRuleSets(); - } catch (error) { - toast({ title: '创建失败', variant: 'destructive' }); - } + } catch (error) { toast.error('创建失败'); } }; const handleUpdateRuleSet = async () => { if (!selectedRuleSet) return; try { await updateRuleSet(selectedRuleSet.id, ruleSetForm); - toast({ title: '更新成功' }); + toast.success('更新成功'); setShowEditDialog(false); loadRuleSets(); - } catch (error) { - toast({ title: '更新失败', variant: 'destructive' }); - } + } catch (error) { toast.error('更新失败'); } }; const handleDeleteRuleSet = async (id: string) => { if (!confirm('确定要删除此规则集吗?')) return; try { await deleteRuleSet(id); - toast({ title: '删除成功' }); + toast.success('删除成功'); loadRuleSets(); - } catch (error: any) { - toast({ title: '删除失败', description: error.message, variant: 'destructive' }); - } + } catch (error: any) { toast.error(error.message || '删除失败'); } }; const handleExport = async (ruleSet: AuditRuleSet) => { @@ -182,494 +154,440 @@ export default function AuditRules() { const blob = await exportRuleSet(ruleSet.id); const url = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; - a.download = `${ruleSet.name}.json`; - a.click(); + a.href = url; a.download = `${ruleSet.name}.json`; a.click(); URL.revokeObjectURL(url); - toast({ title: '导出成功' }); - } catch (error) { - toast({ title: '导出失败', variant: 'destructive' }); - } + toast.success('导出成功'); + } catch (error) { toast.error('导出失败'); } }; const handleImport = async () => { try { const data = JSON.parse(importJson); await importRuleSet(data); - toast({ title: '导入成功' }); + toast.success('导入成功'); setShowImportDialog(false); setImportJson(''); loadRuleSets(); - } catch (error: any) { - toast({ title: '导入失败', description: error.message, variant: 'destructive' }); - } + } catch (error: any) { toast.error(error.message || '导入失败'); } }; const handleAddRule = async () => { if (!selectedRuleSet) return; try { await addRuleToSet(selectedRuleSet.id, ruleForm); - toast({ title: '添加成功' }); + toast.success('添加成功'); setShowRuleDialog(false); - setRuleForm({ - rule_code: '', - name: '', - description: '', - category: 'security', - severity: 'medium', - custom_prompt: '', - fix_suggestion: '', - reference_url: '', - enabled: true, - }); + setRuleForm({ rule_code: '', name: '', description: '', category: 'security', severity: 'medium', custom_prompt: '', fix_suggestion: '', reference_url: '', enabled: true }); loadRuleSets(); - } catch (error) { - toast({ title: '添加失败', variant: 'destructive' }); - } + } catch (error) { toast.error('添加失败'); } }; const handleUpdateRule = async () => { if (!selectedRuleSet || !selectedRule) return; try { await updateRule(selectedRuleSet.id, selectedRule.id, ruleForm); - toast({ title: '更新成功' }); + toast.success('更新成功'); setShowRuleDialog(false); loadRuleSets(); - } catch (error) { - toast({ title: '更新失败', variant: 'destructive' }); - } + } catch (error) { toast.error('更新失败'); } }; const handleDeleteRule = async (ruleSetId: string, ruleId: string) => { if (!confirm('确定要删除此规则吗?')) return; try { await deleteRule(ruleSetId, ruleId); - toast({ title: '删除成功' }); + toast.success('删除成功'); loadRuleSets(); - } catch (error) { - toast({ title: '删除失败', variant: 'destructive' }); - } + } catch (error) { toast.error('删除失败'); } }; const handleToggleRule = async (ruleSetId: string, ruleId: string) => { try { const result = await toggleRule(ruleSetId, ruleId); - toast({ title: result.message }); + toast.success(result.message); loadRuleSets(); - } catch (error) { - toast({ title: '操作失败', variant: 'destructive' }); - } + } catch (error) { toast.error('操作失败'); } }; const openEditRuleSetDialog = (ruleSet: AuditRuleSet) => { setSelectedRuleSet(ruleSet); - setRuleSetForm({ - name: ruleSet.name, - description: ruleSet.description || '', - language: ruleSet.language, - rule_type: ruleSet.rule_type, - }); + setRuleSetForm({ name: ruleSet.name, description: ruleSet.description || '', language: ruleSet.language, rule_type: ruleSet.rule_type }); setShowEditDialog(true); }; const openAddRuleDialog = (ruleSet: AuditRuleSet) => { setSelectedRuleSet(ruleSet); setSelectedRule(null); - setRuleForm({ - rule_code: '', - name: '', - description: '', - category: 'security', - severity: 'medium', - custom_prompt: '', - fix_suggestion: '', - reference_url: '', - enabled: true, - }); + setRuleForm({ rule_code: '', name: '', description: '', category: 'security', severity: 'medium', custom_prompt: '', fix_suggestion: '', reference_url: '', enabled: true }); setShowRuleDialog(true); }; const openEditRuleDialog = (ruleSet: AuditRuleSet, rule: AuditRule) => { setSelectedRuleSet(ruleSet); setSelectedRule(rule); - setRuleForm({ - rule_code: rule.rule_code, - name: rule.name, - description: rule.description || '', - category: rule.category, - severity: rule.severity, - custom_prompt: rule.custom_prompt || '', - fix_suggestion: rule.fix_suggestion || '', - reference_url: rule.reference_url || '', - enabled: rule.enabled, - }); + setRuleForm({ rule_code: rule.rule_code, name: rule.name, description: rule.description || '', category: rule.category, severity: rule.severity, custom_prompt: rule.custom_prompt || '', fix_suggestion: rule.fix_suggestion || '', reference_url: rule.reference_url || '', enabled: rule.enabled }); setShowRuleDialog(true); }; - const getCategoryInfo = (category: string) => { - return CATEGORIES.find(c => c.value === category) || CATEGORIES[0]; - }; + const getCategoryInfo = (category: string) => CATEGORIES.find(c => c.value === category) || CATEGORIES[0]; + const getSeverityInfo = (severity: string) => SEVERITIES.find(s => s.value === severity) || SEVERITIES[2]; - const getSeverityInfo = (severity: string) => { - return SEVERITIES.find(s => s.value === severity) || SEVERITIES[2]; - }; + if (loading) { + return ( +
+
+
+ ); + } return ( -
- {/* 页面标题 */} -
-
-

审计规则管理

-

管理代码审计规则集,自定义检测规范

+
+
+ + {/* 统计卡片 */} +
+
+
+
+

规则集总数

+

{ruleSets.length}

+
+
+ +
+
-
- - + +
+
+
+

系统规则集

+

{ruleSets.filter(r => r.is_system).length}

+
+
+ +
+
+
+ +
+
+
+

总规则数

+

{ruleSets.reduce((acc, r) => acc + r.rules_count, 0)}

+
+
+ +
+
+
+ +
+
+
+

已启用规则

+

{ruleSets.reduce((acc, r) => acc + r.enabled_rules_count, 0)}

+
+
+ +
+
+
+
+ + {/* 操作栏 */} +
+
+
+ + 审计规则管理 +
+
+ + +
{/* 规则集列表 */} -
- {loading ? ( -
加载中...
- ) : ruleSets.length === 0 ? ( - - - 暂无规则集,点击"新建规则集"创建 - - +
+ {ruleSets.length === 0 ? ( +
+
+ +
+

暂无规则集

+

点击"新建规则集"创建自定义审计规则

+ +
) : ( ruleSets.map(ruleSet => ( - - +
+ {/* 规则集头部 */} +
-
toggleExpand(ruleSet.id)}> - {expandedSets.has(ruleSet.id) ? ( - - ) : ( - - )} +
toggleExpand(ruleSet.id)}> +
+ {expandedSets.has(ruleSet.id) ? : } +
- +

{ruleSet.name} - {ruleSet.is_system && 系统} - {ruleSet.is_default && 默认} - - {ruleSet.description} + {ruleSet.is_system && 系统} + {ruleSet.is_default && 默认} +

+

{ruleSet.description}

-
- {LANGUAGES.find(l => l.value === ruleSet.language)?.label} - {RULE_TYPES.find(t => t.value === ruleSet.rule_type)?.label} - - {ruleSet.enabled_rules_count}/{ruleSet.rules_count} 规则启用 + +
+ {LANGUAGES.find(l => l.value === ruleSet.language)?.label} + {RULE_TYPES.find(t => t.value === ruleSet.rule_type)?.label} + + {ruleSet.enabled_rules_count}/{ruleSet.rules_count} 启用 - {!ruleSet.is_system && ( <> - - )}
- - +
+ + {/* 规则列表 */} {expandedSets.has(ruleSet.id) && ( - -
- {!ruleSet.is_system && ( - - )} - - -
- {ruleSet.rules.map(rule => { - const categoryInfo = getCategoryInfo(rule.category); - const severityInfo = getSeverityInfo(rule.severity); - const CategoryIcon = categoryInfo.icon; - - return ( -
-
-
- -
-
- {rule.rule_code} - {rule.name} - {severityInfo.label} -
- {rule.description && ( -

{rule.description}

- )} - {rule.reference_url && ( - - 参考链接 - - )} -
+
+ {!ruleSet.is_system && ( + + )} + +
+ {ruleSet.rules.map(rule => { + const categoryInfo = getCategoryInfo(rule.category); + const severityInfo = getSeverityInfo(rule.severity); + const CategoryIcon = categoryInfo.icon; + return ( +
+
+
+
+
-
- handleToggleRule(ruleSet.id, rule.id)} - /> - {!ruleSet.is_system && ( - <> - - - +
+
+ {rule.rule_code} + {rule.name} + {severityInfo.label} +
+ {rule.description &&

{rule.description}

} + {rule.reference_url && ( + + 参考链接 + )}
+
+ handleToggleRule(ruleSet.id, rule.id)} /> + {!ruleSet.is_system && ( + <> + + + + )} +
- ); - })} -
- -
- +
+ ); + })} +
+ +
)} - +
)) )}
{/* 创建规则集对话框 */} - - - 新建规则集 - 创建自定义审计规则集 + + + + + 新建规则集 + -
-
- - setRuleSetForm({ ...ruleSetForm, name: e.target.value })} - placeholder="规则集名称" - /> +
+
+ + setRuleSetForm({ ...ruleSetForm, name: e.target.value })} placeholder="规则集名称" className="terminal-input" />
-
- -