|
|
const express = require('express');
|
|
|
const multer = require('multer');
|
|
|
const cors = require('cors');
|
|
|
const { exec } = require('child_process');
|
|
|
const fs = require('fs');
|
|
|
const path = require('path');
|
|
|
const os = require('os');
|
|
|
|
|
|
// 创建Express应用
|
|
|
const app = express();
|
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
|
|
// 配置CORS
|
|
|
app.use(cors({
|
|
|
origin: '*',
|
|
|
methods: ['GET', 'POST', 'OPTIONS'],
|
|
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
|
preflightContinue: false,
|
|
|
optionsSuccessStatus: 204
|
|
|
}));
|
|
|
|
|
|
// 使用正则表达式处理所有OPTIONS请求
|
|
|
app.options(/.*/, (req, res) => {
|
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
|
res.sendStatus(204);
|
|
|
});
|
|
|
|
|
|
// 解析JSON请求体
|
|
|
app.use(express.json());
|
|
|
|
|
|
// 请求日志中间件
|
|
|
app.use((req, res, next) => {
|
|
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
|
|
next();
|
|
|
});
|
|
|
|
|
|
// 配置Multer文件上传
|
|
|
const storage = multer.diskStorage({
|
|
|
destination: (req, file, cb) => {
|
|
|
cb(null, os.tmpdir());
|
|
|
},
|
|
|
filename: (req, file, cb) => {
|
|
|
cb(null, `${Date.now()}-${file.originalname}`);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const upload = multer({
|
|
|
storage: storage,
|
|
|
limits: { fileSize: 10 * 1024 * 1024 }
|
|
|
});
|
|
|
|
|
|
// 工具配置
|
|
|
const TOOL_CONFIG = {
|
|
|
bandit: {
|
|
|
command: 'bandit',
|
|
|
args: (filePath) => `-r -f json -o - ${filePath}`,
|
|
|
parser: (stdout) => JSON.parse(stdout)
|
|
|
},
|
|
|
flake8: {
|
|
|
command: 'flake8',
|
|
|
args: (filePath) => `${filePath}`,
|
|
|
parser: (stdout) => {
|
|
|
console.log('Flake8原始输出:', stdout);
|
|
|
console.log('Flake8输出长度:', stdout.length);
|
|
|
console.log('Flake8输出是否为空:', !stdout || stdout.trim() === '');
|
|
|
|
|
|
// 处理空输出情况
|
|
|
if (!stdout || stdout.trim() === '') {
|
|
|
console.log('Flake8输出为空,返回空数组');
|
|
|
return []; // 返回空数组
|
|
|
}
|
|
|
|
|
|
// 检查是否已经是JSON格式
|
|
|
if (stdout.trim().startsWith('[') || stdout.trim().startsWith('{')) {
|
|
|
try {
|
|
|
return JSON.parse(stdout);
|
|
|
} catch (parseError) {
|
|
|
console.log('Flake8 JSON解析失败:', parseError.message);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果不是JSON格式,尝试解析为文本格式
|
|
|
console.log('Flake8输出不是JSON格式,使用文本解析');
|
|
|
const lines = stdout.trim().split('\n').filter(line => line.trim());
|
|
|
const issues = [];
|
|
|
|
|
|
for (const line of lines) {
|
|
|
// 跳过注释行和空行
|
|
|
if (line.startsWith('#') || !line.trim()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
// 尝试解析flake8的标准输出格式
|
|
|
// 支持绝对路径和相对路径,以及不同的分隔符
|
|
|
const match = line.match(/^(.+?):(\d+):(\d+):\s*([A-Z]\d+)\s*(.+)$/);
|
|
|
if (match) {
|
|
|
issues.push({
|
|
|
file: match[1],
|
|
|
line: parseInt(match[2]),
|
|
|
column: parseInt(match[3]),
|
|
|
code: match[4],
|
|
|
message: match[5]
|
|
|
});
|
|
|
} else if (line.includes(':')) {
|
|
|
// 如果不匹配标准格式,但包含冒号,尝试更宽松的解析
|
|
|
const parts = line.split(':');
|
|
|
if (parts.length >= 4) {
|
|
|
const file = parts[0];
|
|
|
const lineNum = parseInt(parts[1]);
|
|
|
const colNum = parseInt(parts[2]);
|
|
|
const codeAndMessage = parts.slice(3).join(':').trim();
|
|
|
|
|
|
// 尝试提取错误代码
|
|
|
const codeMatch = codeAndMessage.match(/^([A-Z]\d+)\s*(.+)$/);
|
|
|
if (codeMatch) {
|
|
|
issues.push({
|
|
|
file: file,
|
|
|
line: lineNum,
|
|
|
column: colNum,
|
|
|
code: codeMatch[1],
|
|
|
message: codeMatch[2]
|
|
|
});
|
|
|
} else {
|
|
|
issues.push({
|
|
|
file: file,
|
|
|
line: lineNum,
|
|
|
column: colNum,
|
|
|
code: 'UNKNOWN',
|
|
|
message: codeAndMessage
|
|
|
});
|
|
|
}
|
|
|
} else {
|
|
|
console.log('无法解析的Flake8输出行:', line);
|
|
|
}
|
|
|
} else {
|
|
|
// 如果行不包含任何错误信息,可能是其他类型的输出
|
|
|
console.log('跳过Flake8输出行:', line);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.log('Flake8文本解析结果:', issues);
|
|
|
return issues;
|
|
|
}
|
|
|
},
|
|
|
pylint: {
|
|
|
command: 'pylint',
|
|
|
args: (filePath) => `--output-format=json ${filePath}`,
|
|
|
parser: (stdout) => {
|
|
|
// 处理空输出情况
|
|
|
if (!stdout || stdout.trim() === '') {
|
|
|
return []; // 返回空数组
|
|
|
}
|
|
|
return JSON.parse(stdout);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 确保out目录存在(放在项目根目录,避免触发开发服务器重载)
|
|
|
function ensureOutDir() {
|
|
|
// 将out目录放在项目根目录而不是view目录内
|
|
|
const outDir = path.join(__dirname, '..', 'out');
|
|
|
console.log(`检查输出目录: ${outDir}`);
|
|
|
|
|
|
if (!fs.existsSync(outDir)) {
|
|
|
console.log(`创建输出目录: ${outDir}`);
|
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
|
console.log(`输出目录创建成功`);
|
|
|
} else {
|
|
|
console.log(`输出目录已存在: ${outDir}`);
|
|
|
}
|
|
|
|
|
|
return outDir;
|
|
|
}
|
|
|
|
|
|
// 运行单个工具
|
|
|
function runTool(tool, filePath) {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
const config = TOOL_CONFIG[tool];
|
|
|
if (!config) {
|
|
|
return reject(new Error(`不支持的工具: ${tool}`));
|
|
|
}
|
|
|
|
|
|
let actualFilePath = filePath;
|
|
|
let actualCommand = `${config.command} ${config.args(filePath)}`;
|
|
|
|
|
|
// 对于Flake8,添加额外的调试和临时文件处理
|
|
|
if (tool === 'flake8') {
|
|
|
console.log(`Flake8原始命令: ${actualCommand}`);
|
|
|
console.log(`文件路径存在性: ${fs.existsSync(filePath)}`);
|
|
|
|
|
|
// 先检查文件内容
|
|
|
try {
|
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
|
console.log(`Flake8检查的文件内容:\n${content}`);
|
|
|
|
|
|
// 创建本地副本进行检查
|
|
|
const localFileName = `temp_check_${Date.now()}.py`;
|
|
|
const localFilePath = path.join(process.cwd(), localFileName);
|
|
|
fs.writeFileSync(localFilePath, content);
|
|
|
console.log(`创建本地文件副本: ${localFilePath}`);
|
|
|
|
|
|
// 修改命令使用本地文件
|
|
|
actualFilePath = localFilePath;
|
|
|
actualCommand = `${config.command} ${config.args(localFilePath)}`;
|
|
|
console.log(`修改后的Flake8命令: ${actualCommand}`);
|
|
|
|
|
|
} catch (e) {
|
|
|
console.error('Flake8文件处理失败:', e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.log(`执行命令: ${actualCommand}`);
|
|
|
|
|
|
exec(actualCommand, { shell: true }, (error, stdout, stderr) => {
|
|
|
console.log(`${tool}执行结果 - 退出码: ${error ? error.code : 0}`);
|
|
|
|
|
|
let result = {
|
|
|
stdout: stdout ? stdout.trim() : '',
|
|
|
stderr: stderr ? stderr.trim() : ''
|
|
|
};
|
|
|
|
|
|
console.log(`${tool} stdout长度: ${result.stdout.length}`);
|
|
|
console.log(`${tool} stderr长度: ${result.stderr.length}`);
|
|
|
|
|
|
if (tool === 'flake8') {
|
|
|
console.log(`Flake8 stdout内容: "${result.stdout}"`);
|
|
|
console.log(`Flake8 stderr内容: "${result.stderr}"`);
|
|
|
|
|
|
// 清理本地临时文件
|
|
|
if (actualFilePath !== filePath) {
|
|
|
setTimeout(() => {
|
|
|
try {
|
|
|
if (fs.existsSync(actualFilePath)) {
|
|
|
fs.unlinkSync(actualFilePath);
|
|
|
console.log(`清理Flake8本地临时文件: ${actualFilePath}`);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('清理Flake8本地临时文件失败:', e);
|
|
|
}
|
|
|
}, 1000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
if (result.stdout) {
|
|
|
result.parsed = config.parser(result.stdout);
|
|
|
}
|
|
|
} catch (parseError) {
|
|
|
console.warn(`解析${tool}输出失败:`, parseError);
|
|
|
// 解析失败时返回原始输出
|
|
|
result.parsed = {
|
|
|
error: `解析失败: ${parseError.message}`,
|
|
|
rawOutput: result.stdout
|
|
|
};
|
|
|
}
|
|
|
|
|
|
resolve(result);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 生成报告
|
|
|
function generateReport(results, filename) {
|
|
|
let report = `# 代码质量检查报告\n\n`;
|
|
|
report += `**文件:** ${filename}\n`;
|
|
|
report += `**检查时间:** ${new Date().toLocaleString()}\n\n`;
|
|
|
|
|
|
results.forEach(toolResult => {
|
|
|
report += `## ${toolResult.tool} 检查结果\n`;
|
|
|
|
|
|
if (toolResult.stdout) {
|
|
|
report += '### 输出:\n```\n';
|
|
|
report += toolResult.stdout;
|
|
|
report += '\n```\n\n';
|
|
|
}
|
|
|
|
|
|
if (toolResult.stderr) {
|
|
|
report += '### 错误:\n```\n';
|
|
|
report += toolResult.stderr;
|
|
|
report += '\n```\n\n';
|
|
|
}
|
|
|
|
|
|
if (toolResult.parsed) {
|
|
|
report += '### 解析结果:\n```json\n';
|
|
|
report += JSON.stringify(toolResult.parsed, null, 2);
|
|
|
report += '\n```\n\n';
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return report;
|
|
|
}
|
|
|
|
|
|
// 健康检查端点
|
|
|
app.get('/health', (req, res) => {
|
|
|
res.status(200).json({
|
|
|
status: 'ok',
|
|
|
timestamp: new Date().toISOString()
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// 检查端点
|
|
|
app.post('/check', upload.single('file'), async (req, res) => {
|
|
|
try {
|
|
|
if (!req.file) {
|
|
|
return res.status(400).json({ error: '未上传文件' });
|
|
|
}
|
|
|
|
|
|
const tools = req.body.tools ? req.body.tools.split(',') : ['bandit', 'flake8', 'pylint'];
|
|
|
const filePath = req.file.path;
|
|
|
const results = [];
|
|
|
|
|
|
// 调试:检查临时文件内容
|
|
|
console.log(`临时文件路径: ${filePath}`);
|
|
|
try {
|
|
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
|
console.log(`临时文件内容 (${fileContent.length} 字符):\n${fileContent}`);
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('读取临时文件失败:', error);
|
|
|
}
|
|
|
|
|
|
for (const tool of tools) {
|
|
|
console.log(`开始执行工具: ${tool}`);
|
|
|
const result = await runTool(tool, filePath);
|
|
|
console.log(`${tool} 执行结果 - stdout长度: ${result.stdout.length}, stderr长度: ${result.stderr.length}`);
|
|
|
results.push({
|
|
|
tool: tool,
|
|
|
stdout: result.stdout,
|
|
|
stderr: result.stderr,
|
|
|
parsed: result.parsed
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const outDir = ensureOutDir();
|
|
|
const reportContent = generateReport(results, req.file.originalname);
|
|
|
const reportFilename = `report_${Date.now()}.md`;
|
|
|
const reportPath = path.join(outDir, reportFilename);
|
|
|
|
|
|
console.log(`正在生成报告文件: ${reportPath}`);
|
|
|
console.log(`报告内容长度: ${reportContent.length} 字符`);
|
|
|
|
|
|
fs.writeFileSync(reportPath, reportContent);
|
|
|
console.log(`报告文件生成成功: ${reportPath}`);
|
|
|
|
|
|
res.json({
|
|
|
success: true,
|
|
|
results: results,
|
|
|
reportUrl: `/reports/${reportFilename}`
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('检查错误:', error);
|
|
|
res.status(500).json({
|
|
|
success: false,
|
|
|
error: error.message
|
|
|
});
|
|
|
} finally {
|
|
|
if (req.file && fs.existsSync(req.file.path)) {
|
|
|
fs.unlink(req.file.path, (err) => {
|
|
|
if (err) console.error('删除临时文件失败:', err);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 报告下载端点
|
|
|
app.get('/reports/:filename', (req, res) => {
|
|
|
const filename = req.params.filename;
|
|
|
// 从项目根目录的out文件夹读取文件
|
|
|
const filePath = path.join(__dirname, '..', 'out', filename);
|
|
|
|
|
|
if (fs.existsSync(filePath)) {
|
|
|
res.setHeader('Content-Type', 'text/markdown');
|
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
|
|
|
|
const fileStream = fs.createReadStream(filePath);
|
|
|
fileStream.pipe(res);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
try {
|
|
|
fs.unlinkSync(filePath);
|
|
|
console.log(`已删除报告文件: ${filename}`);
|
|
|
} catch (err) {
|
|
|
console.error(`删除报告文件失败: ${err.message}`);
|
|
|
}
|
|
|
}, 5000);
|
|
|
} else {
|
|
|
res.status(404).json({
|
|
|
error: '报告未找到',
|
|
|
filename: filename
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 启动服务器
|
|
|
app.listen(PORT, () => {
|
|
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
|
|
console.log(`上传文件临时目录: ${os.tmpdir()}`);
|
|
|
console.log(`报告输出目录: ${path.join(__dirname, '..', 'out')}`);
|
|
|
});
|
|
|
|
|
|
module.exports = app; |