216 lines
6.9 KiB
TypeScript
216 lines
6.9 KiB
TypeScript
import { unzip } from "fflate";
|
|
import { CodeAnalysisEngine } from "@/features/analysis/services";
|
|
import { api } from "@/shared/config/database";
|
|
|
|
const TEXT_EXTENSIONS = [
|
|
".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".go", ".rs", ".cpp", ".c", ".h",
|
|
".cs", ".php", ".rb", ".kt", ".swift", ".sql", ".sh", ".json", ".yml", ".yaml", ".md"
|
|
];
|
|
|
|
const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200KB
|
|
const MAX_ANALYZE_FILES = 50;
|
|
|
|
function isTextFile(path: string): boolean {
|
|
return TEXT_EXTENSIONS.some(ext => path.toLowerCase().endsWith(ext));
|
|
}
|
|
|
|
function shouldExclude(path: string, excludePatterns: string[]): boolean {
|
|
return excludePatterns.some(pattern => {
|
|
if (pattern.includes('*')) {
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
return regex.test(path);
|
|
}
|
|
return path.includes(pattern);
|
|
});
|
|
}
|
|
|
|
function getLanguageFromPath(path: string): string {
|
|
const extension = path.split('.').pop()?.toLowerCase() || '';
|
|
const languageMap: Record<string, string> = {
|
|
'js': 'javascript',
|
|
'jsx': 'javascript',
|
|
'ts': 'typescript',
|
|
'tsx': 'typescript',
|
|
'py': 'python',
|
|
'java': 'java',
|
|
'go': 'go',
|
|
'rs': 'rust',
|
|
'cpp': 'cpp',
|
|
'c': 'cpp',
|
|
'cs': 'csharp',
|
|
'php': 'php',
|
|
'rb': 'ruby',
|
|
'kt': 'kotlin',
|
|
'swift': 'swift'
|
|
};
|
|
|
|
return languageMap[extension] || 'text';
|
|
}
|
|
|
|
export async function scanZipFile(params: {
|
|
projectId: string;
|
|
zipFile: File;
|
|
excludePatterns?: string[];
|
|
createdBy?: string;
|
|
}): Promise<string> {
|
|
const { projectId, zipFile, excludePatterns = [], createdBy } = params;
|
|
|
|
// 创建审计任务
|
|
const task = await api.createAuditTask({
|
|
project_id: projectId,
|
|
task_type: "repository",
|
|
branch_name: "uploaded",
|
|
exclude_patterns: excludePatterns,
|
|
scan_config: { source: "zip_upload" },
|
|
created_by: createdBy
|
|
} as any);
|
|
|
|
const taskId = (task as any).id;
|
|
|
|
try {
|
|
// 更新任务状态为运行中
|
|
await api.updateAuditTask(taskId, {
|
|
status: "running",
|
|
started_at: new Date().toISOString()
|
|
} as any);
|
|
|
|
// 读取ZIP文件
|
|
const arrayBuffer = await zipFile.arrayBuffer();
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
unzip(uint8Array, async (err, unzipped) => {
|
|
if (err) {
|
|
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
|
reject(new Error(`ZIP文件解压失败: ${err.message}`));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 筛选需要分析的文件
|
|
const filesToAnalyze: Array<{ path: string; content: string }> = [];
|
|
|
|
for (const [path, data] of Object.entries(unzipped)) {
|
|
// 跳过目录
|
|
if (path.endsWith('/')) continue;
|
|
|
|
// 检查文件类型和排除模式
|
|
if (!isTextFile(path) || shouldExclude(path, excludePatterns)) continue;
|
|
|
|
// 检查文件大小
|
|
if (data.length > MAX_FILE_SIZE_BYTES) continue;
|
|
|
|
try {
|
|
const content = new TextDecoder('utf-8').decode(data);
|
|
filesToAnalyze.push({ path, content });
|
|
} catch (decodeError) {
|
|
// 跳过无法解码的文件
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// 限制分析文件数量
|
|
const limitedFiles = filesToAnalyze
|
|
.sort((a, b) => a.path.length - b.path.length) // 优先分析路径较短的文件
|
|
.slice(0, MAX_ANALYZE_FILES);
|
|
|
|
let totalFiles = limitedFiles.length;
|
|
let scannedFiles = 0;
|
|
let totalLines = 0;
|
|
let totalIssues = 0;
|
|
let qualityScores: number[] = [];
|
|
|
|
// 分析每个文件
|
|
for (const file of limitedFiles) {
|
|
try {
|
|
const language = getLanguageFromPath(file.path);
|
|
const lines = file.content.split(/\r?\n/).length;
|
|
totalLines += lines;
|
|
|
|
// 使用AI分析代码
|
|
const analysis = await CodeAnalysisEngine.analyzeCode(file.content, language);
|
|
qualityScores.push(analysis.quality_score);
|
|
|
|
// 保存发现的问题
|
|
for (const issue of analysis.issues) {
|
|
await api.createAuditIssue({
|
|
task_id: taskId,
|
|
file_path: file.path,
|
|
line_number: issue.line || null,
|
|
column_number: issue.column || null,
|
|
issue_type: issue.type || "maintainability",
|
|
severity: issue.severity || "low",
|
|
title: issue.title || "Issue",
|
|
description: issue.description || null,
|
|
suggestion: issue.suggestion || null,
|
|
code_snippet: issue.code_snippet || null,
|
|
ai_explanation: issue.ai_explanation || null,
|
|
status: "open"
|
|
} as any);
|
|
|
|
totalIssues++;
|
|
}
|
|
|
|
scannedFiles++;
|
|
|
|
// 每分析10个文件更新一次进度
|
|
if (scannedFiles % 10 === 0) {
|
|
await api.updateAuditTask(taskId, {
|
|
total_files: totalFiles,
|
|
scanned_files: scannedFiles,
|
|
total_lines: totalLines,
|
|
issues_count: totalIssues
|
|
} as any);
|
|
}
|
|
|
|
// 添加延迟避免API限制
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
} catch (analysisError) {
|
|
console.error(`分析文件 ${file.path} 失败:`, analysisError);
|
|
// 继续分析其他文件
|
|
}
|
|
}
|
|
|
|
// 计算平均质量分
|
|
const avgQualityScore = qualityScores.length > 0
|
|
? qualityScores.reduce((sum, score) => sum + score, 0) / qualityScores.length
|
|
: 0;
|
|
|
|
// 更新任务完成状态
|
|
await api.updateAuditTask(taskId, {
|
|
status: "completed",
|
|
total_files: totalFiles,
|
|
scanned_files: scannedFiles,
|
|
total_lines: totalLines,
|
|
issues_count: totalIssues,
|
|
quality_score: avgQualityScore,
|
|
completed_at: new Date().toISOString()
|
|
} as any);
|
|
|
|
resolve(taskId);
|
|
} catch (processingError) {
|
|
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
|
reject(processingError);
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function validateZipFile(file: File): { valid: boolean; error?: string } {
|
|
// 检查文件类型
|
|
if (!file.type.includes('zip') && !file.name.toLowerCase().endsWith('.zip')) {
|
|
return { valid: false, error: '请上传ZIP格式的文件' };
|
|
}
|
|
|
|
// 检查文件大小 (限制为100MB)
|
|
const maxSize = 100 * 1024 * 1024;
|
|
if (file.size > maxSize) {
|
|
return { valid: false, error: '文件大小不能超过100MB' };
|
|
}
|
|
|
|
return { valid: true };
|
|
} |