You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
analysiscode/src/view/backend.js

405 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;