快捷代码检查
-快速开始
Agent 状态
-系统功能
+集成 pylint、flake8、bandit 三大工具,全面检测代码质量和安全问题
+支持上传 setup.cfg、.pylintrc、.flake8 等配置文件,灵活定制检查规则
+内置代码编辑器,支持文件浏览、编辑、保存,快速修复问题
+检查结果精确到文件、行、列,一键定位到问题代码位置
+记录每次检查结果,追踪代码质量变化趋势
+diff --git a/src/backend.js b/src/backend.js index 3f47e85..5aea270 100644 --- a/src/backend.js +++ b/src/backend.js @@ -57,6 +57,9 @@ const upload = multer({ // 装载模块化路由 const rulesRouter = require(path.join(__dirname, 'server', 'routes', 'rules.js')); +// 装载AI分析服务 +const aiAnalyzer = require(path.join(__dirname, 'server', 'services', 'aiAnalyzer.js')); + // 项目存储目录 const PROJECTS_DIR = path.join(__dirname, 'projects_data'); if (!fs.existsSync(PROJECTS_DIR)) { @@ -93,11 +96,72 @@ function saveProjects() { // 初始化 loadProjects(); +// 获取规则集文件路径 +function getRuleSetPath(ruleSetName, tool) { + if (!ruleSetName || !fs.existsSync(RULE_DIR)) { + console.log(`[规则集] ${tool}: 规则集名称为空或规则目录不存在`); + return null; + } + + const ruleSetFile = path.join(RULE_DIR, ruleSetName); + if (!fs.existsSync(ruleSetFile)) { + console.warn(`[规则集] ${tool}: 规则集文件不存在: ${ruleSetFile}`); + return null; + } + + // 根据工具类型和规则集文件名判断是否适用 + const fileName = ruleSetName.toLowerCase(); + const fileExt = path.extname(fileName).toLowerCase(); + + console.log(`[规则集] ${tool}: 检查规则集文件 "${ruleSetName}" (扩展名: ${fileExt})`); + + // pylint 支持 .pylintrc, pylintrc, setup.cfg + if (tool === 'pylint') { + if (fileName.includes('pylint') || fileName.endsWith('.pylintrc') || fileName === 'pylintrc' || + fileName === 'setup.cfg' || fileExt === '.cfg' || fileExt === '.ini') { + console.log(`[规则集] ${tool}: 使用规则集文件: ${ruleSetFile}`); + return ruleSetFile; + } + } + + // flake8 支持 .flake8, flake8.cfg, setup.cfg, tox.ini, 以及所有 .cfg 和 .ini 文件 + if (tool === 'flake8') { + if (fileName.includes('flake8') || fileName === '.flake8' || fileName === 'flake8.cfg' || + fileName === 'setup.cfg' || fileName === 'tox.ini' || fileExt === '.cfg' || fileExt === '.ini') { + console.log(`[规则集] ${tool}: 使用规则集文件: ${ruleSetFile}`); + return ruleSetFile; + } + } + + // bandit 支持 .bandit, bandit.ini, setup.cfg, 以及通用的 .ini, .cfg 文件 + if (tool === 'bandit') { + if (fileName.includes('bandit') || fileExt === '.ini' || + fileExt === '.cfg' || fileName === 'setup.cfg') { + console.log(`[规则集] ${tool}: 使用规则集文件: ${ruleSetFile}`); + return ruleSetFile; + } + } + + console.log(`[规则集] ${tool}: 规则集文件 "${ruleSetName}" 不适用于此工具`); + return null; +} + // 工具配置 const TOOL_CONFIG = { bandit: { command: 'python', - args: (filePath) => `-m bandit -r -f json ${filePath}`, + args: (filePath, ruleSetName = null, projectPath = null) => { + let cmd = `-m bandit -r -f json ${filePath}`; + const rulePath = getRuleSetPath(ruleSetName, 'bandit'); + if (rulePath) { + const absolutePath = path.resolve(rulePath); + cmd += ` --configfile "${absolutePath}"`; + console.log(`[bandit] 使用规则集配置文件: ${absolutePath}`); + } else { + console.log(`[bandit] 未使用规则集,使用默认配置`); + } + return cmd; + }, parseResult: (stdout) => { try { if (!stdout || stdout.trim() === '') return []; @@ -120,7 +184,18 @@ const TOOL_CONFIG = { }, flake8: { command: 'python', - args: (filePath) => `-m flake8 ${filePath}`, + args: (filePath, ruleSetName = null, projectPath = null) => { + // flake8 不支持 --config 参数,需要通过环境变量或复制文件到项目目录 + // 这里不添加命令行参数,环境变量会在 runTool 函数中设置 + let cmd = `-m flake8 ${filePath}`; + const rulePath = getRuleSetPath(ruleSetName, 'flake8'); + if (rulePath) { + console.log(`[flake8] 将使用环境变量 FLAKE8_CONFIG 指定配置文件`); + } else { + console.log(`[flake8] 未使用规则集,使用默认配置`); + } + return cmd; + }, parseResult: (stdout) => { try { if (!stdout || stdout.trim() === '') return []; @@ -152,7 +227,19 @@ const TOOL_CONFIG = { }, pylint: { command: 'python', - args: (filePath) => `-m pylint --output-format=json ${filePath}`, + args: (filePath, ruleSetName = null, projectPath = null) => { + let cmd = `-m pylint --output-format=json ${filePath}`; + const rulePath = getRuleSetPath(ruleSetName, 'pylint'); + if (rulePath) { + // pylint 使用 --rcfile 参数指定配置文件 + const absolutePath = path.resolve(rulePath); + cmd += ` --rcfile="${absolutePath}"`; + console.log(`[pylint] 使用规则集配置文件: ${absolutePath}`); + } else { + console.log(`[pylint] 未使用规则集,使用默认配置`); + } + return cmd; + }, parseResult: (stdout) => { try { if (!stdout || stdout.trim() === '') return []; @@ -174,25 +261,67 @@ const TOOL_CONFIG = { } }; +// 规则集目录 +const RULE_DIR = path.join(__dirname, 'rule'); + // 运行单个工具检查 -async function runTool(tool, filePath) { +async function runTool(tool, filePath, ruleSetName = null, projectPath = null) { return new Promise((resolve) => { const config = TOOL_CONFIG[tool]; if (!config) { return resolve({ status: 'error', issues: [] }); } - const command = `${config.command} ${config.args(filePath)}`; - console.log(`执行命令: ${command}`); + // 构建命令参数,支持规则集 + console.log(`[${tool}] 开始构建命令,规则集: ${ruleSetName || '无'}`); + let args = config.args(filePath, ruleSetName, projectPath); + const command = `${config.command} ${args}`; + console.log(`[${tool}] 执行命令: ${command}`); + if (ruleSetName) { + console.log(`[${tool}] 使用规则集: ${ruleSetName}`); + } else { + console.log(`[${tool}] 未使用规则集,使用默认配置`); + } - exec(command, { shell: true, timeout: 60000 }, (error, stdout, stderr) => { + // 准备执行选项 + const execOptions = { + shell: true, + timeout: 60000, + cwd: projectPath || path.dirname(filePath), + env: { ...process.env } + }; + + // 为 flake8 设置环境变量(如果使用规则集) + // 注意:flake8 可能不支持 FLAKE8_CONFIG 环境变量,如果不行,需要将文件复制到项目目录 + if (tool === 'flake8' && ruleSetName) { + const rulePath = getRuleSetPath(ruleSetName, 'flake8'); + if (rulePath) { + const absolutePath = path.resolve(rulePath); + // 尝试使用环境变量(某些版本的 flake8 可能不支持) + execOptions.env.FLAKE8_CONFIG = absolutePath; + console.log(`[${tool}] 设置环境变量 FLAKE8_CONFIG=${absolutePath}`); + + // 如果环境变量不生效,可以考虑将配置文件复制到项目目录 + // 但为了简化,这里先尝试环境变量方式 + } + } + + exec(command, execOptions, (error, stdout, stderr) => { console.log(`${tool} 执行完成`); + if (stderr && stderr.trim()) { + console.log(`${tool} stderr:`, stderr); + } try { const issues = config.parseResult(stdout || ''); + // 为每个问题添加工具标识 + const issuesWithTool = issues.map(issue => ({ + ...issue, + tool: tool + })); resolve({ status: 'completed', - issues: issues, + issues: issuesWithTool, raw_output: stdout }); } catch (e) { @@ -208,14 +337,13 @@ async function runTool(tool, filePath) { } // 运行代码检查(单文件) -async function runCodeCheck(filePath) { - const tools = ['pylint', 'flake8', 'bandit']; +async function runCodeCheck(filePath, ruleSetName = null, projectPath = null, tools = ['pylint', 'flake8', 'bandit']) { const toolsStatus = {}; const allIssues = []; for (const tool of tools) { try { - const result = await runTool(tool, filePath); + const result = await runTool(tool, filePath, ruleSetName, projectPath); toolsStatus[tool] = result.status; allIssues.push(...result.issues); } catch (error) { @@ -223,6 +351,14 @@ async function runCodeCheck(filePath) { toolsStatus[tool] = 'error'; } } + + // 为未选择的工具设置状态 + const allTools = ['pylint', 'flake8', 'bandit']; + allTools.forEach(tool => { + if (!tools.includes(tool)) { + toolsStatus[tool] = 'skipped'; + } + }); // 统计问题 const errorCount = allIssues.filter(i => i.type === 'error').length; @@ -240,16 +376,24 @@ async function runCodeCheck(filePath) { } // 运行代码检查(多文件,生成相对路径) -async function runCodeCheckOnFiles(filePaths, baseDir) { +async function runCodeCheckOnFiles(filePaths, baseDir, ruleSetName = null, tools = ['pylint', 'flake8', 'bandit']) { const aggregateIssues = []; - const toolsStatusAggregate = { pylint: 'completed', flake8: 'completed', bandit: 'completed' }; + const toolsStatusAggregate = { pylint: 'skipped', flake8: 'skipped', bandit: 'skipped' }; + + // 初始化选择的工具状态 + tools.forEach(tool => { + toolsStatusAggregate[tool] = 'completed'; + }); for (const filePath of filePaths) { - const result = await runCodeCheck(filePath); + const result = await runCodeCheck(filePath, ruleSetName, baseDir, tools); // 汇总工具状态(若任一文件失败则标记) for (const k of Object.keys(result.tools_status)) { - if (result.tools_status[k] !== 'completed') { + if (result.tools_status[k] === 'error' || result.tools_status[k] === 'failed') { toolsStatusAggregate[k] = result.tools_status[k]; + } else if (result.tools_status[k] === 'completed' && toolsStatusAggregate[k] === 'completed') { + // 保持 completed 状态 + toolsStatusAggregate[k] = 'completed'; } } const rel = baseDir ? path.relative(baseDir, filePath).replace(/\\/g, '/') : path.basename(filePath); @@ -541,11 +685,19 @@ app.delete('/api/projects/:id', (req, res) => { // 运行项目检查 app.post('/api/projects/:id/check', async (req, res) => { + console.log('[后端] 收到检查请求'); + console.log('[后端] 请求参数:', req.params); + console.log('[后端] 请求体:', JSON.stringify(req.body, null, 2)); + try { const projectId = parseInt(req.params.id); + console.log('[后端] 项目ID:', projectId); + const project = projects.find(p => p.id === projectId); + console.log('[后端] 找到项目:', project ? project.name : '未找到'); if (!project) { + console.error('[后端] 项目不存在,ID:', projectId); return res.status(404).json({ success: false, error: '项目不存在' @@ -583,8 +735,77 @@ app.post('/api/projects/:id/check', async (req, res) => { }); } + // 获取规则集名称和工具列表(如果提供) + const ruleSetName = req.body.rule_set || null; + const selectedTools = req.body.tools || ['pylint', 'flake8', 'bandit']; + + console.log('[后端] 接收到的工具列表:', selectedTools); + console.log('[后端] 接收到的规则集:', ruleSetName); + + // 验证工具列表 + const validTools = ['pylint', 'flake8', 'bandit']; + const tools = selectedTools.filter(tool => validTools.includes(tool)); + console.log('[后端] 验证后的工具列表:', tools); + + if (tools.length === 0) { + console.error('[后端] 没有有效的工具'); + return res.status(400).json({ + success: false, + error: '至少需要选择一个有效的检查工具' + }); + } + + console.log(`[后端] 使用工具: ${tools.join(', ')}`); + if (ruleSetName) { + console.log(`[后端] 使用规则集: ${ruleSetName}`); + } + + console.log('[后端] 开始检查文件,文件数量:', files.length); // 检查所有文件并生成相对路径 - const result = await runCodeCheckOnFiles(files, projectPath); + const rawResult = await runCodeCheckOnFiles(files, projectPath, ruleSetName, tools); + console.log('[后端] 检查完成,原始结果:', { + total_issues: rawResult.total_issues, + error_count: rawResult.error_count, + warning_count: rawResult.warning_count + }); + + // AI分析(去重、风险评估、建议生成) + console.log('[后端] 开始AI分析...'); + const useAiAnalysis = req.body.use_ai_analysis !== false; // 默认启用 + let result; + + if (useAiAnalysis && rawResult.all_issues && rawResult.all_issues.length > 0) { + try { + const analysisResult = await aiAnalyzer.analyzeIssues(rawResult.all_issues, projectPath); + console.log('[后端] AI分析完成:', { + total_issues: analysisResult.total_issues, + analysis_method: analysisResult.analysis_method, + high_risk: analysisResult.high_risk_count || 0, + duplicates_removed: analysisResult.duplicates_removed || 0 + }); + + result = { + ...rawResult, + all_issues: analysisResult.issues, + total_issues: analysisResult.total_issues, + high_risk_count: analysisResult.high_risk_count || 0, + medium_risk_count: analysisResult.medium_risk_count || 0, + low_risk_count: analysisResult.low_risk_count || 0, + duplicates_removed: analysisResult.duplicates_removed || 0, + analysis_method: analysisResult.analysis_method || 'none', + ai_analyzed: true + }; + } catch (error) { + console.error('[后端] AI分析失败,使用原始结果:', error); + result = rawResult; + result.ai_analyzed = false; + result.analysis_error = error.message; + } + } else { + console.log('[后端] 跳过AI分析(未启用或无问题)'); + result = rawResult; + result.ai_analyzed = false; + } // 更新项目的最新检查记录 project.latest_check = { @@ -601,15 +822,18 @@ app.post('/api/projects/:id/check', async (req, res) => { project.updated_at = new Date().toISOString(); saveProjects(); - res.json({ + const responseData = { success: true, data: { check_id: project.latest_check.id, ...result } - }); + }; + console.log('[后端] 返回响应数据'); + res.json(responseData); } catch (error) { - console.error('项目检查失败:', error); + console.error('[后端] 项目检查失败:', error); + console.error('[后端] 错误堆栈:', error.stack); res.status(500).json({ success: false, error: error.message diff --git a/src/frontend/css/style.css b/src/frontend/css/style.css index 1af6225..ed82315 100644 --- a/src/frontend/css/style.css +++ b/src/frontend/css/style.css @@ -850,6 +850,27 @@ body { background: white; } +/* 模态框编辑器样式 */ +#modalCodeEditor { + font-family: 'Courier New', 'Consolas', 'Monaco', monospace; + font-size: 14px; + line-height: 1.6; + tab-size: 4; + white-space: pre; + word-wrap: normal; + overflow-x: auto; +} + +#modalCodeEditor:focus { + background: white; + box-shadow: inset 0 0 0 1px #1e3c72; +} + +#modalCodeEditor::placeholder { + color: #999; + font-style: italic; +} + /* 成功按钮样式 */ .btn-success { background: #28a745; @@ -1267,6 +1288,64 @@ input:checked + .slider:before { padding: 20px; } +/* 编辑器模态框特殊样式 */ +#editorModal .modal-body { + padding: 0; + min-height: 0; +} + +#editorModal .modal-content { + max-width: 1200px; +} + +#editorModal .panel-header { + background: white; + border-bottom: 1px solid #e9ecef; + margin: 0; +} + +#editorModal .file-info { + margin-top: 8px; + padding: 8px 12px; + background: #f8f9fa; + border-top: 1px solid #e9ecef; +} + +#editorModal #modalSidebar { + background: white; +} + +#editorModal .file-path { + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + font-family: 'Courier New', monospace; + font-size: 12px; + color: #666; +} + +#editorModal #modalFileTree { + background: white; + padding: 8px; +} + +#editorModal .file-item { + margin: 2px 0; + font-size: 13px; +} + +#editorModal .file-item:hover { + background-color: #e3f2fd; +} + +#editorModal .file-item.selected { + background-color: #1e3c72; + color: white; +} + +#editorModal .file-item.selected .file-icon { + color: white; +} + /* 编辑模态框分栏与拖拽条 */ #modalResizer { transition: background 0.1s; diff --git a/src/frontend/index.html b/src/frontend/index.html index 212ff31..408acd9 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -49,11 +49,11 @@
已检查文件
+项目总数
合规率
待修复问题
高危漏洞
已检查文件
+已检查项目
+集成 pylint、flake8、bandit 三大工具,全面检测代码质量和安全问题
+支持上传 setup.cfg、.pylintrc、.flake8 等配置文件,灵活定制检查规则
+内置代码编辑器,支持文件浏览、编辑、保存,快速修复问题
+检查结果精确到文件、行、列,一键定位到问题代码位置
+记录每次检查结果,追踪代码质量变化趋势
+