功能完善 #4

Merged
pu8crm6xf merged 1 commits from zhangyang into main 1 month ago

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

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

@ -49,11 +49,11 @@
<div class="stats-grid fade-in">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-code"></i>
<i class="fas fa-folder"></i>
</div>
<div class="stat-content">
<h3 id="totalFiles">1,247</h3>
<p>已检查文件</p>
<h3 id="totalProjects">0</h3>
<p>项目总数</p>
</div>
</div>
<div class="stat-card">
@ -61,7 +61,7 @@
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-content">
<h3 id="complianceRate">98.5%</h3>
<h3 id="complianceRate">0%</h3>
<p>合规率</p>
</div>
</div>
@ -70,7 +70,7 @@
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="stat-content">
<h3 id="pendingIssues">23</h3>
<h3 id="pendingIssues">0</h3>
<p>待修复问题</p>
</div>
</div>
@ -79,94 +79,119 @@
<i class="fas fa-bug"></i>
</div>
<div class="stat-content">
<h3 id="highRiskIssues">5</h3>
<h3 id="highRiskIssues">0</h3>
<p>高危漏洞</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-file-code"></i>
</div>
<div class="stat-content">
<h3 id="totalFiles">0</h3>
<p>已检查文件</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-tasks"></i>
</div>
<div class="stat-content">
<h3 id="checkedProjects">0</h3>
<p>已检查项目</p>
</div>
</div>
</div>
<!-- 功能区域 -->
<div class="function-grid fade-in">
<!-- 代码上传和检查区域 -->
<div class="function-grid fade-in" style="grid-template-columns: 1fr 1fr;">
<!-- 快速开始指南 -->
<div class="main-panel">
<div class="panel-header">
<h2 class="panel-title">快捷代码检查</h2>
<div class="filter-tabs">
<span class="filter-tab active">Python</span>
</div>
<h2 class="panel-title"><i class="fas fa-rocket"></i> 快速开始</h2>
</div>
<div class="panel-content">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
<div style="line-height: 2;">
<div style="margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; border-left: 4px solid #1e3c72;">
<strong><i class="fas fa-folder-open"></i> 1. 导入项目</strong>
<p style="margin: 8px 0 0 0; color: #666; font-size: 14px;">进入"项目管理"页面,点击"导入文件夹为项目"按钮选择您的Python项目文件夹。</p>
</div>
<div class="upload-text">拖拽文件到此处或点击上传</div>
<div class="upload-hint">支持 .py, .pyx, .pyi 文件,最大 100MB</div>
<input type="file" class="file-input" id="fileInput" multiple accept=".py,.pyx,.pyi">
</div>
<div class="progress-container" id="progressContainer" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%;"></div>
<div style="margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; border-left: 4px solid #28a745;">
<strong><i class="fas fa-play"></i> 2. 运行检查</strong>
<p style="margin: 8px 0 0 0; color: #666; font-size: 14px;">在项目详情页点击"运行检查"按钮,系统将使用 pylint、flake8、bandit 进行代码质量检查。</p>
</div>
<div style="margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; border-left: 4px solid #ffc107;">
<strong><i class="fas fa-search"></i> 3. 查看结果</strong>
<p style="margin: 8px 0 0 0; color: #666; font-size: 14px;">检查完成后,在"检查结果详情"面板查看问题列表,点击"定位"按钮可跳转到问题代码位置。</p>
</div>
<div style="padding: 12px; background: #f8f9fa; border-radius: 6px; border-left: 4px solid #17a2b8;">
<strong><i class="fas fa-edit"></i> 4. 编辑修复</strong>
<p style="margin: 8px 0 0 0; color: #666; font-size: 14px;">使用"文件编辑"功能在线修改代码,保存后重新运行检查验证修复效果。</p>
</div>
<div class="progress-text" id="progressText">准备检查...</div>
</div>
<div style="margin-top: 20px; text-align: center;">
<button class="btn btn-primary" id="startCheckBtn" style="display: none;">
<i class="fas fa-play"></i> 开始检查
</button>
<button class="btn btn-secondary" id="stopCheckBtn" style="display: none;">
<i class="fas fa-stop"></i> 停止检查
</button>
</div>
</div>
</div>
<!-- 多Agent状态面板 -->
<div class="agent-status">
<!-- 系统功能说明 -->
<div class="main-panel">
<div class="panel-header">
<h2 class="panel-title">Agent 状态</h2>
</div>
<div class="agent-list">
<div class="agent-item">
<div class="agent-avatar">A1</div>
<div class="agent-info">
<div class="agent-name">pylint Agent</div>
<span class="agent-status-badge status-idle">空闲</span>
<h2 class="panel-title"><i class="fas fa-info-circle"></i> 系统功能</h2>
</div>
<div class="panel-content">
<div style="line-height: 2;">
<div style="margin-bottom: 12px; display: flex; align-items: start; gap: 10px;">
<i class="fas fa-check" style="color: #28a745; margin-top: 4px;"></i>
<div>
<strong>多工具协同检查</strong>
<p style="margin: 4px 0 0 0; color: #666; font-size: 13px;">集成 pylint、flake8、bandit 三大工具,全面检测代码质量和安全问题</p>
</div>
</div>
</div>
<div class="agent-item">
<div class="agent-avatar">A2</div>
<div class="agent-info">
<div class="agent-name">flake8 Agent</div>
<span class="agent-status-badge status-idle">空闲</span>
<div style="margin-bottom: 12px; display: flex; align-items: start; gap: 10px;">
<i class="fas fa-check" style="color: #28a745; margin-top: 4px;"></i>
<div>
<strong>自定义规则集</strong>
<p style="margin: 4px 0 0 0; color: #666; font-size: 13px;">支持上传 setup.cfg、.pylintrc、.flake8 等配置文件,灵活定制检查规则</p>
</div>
</div>
</div>
<div class="agent-item">
<div class="agent-avatar">A3</div>
<div class="agent-info">
<div class="agent-name">bandit Agent</div>
<span class="agent-status-badge status-idle">空闲</span>
<div style="margin-bottom: 12px; display: flex; align-items: start; gap: 10px;">
<i class="fas fa-check" style="color: #28a745; margin-top: 4px;"></i>
<div>
<strong>在线代码编辑</strong>
<p style="margin: 4px 0 0 0; color: #666; font-size: 13px;">内置代码编辑器,支持文件浏览、编辑、保存,快速修复问题</p>
</div>
</div>
<div style="margin-bottom: 12px; display: flex; align-items: start; gap: 10px;">
<i class="fas fa-check" style="color: #28a745; margin-top: 4px;"></i>
<div>
<strong>精确定位问题</strong>
<p style="margin: 4px 0 0 0; color: #666; font-size: 13px;">检查结果精确到文件、行、列,一键定位到问题代码位置</p>
</div>
</div>
<div style="display: flex; align-items: start; gap: 10px;">
<i class="fas fa-check" style="color: #28a745; margin-top: 4px;"></i>
<div>
<strong>项目历史管理</strong>
<p style="margin: 4px 0 0 0; color: #666; font-size: 13px;">记录每次检查结果,追踪代码质量变化趋势</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 检查结果区域 -->
<div class="results-section fade-in" id="resultsSection" style="display: none;">
<div class="results-header">
<h2 class="results-title">检查结果</h2>
<div class="filter-tabs">
<span class="filter-tab active" data-filter="all">全部</span>
<span class="filter-tab" data-filter="error">错误</span>
<span class="filter-tab" data-filter="warning">警告</span>
<span class="filter-tab" data-filter="info">信息</span>
</div>
<!-- 最近项目列表 -->
<div class="main-panel fade-in" style="margin-top: 20px;">
<div class="panel-header">
<h2 class="panel-title"><i class="fas fa-history"></i> 最近项目</h2>
<a href="#projects" class="btn btn-secondary" style="text-decoration: none; padding: 6px 12px; font-size: 12px;">查看全部</a>
</div>
<div class="results-content" id="resultsContent">
<!-- 结果项将通过 JavaScript 动态生成 -->
<div class="panel-content">
<div id="recentProjectsList" style="min-height: 100px;">
<div style="text-align: center; padding: 40px; color: #999;">
<i class="fas fa-folder-open" style="font-size: 48px; margin-bottom: 15px; opacity: 0.3;"></i>
<p>暂无项目,请先导入项目</p>
</div>
</div>
</div>
</div>
</div>
@ -375,14 +400,99 @@
</div>
<div class="file-info"><span id="modalCurrentFilePath">未选择文件</span></div>
</div>
<div class="editor-content" style="flex:1; padding:0 12px 12px;">
<textarea id="modalCodeEditor" placeholder="选择文件开始编辑..." style="height:100%;"></textarea>
<div class="editor-content" style="flex:1; padding:0; position:relative; overflow:hidden; display:flex; flex-direction:column;">
<textarea id="modalCodeEditor" placeholder="选择文件开始编辑..." style="flex:1; width:100%; border:none; padding:15px; font-family:'Courier New', monospace; font-size:14px; line-height:1.6; resize:none; outline:none; background:#fafafa; color:#333;"></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- 规则集选择对话框 -->
<div id="ruleSetSelectModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2>检查配置</h2>
<span class="close" id="closeRuleSetModal">&times;</span>
</div>
<div class="modal-body">
<p style="margin-bottom: 20px; color: #666; font-size: 14px;">
<i class="fas fa-info-circle" style="color: #1e3c72; margin-right: 8px;"></i>
选择检查工具和规则集配置。至少需要选择一个检查工具。
</p>
<!-- 工具选择 -->
<div class="form-group">
<label style="font-weight: 500; margin-bottom: 12px; display: block;">
<i class="fas fa-tools" style="color: #1e3c72; margin-right: 8px;"></i>
选择检查工具
</label>
<div style="display: flex; flex-direction: column; gap: 10px; padding: 12px; background: #f8f9fa; border-radius: 6px; border: 1px solid #e9ecef;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="toolPylint" class="tool-checkbox" value="pylint" checked style="width: auto; margin: 0; cursor: pointer;">
<span style="flex: 1;">
<strong>Pylint</strong>
<span style="color: #666; font-size: 12px; margin-left: 8px;">代码质量检查</span>
</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="toolFlake8" class="tool-checkbox" value="flake8" checked style="width: auto; margin: 0; cursor: pointer;">
<span style="flex: 1;">
<strong>Flake8</strong>
<span style="color: #666; font-size: 12px; margin-left: 8px;">代码风格检查</span>
</span>
</label>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="toolBandit" class="tool-checkbox" value="bandit" checked style="width: auto; margin: 0; cursor: pointer;">
<span style="flex: 1;">
<strong>Bandit</strong>
<span style="color: #666; font-size: 12px; margin-left: 8px;">安全漏洞检查</span>
</span>
</label>
</div>
</div>
<!-- 规则集选择 -->
<div class="form-group" style="margin-top: 20px;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 12px; background: #f8f9fa; border-radius: 6px; border: 1px solid #e9ecef;">
<input type="checkbox" id="useCustomRuleSet" style="width: auto; margin: 0; cursor: pointer;">
<span style="font-weight: 500;">使用自定义规则集</span>
</label>
</div>
<div class="form-group" id="ruleSetSelectGroup" style="display: none; margin-top: 15px;">
<label for="ruleSetSelect" style="font-weight: 500; margin-bottom: 8px; display: block;">选择规则集</label>
<select id="ruleSetSelect" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; background: white; cursor: pointer;">
<option value="">请选择规则集...</option>
</select>
<p style="margin-top: 8px; font-size: 12px; color: #666; line-height: 1.5;">
<i class="fas fa-check-circle" style="color: #28a745; margin-right: 4px;"></i>
选中的规则集文件将应用于已选择的检查工具
</p>
</div>
<!-- AI分析选项 -->
<div class="form-group" style="margin-top: 20px; padding: 12px; background: #f0f7ff; border-radius: 6px; border: 1px solid #b3d9ff;">
<label style="display: flex; align-items: start; gap: 10px; cursor: pointer;">
<input type="checkbox" id="useAiAnalysis" checked style="width: auto; margin: 0; cursor: pointer; margin-top: 2px;">
<div style="flex: 1;">
<span style="font-weight: 500; display: flex; align-items: center; gap: 6px;">
<i class="fas fa-robot" style="color: #1e3c72;"></i>
启用AI智能分析
</span>
<p style="margin: 6px 0 0 0; font-size: 12px; color: #666; line-height: 1.5;">
自动去重、风险评估和生成修改建议。需要配置OPENAI_API_KEY环境变量才能使用完整AI功能。
</p>
</div>
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="cancelRuleSetBtn">取消</button>
<button type="button" class="btn btn-primary" id="confirmRuleSetBtn">开始检查</button>
</div>
</div>
</div>
<script src="js/app.js"></script>
</body>

