feat(prompts-scan): integrate prompt templates and audit rules into scan and analysis workflows
- Add user configuration retrieval with LLM API key decryption in prompt testing endpoint - Support output language parameter in prompt template testing - Integrate rule sets and prompt templates into ZIP file scanning process - Add rule_set_id and prompt_template_id parameters to ScanRequest model - Implement analyze_code_with_rules method for custom rule-based code analysis - Add prompt_template_id support to instant analysis endpoint - Update scan configuration to include rule set and prompt template selection - Enhance error handling and logging in prompt testing with traceback output - Extend InstantAnalysisRequest with optional prompt template ID parameter - Add test code samples utility for prompt template validation
This commit is contained in:
parent
357b9cc0a7
commit
4d71ed546a
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}"""
|
||||
|
|
|
|||
|
|
@ -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分析后)
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
|
||||
// 规则集和提示词模板
|
||||
const [ruleSets, setRuleSets] = useState<AuditRuleSet[]>([]);
|
||||
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [selectedRuleSetId, setSelectedRuleSetId] = useState<string>("");
|
||||
const [selectedPromptTemplateId, setSelectedPromptTemplateId] = useState<string>("");
|
||||
|
||||
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({
|
|||
)}
|
||||
|
||||
{/* 高级选项 */}
|
||||
{/* 规则集和提示词选择 */}
|
||||
<div className="p-3 border-2 border-black bg-purple-50 space-y-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Zap className="w-4 h-4 text-purple-700" />
|
||||
<span className="font-mono text-sm font-bold text-purple-900 uppercase">审计配置</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-mono font-bold text-gray-600 mb-1 uppercase">规则集</label>
|
||||
<Select value={selectedRuleSetId} onValueChange={setSelectedRuleSetId}>
|
||||
<SelectTrigger className="h-9 rounded-none border-2 border-black font-mono text-xs focus:ring-0">
|
||||
<SelectValue placeholder="默认规则" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-none border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<SelectItem value="" className="font-mono text-xs">默认规则</SelectItem>
|
||||
{ruleSets.map((rs) => (
|
||||
<SelectItem key={rs.id} value={rs.id} className="font-mono text-xs">
|
||||
{rs.name} ({rs.enabled_rules_count})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-mono font-bold text-gray-600 mb-1 uppercase">提示词</label>
|
||||
<Select value={selectedPromptTemplateId} onValueChange={setSelectedPromptTemplateId}>
|
||||
<SelectTrigger className="h-9 rounded-none border-2 border-black font-mono text-xs focus:ring-0">
|
||||
<SelectValue placeholder="默认提示词" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-none border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<SelectItem value="" className="font-mono text-xs">默认提示词</SelectItem>
|
||||
{promptTemplates.map((pt) => (
|
||||
<SelectItem key={pt.id} value={pt.id} className="font-mono text-xs">
|
||||
{pt.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-sm font-mono text-gray-600 hover:text-black transition-colors">
|
||||
<ChevronRight
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@ export class CodeAnalysisEngine {
|
|||
return [...SUPPORTED_LANGUAGES];
|
||||
}
|
||||
|
||||
static async analyzeCode(code: string, language: string): Promise<CodeAnalysisResult> {
|
||||
static async analyzeCode(code: string, language: string, promptTemplateId?: string): Promise<CodeAnalysisResult> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export async function scanZipFile(params: {
|
|||
excludePatterns?: string[];
|
||||
createdBy?: string;
|
||||
filePaths?: string[];
|
||||
ruleSetId?: string;
|
||||
promptTemplateId?: string;
|
||||
}): Promise<string> {
|
||||
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<string> {
|
||||
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 },
|
||||
|
|
|
|||
|
|
@ -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<AuditRuleSet[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedSets, setExpandedSets] = useState<Set<string>>(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<AuditRuleSet | null>(null);
|
||||
const [selectedRule, setSelectedRule] = useState<AuditRule | null>(null);
|
||||
|
||||
// 表单状态
|
||||
|
||||
const [ruleSetForm, setRuleSetForm] = useState<AuditRuleSetCreate>({
|
||||
name: '',
|
||||
description: '',
|
||||
language: 'all',
|
||||
rule_type: 'custom',
|
||||
name: '', description: '', language: 'all', rule_type: 'custom',
|
||||
});
|
||||
|
||||
const [ruleForm, setRuleForm] = useState<AuditRuleCreate>({
|
||||
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 (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||
<div className="animate-spin rounded-none h-32 w-32 border-8 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">审计规则管理</h1>
|
||||
<p className="text-muted-foreground">管理代码审计规则集,自定义检测规范</p>
|
||||
<div className="flex flex-col gap-6 px-6 py-4 bg-background min-h-screen font-mono relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 relative z-10">
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">规则集总数</p>
|
||||
<p className="text-3xl font-bold text-black">{ruleSets.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-primary border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Shield className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowImportDialog(true)}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
导入规则集
|
||||
</Button>
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建规则集
|
||||
</Button>
|
||||
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">系统规则集</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{ruleSets.filter(r => r.is_system).length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-blue-600 border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Settings className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">总规则数</p>
|
||||
<p className="text-3xl font-bold text-green-600">{ruleSets.reduce((acc, r) => acc + r.rules_count, 0)}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-green-600 border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">已启用规则</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{ruleSets.reduce((acc, r) => acc + r.enabled_rules_count, 0)}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-orange-500 border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Activity className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-4 relative z-10">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5" />
|
||||
<span className="font-bold uppercase">审计规则管理</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowImportDialog(true)} className="retro-btn bg-white text-black hover:bg-gray-100 h-10">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
导入规则集
|
||||
</Button>
|
||||
<Button onClick={() => setShowCreateDialog(true)} className="retro-btn bg-primary text-white hover:bg-primary/90 h-10 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建规则集
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 规则集列表 */}
|
||||
<div className="space-y-4">
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">加载中...</div>
|
||||
) : ruleSets.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
暂无规则集,点击"新建规则集"创建
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-4 relative z-10">
|
||||
{ruleSets.length === 0 ? (
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-16 flex flex-col items-center justify-center text-center">
|
||||
<div className="w-20 h-20 bg-gray-100 border-2 border-black flex items-center justify-center mb-6 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Shield className="w-10 h-10 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-black uppercase mb-2">暂无规则集</h3>
|
||||
<p className="text-gray-500 mb-8 max-w-md">点击"新建规则集"创建自定义审计规则</p>
|
||||
<Button className="retro-btn bg-primary text-white h-12 px-8 text-lg font-bold uppercase" onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
创建规则集
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
ruleSets.map(ruleSet => (
|
||||
<Card key={ruleSet.id} className={!ruleSet.is_active ? 'opacity-60' : ''}>
|
||||
<CardHeader className="pb-2">
|
||||
<div key={ruleSet.id} className={`retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] ${!ruleSet.is_active ? 'opacity-60' : ''}`}>
|
||||
{/* 规则集头部 */}
|
||||
<div className="p-6 border-b-2 border-dashed border-gray-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 cursor-pointer" onClick={() => toggleExpand(ruleSet.id)}>
|
||||
{expandedSets.has(ruleSet.id) ? (
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
)}
|
||||
<div className="flex items-center gap-4 cursor-pointer" onClick={() => toggleExpand(ruleSet.id)}>
|
||||
<div className="w-10 h-10 bg-gray-100 border-2 border-black flex items-center justify-center shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
{expandedSets.has(ruleSet.id) ? <ChevronDown className="w-5 h-5" /> : <ChevronRight className="w-5 h-5" />}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<h3 className="font-bold text-xl text-black uppercase flex items-center gap-2">
|
||||
{ruleSet.name}
|
||||
{ruleSet.is_system && <Badge variant="secondary">系统</Badge>}
|
||||
{ruleSet.is_default && <Badge>默认</Badge>}
|
||||
</CardTitle>
|
||||
<CardDescription>{ruleSet.description}</CardDescription>
|
||||
{ruleSet.is_system && <Badge className="rounded-none border-2 border-black bg-blue-100 text-blue-800">系统</Badge>}
|
||||
{ruleSet.is_default && <Badge className="rounded-none border-2 border-black bg-green-100 text-green-800">默认</Badge>}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">{ruleSet.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">{LANGUAGES.find(l => l.value === ruleSet.language)?.label}</Badge>
|
||||
<Badge variant="outline">{RULE_TYPES.find(t => t.value === ruleSet.rule_type)?.label}</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{ruleSet.enabled_rules_count}/{ruleSet.rules_count} 规则启用
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="rounded-none border-2 border-black">{LANGUAGES.find(l => l.value === ruleSet.language)?.label}</Badge>
|
||||
<Badge variant="outline" className="rounded-none border-2 border-black">{RULE_TYPES.find(t => t.value === ruleSet.rule_type)?.label}</Badge>
|
||||
<span className="text-sm font-bold px-3 py-1 bg-gray-100 border-2 border-black">
|
||||
{ruleSet.enabled_rules_count}/{ruleSet.rules_count} 启用
|
||||
</span>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleExport(ruleSet)}>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleExport(ruleSet)} className="hover:bg-gray-100">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
{!ruleSet.is_system && (
|
||||
<>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEditRuleSetDialog(ruleSet)}>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEditRuleSetDialog(ruleSet)} className="hover:bg-gray-100">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteRuleSet(ruleSet.id)}>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteRuleSet(ruleSet.id)} className="hover:bg-red-100 hover:text-red-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 规则列表 */}
|
||||
{expandedSets.has(ruleSet.id) && (
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{!ruleSet.is_system && (
|
||||
<Button variant="outline" size="sm" onClick={() => openAddRuleDialog(ruleSet)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加规则
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="space-y-2">
|
||||
{ruleSet.rules.map(rule => {
|
||||
const categoryInfo = getCategoryInfo(rule.category);
|
||||
const severityInfo = getSeverityInfo(rule.severity);
|
||||
const CategoryIcon = categoryInfo.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={rule.id}
|
||||
className={`p-3 border rounded-lg ${!rule.enabled ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<CategoryIcon className={`w-5 h-5 mt-0.5 ${categoryInfo.color}`} />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm text-muted-foreground">{rule.rule_code}</span>
|
||||
<span className="font-medium">{rule.name}</span>
|
||||
<Badge className={severityInfo.color}>{severityInfo.label}</Badge>
|
||||
</div>
|
||||
{rule.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{rule.description}</p>
|
||||
)}
|
||||
{rule.reference_url && (
|
||||
<a
|
||||
href={rule.reference_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-500 hover:underline flex items-center gap-1 mt-1"
|
||||
>
|
||||
参考链接 <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{!ruleSet.is_system && (
|
||||
<Button variant="outline" size="sm" onClick={() => openAddRuleDialog(ruleSet)} className="mb-4 retro-btn bg-white text-black hover:bg-gray-100">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加规则
|
||||
</Button>
|
||||
)}
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="space-y-3">
|
||||
{ruleSet.rules.map(rule => {
|
||||
const categoryInfo = getCategoryInfo(rule.category);
|
||||
const severityInfo = getSeverityInfo(rule.severity);
|
||||
const CategoryIcon = categoryInfo.icon;
|
||||
return (
|
||||
<div key={rule.id} className={`p-4 border-2 border-black bg-gray-50 hover:bg-white transition-all ${!rule.enabled ? 'opacity-50' : ''}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-10 h-10 ${categoryInfo.bg} border-2 border-black flex items-center justify-center shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]`}>
|
||||
<CategoryIcon className={`w-5 h-5 ${categoryInfo.color}`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={rule.enabled}
|
||||
onCheckedChange={() => handleToggleRule(ruleSet.id, rule.id)}
|
||||
/>
|
||||
{!ruleSet.is_system && (
|
||||
<>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEditRuleDialog(ruleSet, rule)}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteRule(ruleSet.id, rule.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-mono text-xs bg-black text-white px-2 py-0.5">{rule.rule_code}</span>
|
||||
<span className="font-bold uppercase">{rule.name}</span>
|
||||
<Badge className={`rounded-none border-2 border-black ${severityInfo.color}`}>{severityInfo.label}</Badge>
|
||||
</div>
|
||||
{rule.description && <p className="text-sm text-gray-600 mb-2">{rule.description}</p>}
|
||||
{rule.reference_url && (
|
||||
<a href={rule.reference_url} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-600 hover:underline flex items-center gap-1">
|
||||
参考链接 <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={rule.enabled} onCheckedChange={() => handleToggleRule(ruleSet.id, rule.id)} />
|
||||
{!ruleSet.is_system && (
|
||||
<>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEditRuleDialog(ruleSet, rule)}><Edit className="w-4 h-4" /></Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteRule(ruleSet.id, rule.id)} className="hover:bg-red-100 hover:text-red-600"><Trash2 className="w-4 h-4" /></Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 创建规则集对话框 */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建规则集</DialogTitle>
|
||||
<DialogDescription>创建自定义审计规则集</DialogDescription>
|
||||
<DialogContent className="max-w-lg retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5" />
|
||||
新建规则集
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={ruleSetForm.name}
|
||||
onChange={e => setRuleSetForm({ ...ruleSetForm, name: e.target.value })}
|
||||
placeholder="规则集名称"
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">名称 *</Label>
|
||||
<Input value={ruleSetForm.name} onChange={e => setRuleSetForm({ ...ruleSetForm, name: e.target.value })} placeholder="规则集名称" className="terminal-input" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Textarea
|
||||
value={ruleSetForm.description}
|
||||
onChange={e => setRuleSetForm({ ...ruleSetForm, description: e.target.value })}
|
||||
placeholder="规则集描述"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">描述</Label>
|
||||
<Textarea value={ruleSetForm.description} onChange={e => setRuleSetForm({ ...ruleSetForm, description: e.target.value })} placeholder="规则集描述" className="terminal-input" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>适用语言</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">适用语言</Label>
|
||||
<Select value={ruleSetForm.language} onValueChange={v => setRuleSetForm({ ...ruleSetForm, language: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent className="retro-card border border-border">
|
||||
{LANGUAGES.map(l => <SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>规则类型</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">规则类型</Label>
|
||||
<Select value={ruleSetForm.rule_type} onValueChange={v => setRuleSetForm({ ...ruleSetForm, rule_type: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent className="retro-card border border-border">
|
||||
{RULE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>取消</Button>
|
||||
<Button onClick={handleCreateRuleSet}>创建</Button>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200">
|
||||
<Button variant="outline" onClick={() => setShowCreateDialog(false)} className="retro-btn bg-white text-black">取消</Button>
|
||||
<Button onClick={handleCreateRuleSet} className="retro-btn bg-primary text-white">创建</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 编辑规则集对话框 */}
|
||||
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑规则集</DialogTitle>
|
||||
<DialogContent className="max-w-lg retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest">编辑规则集</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={ruleSetForm.name}
|
||||
onChange={e => setRuleSetForm({ ...ruleSetForm, name: e.target.value })}
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">名称</Label>
|
||||
<Input value={ruleSetForm.name} onChange={e => setRuleSetForm({ ...ruleSetForm, name: e.target.value })} className="terminal-input" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Textarea
|
||||
value={ruleSetForm.description}
|
||||
onChange={e => setRuleSetForm({ ...ruleSetForm, description: e.target.value })}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">描述</Label>
|
||||
<Textarea value={ruleSetForm.description} onChange={e => setRuleSetForm({ ...ruleSetForm, description: e.target.value })} className="terminal-input" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>适用语言</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">适用语言</Label>
|
||||
<Select value={ruleSetForm.language} onValueChange={v => setRuleSetForm({ ...ruleSetForm, language: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGES.map(l => <SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{LANGUAGES.map(l => <SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>规则类型</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">规则类型</Label>
|
||||
<Select value={ruleSetForm.rule_type} onValueChange={v => setRuleSetForm({ ...ruleSetForm, rule_type: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{RULE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{RULE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowEditDialog(false)}>取消</Button>
|
||||
<Button onClick={handleUpdateRuleSet}>保存</Button>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200">
|
||||
<Button variant="outline" onClick={() => setShowEditDialog(false)} className="retro-btn bg-white text-black">取消</Button>
|
||||
<Button onClick={handleUpdateRuleSet} className="retro-btn bg-primary text-white">保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 规则编辑对话框 */}
|
||||
<Dialog open={showRuleDialog} onOpenChange={setShowRuleDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedRule ? '编辑规则' : '添加规则'}</DialogTitle>
|
||||
<DialogContent className="max-w-2xl retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0 max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest">{selectedRule ? '编辑规则' : '添加规则'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>规则代码</Label>
|
||||
<Input
|
||||
value={ruleForm.rule_code}
|
||||
onChange={e => setRuleForm({ ...ruleForm, rule_code: e.target.value })}
|
||||
placeholder="如 SEC001"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">规则代码 *</Label>
|
||||
<Input value={ruleForm.rule_code} onChange={e => setRuleForm({ ...ruleForm, rule_code: e.target.value })} placeholder="如 SEC001" className="terminal-input" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>规则名称</Label>
|
||||
<Input
|
||||
value={ruleForm.name}
|
||||
onChange={e => setRuleForm({ ...ruleForm, name: e.target.value })}
|
||||
placeholder="规则名称"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">规则名称 *</Label>
|
||||
<Input value={ruleForm.name} onChange={e => setRuleForm({ ...ruleForm, name: e.target.value })} placeholder="规则名称" className="terminal-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Textarea
|
||||
value={ruleForm.description}
|
||||
onChange={e => setRuleForm({ ...ruleForm, description: e.target.value })}
|
||||
placeholder="规则描述"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">描述</Label>
|
||||
<Textarea value={ruleForm.description} onChange={e => setRuleForm({ ...ruleForm, description: e.target.value })} placeholder="规则描述" className="terminal-input" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>类别</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">类别</Label>
|
||||
<Select value={ruleForm.category} onValueChange={v => setRuleForm({ ...ruleForm, category: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{CATEGORIES.map(c => <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{CATEGORIES.map(c => <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>严重程度</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">严重程度</Label>
|
||||
<Select value={ruleForm.severity} onValueChange={v => setRuleForm({ ...ruleForm, severity: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{SEVERITIES.map(s => <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{SEVERITIES.map(s => <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>自定义检测提示词</Label>
|
||||
<Textarea
|
||||
value={ruleForm.custom_prompt}
|
||||
onChange={e => setRuleForm({ ...ruleForm, custom_prompt: e.target.value })}
|
||||
placeholder="用于增强LLM检测的自定义提示词"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">自定义检测提示词</Label>
|
||||
<Textarea value={ruleForm.custom_prompt} onChange={e => setRuleForm({ ...ruleForm, custom_prompt: e.target.value })} placeholder="用于增强LLM检测的自定义提示词" rows={3} className="terminal-input" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>修复建议</Label>
|
||||
<Textarea
|
||||
value={ruleForm.fix_suggestion}
|
||||
onChange={e => setRuleForm({ ...ruleForm, fix_suggestion: e.target.value })}
|
||||
placeholder="修复建议模板"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">修复建议</Label>
|
||||
<Textarea value={ruleForm.fix_suggestion} onChange={e => setRuleForm({ ...ruleForm, fix_suggestion: e.target.value })} placeholder="修复建议模板" rows={2} className="terminal-input" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>参考链接</Label>
|
||||
<Input
|
||||
value={ruleForm.reference_url}
|
||||
onChange={e => setRuleForm({ ...ruleForm, reference_url: e.target.value })}
|
||||
placeholder="如 https://owasp.org/..."
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">参考链接</Label>
|
||||
<Input value={ruleForm.reference_url} onChange={e => setRuleForm({ ...ruleForm, reference_url: e.target.value })} placeholder="如 https://owasp.org/..." className="terminal-input" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowRuleDialog(false)}>取消</Button>
|
||||
<Button onClick={selectedRule ? handleUpdateRule : handleAddRule}>
|
||||
{selectedRule ? '保存' : '添加'}
|
||||
</Button>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200">
|
||||
<Button variant="outline" onClick={() => setShowRuleDialog(false)} className="retro-btn bg-white text-black">取消</Button>
|
||||
<Button onClick={selectedRule ? handleUpdateRule : handleAddRule} className="retro-btn bg-primary text-white">{selectedRule ? '保存' : '添加'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 导入对话框 */}
|
||||
<Dialog open={showImportDialog} onOpenChange={setShowImportDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入规则集</DialogTitle>
|
||||
<DialogDescription>粘贴导出的 JSON 内容</DialogDescription>
|
||||
<DialogContent className="max-w-2xl retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest flex items-center gap-2">
|
||||
<Upload className="w-5 h-5" />
|
||||
导入规则集
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-300">粘贴导出的 JSON 内容</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
value={importJson}
|
||||
onChange={e => setImportJson(e.target.value)}
|
||||
placeholder='{"name": "...", "rules": [...]}'
|
||||
rows={15}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowImportDialog(false)}>取消</Button>
|
||||
<Button onClick={handleImport}>导入</Button>
|
||||
<div className="p-6">
|
||||
<Textarea value={importJson} onChange={e => setImportJson(e.target.value)} placeholder='{"name": "...", "rules": [...]}' rows={15} className="terminal-input font-mono text-sm" />
|
||||
</div>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200">
|
||||
<Button variant="outline" onClick={() => setShowImportDialog(false)} className="retro-btn bg-white text-black">取消</Button>
|
||||
<Button onClick={handleImport} className="retro-btn bg-primary text-white">导入</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ import {
|
|||
import {
|
||||
Activity, AlertTriangle, Clock, Code,
|
||||
FileText, GitBranch, Shield, TrendingUp, Zap,
|
||||
BarChart3, Target, ArrowUpRight, Calendar
|
||||
BarChart3, Target, ArrowUpRight, Calendar,
|
||||
MessageSquare
|
||||
} from "lucide-react";
|
||||
import { api, dbMode, isDemoMode } from "@/shared/config/database";
|
||||
import type { Project, AuditTask, ProjectStats } from "@/shared/types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { getRuleSets } from "@/shared/api/rules";
|
||||
import { getPromptTemplates } from "@/shared/api/prompts";
|
||||
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
|
|
@ -27,6 +30,10 @@ export default function Dashboard() {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [issueTypeData, setIssueTypeData] = useState<Array<{ name: string; value: number; color: string }>>([]);
|
||||
const [qualityTrendData, setQualityTrendData] = useState<Array<{ date: string; score: number }>>([]);
|
||||
|
||||
// 规则和模板统计
|
||||
const [ruleStats, setRuleStats] = useState({ total: 0, enabled: 0 });
|
||||
const [templateStats, setTemplateStats] = useState({ total: 0, active: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
|
|
@ -131,6 +138,23 @@ export default function Dashboard() {
|
|||
console.error('获取问题数据失败:', error);
|
||||
setIssueTypeData([]);
|
||||
}
|
||||
|
||||
// 加载规则和模板统计
|
||||
try {
|
||||
const [rulesRes, promptsRes] = await Promise.all([
|
||||
getRuleSets(),
|
||||
getPromptTemplates(),
|
||||
]);
|
||||
const totalRules = rulesRes.items.reduce((acc, rs) => acc + rs.rules_count, 0);
|
||||
const enabledRules = rulesRes.items.reduce((acc, rs) => acc + rs.enabled_rules_count, 0);
|
||||
setRuleStats({ total: totalRules, enabled: enabledRules });
|
||||
setTemplateStats({
|
||||
total: promptsRes.items.length,
|
||||
active: promptsRes.items.filter(t => t.is_active).length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取规则和模板统计失败:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('仪表盘数据加载失败:', error);
|
||||
toast.error("数据加载失败");
|
||||
|
|
@ -487,6 +511,18 @@ export default function Dashboard() {
|
|||
启动审计任务
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/audit-rules" className="block">
|
||||
<Button variant="outline" className="w-full justify-start retro-btn bg-white text-black hover:bg-gray-100 h-10">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
审计规则管理
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/prompts" className="block">
|
||||
<Button variant="outline" className="w-full justify-start retro-btn bg-white text-black hover:bg-gray-100 h-10">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
提示词模板
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -528,6 +564,24 @@ export default function Dashboard() {
|
|||
{stats ? stats.total_issues - stats.resolved_issues : 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600 uppercase flex items-center gap-1">
|
||||
<Shield className="w-3 h-3" />
|
||||
审计规则
|
||||
</span>
|
||||
<span className="text-sm font-bold text-black border-2 border-black px-2 bg-purple-100 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
{ruleStats.enabled}/{ruleStats.total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600 uppercase flex items-center gap-1">
|
||||
<MessageSquare className="w-3 h-3" />
|
||||
提示词模板
|
||||
</span>
|
||||
<span className="text-sm font-bold text-black border-2 border-black px-2 bg-green-100 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
{templateStats.active}/{templateStats.total}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,15 @@ import {
|
|||
X,
|
||||
Download,
|
||||
History,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
MessageSquare
|
||||
} from "lucide-react";
|
||||
import { CodeAnalysisEngine } from "@/features/analysis/services";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { CodeAnalysisResult, InstantAnalysis as InstantAnalysisType } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
import InstantExportDialog from "@/components/reports/InstantExportDialog";
|
||||
import { getPromptTemplates, type PromptTemplate } from "@/shared/api/prompts";
|
||||
|
||||
// AI解释解析函数
|
||||
function parseAIExplanation(aiExplanation: string) {
|
||||
|
|
@ -52,7 +54,6 @@ function parseAIExplanation(aiExplanation: string) {
|
|||
}
|
||||
|
||||
export default function InstantAnalysis() {
|
||||
const user = null as any;
|
||||
const [code, setCode] = useState("");
|
||||
const [language, setLanguage] = useState("");
|
||||
const [analyzing, setAnalyzing] = useState(false);
|
||||
|
|
@ -68,8 +69,25 @@ export default function InstantAnalysis() {
|
|||
const [historyRecords, setHistoryRecords] = useState<InstantAnalysisType[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState<string | null>(null);
|
||||
|
||||
// 提示词模板
|
||||
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [selectedPromptTemplateId, setSelectedPromptTemplateId] = useState<string>("");
|
||||
|
||||
const supportedLanguages = CodeAnalysisEngine.getSupportedLanguages();
|
||||
|
||||
// 加载提示词模板
|
||||
useEffect(() => {
|
||||
const loadPromptTemplates = async () => {
|
||||
try {
|
||||
const res = await getPromptTemplates({ is_active: true });
|
||||
setPromptTemplates(res.items);
|
||||
} catch (error) {
|
||||
console.error("加载提示词模板失败:", error);
|
||||
}
|
||||
};
|
||||
loadPromptTemplates();
|
||||
}, []);
|
||||
|
||||
// 加载历史记录
|
||||
const loadHistory = async () => {
|
||||
|
|
@ -323,7 +341,7 @@ class UserManager {
|
|||
|
||||
const startTime = Date.now();
|
||||
|
||||
const analysisResult = await CodeAnalysisEngine.analyzeCode(code, language);
|
||||
const analysisResult = await CodeAnalysisEngine.analyzeCode(code, language, selectedPromptTemplateId || undefined);
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
|
||||
|
|
@ -708,6 +726,24 @@ class UserManager {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Select value={selectedPromptTemplateId} onValueChange={setSelectedPromptTemplateId}>
|
||||
<SelectTrigger className="h-10 retro-input rounded-none border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] focus:ring-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4 text-purple-600" />
|
||||
<SelectValue placeholder="默认提示词" />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-none border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<SelectItem value="">默认提示词</SelectItem>
|
||||
{promptTemplates.map((pt) => (
|
||||
<SelectItem key={pt.id} value={pt.id}>
|
||||
{pt.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
|
|
|
|||
|
|
@ -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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useToast } from '@/shared/hooks/use-toast';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
|
|
@ -24,6 +23,12 @@ import {
|
|||
Sparkles,
|
||||
Check,
|
||||
Loader2,
|
||||
Terminal,
|
||||
MessageSquare,
|
||||
Shield,
|
||||
Zap,
|
||||
Code,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getPromptTemplates,
|
||||
|
|
@ -34,6 +39,7 @@ import {
|
|||
type PromptTemplate,
|
||||
type PromptTemplateCreate,
|
||||
} from '@/shared/api/prompts';
|
||||
import { TEST_CODE_SAMPLES, TEMPLATE_TEST_CODES } from './prompt-manager/testCodeSamples';
|
||||
|
||||
const TEMPLATE_TYPES = [
|
||||
{ value: 'system', label: '系统提示词' },
|
||||
|
|
@ -41,54 +47,32 @@ const TEMPLATE_TYPES = [
|
|||
{ value: 'analysis', label: '分析提示词' },
|
||||
];
|
||||
|
||||
const TEST_CODE_SAMPLES: Record<string, string> = {
|
||||
python: `def login(username, password):
|
||||
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
|
||||
cursor.execute(query)
|
||||
return cursor.fetchone()`,
|
||||
javascript: `function getUserData(userId) {
|
||||
const query = "SELECT * FROM users WHERE id = " + userId;
|
||||
return db.query(query);
|
||||
}`,
|
||||
java: `public User findUser(String username) {
|
||||
String query = "SELECT * FROM users WHERE username = '" + username + "'";
|
||||
return jdbcTemplate.queryForObject(query, User.class);
|
||||
}`,
|
||||
const getTemplateIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'system': return Shield;
|
||||
case 'user': return MessageSquare;
|
||||
case 'analysis': return Code;
|
||||
default: return FileText;
|
||||
}
|
||||
};
|
||||
|
||||
export default function PromptManager() {
|
||||
const { toast } = useToast();
|
||||
const [templates, setTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 对话框状态
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showTestDialog, setShowTestDialog] = useState(false);
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<any>(null);
|
||||
|
||||
// 表单状态
|
||||
const [form, setForm] = useState<PromptTemplateCreate>({
|
||||
name: '',
|
||||
description: '',
|
||||
template_type: 'system',
|
||||
content_zh: '',
|
||||
content_en: '',
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
// 测试表单
|
||||
const [testForm, setTestForm] = useState({
|
||||
language: 'python',
|
||||
code: TEST_CODE_SAMPLES.python,
|
||||
name: '', description: '', template_type: 'system', content_zh: '', content_en: '', is_active: true,
|
||||
});
|
||||
const [testForm, setTestForm] = useState({ language: 'python', code: TEST_CODE_SAMPLES.python, promptLang: 'zh' as 'zh' | 'en' });
|
||||
const [showViewDialog, setShowViewDialog] = useState(false);
|
||||
const [viewTemplate, setViewTemplate] = useState<PromptTemplate | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
useEffect(() => { loadTemplates(); }, []);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
|
|
@ -96,7 +80,7 @@ export default function PromptManager() {
|
|||
const response = await getPromptTemplates();
|
||||
setTemplates(response.items);
|
||||
} catch (error) {
|
||||
toast({ title: '加载失败', description: '无法加载提示词模板', variant: 'destructive' });
|
||||
toast.error('加载提示词模板失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -105,168 +89,228 @@ export default function PromptManager() {
|
|||
const handleCreate = async () => {
|
||||
try {
|
||||
await createPromptTemplate(form);
|
||||
toast({ title: '创建成功' });
|
||||
toast.success('创建成功');
|
||||
setShowCreateDialog(false);
|
||||
resetForm();
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
toast({ title: '创建失败', variant: 'destructive' });
|
||||
}
|
||||
} catch (error) { toast.error('创建失败'); }
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!selectedTemplate) return;
|
||||
try {
|
||||
await updatePromptTemplate(selectedTemplate.id, form);
|
||||
toast({ title: '更新成功' });
|
||||
toast.success('更新成功');
|
||||
setShowEditDialog(false);
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
toast({ title: '更新失败', variant: 'destructive' });
|
||||
}
|
||||
} catch (error) { toast.error('更新失败'); }
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('确定要删除此模板吗?')) return;
|
||||
try {
|
||||
await deletePromptTemplate(id);
|
||||
toast({ title: '删除成功' });
|
||||
toast.success('删除成功');
|
||||
loadTemplates();
|
||||
} catch (error: any) {
|
||||
toast({ title: '删除失败', description: error.message, variant: 'destructive' });
|
||||
}
|
||||
} catch (error: any) { toast.error(error.message || '删除失败'); }
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!selectedTemplate) return;
|
||||
|
||||
const content = selectedTemplate.content_zh || selectedTemplate.content_en || '';
|
||||
if (!content) {
|
||||
toast({ title: '提示词内容为空', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
|
||||
const content = testForm.promptLang === 'zh'
|
||||
? (selectedTemplate.content_zh || selectedTemplate.content_en || '')
|
||||
: (selectedTemplate.content_en || selectedTemplate.content_zh || '');
|
||||
if (!content) { toast.error('提示词内容为空'); return; }
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
const result = await testPromptTemplate({
|
||||
content,
|
||||
language: testForm.language,
|
||||
code: testForm.code,
|
||||
});
|
||||
const result = await testPromptTemplate({ content, language: testForm.language, code: testForm.code, output_language: testForm.promptLang });
|
||||
setTestResult(result);
|
||||
if (result.success) {
|
||||
toast({ title: '测试完成', description: `耗时 ${result.execution_time}s` });
|
||||
} else {
|
||||
toast({ title: '测试失败', description: result.error, variant: 'destructive' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({ title: '测试失败', description: error.message, variant: 'destructive' });
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
if (result.success) toast.success(`测试完成,耗时 ${result.execution_time}s`);
|
||||
else toast.error(result.error || '测试失败');
|
||||
} catch (error: any) { toast.error(error.message || '测试失败'); }
|
||||
finally { setTesting(false); }
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setForm({
|
||||
name: '',
|
||||
description: '',
|
||||
template_type: 'system',
|
||||
content_zh: '',
|
||||
content_en: '',
|
||||
is_active: true,
|
||||
});
|
||||
setForm({ name: '', description: '', template_type: 'system', content_zh: '', content_en: '', is_active: true });
|
||||
};
|
||||
|
||||
const openEditDialog = (template: PromptTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setForm({
|
||||
name: template.name,
|
||||
description: template.description || '',
|
||||
template_type: template.template_type,
|
||||
content_zh: template.content_zh || '',
|
||||
content_en: template.content_en || '',
|
||||
is_active: template.is_active,
|
||||
});
|
||||
setForm({ name: template.name, description: template.description || '', template_type: template.template_type, content_zh: template.content_zh || '', content_en: template.content_en || '', is_active: template.is_active });
|
||||
setShowEditDialog(true);
|
||||
};
|
||||
|
||||
const openTestDialog = (template: PromptTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setTestResult(null);
|
||||
|
||||
// 根据模板名称加载对应的测试代码
|
||||
const templateCodes = TEMPLATE_TEST_CODES[template.name];
|
||||
const defaultLang = 'python';
|
||||
if (templateCodes && templateCodes[defaultLang]) {
|
||||
setTestForm(prev => ({
|
||||
...prev,
|
||||
language: defaultLang,
|
||||
code: templateCodes[defaultLang]
|
||||
}));
|
||||
} else {
|
||||
// 使用通用测试代码
|
||||
setTestForm(prev => ({
|
||||
...prev,
|
||||
language: defaultLang,
|
||||
code: TEST_CODE_SAMPLES[defaultLang]
|
||||
}));
|
||||
}
|
||||
|
||||
setShowTestDialog(true);
|
||||
};
|
||||
|
||||
const openViewDialog = (template: PromptTemplate) => {
|
||||
setViewTemplate(template);
|
||||
setShowViewDialog(true);
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast({ title: '已复制到剪贴板' });
|
||||
toast.success('已复制到剪贴板');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||
<div className="animate-spin rounded-none h-32 w-32 border-8 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">提示词管理</h1>
|
||||
<p className="text-muted-foreground">管理代码审计提示词模板,自定义分析策略</p>
|
||||
<div className="flex flex-col gap-6 px-6 py-4 bg-background min-h-screen font-mono relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 relative z-10">
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">模板总数</p>
|
||||
<p className="text-3xl font-bold text-black">{templates.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-primary border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">系统模板</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{templates.filter(t => t.is_system).length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-blue-600 border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Shield className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">自定义模板</p>
|
||||
<p className="text-3xl font-bold text-green-600">{templates.filter(t => !t.is_system).length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-green-600 border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-5 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-600 uppercase mb-1">已启用</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{templates.filter(t => t.is_active).length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-orange-500 border-2 border-black flex items-center justify-center text-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Zap className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-4 relative z-10">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5" />
|
||||
<span className="font-bold uppercase">提示词模板管理</span>
|
||||
</div>
|
||||
<Button onClick={() => { resetForm(); setShowCreateDialog(true); }} className="retro-btn bg-primary text-white hover:bg-primary/90 h-10 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建模板
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={() => { resetForm(); setShowCreateDialog(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建模板
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 模板列表 */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{loading ? (
|
||||
<div className="col-span-full text-center py-8 text-muted-foreground">加载中...</div>
|
||||
) : templates.length === 0 ? (
|
||||
<Card className="col-span-full">
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
暂无提示词模板,点击"新建模板"创建
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 relative z-10">
|
||||
{templates.length === 0 ? (
|
||||
<div className="col-span-full retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-16 flex flex-col items-center justify-center text-center">
|
||||
<div className="w-20 h-20 bg-gray-100 border-2 border-black flex items-center justify-center mb-6 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<FileText className="w-10 h-10 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-black uppercase mb-2">暂无提示词模板</h3>
|
||||
<p className="text-gray-500 mb-8 max-w-md">点击"新建模板"创建自定义提示词</p>
|
||||
<Button className="retro-btn bg-primary text-white h-12 px-8 text-lg font-bold uppercase" onClick={() => { resetForm(); setShowCreateDialog(true); }}>
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
创建模板
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
templates.map(template => (
|
||||
<Card key={template.id} className={!template.is_active ? 'opacity-60' : ''}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="w-5 h-5" />
|
||||
{template.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">{template.description}</CardDescription>
|
||||
templates.map(template => {
|
||||
const TemplateIcon = getTemplateIcon(template.template_type);
|
||||
return (
|
||||
<div key={template.id} className={`retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all ${!template.is_active ? 'opacity-60' : ''}`}>
|
||||
<div className="p-5 border-b-2 border-dashed border-gray-300">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gray-100 border-2 border-black flex items-center justify-center shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<TemplateIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg uppercase">{template.name}</h3>
|
||||
<p className="text-xs text-gray-500">{template.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{template.is_system && <Badge variant="secondary">系统</Badge>}
|
||||
{template.is_default && <Badge>默认</Badge>}
|
||||
<Badge variant="outline">
|
||||
{TEMPLATE_TYPES.find(t => t.value === template.template_type)?.label}
|
||||
</Badge>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{template.is_system && <Badge className="rounded-none border-2 border-black bg-blue-100 text-blue-800">系统</Badge>}
|
||||
{template.is_default && <Badge className="rounded-none border-2 border-black bg-green-100 text-green-800">默认</Badge>}
|
||||
<Badge variant="outline" className="rounded-none border-2 border-black">{TEMPLATE_TYPES.find(t => t.value === template.template_type)?.label}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{/* 预览 */}
|
||||
<div className="text-sm text-muted-foreground line-clamp-3 bg-muted p-2 rounded">
|
||||
|
||||
<div className="p-4">
|
||||
<div
|
||||
className="text-sm text-gray-600 line-clamp-3 bg-gray-50 p-3 border-2 border-black font-mono text-xs mb-4 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
onClick={() => openViewDialog(template)}
|
||||
title="点击查看完整内容"
|
||||
>
|
||||
{template.content_zh || template.content_en || '(无内容)'}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-1">
|
||||
<Button variant="ghost" size="sm" onClick={() => openTestDialog(template)}>
|
||||
<Button variant="ghost" size="sm" onClick={() => openViewDialog(template)} className="hover:bg-purple-100">
|
||||
<FileText className="w-4 h-4 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => openTestDialog(template)} className="hover:bg-green-100">
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
测试
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(template.content_zh || template.content_en || '')}
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(template.content_zh || template.content_en || '')} className="hover:bg-blue-100">
|
||||
<Copy className="w-4 h-4 mr-1" />
|
||||
复制
|
||||
</Button>
|
||||
|
|
@ -274,235 +318,302 @@ export default function PromptManager() {
|
|||
<div className="flex gap-1">
|
||||
{!template.is_system && (
|
||||
<>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEditDialog(template)}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleDelete(template.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => openEditDialog(template)} className="hover:bg-gray-100"><Edit className="w-4 h-4" /></Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleDelete(template.id)} className="hover:bg-red-100 hover:text-red-600"><Trash2 className="w-4 h-4" /></Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 创建/编辑对话框 */}
|
||||
<Dialog open={showCreateDialog || showEditDialog} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowCreateDialog(false);
|
||||
setShowEditDialog(false);
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{showEditDialog ? '编辑模板' : '新建模板'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{showEditDialog ? '修改提示词模板内容' : '创建自定义提示词模板'}
|
||||
</DialogDescription>
|
||||
<Dialog open={showCreateDialog || showEditDialog} onOpenChange={(open) => { if (!open) { setShowCreateDialog(false); setShowEditDialog(false); } }}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5" />
|
||||
{showEditDialog ? '编辑模板' : '新建模板'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>模板名称</Label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={e => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="如:安全专项审计"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">模板名称 *</Label>
|
||||
<Input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="如:安全专项审计" className="terminal-input" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>模板类型</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">模板类型</Label>
|
||||
<Select value={form.template_type} onValueChange={v => setForm({ ...form, template_type: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{TEMPLATE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{TEMPLATE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Input
|
||||
value={form.description}
|
||||
onChange={e => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="模板用途描述"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">描述</Label>
|
||||
<Input value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} placeholder="模板用途描述" className="terminal-input" />
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="zh" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="zh">中文提示词</TabsTrigger>
|
||||
<TabsTrigger value="en">英文提示词</TabsTrigger>
|
||||
<TabsList className="flex w-full bg-gray-100 border-2 border-black p-1 h-auto gap-1">
|
||||
<TabsTrigger value="zh" className="flex-1 data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase py-2">中文提示词</TabsTrigger>
|
||||
<TabsTrigger value="en" className="flex-1 data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase py-2">英文提示词</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="zh">
|
||||
<Textarea
|
||||
value={form.content_zh}
|
||||
onChange={e => setForm({ ...form, content_zh: e.target.value })}
|
||||
placeholder="输入中文提示词内容..."
|
||||
rows={15}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<TabsContent value="zh" className="mt-4">
|
||||
<Textarea value={form.content_zh} onChange={e => setForm({ ...form, content_zh: e.target.value })} placeholder="输入中文提示词内容..." rows={15} className="terminal-input font-mono text-sm" />
|
||||
</TabsContent>
|
||||
<TabsContent value="en">
|
||||
<Textarea
|
||||
value={form.content_en}
|
||||
onChange={e => setForm({ ...form, content_en: e.target.value })}
|
||||
placeholder="Enter English prompt content..."
|
||||
rows={15}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<TabsContent value="en" className="mt-4">
|
||||
<Textarea value={form.content_en} onChange={e => setForm({ ...form, content_en: e.target.value })} placeholder="Enter English prompt content..." rows={15} className="terminal-input font-mono text-sm" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={form.is_active}
|
||||
onCheckedChange={v => setForm({ ...form, is_active: v })}
|
||||
/>
|
||||
<Label>启用此模板</Label>
|
||||
<Switch checked={form.is_active} onCheckedChange={v => setForm({ ...form, is_active: v })} />
|
||||
<Label className="font-mono font-bold uppercase text-xs">启用此模板</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => { setShowCreateDialog(false); setShowEditDialog(false); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={showEditDialog ? handleUpdate : handleCreate}>
|
||||
{showEditDialog ? '保存' : '创建'}
|
||||
</Button>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200">
|
||||
<Button variant="outline" onClick={() => { setShowCreateDialog(false); setShowEditDialog(false); }} className="retro-btn bg-white text-black">取消</Button>
|
||||
<Button onClick={showEditDialog ? handleUpdate : handleCreate} className="retro-btn bg-primary text-white">{showEditDialog ? '保存' : '创建'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 测试对话框 */}
|
||||
<Dialog open={showTestDialog} onOpenChange={setShowTestDialog}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<DialogContent className="!max-w-6xl w-[90vw] max-h-[90vh] overflow-y-auto retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
测试提示词: {selectedTemplate?.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
使用示例代码测试提示词效果
|
||||
</DialogDescription>
|
||||
<DialogDescription className="text-gray-300">使用示例代码测试提示词效果</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-6 grid grid-cols-2 gap-6">
|
||||
{/* 左侧:输入 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>编程语言</Label>
|
||||
<Select
|
||||
value={testForm.language}
|
||||
onValueChange={v => {
|
||||
setTestForm({
|
||||
language: v,
|
||||
code: TEST_CODE_SAMPLES[v] || TEST_CODE_SAMPLES.python,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="javascript">JavaScript</SelectItem>
|
||||
<SelectItem value="java">Java</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">编程语言</Label>
|
||||
<Select value={testForm.language} onValueChange={v => {
|
||||
// 优先使用模板专属测试代码,否则使用通用测试代码
|
||||
const templateCodes = selectedTemplate ? TEMPLATE_TEST_CODES[selectedTemplate.name] : null;
|
||||
const code = templateCodes?.[v] || TEST_CODE_SAMPLES[v] || TEST_CODE_SAMPLES.python;
|
||||
setTestForm({ ...testForm, language: v, code });
|
||||
}}>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="javascript">JavaScript</SelectItem>
|
||||
<SelectItem value="java">Java</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">提示词语言</Label>
|
||||
<Select value={testForm.promptLang} onValueChange={(v: 'zh' | 'en') => setTestForm({ ...testForm, promptLang: v })}>
|
||||
<SelectTrigger className="terminal-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zh">中文提示词</SelectItem>
|
||||
<SelectItem value="en">英文提示词</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>测试代码</Label>
|
||||
<Textarea
|
||||
value={testForm.code}
|
||||
onChange={e => setTestForm({ ...testForm, code: e.target.value })}
|
||||
rows={10}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="font-mono font-bold uppercase text-xs">测试代码</Label>
|
||||
<Textarea value={testForm.code} onChange={e => setTestForm({ ...testForm, code: e.target.value })} rows={10} className="terminal-input font-mono text-sm" />
|
||||
</div>
|
||||
|
||||
<Button onClick={handleTest} disabled={testing} className="w-full">
|
||||
{testing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
分析中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
运行测试
|
||||
</>
|
||||
)}
|
||||
<Button onClick={handleTest} disabled={testing} className="w-full retro-btn bg-primary text-white h-12">
|
||||
{testing ? (<><Loader2 className="w-4 h-4 mr-2 animate-spin" />分析中...</>) : (<><Play className="w-4 h-4 mr-2" />运行测试</>)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 右侧:结果 */}
|
||||
<div className="space-y-4">
|
||||
<Label>分析结果</Label>
|
||||
<div className="border rounded-lg p-4 h-[400px] overflow-auto bg-muted">
|
||||
<Label className="font-mono font-bold uppercase text-xs">分析结果</Label>
|
||||
<div className="border-2 border-black h-[400px] overflow-auto bg-gray-50">
|
||||
{testResult ? (
|
||||
testResult.success ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-green-600">
|
||||
<Check className="w-5 h-5" />
|
||||
<span>分析成功 (耗时 {testResult.execution_time}s)</span>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 成功状态头部 */}
|
||||
<div className="flex items-center justify-between p-3 bg-green-100 border-b-2 border-black">
|
||||
<div className="flex items-center gap-2 text-green-700 font-bold">
|
||||
<Check className="w-5 h-5" />
|
||||
<span className="uppercase text-sm">分析成功</span>
|
||||
</div>
|
||||
<Badge className="rounded-none border-2 border-black bg-white text-black font-mono">
|
||||
{testResult.execution_time}s
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{testResult.result?.issues?.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium">发现 {testResult.result.issues.length} 个问题:</div>
|
||||
{testResult.result.issues.map((issue: any, idx: number) => (
|
||||
<div key={idx} className="p-2 bg-background rounded border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={
|
||||
issue.severity === 'critical' ? 'destructive' :
|
||||
issue.severity === 'high' ? 'destructive' :
|
||||
issue.severity === 'medium' ? 'default' : 'secondary'
|
||||
}>
|
||||
{issue.severity}
|
||||
</Badge>
|
||||
<span className="font-medium">{issue.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">{issue.description}</p>
|
||||
{issue.line && (
|
||||
<p className="text-xs text-muted-foreground">行 {issue.line}</p>
|
||||
)}
|
||||
{/* 质量评分 */}
|
||||
{testResult.result?.quality_score !== undefined && (
|
||||
<div className="p-3 bg-white border-b-2 border-dashed border-gray-300 flex items-center justify-between">
|
||||
<span className="text-xs font-bold uppercase text-gray-600">质量评分</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`text-2xl font-bold ${
|
||||
testResult.result.quality_score >= 80 ? 'text-green-600' :
|
||||
testResult.result.quality_score >= 60 ? 'text-yellow-600' : 'text-red-600'
|
||||
}`}>
|
||||
{testResult.result.quality_score}
|
||||
</div>
|
||||
))}
|
||||
<span className="text-xs text-gray-500">/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">未发现问题</div>
|
||||
)}
|
||||
|
||||
{testResult.result?.quality_score !== undefined && (
|
||||
<div className="text-sm">
|
||||
质量评分: <span className="font-bold">{testResult.result.quality_score}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 问题列表 */}
|
||||
<div className="flex-1 overflow-auto p-3">
|
||||
{testResult.result?.issues?.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-bold uppercase text-gray-600">发现问题</span>
|
||||
<Badge className="rounded-none border-2 border-black bg-red-100 text-red-800">
|
||||
{testResult.result.issues.length} 个
|
||||
</Badge>
|
||||
</div>
|
||||
{testResult.result.issues.map((issue: any, idx: number) => (
|
||||
<div key={idx} className="bg-white border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] overflow-hidden">
|
||||
<div className={`px-3 py-2 border-b-2 border-black flex items-center justify-between ${
|
||||
issue.severity === 'critical' ? 'bg-red-500 text-white' :
|
||||
issue.severity === 'high' ? 'bg-orange-500 text-white' :
|
||||
issue.severity === 'medium' ? 'bg-yellow-400 text-black' : 'bg-blue-400 text-white'
|
||||
}`}>
|
||||
<span className="font-bold text-xs uppercase">{issue.severity}</span>
|
||||
{issue.line && <span className="text-xs opacity-80">行 {issue.line}</span>}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h4 className="font-bold text-sm mb-1">{issue.title}</h4>
|
||||
{issue.description && (
|
||||
<p className="text-xs text-gray-600 leading-relaxed">{issue.description}</p>
|
||||
)}
|
||||
{issue.suggestion && (
|
||||
<div className="mt-2 p-2 bg-blue-50 border-l-4 border-blue-500">
|
||||
<p className="text-xs text-blue-800">
|
||||
<span className="font-bold">建议: </span>
|
||||
{issue.suggestion}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-12 h-12 bg-green-100 border-2 border-black flex items-center justify-center mx-auto mb-3 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
|
||||
<Check className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<p className="font-bold text-green-700 uppercase text-sm">未发现问题</p>
|
||||
<p className="text-xs text-gray-500 mt-1">代码质量良好</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-red-500">
|
||||
<div className="font-medium">测试失败</div>
|
||||
<div className="text-sm mt-1">{testResult.error}</div>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 失败状态头部 */}
|
||||
<div className="flex items-center justify-between p-3 bg-red-100 border-b-2 border-black">
|
||||
<div className="flex items-center gap-2 text-red-700 font-bold">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span className="uppercase text-sm">测试失败</span>
|
||||
</div>
|
||||
{testResult.execution_time && (
|
||||
<Badge className="rounded-none border-2 border-black bg-white text-black font-mono">
|
||||
{testResult.execution_time}s
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* 错误详情 */}
|
||||
<div className="flex-1 p-4">
|
||||
<div className="bg-red-50 border-2 border-red-300 p-4 h-full overflow-auto">
|
||||
<pre className="text-sm text-red-800 font-mono whitespace-pre-wrap break-words">
|
||||
{testResult.error || '未知错误'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-muted-foreground text-center py-8">
|
||||
点击"运行测试"查看分析结果
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<div className="w-16 h-16 bg-gray-100 border-2 border-dashed border-gray-300 flex items-center justify-center mb-4">
|
||||
<Play className="w-8 h-8 opacity-50" />
|
||||
</div>
|
||||
<p className="font-mono uppercase text-sm">点击"运行测试"</p>
|
||||
<p className="font-mono text-xs mt-1">查看分析结果</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowTestDialog(false)}>关闭</Button>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200">
|
||||
<Button variant="outline" onClick={() => setShowTestDialog(false)} className="retro-btn bg-white text-black">关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 查看详情对话框 */}
|
||||
<Dialog open={showViewDialog} onOpenChange={setShowViewDialog}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto retro-card border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] bg-white p-0">
|
||||
<DialogHeader className="bg-black text-white p-4 border-b-4 border-black">
|
||||
<DialogTitle className="font-mono text-xl uppercase tracking-widest flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
{viewTemplate?.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-300">{viewTemplate?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{viewTemplate?.is_system && <Badge className="rounded-none border-2 border-black bg-blue-100 text-blue-800">系统模板</Badge>}
|
||||
{viewTemplate?.is_default && <Badge className="rounded-none border-2 border-black bg-green-100 text-green-800">默认</Badge>}
|
||||
<Badge variant="outline" className="rounded-none border-2 border-black">{TEMPLATE_TYPES.find(t => t.value === viewTemplate?.template_type)?.label}</Badge>
|
||||
{viewTemplate?.is_active ? (
|
||||
<Badge className="rounded-none border-2 border-black bg-green-100 text-green-800">已启用</Badge>
|
||||
) : (
|
||||
<Badge className="rounded-none border-2 border-black bg-gray-100 text-gray-800">已禁用</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="zh" className="w-full">
|
||||
<TabsList className="flex w-full bg-gray-100 border-2 border-black p-1 h-auto gap-1">
|
||||
<TabsTrigger value="zh" className="flex-1 data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase py-2">
|
||||
中文提示词
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="en" className="flex-1 data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase py-2">
|
||||
英文提示词
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="zh" className="mt-4">
|
||||
<div className="bg-gray-900 text-green-400 p-4 border-2 border-black font-mono text-sm whitespace-pre-wrap max-h-[500px] overflow-y-auto">
|
||||
{viewTemplate?.content_zh || '(无中文内容)'}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="en" className="mt-4">
|
||||
<div className="bg-gray-900 text-green-400 p-4 border-2 border-black font-mono text-sm whitespace-pre-wrap max-h-[500px] overflow-y-auto">
|
||||
{viewTemplate?.content_en || '(No English content)'}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter className="p-4 border-t-2 border-dashed border-gray-200 flex gap-2">
|
||||
<Button variant="outline" onClick={() => copyToClipboard(viewTemplate?.content_zh || viewTemplate?.content_en || '')} className="retro-btn bg-white text-black">
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
复制内容
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => { setShowViewDialog(false); if (viewTemplate) openTestDialog(viewTemplate); }} className="retro-btn bg-green-100 text-green-800 hover:bg-green-200">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
测试
|
||||
</Button>
|
||||
{!viewTemplate?.is_system && (
|
||||
<Button variant="outline" onClick={() => { setShowViewDialog(false); if (viewTemplate) openEditDialog(viewTemplate); }} className="retro-btn bg-blue-100 text-blue-800 hover:bg-blue-200">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setShowViewDialog(false)} className="retro-btn bg-primary text-white">关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,508 @@
|
|||
/**
|
||||
* 提示词模板测试代码示例
|
||||
*/
|
||||
|
||||
// 按编程语言分类的通用测试代码
|
||||
export const TEST_CODE_SAMPLES: Record<string, string> = {
|
||||
python: `def login(username, password):
|
||||
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
|
||||
cursor.execute(query)
|
||||
return cursor.fetchone()`,
|
||||
javascript: `function getUserData(userId) {
|
||||
const query = "SELECT * FROM users WHERE id = " + userId;
|
||||
return db.query(query);
|
||||
}`,
|
||||
java: `public User findUser(String username) {
|
||||
String query = "SELECT * FROM users WHERE username = '" + username + "'";
|
||||
return jdbcTemplate.queryForObject(query, User.class);
|
||||
}`,
|
||||
};
|
||||
|
||||
// 按模板名称分类的测试代码(针对不同审计场景)
|
||||
export const TEMPLATE_TEST_CODES: Record<string, Record<string, string>> = {
|
||||
// 默认代码审计 - 包含多种问题的综合示例
|
||||
'默认代码审计': {
|
||||
python: `import os
|
||||
import pickle
|
||||
|
||||
API_KEY = "sk-1234567890abcdef" # 硬编码密钥
|
||||
|
||||
def process_user_input(user_input):
|
||||
# SQL注入风险
|
||||
query = f"SELECT * FROM users WHERE name = '{user_input}'"
|
||||
cursor.execute(query)
|
||||
|
||||
# 命令注入风险
|
||||
os.system(f"echo {user_input}")
|
||||
|
||||
# 不安全的反序列化
|
||||
data = pickle.loads(user_input.encode())
|
||||
|
||||
# 性能问题:循环中查询
|
||||
for item in items:
|
||||
db.query(f"SELECT * FROM orders WHERE item_id = {item.id}")
|
||||
|
||||
return data`,
|
||||
javascript: `const API_SECRET = "secret123"; // 硬编码密钥
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
// SQL注入
|
||||
const query = "SELECT * FROM users WHERE id = " + req.params.id;
|
||||
const user = await db.query(query);
|
||||
|
||||
// XSS风险
|
||||
res.send("<div>" + req.body.content + "</div>");
|
||||
|
||||
// 命令注入
|
||||
exec("ls " + req.query.path);
|
||||
|
||||
// 性能问题
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
await db.query("SELECT * FROM orders WHERE user_id = " + users[i].id);
|
||||
}
|
||||
}`,
|
||||
java: `public class UserService {
|
||||
private static final String DB_PASSWORD = "admin123"; // 硬编码
|
||||
|
||||
public User findUser(String input) {
|
||||
// SQL注入
|
||||
String sql = "SELECT * FROM users WHERE name = '" + input + "'";
|
||||
return jdbcTemplate.queryForObject(sql, User.class);
|
||||
}
|
||||
|
||||
public void processFile(String filename) {
|
||||
// 路径遍历
|
||||
File file = new File("/data/" + filename);
|
||||
// 缺少错误处理
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
}
|
||||
}`,
|
||||
},
|
||||
|
||||
// 安全专项审计 - 专注安全漏洞的示例
|
||||
'安全专项审计': {
|
||||
python: `import os
|
||||
import subprocess
|
||||
import pickle
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
SECRET_KEY = "super_secret_key_12345"
|
||||
DB_PASSWORD = "root:password123"
|
||||
|
||||
def authenticate(username, password):
|
||||
# SQL注入
|
||||
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
|
||||
return db.execute(query)
|
||||
|
||||
def run_command(cmd):
|
||||
# 命令注入
|
||||
os.system(cmd)
|
||||
subprocess.call(cmd, shell=True)
|
||||
|
||||
def load_data(data):
|
||||
# 不安全的反序列化
|
||||
return pickle.loads(data)
|
||||
|
||||
def parse_xml(xml_string):
|
||||
# XXE漏洞
|
||||
tree = ET.fromstring(xml_string)
|
||||
return tree
|
||||
|
||||
def fetch_url(url):
|
||||
# SSRF
|
||||
import requests
|
||||
return requests.get(url).text
|
||||
|
||||
def read_file(filename):
|
||||
# 路径遍历
|
||||
with open(f"/var/data/{filename}", "r") as f:
|
||||
return f.read()`,
|
||||
javascript: `const crypto = require('crypto');
|
||||
|
||||
const API_KEY = "sk-live-abcdef123456";
|
||||
const JWT_SECRET = "mysecret";
|
||||
|
||||
function login(username, password) {
|
||||
// SQL注入
|
||||
const query = \`SELECT * FROM users WHERE username='\${username}' AND password='\${password}'\`;
|
||||
return db.query(query);
|
||||
}
|
||||
|
||||
function renderPage(userInput) {
|
||||
// XSS
|
||||
document.innerHTML = userInput;
|
||||
return \`<div>\${userInput}</div>\`;
|
||||
}
|
||||
|
||||
function executeCommand(cmd) {
|
||||
// 命令注入
|
||||
const { exec } = require('child_process');
|
||||
exec(cmd);
|
||||
}
|
||||
|
||||
function hashPassword(password) {
|
||||
// 弱加密
|
||||
return crypto.createHash('md5').update(password).digest('hex');
|
||||
}
|
||||
|
||||
function verifyToken(token) {
|
||||
// 硬编码密钥
|
||||
return jwt.verify(token, "hardcoded_secret");
|
||||
}`,
|
||||
java: `import java.io.*;
|
||||
import java.sql.*;
|
||||
|
||||
public class VulnerableService {
|
||||
private static final String API_KEY = "AIzaSyD-xxxxx";
|
||||
private static final String DB_PASS = "root123";
|
||||
|
||||
// SQL注入
|
||||
public User getUser(String id) {
|
||||
String sql = "SELECT * FROM users WHERE id = '" + id + "'";
|
||||
return jdbcTemplate.queryForObject(sql, User.class);
|
||||
}
|
||||
|
||||
// 命令注入
|
||||
public void runCommand(String cmd) {
|
||||
Runtime.getRuntime().exec(cmd);
|
||||
}
|
||||
|
||||
// 路径遍历
|
||||
public String readFile(String name) throws IOException {
|
||||
return new String(Files.readAllBytes(Paths.get("/data/" + name)));
|
||||
}
|
||||
|
||||
// 不安全的反序列化
|
||||
public Object deserialize(byte[] data) throws Exception {
|
||||
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
|
||||
return ois.readObject();
|
||||
}
|
||||
|
||||
// 弱加密
|
||||
public String hashPassword(String password) {
|
||||
return DigestUtils.md5Hex(password);
|
||||
}
|
||||
}`,
|
||||
},
|
||||
|
||||
// 性能优化审计 - 专注性能问题的示例
|
||||
'性能优化审计': {
|
||||
python: `import time
|
||||
|
||||
def get_user_orders(user_ids):
|
||||
# N+1查询问题
|
||||
results = []
|
||||
for user_id in user_ids:
|
||||
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
|
||||
orders = db.query(f"SELECT * FROM orders WHERE user_id = {user_id}")
|
||||
results.append({"user": user, "orders": orders})
|
||||
return results
|
||||
|
||||
def process_large_file(filename):
|
||||
# 一次性加载大文件到内存
|
||||
with open(filename, 'r') as f:
|
||||
content = f.read() # 可能导致内存溢出
|
||||
return process(content)
|
||||
|
||||
def find_duplicates(items):
|
||||
# O(n²) 算法
|
||||
duplicates = []
|
||||
for i in range(len(items)):
|
||||
for j in range(len(items)):
|
||||
if i != j and items[i] == items[j]:
|
||||
duplicates.append(items[i])
|
||||
return duplicates
|
||||
|
||||
def create_reports(data):
|
||||
# 循环中创建对象
|
||||
for item in data:
|
||||
formatter = ReportFormatter() # 应该移到循环外
|
||||
report = formatter.format(item)
|
||||
reports.append(report)
|
||||
|
||||
class DataProcessor:
|
||||
# 未关闭资源
|
||||
def process(self, filename):
|
||||
f = open(filename, 'r')
|
||||
data = f.read()
|
||||
return self.transform(data)
|
||||
# 文件未关闭`,
|
||||
javascript: `// N+1查询问题
|
||||
async function getUsersWithOrders(userIds) {
|
||||
const results = [];
|
||||
for (const userId of userIds) {
|
||||
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
|
||||
const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [userId]);
|
||||
results.push({ user, orders });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// 低效的数组操作
|
||||
function findCommonElements(arr1, arr2) {
|
||||
const common = [];
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
for (let j = 0; j < arr2.length; j++) {
|
||||
if (arr1[i] === arr2[j]) {
|
||||
common.push(arr1[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return common;
|
||||
}
|
||||
|
||||
// 内存泄漏风险
|
||||
const cache = {};
|
||||
function processData(key, data) {
|
||||
cache[key] = data; // 缓存无限增长
|
||||
return transform(data);
|
||||
}
|
||||
|
||||
// 同步阻塞
|
||||
function readFiles(filenames) {
|
||||
const contents = [];
|
||||
for (const filename of filenames) {
|
||||
contents.push(fs.readFileSync(filename, 'utf8')); // 同步阻塞
|
||||
}
|
||||
return contents;
|
||||
}`,
|
||||
java: `public class PerformanceIssues {
|
||||
|
||||
// N+1查询
|
||||
public List<UserDTO> getUsersWithOrders(List<Long> userIds) {
|
||||
List<UserDTO> results = new ArrayList<>();
|
||||
for (Long userId : userIds) {
|
||||
User user = userRepository.findById(userId);
|
||||
List<Order> orders = orderRepository.findByUserId(userId);
|
||||
results.add(new UserDTO(user, orders));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// 循环中创建对象
|
||||
public List<String> formatDates(List<Date> dates) {
|
||||
List<String> results = new ArrayList<>();
|
||||
for (Date date : dates) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 应复用
|
||||
results.add(sdf.format(date));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// 字符串拼接性能问题
|
||||
public String buildReport(List<String> items) {
|
||||
String result = "";
|
||||
for (String item : items) {
|
||||
result += item + "\\n"; // 应使用StringBuilder
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 未关闭资源
|
||||
public String readFile(String path) throws IOException {
|
||||
FileInputStream fis = new FileInputStream(path);
|
||||
byte[] data = fis.readAllBytes();
|
||||
return new String(data); // fis未关闭
|
||||
}
|
||||
}`,
|
||||
},
|
||||
|
||||
// 代码质量审计 - 专注代码质量的示例
|
||||
'代码质量审计': {
|
||||
python: `# 魔法数字和命名问题
|
||||
def calc(x, y, z):
|
||||
if x > 100: # 魔法数字
|
||||
return y * 0.15 + z * 0.85 # 魔法数字
|
||||
return y + z
|
||||
|
||||
# 函数过长、职责不单一
|
||||
def process_order(order):
|
||||
# 验证订单
|
||||
if not order.items:
|
||||
return None
|
||||
if not order.user_id:
|
||||
return None
|
||||
if order.total < 0:
|
||||
return None
|
||||
|
||||
# 计算价格
|
||||
subtotal = 0
|
||||
for item in order.items:
|
||||
subtotal += item.price * item.quantity
|
||||
|
||||
# 应用折扣
|
||||
if subtotal > 1000:
|
||||
discount = subtotal * 0.1
|
||||
elif subtotal > 500:
|
||||
discount = subtotal * 0.05
|
||||
else:
|
||||
discount = 0
|
||||
|
||||
# 计算税费
|
||||
tax = (subtotal - discount) * 0.08
|
||||
|
||||
# 保存订单
|
||||
order.subtotal = subtotal
|
||||
order.discount = discount
|
||||
order.tax = tax
|
||||
order.total = subtotal - discount + tax
|
||||
db.save(order)
|
||||
|
||||
# 发送通知
|
||||
send_email(order.user.email, "Order confirmed")
|
||||
send_sms(order.user.phone, "Order confirmed")
|
||||
|
||||
return order
|
||||
|
||||
# 嵌套过深
|
||||
def check_permission(user, resource, action):
|
||||
if user:
|
||||
if user.is_active:
|
||||
if resource:
|
||||
if resource.owner_id == user.id:
|
||||
if action in ['read', 'write']:
|
||||
return True
|
||||
return False
|
||||
|
||||
# 重复代码
|
||||
def get_admin_users():
|
||||
users = db.query("SELECT * FROM users WHERE role = 'admin'")
|
||||
result = []
|
||||
for u in users:
|
||||
result.append({"id": u.id, "name": u.name, "email": u.email})
|
||||
return result
|
||||
|
||||
def get_normal_users():
|
||||
users = db.query("SELECT * FROM users WHERE role = 'user'")
|
||||
result = []
|
||||
for u in users:
|
||||
result.append({"id": u.id, "name": u.name, "email": u.email})
|
||||
return result`,
|
||||
javascript: `// 命名不规范
|
||||
function fn(a, b, c) {
|
||||
let x = a + b;
|
||||
let y = x * c;
|
||||
return y;
|
||||
}
|
||||
|
||||
// 缺少错误处理
|
||||
async function fetchData(url) {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
// 嵌套过深
|
||||
function processData(data) {
|
||||
if (data) {
|
||||
if (data.items) {
|
||||
if (data.items.length > 0) {
|
||||
for (let item of data.items) {
|
||||
if (item.active) {
|
||||
if (item.value > 0) {
|
||||
console.log(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重复代码
|
||||
function validateEmail(email) {
|
||||
if (!email) return false;
|
||||
if (email.length < 5) return false;
|
||||
if (!email.includes('@')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateUsername(username) {
|
||||
if (!username) return false;
|
||||
if (username.length < 3) return false;
|
||||
if (username.length > 20) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 魔法数字
|
||||
function calculatePrice(quantity, type) {
|
||||
if (type === 1) {
|
||||
return quantity * 9.99;
|
||||
} else if (type === 2) {
|
||||
return quantity * 19.99;
|
||||
} else {
|
||||
return quantity * 29.99;
|
||||
}
|
||||
}`,
|
||||
java: `public class CodeQualityIssues {
|
||||
|
||||
// 魔法数字
|
||||
public double calculate(int qty) {
|
||||
if (qty > 100) {
|
||||
return qty * 0.85;
|
||||
} else if (qty > 50) {
|
||||
return qty * 0.9;
|
||||
}
|
||||
return qty * 1.0;
|
||||
}
|
||||
|
||||
// 函数过长 + 职责不单一
|
||||
public void processOrder(Order order) {
|
||||
// 验证
|
||||
if (order == null) return;
|
||||
if (order.getItems() == null) return;
|
||||
if (order.getItems().isEmpty()) return;
|
||||
|
||||
// 计算
|
||||
double total = 0;
|
||||
for (OrderItem item : order.getItems()) {
|
||||
total += item.getPrice() * item.getQuantity();
|
||||
}
|
||||
|
||||
// 折扣
|
||||
double discount = 0;
|
||||
if (total > 1000) discount = total * 0.1;
|
||||
else if (total > 500) discount = total * 0.05;
|
||||
|
||||
// 保存
|
||||
order.setTotal(total - discount);
|
||||
orderRepository.save(order);
|
||||
|
||||
// 通知
|
||||
emailService.send(order.getUser().getEmail(), "Confirmed");
|
||||
smsService.send(order.getUser().getPhone(), "Confirmed");
|
||||
}
|
||||
|
||||
// 嵌套过深
|
||||
public boolean checkAccess(User user, Resource res) {
|
||||
if (user != null) {
|
||||
if (user.isActive()) {
|
||||
if (res != null) {
|
||||
if (res.getOwnerId().equals(user.getId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 命名不规范
|
||||
public int fn(int a, int b) {
|
||||
int x = a + b;
|
||||
return x;
|
||||
}
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取模板对应的测试代码
|
||||
*/
|
||||
export function getTestCodeForTemplate(templateName: string, language: string): string {
|
||||
const templateCodes = TEMPLATE_TEST_CODES[templateName];
|
||||
if (templateCodes && templateCodes[language]) {
|
||||
return templateCodes[language];
|
||||
}
|
||||
return TEST_CODE_SAMPLES[language] || TEST_CODE_SAMPLES.python;
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ export interface PromptTestRequest {
|
|||
content: string;
|
||||
language: string;
|
||||
code: string;
|
||||
output_language?: string;
|
||||
}
|
||||
|
||||
export interface PromptTestResponse {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,8 @@ export interface CreateAuditTaskForm {
|
|||
task_type: 'repository' | 'instant';
|
||||
branch_name?: string;
|
||||
exclude_patterns: string[];
|
||||
rule_set_id?: string;
|
||||
prompt_template_id?: string;
|
||||
scan_config: {
|
||||
include_tests?: boolean;
|
||||
include_docs?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue