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:
lintsinghua
2025-12-09 23:03:08 +08:00
parent 357b9cc0a7
commit 4d71ed546a
16 changed files with 1525 additions and 712 deletions
+29 -2
View File
@@ -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),
+41 -8
View File
@@ -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)
+1
View File
@@ -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):
+23 -5
View File
@@ -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}"""
+14 -2
View File
@@ -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 },
+298 -380
View File
@@ -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>
+55 -1
View File
@@ -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>
+39 -3
View File
@@ -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()}
+417 -306
View File
@@ -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;
}
+1
View File
@@ -52,6 +52,7 @@ export interface PromptTestRequest {
content: string;
language: string;
code: string;
output_language?: string;
}
export interface PromptTestResponse {
+2
View File
@@ -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;