@ -9,7 +9,7 @@ let currentProjectLastResults = null;
// API 基础URL - 自动适配当前域名
const API_BASE_URL = window.location.origin + '/api';
// DOM 元素
// DOM 元素(快捷检查相关元素已移除,保留变量定义以避免错误)
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const progressContainer = document.getElementById('progressContainer');
@ -61,26 +61,28 @@ function showPage(pageId) {
}
}
// 文件上传处理
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
fileInput.addEventListener('change', handleFileSelect);
// 文件上传处理(快捷检查功能已移除,保留代码以防将来需要)
if (uploadArea && fileInput) {
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
fileInput.addEventListener('change', handleFileSelect);
}
function handleDragOver(e) {
e.preventDefault();
uploadArea.classList.add('dragover');
if (uploadArea) uploadArea.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (uploadArea) uploadArea.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (uploadArea) uploadArea.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
processFiles(files);
}
@ -104,10 +106,11 @@ function processFiles(files) {
uploadedFiles = validFiles;
updateUploadArea();
startCheckBtn.style.display = 'inline-block';
if (startCheckBtn) startCheckBtn.style.display = 'inline-block';
}
function updateUploadArea() {
if (!uploadArea) return;
if (uploadedFiles.length > 0) {
uploadArea.innerHTML = `
<div class="upload-icon">
@ -119,9 +122,9 @@ function updateUploadArea() {
}
}
// 开始检查
startCheckBtn.addEventListener('click', startCheck);
stopCheckBtn.addEventListener('click', stopCheck);
// 开始检查(快捷检查功能已移除)
if (startCheckBtn) startCheckBtn.addEventListener('click', startCheck);
if (stopCheckBtn) stopCheckBtn.addEventListener('click', stopCheck);
async function startCheck() {
if (uploadedFiles.length === 0) {
@ -130,10 +133,10 @@ async function startCheck() {
}
isChecking = true;
startCheckBtn.style.display = 'none';
stopCheckBtn.style.display = 'inline-block';
progressContainer.style.display = 'block';
resultsSection.style.display = 'none';
if (startCheckBtn) startCheckBtn.style.display = 'none';
if (stopCheckBtn) stopCheckBtn.style.display = 'inline-block';
if (progressContainer) progressContainer.style.display = 'block';
if (resultsSection) resultsSection.style.display = 'none';
// 更新 Agent 状态
updateAgentStatus('working');
@ -172,28 +175,32 @@ async function startCheck() {
function stopCheck() {
isChecking = false;
startCheckBtn.style.display = 'inline-block';
stopCheckBtn.style.display = 'none';
progressContainer.style.display = 'none';
if (startCheckBtn) startCheckBtn.style.display = 'inline-block';
if (stopCheckBtn) stopCheckBtn.style.display = 'none';
if (progressContainer) progressContainer.style.display = 'none';
// 更新 Agent 状态
updateAgentStatus('idle');
}
function updateAgentStatus(status) {
// Agent状态面板已移除此函数保留以防将来需要
const agentItems = document.querySelectorAll('.agent-item');
if (agentItems.length === 0) return;
agentItems.forEach(item => {
const badge = item.querySelector('.agent-status-badge');
badge.className = `agent-status-badge status-${status}`;
badge.textContent = status === 'working' ? '工作中' :
status === 'completed' ? '完成' : '空闲';
if (badge) {
badge.className = `agent-status-badge status-${status}`;
badge.textContent = status === 'working' ? '工作中' :
status === 'completed' ? '完成' : '空闲';
}
});
}
// 更新进度条
function updateProgress(progress, text) {
progressFill.style.width = progress + '%';
progressText.textContent = text;
if (progressFill) progressFill.style.width = progress + '%';
if (progressText) progressText.textContent = text;
}
// 上传文件到服务器
@ -335,7 +342,7 @@ function updateToolsStatus(toolsStatus) {
}
// 查看问题详情
function viewIssueDetail(rule, message) {
function viewIssueDetail(rule, message, suggestion = null) {
const detailModal = document.createElement('div');
detailModal.className = 'modal';
detailModal.style.display = 'block';
@ -356,7 +363,7 @@ function viewIssueDetail(rule, message) {
</div>
<div class="form-group">
<label>建议操作:</label>
<p>请根据规则要求修改代码确保符合军事代码合规性标准</p>
<p>${suggestion || '请根据规则要求修改代码,确保符合军事代码合规性标准。'}</p>
</div>
</div>
<div class="modal-footer">
@ -368,7 +375,7 @@ function viewIssueDetail(rule, message) {
}
// 显示修复建议
function showFixSuggestion(rule, message) {
function showFixSuggestion(rule, message, aiSuggestion = null) {
const suggestions = {
'B608': '使用参数化查询替代字符串拼接,例如使用 cursor.execute("SELECT * FROM table WHERE id = %s", (user_id,))',
'E501': '将长行拆分为多行,使用括号或反斜杠进行换行',
@ -377,7 +384,7 @@ function showFixSuggestion(rule, message) {
'default': '请参考相关代码规范文档,确保代码符合军事项目标准'
};
const suggestion = suggestions[rule] || suggestions['default'];
const suggestion = aiSuggestion || suggestions[rule] || suggestions['default'];
const suggestionModal = document.createElement('div');
suggestionModal.className = 'modal';
@ -412,23 +419,24 @@ function showFixSuggestion(rule, message) {
function completeCheck() {
isChecking = false;
startCheckBtn.style.display = 'inline-block';
stopCheckBtn.style.display = 'none';
progressContainer.style.display = 'none';
if (startCheckBtn) startCheckBtn.style.display = 'inline-block';
if (stopCheckBtn) stopCheckBtn.style.display = 'none';
if (progressContainer) progressContainer.style.display = 'none';
// 更新 Agent 状态
updateAgentStatus('completed');
// 显示结果区域
resultsSection.style.display = 'block';
if (resultsSection) resultsSection.style.display = 'block';
}
function showResults() {
resultsSection.style.display = 'block';
if (resultsSection) resultsSection.style.display = 'block';
// 结果现在通过 processCheckResults 函数处理
}
function displayResults(results) {
if (!resultsContent) return;
resultsContent.innerHTML = '';
if (!results || results.length === 0) {
@ -463,38 +471,77 @@ function createResultItem(result) {
const iconClass = result.type === 'error' ? 'times' : result.type === 'warning' ? 'exclamation-triangle' : 'info-circle';
const displayPath = result.relative_path || result.file || '未知文件';
const location = `${displayPath}:${result.line}:${result.column || 0}`;
// 风险等级显示
const riskLevel = result.risk_level || 'low';
const riskColors = {
high: '#dc3545',
medium: '#ffc107',
low: '#28a745'
};
const riskLabels = {
high: '高风险',
medium: '中风险',
low: '低风险'
};
const riskBadge = result.risk_level ?
`<span style="background: ${riskColors[riskLevel]}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; margin-left: 8px;">${riskLabels[riskLevel]}</span>` : '';
// 检测工具显示
const detectedBy = result.detected_by && result.detected_by.length > 0
? result.detected_by.join(', ')
: (result.tool || '未知');
resultItem.innerHTML = `
<div class="result-icon ${result.type}"><i class="fas fa-${iconClass}"></i></div>
<div class="result-content">
<div class="result-title"></div>
<div class="result-content" style="flex: 1;">
<div class="result-title" style="display: flex; align-items: center; flex-wrap: wrap;">
<span>${result.message || '代码问题'}</span>
${riskBadge}
</div>
<div class="result-description"></div>
<div class="result-location">${location}</div>
${result.suggestion ? `<div class="result-suggestion" style="margin-top: 8px; padding: 8px; background: #f8f9fa; border-left: 3px solid #1e3c72; border-radius: 4px; font-size: 13px; color: #555;">
<strong style="color: #1e3c72;">💡 修改建议:</strong> ${result.suggestion}
</div>` : ''}
</div>
<div class="result-actions">
<button class="btn btn-primary">查看详情</button>
<button class="btn btn-secondary">修复建议</button>
</div>
`;
resultItem.querySelector('.result-title').textContent = result.message || '代码问题';
const desc = resultItem.querySelector('.result-description');
desc.innerHTML = `<strong>规则:</strong> ${result.rule || '未知规则'} | <strong>严重程度:</strong> ${result.severity || 'medium'}${result.confidence ? ` | <strong>置信度:</strong> ${result.confidence}` : ''}`;
desc.innerHTML = `<strong>规则:</strong> ${result.rule || '未知规则'} | <strong>严重程度:</strong> ${result.severity || 'medium'} | <strong>检测工具:</strong> ${detectedBy}${result.confidence ? ` | <strong>置信度:</strong> ${result.confidence}` : ''}${result.risk_score !== undefined ? ` | <strong>风险评分:</strong> ${result.risk_score}` : ''}`;
const [detailBtn, fixBtn] = resultItem.querySelectorAll('.result-actions .btn');
detailBtn.addEventListener('click', () => viewIssueDetail(result.rule || '', result.message || ''));
fixBtn.addEventListener('click', () => showFixSuggestion(result.rule || '', result.message || ''));
detailBtn.addEventListener('click', () => viewIssueDetail(result.rule || '', result.message || '', result.suggestion));
fixBtn.addEventListener('click', () => showFixSuggestion(result.rule || '', result.message || '', result.suggestion));
return resultItem;
}
// 创建结果摘要
function createResultsSummary(results) {
function createResultsSummary(results, resultData = {}) {
const errorCount = results.filter(r => r.type === 'error').length;
const warningCount = results.filter(r => r.type === 'warning').length;
const infoCount = results.filter(r => r.type === 'info').length;
const totalCount = results.length;
// AI分析统计
const highRiskCount = resultData.high_risk_count || results.filter(r => r.risk_level === 'high').length;
const mediumRiskCount = resultData.medium_risk_count || results.filter(r => r.risk_level === 'medium').length;
const lowRiskCount = resultData.low_risk_count || results.filter(r => r.risk_level === 'low').length;
const duplicatesRemoved = resultData.duplicates_removed || 0;
const analysisMethod = resultData.analysis_method || 'none';
const aiAnalyzed = resultData.ai_analyzed || false;
return `
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #1e3c72;">
<h3 style="margin: 0 0 15px 0; color: #1e3c72;">检查结果摘要</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px;">
<h3 style="margin: 0 0 15px 0; color: #1e3c72; display: flex; align-items: center; gap: 8px;">
检查结果摘要
${aiAnalyzed ? `<span style="background: #1e3c72; color: white; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: normal;">
<i class="fas fa-robot"></i> AI: ${analysisMethod === 'ai-powered' ? 'AI' : ''}
</span>` : ''}
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; margin-bottom: 15px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #dc3545;">${errorCount}</div>
<div style="color: #666; font-size: 14px;">错误</div>
@ -512,6 +559,28 @@ function createResultsSummary(results) {
<div style="color: #666; font-size: 14px;">总计</div>
</div>
</div>
${aiAnalyzed ? `
<div style="border-top: 1px solid #dee2e6; padding-top: 15px; margin-top: 15px;">
<h4 style="margin: 0 0 10px 0; color: #495057; font-size: 14px;">风险评估</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 10px;">
<div style="text-align: center;">
<div style="font-size: 20px; font-weight: bold; color: #dc3545;">${highRiskCount}</div>
<div style="color: #666; font-size: 12px;">高风险</div>
</div>
<div style="text-align: center;">
<div style="font-size: 20px; font-weight: bold; color: #ffc107;">${mediumRiskCount}</div>
<div style="color: #666; font-size: 12px;">中风险</div>
</div>
<div style="text-align: center;">
<div style="font-size: 20px; font-weight: bold; color: #28a745;">${lowRiskCount}</div>
<div style="color: #666; font-size: 12px;">低风险</div>
</div>
</div>
${duplicatesRemoved > 0 ? `<div style="margin-top: 10px; padding: 8px; background: #e7f3ff; border-radius: 4px; font-size: 12px; color: #0066cc;">
<i class="fas fa-filter"></i> ${duplicatesRemoved}
</div>` : ''}
</div>
` : ''}
</div>
`;
}
@ -604,7 +673,7 @@ function displayProjects(projects) {
</div>
<div class="project-actions">
<button class="btn btn-primary" onclick="event.stopPropagation(); showProjectDetail(${JSON.stringify(project).replace(/"/g, '&quot;')})">查看详情</button>
<button class="btn btn-secondary" onclick="event.stopPropagation(); runProjectCheck(${project.id})">运行检查</button>
<button class="btn btn-secondary" onclick="event.stopPropagation(); showRuleSetSelectDialog(${project.id})">运行检查</button>
<button class="btn btn-danger" onclick="event.stopPropagation(); deleteProject(${project.id}, '${project.name}')">删除</button>
</div>
`;
@ -907,24 +976,70 @@ function showProjectDetail(project) {
// 已移除检查历史渲染
// 运行项目检查
async function runProjectCheck(projectId) {
async function runProjectCheck(projectId, ruleSetName = null, tools = ['pylint', 'flake8', 'bandit'], useAiAnalysis = true) {
console.log('[运行检查] 开始执行检查,参数:', { projectId, ruleSetName, tools, useAiAnalysis });
if (!projectId) {
console.error('[运行检查] 项目ID为空');
showErrorMessage('项目ID无效');
return;
}
showLoadingOverlay('正在执行检查...');
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/check`, { method: 'POST' });
const requestBody = {
tools: tools,
use_ai_analysis: useAiAnalysis
};
if (ruleSetName) {
requestBody.rule_set = ruleSetName;
console.log('[运行检查] 使用规则集:', ruleSetName);
} else {
console.log('[运行检查] 使用默认规则');
}
console.log('[运行检查] 选择的工具:', tools);
console.log('[运行检查] 启用AI分析:', useAiAnalysis);
const apiUrl = `${API_BASE_URL}/projects/${projectId}/check`;
console.log('[运行检查] 发送检查请求到:', apiUrl);
console.log('[运行检查] 请求体:', JSON.stringify(requestBody, null, 2));
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
console.log('[运行检查] 响应状态:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error('[运行检查] 响应错误:', errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('[运行检查] 响应数据:', data);
if (data.success) {
currentProjectLastResults = data.data.all_issues || [];
console.log('检查完成,问题数量:', currentProjectLastResults.length);
showSuccessMessage('检查完成');
displayCheckResultsPanel(data.data);
if (currentProject && currentProject.id === projectId) {
displayCheckResultsPanel(data.data);
if (currentProject && currentProject.id === projectId) {
try {
const pr = await fetch(`${API_BASE_URL}/projects/${projectId}`);
const pd = await pr.json();
if (pd.success) currentProject = pd.data;
} catch {}
// 不再渲染检查历史
} catch (e) {
console.error('更新项目信息失败:', e);
}
// 不再渲染检查历史
}
} else {
console.error('检查失败:', data.error);
showErrorMessage('检查失败:' + (data.error || ''));
}
} catch (error) {
@ -1083,7 +1198,7 @@ function showErrorMessage(message) {
function displayCheckResultsPanel(resultData) {
const panel = document.getElementById('checkResultsPanel');
if (!panel) return;
renderProjectResults(resultData.all_issues || []);
renderProjectResults(resultData.all_issues || [], resultData);
panel.style.display = 'block';
}
@ -1110,7 +1225,7 @@ function hideLoadingOverlay() {
}
// 在项目详情页的结果面板里渲染结果
function renderProjectResults(results) {
function renderProjectResults(results, resultData = {}) {
const container = document.getElementById('checkResultsContent');
if (!container) return;
container.innerHTML = '';
@ -1126,23 +1241,9 @@ function renderProjectResults(results) {
return;
}
const errorCount = results.filter(r => r.type === 'error').length;
const warningCount = results.filter(r => r.type === 'warning').length;
const infoCount = results.filter(r => r.type === 'info').length;
const totalCount = results.length;
// 使用 createResultsSummary 生成摘要
const summary = document.createElement('div');
summary.innerHTML = `
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #1e3c72;">
<h3 style="margin: 0 0 15px 0; color: #1e3c72;">检查结果摘要</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px;">
<div style="text-align: center;"><div style="font-size: 24px; font-weight: bold; color: #dc3545;">${errorCount}</div><div style="color: #666; font-size: 14px;"></div></div>
<div style="text-align: center;"><div style="font-size: 24px; font-weight: bold; color: #ffc107;">${warningCount}</div><div style="color: #666; font-size: 14px;"></div></div>
<div style="text-align: center;"><div style="font-size: 24px; font-weight: bold; color: #17a2b8;">${infoCount}</div><div style="color: #666; font-size: 14px;"></div></div>
<div style="text-align: center;"><div style="font-size: 24px; font-weight: bold; color: #1e3c72;">${totalCount}</div><div style="color: #666; font-size: 14px;"></div></div>
</div>
</div>
`;
summary.innerHTML = createResultsSummary(results, resultData);
container.appendChild(summary);
const list = document.createElement('div');
@ -1163,11 +1264,234 @@ const runCheckBtn = document.getElementById('runCheckBtn');
if (runCheckBtn) {
runCheckBtn.addEventListener('click', () => {
if (currentProject) {
runProjectCheck(currentProject.id);
showRuleSetSelectDialog(currentProject.id);
}
});
}
// 显示规则集选择对话框
let pendingCheckProjectId = null;
async function showRuleSetSelectDialog(projectId) {
console.log('[检查配置] 显示对话框项目ID:', projectId);
pendingCheckProjectId = projectId;
const modal = document.getElementById('ruleSetSelectModal');
const useCustomCheckbox = document.getElementById('useCustomRuleSet');
const ruleSetSelectGroup = document.getElementById('ruleSetSelectGroup');
const ruleSetSelect = document.getElementById('ruleSetSelect');
if (!modal) {
console.error('[检查配置] 模态框元素未找到');
showErrorMessage('检查配置对话框未找到');
return;
}
console.log('[检查配置] 找到的元素:', {
modal: !!modal,
useCustomCheckbox: !!useCustomCheckbox,
ruleSetSelectGroup: !!ruleSetSelectGroup,
ruleSetSelect: !!ruleSetSelect
});
// 重置状态
if (useCustomCheckbox) {
useCustomCheckbox.checked = false;
}
if (ruleSetSelectGroup) {
ruleSetSelectGroup.style.display = 'none';
}
if (ruleSetSelect) {
ruleSetSelect.value = '';
}
// 重置工具选择(默认全选)
const toolPylint = document.getElementById('toolPylint');
const toolFlake8 = document.getElementById('toolFlake8');
const toolBandit = document.getElementById('toolBandit');
if (toolPylint) toolPylint.checked = true;
if (toolFlake8) toolFlake8.checked = true;
if (toolBandit) toolBandit.checked = true;
console.log('[检查配置] 工具选择已重置为默认全选');
// 加载规则集列表
try {
console.log('[检查配置] 开始加载规则集列表');
const response = await fetch(`${API_BASE_URL}/rules`);
const data = await response.json();
console.log('[检查配置] 规则集列表响应:', data);
if (ruleSetSelect) {
ruleSetSelect.innerHTML = '<option value="">请选择规则集...</option>';
if (data.success && data.data && data.data.length > 0) {
data.data.forEach(rule => {
const option = document.createElement('option');
option.value = rule.name;
option.textContent = rule.name;
ruleSetSelect.appendChild(option);
});
console.log('[检查配置] 已加载', data.data.length, '个规则集');
} else {
const option = document.createElement('option');
option.value = '';
option.textContent = '暂无可用规则集';
option.disabled = true;
ruleSetSelect.appendChild(option);
console.log('[检查配置] 没有可用的规则集');
}
}
} catch (error) {
console.error('[检查配置] 加载规则集列表失败:', error);
showErrorMessage('加载规则集列表失败');
}
modal.style.display = 'block';
console.log('[检查配置] 对话框已显示');
}
// 规则集选择对话框事件绑定
document.addEventListener('DOMContentLoaded', () => {
console.log('[检查配置] DOMContentLoaded - 开始绑定事件');
const useCustomCheckbox = document.getElementById('useCustomRuleSet');
const ruleSetSelectGroup = document.getElementById('ruleSetSelectGroup');
const closeRuleSetModal = document.getElementById('closeRuleSetModal');
const cancelRuleSetBtn = document.getElementById('cancelRuleSetBtn');
const confirmRuleSetBtn = document.getElementById('confirmRuleSetBtn');
console.log('[检查配置] 找到的元素:', {
useCustomCheckbox: !!useCustomCheckbox,
ruleSetSelectGroup: !!ruleSetSelectGroup,
closeRuleSetModal: !!closeRuleSetModal,
cancelRuleSetBtn: !!cancelRuleSetBtn,
confirmRuleSetBtn: !!confirmRuleSetBtn
});
if (useCustomCheckbox) {
useCustomCheckbox.addEventListener('change', (e) => {
console.log('[检查配置] 使用自定义规则集复选框状态改变:', e.target.checked);
if (ruleSetSelectGroup) {
ruleSetSelectGroup.style.display = e.target.checked ? 'block' : 'none';
}
if (!e.target.checked) {
const ruleSetSelect = document.getElementById('ruleSetSelect');
if (ruleSetSelect) ruleSetSelect.value = '';
}
});
}
function closeRuleSetModalFunc() {
console.log('[检查配置] 关闭对话框');
const modal = document.getElementById('ruleSetSelectModal');
if (modal) modal.style.display = 'none';
pendingCheckProjectId = null;
}
if (closeRuleSetModal) {
closeRuleSetModal.addEventListener('click', () => {
console.log('[检查配置] 点击关闭按钮');
closeRuleSetModalFunc();
});
} else {
console.warn('[检查配置] closeRuleSetModal 元素未找到');
}
if (cancelRuleSetBtn) {
cancelRuleSetBtn.addEventListener('click', () => {
console.log('[检查配置] 点击取消按钮');
closeRuleSetModalFunc();
});
} else {
console.warn('[检查配置] cancelRuleSetBtn 元素未找到');
}
if (confirmRuleSetBtn) {
confirmRuleSetBtn.addEventListener('click', () => {
console.log('[检查配置] 确认按钮被点击');
try {
// 获取选择的工具
const toolPylint = document.getElementById('toolPylint');
const toolFlake8 = document.getElementById('toolFlake8');
const toolBandit = document.getElementById('toolBandit');
console.log('[检查配置] 工具复选框状态:', {
pylint: toolPylint ? toolPylint.checked : '元素不存在',
flake8: toolFlake8 ? toolFlake8.checked : '元素不存在',
bandit: toolBandit ? toolBandit.checked : '元素不存在'
});
const selectedTools = [];
if (toolPylint && toolPylint.checked) selectedTools.push('pylint');
if (toolFlake8 && toolFlake8.checked) selectedTools.push('flake8');
if (toolBandit && toolBandit.checked) selectedTools.push('bandit');
console.log('[检查配置] 选择的工具:', selectedTools);
// 验证至少选择一个工具
if (selectedTools.length === 0) {
console.warn('[检查配置] 未选择任何工具');
showErrorMessage('请至少选择一个检查工具');
return;
}
// 获取规则集选择
const useCustomCheckbox = document.getElementById('useCustomRuleSet');
const ruleSetSelect = document.getElementById('ruleSetSelect');
const useAiAnalysisCheckbox = document.getElementById('useAiAnalysis');
const useCustom = useCustomCheckbox ? useCustomCheckbox.checked : false;
const selectedRuleSet = ruleSetSelect ? ruleSetSelect.value : '';
const useAiAnalysis = useAiAnalysisCheckbox ? useAiAnalysisCheckbox.checked : true;
console.log('[检查配置] 规则集选择:', { useCustom, selectedRuleSet });
console.log('[检查配置] AI分析:', useAiAnalysis);
if (useCustom && !selectedRuleSet) {
console.warn('[检查配置] 选择了使用规则集但未选择具体规则集');
showErrorMessage('请选择一个规则集');
return;
}
console.log('[检查配置] pendingCheckProjectId:', pendingCheckProjectId);
// 先保存项目ID因为 closeRuleSetModalFunc 会清空它
const savedProjectId = pendingCheckProjectId;
// 关闭对话框
closeRuleSetModalFunc();
if (savedProjectId) {
const ruleSetName = useCustom ? selectedRuleSet : null;
console.log('[检查配置] 调用 runProjectCheck:', {
projectId: savedProjectId,
ruleSetName: ruleSetName,
tools: selectedTools,
useAiAnalysis: useAiAnalysis
});
runProjectCheck(savedProjectId, ruleSetName, selectedTools, useAiAnalysis);
} else {
console.error('[检查配置] savedProjectId 为空,无法执行检查');
showErrorMessage('项目ID丢失请重新选择项目');
}
} catch (error) {
console.error('[检查配置] 处理确认按钮时出错:', error);
showErrorMessage('处理检查配置时出错: ' + error.message);
}
});
} else {
console.error('[检查配置] confirmRuleSetBtn 元素未找到');
}
// 点击模态框外部关闭
const ruleSetModal = document.getElementById('ruleSetSelectModal');
if (ruleSetModal) {
ruleSetModal.addEventListener('click', (e) => {
if (e.target === ruleSetModal) {
closeRuleSetModalFunc();
}
});
}
});
// 删除项目按钮
const deleteProjectBtn = document.getElementById('deleteProjectBtn');
if (deleteProjectBtn) {
@ -1186,20 +1510,78 @@ async function loadDashboardData() {
if (!data.success) return;
const stats = data.data || {};
const totalProjectsElement = document.getElementById('totalProjects');
const totalFilesElement = document.getElementById('totalFiles');
const complianceRateElement = document.getElementById('complianceRate');
const pendingIssuesElement = document.getElementById('pendingIssues');
const highRiskIssuesElement = document.getElementById('highRiskIssues');
const checkedProjectsElement = document.getElementById('checkedProjects');
if (totalProjectsElement) totalProjectsElement.textContent = stats.num_projects ?? 0;
if (totalFilesElement) totalFilesElement.textContent = stats.total_files_checked ?? 0;
if (complianceRateElement) complianceRateElement.textContent = (stats.compliance_rate ?? 0) + '%';
if (pendingIssuesElement) pendingIssuesElement.textContent = stats.warning_count ?? 0;
if (highRiskIssuesElement) highRiskIssuesElement.textContent = stats.error_count ?? 0;
if (checkedProjectsElement) checkedProjectsElement.textContent = stats.checked_projects ?? 0;
// 加载最近项目列表
loadRecentProjects();
} catch (e) {
console.error('加载仪表板数据失败:', e);
}
}
// 加载最近项目列表
async function loadRecentProjects() {
try {
const response = await fetch(`${API_BASE_URL}/projects`);
const data = await response.json();
const container = document.getElementById('recentProjectsList');
if (!container) return;
if (data.success && data.data && data.data.length > 0) {
const recentProjects = data.data
.sort((a, b) => new Date(b.updated_at || b.created_at) - new Date(a.updated_at || a.created_at))
.slice(0, 5);
container.innerHTML = '';
recentProjects.forEach(project => {
const item = document.createElement('div');
item.style.cssText = 'display:flex; align-items:center; justify-content:space-between; padding:12px; border-bottom:1px solid #e9ecef; cursor:pointer; transition:background 0.2s;';
item.onmouseenter = () => item.style.background = '#f8f9fa';
item.onmouseleave = () => item.style.background = 'transparent';
item.onclick = () => showProjectDetail(project);
const latestCheck = project.latest_check;
const totalIssues = latestCheck ? latestCheck.total_issues : 0;
const lastCheckDate = latestCheck && latestCheck.completed_at ?
new Date(latestCheck.completed_at).toLocaleString() : '从未检查';
item.innerHTML = `
<div style="flex:1;">
<div style="font-weight:600; color:#1e3c72; margin-bottom:4px;">${project.name}</div>
<div style="font-size:12px; color:#666;">
<span>问题数: ${totalIssues}</span>
<span style="margin-left:15px;">最后检查: ${lastCheckDate}</span>
</div>
</div>
<i class="fas fa-chevron-right" style="color:#999;"></i>
`;
container.appendChild(item);
});
} else {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: #999;">
<i class="fas fa-folder-open" style="font-size: 48px; margin-bottom: 15px; opacity: 0.3;"></i>
<p>暂无项目请先导入项目</p>
</div>
`;
}
} catch (e) {
console.error('加载最近项目失败:', e);
}
}
// 文件浏览器相关变量
let currentFilePath = '';
let currentFileContent = '';

@ -1,7 +1,7 @@
{
"name": "fortifycode",
"version": "1.0.0",
"lockfileVersion": 3,
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/accepts": {
@ -931,6 +931,26 @@
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",

@ -11,7 +11,8 @@
"dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0",
"multer": "^2.0.2"
"multer": "^2.0.2",
"ws": "^8.18.3"
}
},
"node_modules/accepts": {
@ -941,6 +942,26 @@
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
@ -1610,6 +1631,12 @@
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"ws": {
"version": "8.18.3",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"requires": {}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",

@ -24,4 +24,4 @@
"express": "^5.1.0",
"multer": "^2.0.2"
}
}
}

@ -0,0 +1,551 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const crypto = require('crypto');
/**
* AI分析服务
* 用于对代码检查结果进行智能分析去重风险评估和生成修改建议
* 支持OpenAI和科大讯飞Spark API
*/
class AIAnalyzer {
constructor() {
// OpenAI配置
this.openaiApiKey = process.env.OPENAI_API_KEY || '';
this.openaiApiBase = process.env.OPENAI_API_BASE || 'https://api.openai.com/v1';
this.openaiModel = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';
// 科大讯飞配置HTTP API
this.xfAppId = process.env.XF_APP_ID || '96c72c9e';
this.xfApiKey = process.env.XF_API_KEY || 'mfxzmIRlAymtEmsgVojM';
this.xfApiSecret = process.env.XF_API_SECRET || 'ZZIvBMLpPhAHSeATcoDY';
this.xfModel = process.env.XF_MODEL || 'x1'; // Spark X1.5 模型
this.xfApiUrl = process.env.XF_API_URL || 'https://spark-api-open.xf-yun.com/v2/chat/completions';
// 选择使用的API提供商
this.provider = process.env.AI_PROVIDER || 'xf'; // 'openai' 或 'xf'
this.enabled = process.env.AI_ANALYZER_ENABLED !== 'false';
console.log(`[AI分析] 使用提供商: ${this.provider}`);
if (this.provider === 'xf') {
console.log(`[AI分析] 科大讯飞 APPID: ${this.xfAppId}`);
} else if (this.provider === 'openai') {
console.log(`[AI分析] OpenAI API Key: ${this.openaiApiKey ? '已配置' : '未配置'}`);
}
}
/**
* 分析检查结果
* @param {Array} issues - 检查结果列表
* @param {string} projectPath - 项目路径
* @returns {Promise<Object>} 分析结果
*/
async analyzeIssues(issues, projectPath = null) {
if (!this.enabled) {
console.log('[AI分析] AI分析功能已禁用使用基础去重');
return this.basicDeduplication(issues);
}
try {
// 先进行基础去重
const deduplicated = this.basicDeduplication(issues);
// 检查是否有可用的API配置
const hasApiConfig = (this.provider === 'openai' && this.openaiApiKey) ||
(this.provider === 'xf' && this.xfApiKey && this.xfApiSecret);
if (hasApiConfig) {
return await this.aiAnalysis(deduplicated.issues, projectPath);
} else {
// 否则使用规则基础分析
console.log('[AI分析] 未配置API密钥使用规则基础分析');
return this.ruleBasedAnalysis(deduplicated.issues);
}
} catch (error) {
console.error('[AI分析] 分析失败,回退到基础分析:', error);
return this.basicDeduplication(issues);
}
}
/**
* 基础去重 - 基于文件行号规则和消息相似度
*/
basicDeduplication(issues) {
const seen = new Map();
const uniqueIssues = [];
const duplicates = [];
for (const issue of issues) {
// 生成唯一键:文件路径 + 行号 + 规则ID
const key = `${issue.relative_path || issue.file}:${issue.line}:${issue.rule || 'unknown'}`;
if (seen.has(key)) {
// 检查消息相似度
const existing = seen.get(key);
const similarity = this.calculateSimilarity(
issue.message || '',
existing.message || ''
);
if (similarity < 0.7) {
// 消息差异较大,可能是不同的问题
uniqueIssues.push(issue);
} else {
// 相似度高,合并工具来源
if (!existing.detected_by) {
existing.detected_by = [existing.tool || 'unknown'];
}
if (issue.tool && !existing.detected_by.includes(issue.tool)) {
existing.detected_by.push(issue.tool);
}
duplicates.push(issue);
}
} else {
issue.detected_by = issue.tool ? [issue.tool] : ['unknown'];
seen.set(key, issue);
uniqueIssues.push(issue);
}
}
return {
issues: uniqueIssues,
total_issues: uniqueIssues.length,
duplicates_removed: duplicates.length,
deduplication_rate: issues.length > 0
? ((duplicates.length / issues.length) * 100).toFixed(2) + '%'
: '0%'
};
}
/**
* 计算字符串相似度简单的Jaccard相似度
*/
calculateSimilarity(str1, str2) {
if (!str1 || !str2) return 0;
const words1 = new Set(str1.toLowerCase().split(/\s+/));
const words2 = new Set(str2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
/**
* 基于规则的分析不使用AI
*/
ruleBasedAnalysis(issues) {
const analyzed = issues.map(issue => {
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
});
// 按风险评分排序
analyzed.sort((a, b) => b.risk_score - a.risk_score);
return {
issues: analyzed,
total_issues: analyzed.length,
high_risk_count: analyzed.filter(i => i.risk_level === 'high').length,
medium_risk_count: analyzed.filter(i => i.risk_level === 'medium').length,
low_risk_count: analyzed.filter(i => i.risk_level === 'low').length,
analysis_method: 'rule-based'
};
}
/**
* 使用AI进行分析
*/
async aiAnalysis(issues, projectPath) {
try {
// 将问题分组,每批处理一定数量
const batchSize = this.provider === 'xf' ? 5 : 10; // 科大讯飞建议小批次
const batches = [];
for (let i = 0; i < issues.length; i += batchSize) {
batches.push(issues.slice(i, i + batchSize));
}
const analyzedIssues = [];
for (const batch of batches) {
const analyzed = this.provider === 'xf'
? await this.analyzeBatchXf(batch, projectPath)
: await this.analyzeBatch(batch, projectPath);
analyzedIssues.push(...analyzed);
}
// 按风险评分排序
analyzedIssues.sort((a, b) => b.risk_score - a.risk_score);
return {
issues: analyzedIssues,
total_issues: analyzedIssues.length,
high_risk_count: analyzedIssues.filter(i => i.risk_level === 'high').length,
medium_risk_count: analyzedIssues.filter(i => i.risk_level === 'medium').length,
low_risk_count: analyzedIssues.filter(i => i.risk_level === 'low').length,
analysis_method: 'ai-powered'
};
} catch (error) {
console.error('[AI分析] AI分析失败:', error);
// 回退到规则基础分析
return this.ruleBasedAnalysis(issues);
}
}
/**
* 批量分析问题
*/
async analyzeBatch(issues, projectPath) {
const prompt = this.buildAnalysisPrompt(issues, projectPath);
try {
const requestData = JSON.stringify({
model: this.model,
messages: [
{
role: 'system',
content: '你是一个专业的代码质量分析专家擅长分析Python代码问题评估风险并提供修改建议。'
},
{
role: 'user',
content: prompt
}
],
temperature: 0.3,
max_tokens: 2000
});
const url = new URL(`${this.apiBase}/chat/completions`);
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'Content-Length': Buffer.byteLength(requestData)
}
};
const data = await new Promise((resolve, reject) => {
const client = url.protocol === 'https:' ? https : http;
const req = client.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
reject(new Error(`AI API错误: ${res.statusCode} ${res.statusMessage}`));
return;
}
try {
resolve(JSON.parse(responseData));
} catch (e) {
reject(new Error(`解析AI响应失败: ${e.message}`));
}
});
});
req.on('error', reject);
req.write(requestData);
req.end();
});
const analysisResult = JSON.parse(data.choices[0].message.content);
// 合并分析结果到原始问题
return issues.map((issue, index) => {
const analysis = analysisResult.issues[index] || {};
return {
...issue,
risk_level: analysis.risk_level || this.assessRisk(issue).level,
risk_score: analysis.risk_score || this.assessRisk(issue).score,
suggestion: analysis.suggestion || this.generateSuggestion(issue),
ai_analyzed: true
};
});
} catch (error) {
console.error('[AI分析] 批量分析失败:', error);
// 回退到规则基础分析
return issues.map(issue => {
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
});
}
}
/**
* 使用科大讯飞API批量分析问题HTTP方式
*/
async analyzeBatchXf(issues, projectPath) {
const prompt = this.buildAnalysisPrompt(issues, projectPath);
try {
// 构建请求数据 - 使用类似OpenAI的格式
const requestData = JSON.stringify({
model: this.xfModel,
messages: [
{
role: 'system',
content: '你是一个专业的代码质量分析专家擅长分析Python代码问题评估风险并提供修改建议。'
},
{
role: 'user',
content: `${prompt}\n\n请以JSON格式返回分析结果格式如下\n{\n "issues": [\n {\n "risk_level": "high|medium|low",\n "risk_score": 0-100,\n "suggestion": "具体的修改建议"\n }\n ]\n}`
}
],
temperature: 0.3,
max_tokens: 2000
});
// 解析API URL
const url = new URL(this.xfApiUrl);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${this.xfApiKey}:${this.xfApiSecret}`).toString('base64')}`,
'Content-Length': Buffer.byteLength(requestData)
}
};
console.log(`[AI分析-讯飞] 发送HTTP请求到: ${this.xfApiUrl}`);
const data = await new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
console.error(`[AI分析-讯飞] HTTP错误: ${res.statusCode} ${res.statusMessage}`);
console.error(`[AI分析-讯飞] 响应内容:`, responseData.substring(0, 500));
reject(new Error(`科大讯飞API错误: ${res.statusCode} ${res.statusMessage}`));
return;
}
try {
resolve(JSON.parse(responseData));
} catch (e) {
console.error('[AI分析-讯飞] 解析响应失败:', e);
console.error('[AI分析-讯飞] 响应内容:', responseData.substring(0, 500));
reject(new Error(`解析AI响应失败: ${e.message}`));
}
});
});
req.on('error', (error) => {
console.error('[AI分析-讯飞] 请求错误:', error);
reject(error);
});
req.write(requestData);
req.end();
});
// 提取AI返回的内容
let aiContent = '';
if (data.choices && data.choices.length > 0) {
aiContent = data.choices[0].message.content || '';
} else if (data.content) {
aiContent = data.content;
} else {
throw new Error('AI响应中未找到内容');
}
console.log(`[AI分析-讯飞] 收到响应,长度: ${aiContent.length} 字符`);
// 解析AI响应中的JSON
let analysisResult;
try {
// 尝试直接解析
analysisResult = JSON.parse(aiContent);
} catch (e) {
// 如果不是纯JSON尝试提取JSON部分
console.log('[AI分析-讯飞] 直接解析失败尝试提取JSON部分');
const jsonMatch = aiContent.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
analysisResult = JSON.parse(jsonMatch[0]);
console.log('[AI分析-讯飞] 成功提取并解析JSON');
} catch (e2) {
console.error('[AI分析-讯飞] 提取的JSON解析失败:', e2);
console.error('[AI分析-讯飞] 响应内容:', aiContent.substring(0, 500));
throw new Error('无法解析AI响应为JSON: ' + e2.message);
}
} else {
console.error('[AI分析-讯飞] 响应中未找到JSON格式');
console.error('[AI分析-讯飞] 响应内容:', aiContent.substring(0, 500));
throw new Error('AI响应中未找到有效的JSON格式');
}
}
// 验证分析结果格式
if (!analysisResult.issues || !Array.isArray(analysisResult.issues)) {
console.warn('[AI分析-讯飞] AI返回的格式不正确使用规则基础分析');
throw new Error('AI返回格式不正确');
}
console.log(`[AI分析-讯飞] 成功解析,包含 ${analysisResult.issues.length} 个问题的分析结果`);
// 合并分析结果到原始问题
return issues.map((issue, index) => {
const analysis = analysisResult.issues && analysisResult.issues[index] ? analysisResult.issues[index] : {};
return {
...issue,
risk_level: analysis.risk_level || this.assessRisk(issue).level,
risk_score: analysis.risk_score || this.assessRisk(issue).score,
suggestion: analysis.suggestion || this.generateSuggestion(issue),
ai_analyzed: true
};
});
} catch (error) {
console.error('[AI分析-讯飞] 批量分析失败:', error);
// 回退到规则基础分析
return issues.map(issue => {
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
});
}
}
/**
* 构建AI分析提示词
*/
buildAnalysisPrompt(issues, projectPath) {
const issuesText = issues.map((issue, index) => {
return `${index + 1}. 文件: ${issue.relative_path || issue.file}
行号: ${issue.line}
列号: ${issue.column || 0}
规则: ${issue.rule || '未知'}
消息: ${issue.message || '无'}
类型: ${issue.type || 'unknown'}
严重程度: ${issue.severity || 'medium'}
检测工具: ${(issue.detected_by || []).join(', ')}`;
}).join('\n\n');
return `请分析以下Python代码检查问题对每个问题提供
1. 风险等级high/medium/low
2. 风险评分0-100数值越高风险越大
3. 具体的修改建议
问题列表
${issuesText}
请以JSON格式返回格式如下
{
"issues": [
{
"risk_level": "high|medium|low",
"risk_score": 0-100,
"suggestion": "具体的修改建议,包括代码示例"
}
]
}`;
}
/**
* 评估风险规则基础
*/
assessRisk(issue) {
let score = 0;
let level = 'low';
// 基于类型
if (issue.type === 'error') score += 50;
else if (issue.type === 'warning') score += 30;
else score += 10;
// 基于严重程度
if (issue.severity === 'high') score += 40;
else if (issue.severity === 'medium') score += 20;
else score += 10;
// 基于规则类型(安全相关规则风险更高)
const securityKeywords = ['security', 'injection', 'xss', 'csrf', 'sql', 'password', 'secret', 'key', 'token'];
const ruleLower = (issue.rule || '').toLowerCase();
const messageLower = (issue.message || '').toLowerCase();
if (securityKeywords.some(keyword =>
ruleLower.includes(keyword) || messageLower.includes(keyword)
)) {
score += 30;
}
// 确定风险等级
if (score >= 70) level = 'high';
else if (score >= 40) level = 'medium';
else level = 'low';
return { level, score };
}
/**
* 生成修改建议规则基础
*/
generateSuggestion(issue) {
const rule = issue.rule || '';
const message = issue.message || '';
const ruleLower = rule.toLowerCase();
const messageLower = message.toLowerCase();
// 基于规则类型生成建议
if (ruleLower.includes('import') || messageLower.includes('import')) {
return '检查导入语句确保导入的模块存在且路径正确。考虑使用相对导入或检查PYTHONPATH设置。';
}
if (ruleLower.includes('unused') || messageLower.includes('unused')) {
return '删除未使用的变量、函数或导入。如果确实需要保留,可以在变量名前加下划线(如 _unused_var。';
}
if (ruleLower.includes('naming') || messageLower.includes('naming')) {
return '遵循PEP 8命名规范类名使用驼峰命名CamelCase函数和变量使用下划线命名snake_case。';
}
if (ruleLower.includes('security') || messageLower.includes('security') ||
ruleLower.includes('bandit')) {
return '这是一个安全问题。请仔细检查代码,确保没有安全漏洞。考虑使用安全的替代方案,如使用参数化查询而不是字符串拼接。';
}
if (ruleLower.includes('complexity') || messageLower.includes('complexity')) {
return '代码复杂度过高。考虑将函数拆分为更小的函数,提高代码可读性和可维护性。';
}
if (messageLower.includes('line too long')) {
return '行长度超过限制。将长行拆分为多行,或使用括号、反斜杠进行换行。';
}
if (messageLower.includes('missing docstring')) {
return '添加函数或类的文档字符串docstring说明其功能、参数和返回值。';
}
// 默认建议
return `根据规则"${rule}"的建议:${message}。请参考相关代码规范文档进行修改。`;
}
}
module.exports = new AIAnalyzer();

@ -61,6 +61,21 @@ if %ERRORLEVEL% NEQ 0 (
)
echo.
echo ==========================================
echo 配置AI分析服务...
echo ==========================================
echo.
echo 设置科大讯飞API配置...
set AI_PROVIDER=xf
set XF_APP_ID=96c72c9e
set XF_API_KEY=mfxzmIRlAymtEmsgVojM
set XF_API_SECRET=ZZIvBMLpPhAHSeATcoDY
set XF_MODEL=x1
set XF_API_URL=https://spark-api-open.xf-yun.com/v2/chat/completions
set AI_ANALYZER_ENABLED=true
echo [提示] AI分析服务已配置为使用科大讯飞Spark X1.5 HTTP API
echo.
echo ==========================================
echo 启动服务器...
echo ==========================================
@ -69,6 +84,8 @@ echo 服务器将在以下地址运行:
echo - 前端界面: http://localhost:5000
echo - API接口: http://localhost:5000/api
echo.
echo AI分析服务: 科大讯飞 Spark X1.5 API
echo.
echo 按 Ctrl+C 停止服务器
echo.

Loading…
Cancel
Save