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

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),

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)

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):

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}"""

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分析后

View File

@ -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";
@ -87,6 +90,12 @@ export default function CreateTaskDialog({
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);
const zipState = useZipFile(selectedProject, projects);
@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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 },

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>

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";
@ -28,6 +31,10 @@ export default function Dashboard() {
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>

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);
@ -69,8 +70,25 @@ export default function InstantAnalysis() {
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 () => {
setLoadingHistory(true);
@ -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()}

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,
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);
// 测试表单
const [testForm, setTestForm] = useState({
language: 'python',
code: TEST_CODE_SAMPLES.python,
});
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 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>
<DialogFooter>
<Button variant="outline" onClick={() => setShowTestDialog(false)}></Button>
{/* 查看详情对话框 */}
<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>

View File

@ -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;
}

View File

@ -52,6 +52,7 @@ export interface PromptTestRequest {
content: string;
language: string;
code: string;
output_language?: string;
}
export interface PromptTestResponse {

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;