From a61dec369b3688c988d9d00653e0ccfcb993d844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B4=8B?= <2146658457@qq.com> Date: Wed, 19 Nov 2025 21:06:43 +0800 Subject: [PATCH] tijiao --- src/Ollama部署指南.md | 320 +++++++++++ src/backend.js | 508 +++++++++++++++++ src/config/ai_config.json | 4 + src/config/local_models.json | 10 + src/frontend/css/style.css | 6 + src/frontend/index.html | 158 +++++- src/frontend/js/app.js | 914 ++++++++++++++++++++++++++++-- src/out/report_1758176934803.md | 48 -- src/out/report_1758177201533.md | 370 ------------ src/out/report_1758177353371.md | 370 ------------ src/server/services/aiAnalyzer.js | 544 +++++++++++++++--- src/test_code_sample.py | 50 -- src/test_flake8.js | 80 --- src/test_path.js | 31 - src/test_sample.py | 8 - src/创新点说明.md | 169 ++++++ src/清理说明.md | 83 +++ src/研究背景.md | 6 + src/配置中心设计.md | 424 -------------- src/项目功能说明.md | 172 ++++++ src/项目集成说明.md | 405 ------------- 21 files changed, 2765 insertions(+), 1915 deletions(-) create mode 100644 src/Ollama部署指南.md create mode 100644 src/config/ai_config.json create mode 100644 src/config/local_models.json delete mode 100644 src/out/report_1758176934803.md delete mode 100644 src/out/report_1758177201533.md delete mode 100644 src/out/report_1758177353371.md delete mode 100644 src/test_code_sample.py delete mode 100644 src/test_flake8.js delete mode 100644 src/test_path.js delete mode 100644 src/test_sample.py create mode 100644 src/创新点说明.md create mode 100644 src/清理说明.md create mode 100644 src/研究背景.md delete mode 100644 src/配置中心设计.md create mode 100644 src/项目功能说明.md delete mode 100644 src/项目集成说明.md diff --git a/src/Ollama部署指南.md b/src/Ollama部署指南.md new file mode 100644 index 0000000..7d95e3d --- /dev/null +++ b/src/Ollama部署指南.md @@ -0,0 +1,320 @@ +# Ollama 本地AI模型部署指南 + +## 一、前提条件 + +1. ✅ 已安装 Ollama(您已完成) +2. ✅ 已下载并运行至少一个模型(如 llama2、qwen、mistral 等) +3. ✅ Ollama 服务正在运行(默认端口 11434) + +## 二、验证 Ollama 安装 + +### 1. 检查 Ollama 服务状态 + +打开命令行,运行: + +```bash +# Windows PowerShell +curl http://localhost:11434/api/tags + +# 或者使用浏览器访问 +# http://localhost:11434/api/tags +``` + +如果返回模型列表,说明 Ollama 正常运行。 + +### 2. 测试模型是否可用 + +```bash +# 测试模型响应(使用 curl) +curl http://localhost:11434/api/generate -d "{ + \"model\": \"llama2\", + \"prompt\": \"Hello\", + \"stream\": false +}" +``` + +**注意**:将 `llama2` 替换为您实际下载的模型名称。 + +## 三、在系统中配置 Ollama + +### 方法一:通过Web界面配置(推荐) + +1. **启动系统** + ```bash + # 运行启动脚本 + start_server.bat + ``` + +2. **打开配置中心** + - 在浏览器中访问:`http://localhost:5000` + - 点击顶部导航栏的 **"配置中心"** + - 切换到 **"本地模型"** 标签页 + +3. **添加 Ollama 模型** + - 点击 **"上传模型"** 或 **"添加模型"** 按钮 + - 填写以下信息: + - **模型名称**:输入您在 Ollama 中下载的模型名称(如 `llama2`、`qwen`、`mistral` 等) + - **模型描述**:可选,如 "Ollama本地模型 - Llama2" + - **API地址**:`http://localhost:11434/v1` + - **重要**:必须是 `/v1` 结尾,这是 OpenAI 兼容格式的端点 + - 点击 **"确认添加"** + +4. **启用本地AI** + - 切换到 **"AI配置"** 标签页 + - 勾选 **"使用本地AI模型"** + - 在下拉菜单中选择刚才添加的模型 + - 点击 **"保存配置"** + +5. **测试连接** + - 在本地模型列表中,找到您添加的模型 + - 点击 **"测试连接"** 按钮 + - 如果显示 "连接成功",说明配置正确 + +### 方法二:手动配置文件(高级) + +如果Web界面无法使用,可以手动编辑配置文件: + +1. **创建配置文件目录**(如果不存在) + ``` + config/ + ├── ai_config.json + └── local_models.json + ``` + +2. **编辑 `config/local_models.json`** + ```json + [ + { + "id": "ollama-llama2", + "name": "llama2", + "description": "Ollama本地模型 - Llama2", + "apiUrl": "http://localhost:11434/v1", + "createdAt": "2025-01-XX" + } + ] + ``` + +3. **编辑 `config/ai_config.json`** + ```json + { + "useLocalAi": true, + "selectedModelId": "ollama-llama2" + } + ``` + +4. **重启服务器** + - 停止当前运行的服务器(Ctrl+C) + - 重新运行 `start_server.bat` + +## 四、常见模型配置示例 + +### 1. Llama2 +```json +{ + "id": "ollama-llama2", + "name": "llama2", + "description": "Meta Llama2 模型", + "apiUrl": "http://localhost:11434/v1" +} +``` + +### 2. Qwen(通义千问) +```json +{ + "id": "ollama-qwen", + "name": "qwen", + "description": "阿里通义千问模型", + "apiUrl": "http://localhost:11434/v1" +} +``` + +### 3. Mistral +```json +{ + "id": "ollama-mistral", + "name": "mistral", + "description": "Mistral AI 模型", + "apiUrl": "http://localhost:11434/v1" +} +``` + +### 4. CodeLlama(代码专用) +```json +{ + "id": "ollama-codellama", + "name": "codellama", + "description": "CodeLlama 代码生成模型", + "apiUrl": "http://localhost:11434/v1" +} +``` + +## 五、下载推荐的模型 + +对于代码质量分析,推荐使用以下模型: + +### 1. CodeLlama(推荐) +```bash +ollama pull codellama +``` +- **优点**:专门针对代码优化,理解代码结构更好 +- **大小**:约 3.8GB(7B参数版本) + +### 2. Qwen(中文推荐) +```bash +ollama pull qwen +``` +- **优点**:中文理解能力强,适合中文项目 +- **大小**:约 4.4GB(7B参数版本) + +### 3. Llama2(通用) +```bash +ollama pull llama2 +``` +- **优点**:通用性强,性能稳定 +- **大小**:约 3.8GB(7B参数版本) + +### 4. Mistral(高性能) +```bash +ollama pull mistral +``` +- **优点**:性能优秀,响应速度快 +- **大小**:约 4.1GB(7B参数版本) + +## 六、验证配置 + +### 1. 检查配置是否正确 + +启动服务器后,查看控制台输出,应该看到: + +``` +[AI分析] 使用提供商: 本地模型 +[AI分析] 本地模型: llama2 (http://localhost:11434/v1) +``` + +### 2. 测试代码分析功能 + +1. 创建一个测试项目 +2. 运行代码检查 +3. 查看AI分析结果 + +如果AI分析正常工作,说明配置成功。 + +## 七、常见问题排查 + +### 问题1:连接失败 + +**症状**:测试连接时显示 "连接失败" + +**解决方案**: +1. 确认 Ollama 服务正在运行 + ```bash + # 检查进程(Windows) + tasklist | findstr ollama + ``` +2. 确认端口 11434 未被占用 + ```bash + # 检查端口(Windows) + netstat -ano | findstr 11434 + ``` +3. 确认API地址正确:`http://localhost:11434/v1`(注意 `/v1` 结尾) +4. 检查防火墙是否阻止了连接 + +### 问题2:模型名称不匹配 + +**症状**:配置后无法使用,提示模型不存在 + +**解决方案**: +1. 查看已下载的模型列表 + ```bash + ollama list + ``` +2. 确保配置中的模型名称与 Ollama 中的名称完全一致(区分大小写) +3. 如果模型名称不同,重新添加模型配置 + +### 问题3:响应速度慢 + +**症状**:AI分析需要很长时间 + +**解决方案**: +1. 使用更小的模型(如 7B 参数版本) +2. 确保有足够的系统内存(建议至少 8GB) +3. 如果使用 GPU,确保 GPU 驱动正确安装 +4. 考虑使用 CodeLlama 等针对代码优化的模型 + +### 问题4:内存不足 + +**症状**:Ollama 崩溃或响应超时 + +**解决方案**: +1. 关闭其他占用内存的程序 +2. 使用更小的模型(3B 或 7B 参数) +3. 增加系统虚拟内存 +4. 如果使用 GPU,确保显存足够 + +## 八、性能优化建议 + +### 1. 选择合适的模型 + +- **代码分析**:推荐 CodeLlama 或 Qwen +- **中文项目**:推荐 Qwen +- **快速响应**:推荐 Mistral 或较小的模型 + +### 2. 系统要求 + +- **最低配置**:8GB RAM,CPU 模式 +- **推荐配置**:16GB RAM,GPU 加速(NVIDIA GPU) +- **最佳配置**:32GB+ RAM,高端 GPU + +### 3. GPU 加速(可选) + +如果您的系统有 NVIDIA GPU,可以启用 GPU 加速: + +1. 安装 CUDA 和 cuDNN +2. Ollama 会自动检测并使用 GPU +3. 查看 GPU 使用情况: + ```bash + # Windows + nvidia-smi + ``` + +## 九、下一步 + +配置完成后,您可以: + +1. ✅ 在代码检查中使用本地AI分析 +2. ✅ 享受完全离线的代码质量分析 +3. ✅ 保护代码隐私(数据不出本地) +4. ✅ 根据需要切换不同的模型 + +## 十、快速参考 + +### 常用命令 + +```bash +# 查看已安装的模型 +ollama list + +# 下载新模型 +ollama pull + +# 运行模型测试 +ollama run + +# 查看模型信息 +ollama show +``` + +### 配置文件位置 + +- AI配置:`config/ai_config.json` +- 本地模型列表:`config/local_models.json` + +### API端点 + +- Ollama API:`http://localhost:11434/v1` +- OpenAI兼容端点:`http://localhost:11434/v1/chat/completions` + +--- + +**提示**:如果遇到问题,请检查服务器控制台的错误日志,通常会有详细的错误信息。 + diff --git a/src/backend.js b/src/backend.js index 7625b75..01046fd 100644 --- a/src/backend.js +++ b/src/backend.js @@ -96,6 +96,70 @@ function saveProjects() { // 初始化 loadProjects(); +// 配置存储目录 +const CONFIG_DIR = path.join(__dirname, 'config'); +if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); +} + +// 本地模型存储目录 +const LOCAL_MODELS_DIR = path.join(__dirname, 'local_models'); +if (!fs.existsSync(LOCAL_MODELS_DIR)) { + fs.mkdirSync(LOCAL_MODELS_DIR, { recursive: true }); +} + +// 配置文件路径 +const CONFIG_FILE = path.join(CONFIG_DIR, 'ai_config.json'); +const LOCAL_MODELS_FILE = path.join(CONFIG_DIR, 'local_models.json'); + +// 加载配置 +function loadConfig() { + try { + if (fs.existsSync(CONFIG_FILE)) { + const data = fs.readFileSync(CONFIG_FILE, 'utf8'); + return JSON.parse(data); + } + } catch (error) { + console.error('加载配置失败:', error); + } + return { useLocalAi: false, selectedModelId: null }; +} + +// 保存配置 +function saveConfig(config) { + try { + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + return true; + } catch (error) { + console.error('保存配置失败:', error); + return false; + } +} + +// 加载本地模型列表 +function loadLocalModels() { + try { + if (fs.existsSync(LOCAL_MODELS_FILE)) { + const data = fs.readFileSync(LOCAL_MODELS_FILE, 'utf8'); + return JSON.parse(data); + } + } catch (error) { + console.error('加载本地模型列表失败:', error); + } + return []; +} + +// 保存本地模型列表 +function saveLocalModels(models) { + try { + fs.writeFileSync(LOCAL_MODELS_FILE, JSON.stringify(models, null, 2)); + return true; + } catch (error) { + console.error('保存本地模型列表失败:', error); + return false; + } +} + // 获取规则集文件路径 function getRuleSetPath(ruleSetName, tool) { if (!ruleSetName || !fs.existsSync(RULE_DIR)) { @@ -1209,6 +1273,448 @@ app.put('/api/projects/:id/files/content', (req, res) => { } }); +// ==================== 本地模型管理API ==================== +// 注意:这些路由必须在静态文件服务之前,否则会被拦截 + +// 获取本地模型列表 +app.get('/api/local-models', (req, res) => { + try { + const models = loadLocalModels(); + res.json({ + success: true, + models: models + }); + } catch (error) { + console.error('获取本地模型列表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 上传/添加本地模型 +app.post('/api/local-models', upload.single('file'), (req, res) => { + try { + const { name, description, apiUrl } = req.body; + + if (!name || !apiUrl) { + return res.status(400).json({ + success: false, + error: '模型名称和API地址不能为空' + }); + } + + const models = loadLocalModels(); + const modelId = `model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + const model = { + id: modelId, + name: name, + description: description || '', + apiUrl: apiUrl, + createdAt: new Date().toISOString(), + filePath: null + }; + + // 如果有上传文件,保存文件 + if (req.file) { + const modelFileDir = path.join(LOCAL_MODELS_DIR, modelId); + if (!fs.existsSync(modelFileDir)) { + fs.mkdirSync(modelFileDir, { recursive: true }); + } + const filePath = path.join(modelFileDir, req.file.originalname); + fs.renameSync(req.file.path, filePath); + model.filePath = filePath; + } + + models.push(model); + + if (saveLocalModels(models)) { + res.json({ + success: true, + message: '模型添加成功', + model: model + }); + } else { + res.status(500).json({ + success: false, + error: '保存模型信息失败' + }); + } + } catch (error) { + console.error('添加本地模型失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 测试本地模型连接 +app.post('/api/local-models/:id/test', async (req, res) => { + try { + const modelId = req.params.id; + console.log(`[测试模型] 开始测试模型: ${modelId}`); + const models = loadLocalModels(); + console.log(`[测试模型] 当前模型列表数量: ${models.length}`); + const model = models.find(m => m.id === modelId); + + if (!model) { + console.error(`[测试模型] 模型不存在: ${modelId}`); + return res.status(404).json({ + success: false, + error: '模型不存在' + }); + } + + console.log(`[测试模型] 找到模型: ${model.name}, API地址: ${model.apiUrl}`); + + // 测试本地模型连接(发送一个简单的测试请求) + const http = require('http'); + const url = require('url'); + let apiUrl; + try { + apiUrl = new URL(model.apiUrl); + } catch (error) { + console.error(`[测试模型] API地址格式错误: ${model.apiUrl}`, error); + return res.status(400).json({ + success: false, + error: 'API地址格式错误: ' + error.message + }); + } + + // 确保使用正确的OpenAI兼容端点 + let apiPath = apiUrl.pathname; + if (!apiPath.endsWith('/chat/completions')) { + // 如果路径是 /v1,自动添加 /chat/completions + if (apiPath.endsWith('/v1') || apiPath === '/v1') { + apiPath = '/v1/chat/completions'; + } else if (apiPath.endsWith('/v1/')) { + apiPath = '/v1/chat/completions'; + } else if (!apiPath || apiPath === '/') { + apiPath = '/v1/chat/completions'; + } + } + + console.log(`[测试模型] 使用路径: ${apiPath}`); + console.log(`[测试模型] 模型名称: ${model.name}`); + + const testData = JSON.stringify({ + model: model.name, + messages: [ + { role: 'user', content: 'test' } + ], + max_tokens: 10 + }); + + const options = { + hostname: apiUrl.hostname, + port: apiUrl.port || (apiUrl.protocol === 'https:' ? 443 : 80), + path: apiPath + (apiUrl.search || ''), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(testData) + }, + timeout: 10000 // 增加到10秒 + }; + + console.log(`[测试模型] 发送请求到: ${apiUrl.protocol}//${apiUrl.hostname}:${options.port}${options.path}`); + + const testPromise = new Promise((resolve, reject) => { + const protocol = apiUrl.protocol === 'https:' ? require('https') : http; + const req = protocol.request(options, (res) => { + let responseData = ''; + res.on('data', (chunk) => { + responseData += chunk; + }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(true); + } else { + console.error(`[测试模型] HTTP错误: ${res.statusCode} ${res.statusMessage}`); + console.error(`[测试模型] 响应内容:`, responseData.substring(0, 500)); + reject(new Error(`HTTP ${res.statusCode}: ${responseData.substring(0, 200)}`)); + } + }); + }); + req.on('error', (error) => { + console.error('[测试模型] 请求错误:', error); + reject(error); + }); + req.on('timeout', () => { + req.destroy(); + reject(new Error('连接超时')); + }); + req.write(testData); + req.end(); + }); + + await testPromise; + + console.log(`[测试模型] 测试成功: ${modelId}`); + res.json({ + success: true, + message: '模型连接测试成功' + }); + } catch (error) { + console.error(`[测试模型] 测试失败: ${error.message}`); + console.error(`[测试模型] 错误堆栈:`, error.stack); + res.status(500).json({ + success: false, + error: '连接测试失败: ' + error.message + }); + } +}); + +// 删除本地模型 +app.delete('/api/local-models/:id', (req, res) => { + try { + const modelId = req.params.id; + const models = loadLocalModels(); + const modelIndex = models.findIndex(m => m.id === modelId); + + if (modelIndex === -1) { + return res.status(404).json({ + success: false, + error: '模型不存在' + }); + } + + const model = models[modelIndex]; + + // 删除模型文件目录 + if (model.filePath) { + const modelDir = path.dirname(model.filePath); + if (fs.existsSync(modelDir)) { + fs.rmSync(modelDir, { recursive: true, force: true }); + } + } + + // 从列表中移除 + models.splice(modelIndex, 1); + + if (saveLocalModels(models)) { + res.json({ + success: true, + message: '模型删除成功' + }); + } else { + res.status(500).json({ + success: false, + error: '保存模型列表失败' + }); + } + } catch (error) { + console.error('删除本地模型失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// ==================== 配置管理API ==================== +// 注意:这些路由必须在静态文件服务之前,否则会被拦截 + +// 获取配置 +app.get('/api/config', (req, res) => { + try { + const config = loadConfig(); + const cloudProvider = process.env.AI_PROVIDER || 'xf'; + const providerName = cloudProvider === 'xf' ? '科大讯飞 Spark' : cloudProvider === 'openai' ? 'OpenAI' : '未配置'; + + res.json({ + success: true, + config: config, + cloudProvider: providerName + }); + } catch (error) { + console.error('获取配置失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 保存配置 +app.post('/api/config', (req, res) => { + try { + const { useLocalAi, selectedModelId } = req.body; + + if (useLocalAi && !selectedModelId) { + return res.status(400).json({ + success: false, + error: '使用本地AI时必须选择模型' + }); + } + + const config = { + useLocalAi: useLocalAi || false, + selectedModelId: useLocalAi ? selectedModelId : null + }; + + if (saveConfig(config)) { + res.json({ + success: true, + message: '配置保存成功', + config: config + }); + } else { + res.status(500).json({ + success: false, + error: '保存配置失败' + }); + } + } catch (error) { + console.error('保存配置失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 测试配置连接 +app.post('/api/config/test', async (req, res) => { + try { + console.log('[测试配置] 收到测试请求'); + const { useLocalAi, selectedModelId } = req.body; + console.log('[测试配置] 请求参数:', { useLocalAi, selectedModelId }); + + if (useLocalAi) { + if (!selectedModelId) { + console.error('[测试配置] 未选择本地模型'); + return res.status(400).json({ + success: false, + error: '请选择本地模型' + }); + } + + const models = loadLocalModels(); + console.log(`[测试配置] 当前模型列表数量: ${models.length}`); + const model = models.find(m => m.id === selectedModelId); + + if (!model) { + console.error(`[测试配置] 模型不存在: ${selectedModelId}`); + return res.status(404).json({ + success: false, + error: '模型不存在' + }); + } + + console.log(`[测试配置] 找到模型: ${model.name}, API地址: ${model.apiUrl}`); + + // 测试本地模型连接(发送一个简单的测试请求) + const http = require('http'); + const url = require('url'); + let apiUrl; + try { + apiUrl = new URL(model.apiUrl); + } catch (error) { + console.error(`[测试配置] API地址格式错误: ${model.apiUrl}`, error); + return res.status(400).json({ + success: false, + error: 'API地址格式错误: ' + error.message + }); + } + + // 确保使用正确的OpenAI兼容端点 + let apiPath = apiUrl.pathname; + if (!apiPath.endsWith('/chat/completions')) { + // 如果路径是 /v1,自动添加 /chat/completions + if (apiPath.endsWith('/v1') || apiPath === '/v1') { + apiPath = '/v1/chat/completions'; + } else if (apiPath.endsWith('/v1/')) { + apiPath = '/v1/chat/completions'; + } else if (!apiPath || apiPath === '/') { + apiPath = '/v1/chat/completions'; + } + } + + console.log(`[测试配置] 使用路径: ${apiPath}`); + console.log(`[测试配置] 模型名称: ${model.name}`); + + const testData = JSON.stringify({ + model: model.name, + messages: [ + { role: 'user', content: 'test' } + ], + max_tokens: 10 + }); + + const options = { + hostname: apiUrl.hostname, + port: apiUrl.port || (apiUrl.protocol === 'https:' ? 443 : 80), + path: apiPath + (apiUrl.search || ''), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(testData) + }, + timeout: 10000 // 增加到10秒 + }; + + console.log(`[测试配置] 发送请求到: ${apiUrl.protocol}//${apiUrl.hostname}:${options.port}${options.path}`); + + const testPromise = new Promise((resolve, reject) => { + const protocol = apiUrl.protocol === 'https:' ? require('https') : http; + const req = protocol.request(options, (res) => { + let responseData = ''; + res.on('data', (chunk) => { + responseData += chunk; + }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(true); + } else { + console.error(`[测试配置] HTTP错误: ${res.statusCode} ${res.statusMessage}`); + console.error(`[测试配置] 响应内容:`, responseData.substring(0, 500)); + reject(new Error(`HTTP ${res.statusCode}: ${responseData.substring(0, 200)}`)); + } + }); + }); + req.on('error', (error) => { + console.error('[测试配置] 请求错误:', error); + reject(error); + }); + req.on('timeout', () => { + req.destroy(); + reject(new Error('连接超时')); + }); + req.write(testData); + req.end(); + }); + + await testPromise; + + console.log(`[测试配置] 测试成功`); + res.json({ + success: true, + message: '本地模型连接测试成功' + }); + } else { + // 测试云端AI连接 + console.log('[测试配置] 使用云端AI,跳过连接测试'); + res.json({ + success: true, + message: '云端AI配置通过环境变量设置,请检查环境变量' + }); + } + } catch (error) { + console.error(`[测试配置] 测试失败: ${error.message}`); + console.error(`[测试配置] 错误堆栈:`, error.stack); + res.status(500).json({ + success: false, + error: '连接测试失败: ' + error.message + }); + } +}); + +// 注意:本地模型管理API已在第1276行定义(静态文件服务之前),此处不再重复定义 + // 静态文件服务 - 前端 app.use(express.static(path.join(__dirname, 'frontend'))); @@ -1234,6 +1740,8 @@ app.use((err, req, res, next) => { }); }); +// 注意:配置管理API已在第1486行定义(静态文件服务之前),此处不再重复定义 + // 启动服务器 app.listen(PORT, () => { console.log('================================='); diff --git a/src/config/ai_config.json b/src/config/ai_config.json new file mode 100644 index 0000000..6a91d42 --- /dev/null +++ b/src/config/ai_config.json @@ -0,0 +1,4 @@ +{ + "useLocalAi": false, + "selectedModelId": null +} \ No newline at end of file diff --git a/src/config/local_models.json b/src/config/local_models.json new file mode 100644 index 0000000..1677925 --- /dev/null +++ b/src/config/local_models.json @@ -0,0 +1,10 @@ +[ + { + "id": "model_1763556444768_81a9s6dh0", + "name": "qwen:7b", + "description": "测试", + "apiUrl": "http://localhost:11434/v1", + "createdAt": "2025-11-19T12:47:24.768Z", + "filePath": null + } +] \ No newline at end of file diff --git a/src/frontend/css/style.css b/src/frontend/css/style.css index ed82315..a4bfe8a 100644 --- a/src/frontend/css/style.css +++ b/src/frontend/css/style.css @@ -398,6 +398,12 @@ body { color: #999; font-size: 12px; margin-top: 5px; + transition: all 0.3s; +} + +.result-location:hover { + transform: translateX(2px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .result-actions { diff --git a/src/frontend/index.html b/src/frontend/index.html index 75abd82..7749045 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -242,6 +242,159 @@ + +
+ + +
+
+
+ AI配置 +
+
+ 本地模型 +
+
+ +
+ +
+
+

AI分析配置

+
+ +

+ 启用后,系统将使用本地部署的AI模型进行分析,而不是云端API。需要先上传并配置本地模型。 +

+
+ + + +
+
+ +
+

+ 当前使用的云端AI服务:未配置 +

+

+ 云端AI配置通过环境变量设置,请修改启动脚本中的环境变量。 +

+
+
+
+ +
+ + +
+
+
+ + +
+
+

本地模型管理

+ +
+ + +
+ +
+
+ +

暂无本地模型,请先上传模型

+
+
+ +
+

+ 使用说明 +

+
    +
  • 本地模型需要部署Ollama服务(推荐)或兼容OpenAI API的本地服务
  • +
  • 上传的模型文件将存储在本地,确保有足够的磁盘空间
  • +
  • 支持的模型格式:Ollama模型、GGUF格式等
  • +
  • 模型文件较大,上传可能需要较长时间
  • +
+
+
+
+
+
+
+ + + +
- +
+
+ +
diff --git a/src/frontend/js/app.js b/src/frontend/js/app.js index bc1bca5..23dc5d1 100644 --- a/src/frontend/js/app.js +++ b/src/frontend/js/app.js @@ -57,6 +57,9 @@ function showPage(pageId) { case 'rules': loadRules(); break; + case 'settings': + loadSettings(); + break; } } } @@ -178,7 +181,7 @@ function stopCheck() { if (startCheckBtn) startCheckBtn.style.display = 'inline-block'; if (stopCheckBtn) stopCheckBtn.style.display = 'none'; if (progressContainer) progressContainer.style.display = 'none'; - + // 更新 Agent 状态 updateAgentStatus('idle'); } @@ -422,10 +425,10 @@ function completeCheck() { if (startCheckBtn) startCheckBtn.style.display = 'inline-block'; if (stopCheckBtn) stopCheckBtn.style.display = 'none'; if (progressContainer) progressContainer.style.display = 'none'; - + // 更新 Agent 状态 updateAgentStatus('completed'); - + // 显示结果区域 if (resultsSection) resultsSection.style.display = 'block'; } @@ -471,7 +474,7 @@ 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 = { @@ -484,14 +487,29 @@ function createResultItem(result) { medium: '中风险', low: '低风险' }; - const riskBadge = result.risk_level ? + const riskBadge = result.risk_level ? `${riskLabels[riskLevel]}` : ''; - + // 检测工具显示 - const detectedBy = result.detected_by && result.detected_by.length > 0 - ? result.detected_by.join(', ') + const detectedBy = result.detected_by && result.detected_by.length > 0 + ? result.detected_by.join(', ') : (result.tool || '未知'); - + + // 根据风险等级设置位置信息的颜色和样式 + const locationColors = { + high: '#dc3545', + medium: '#ff9800', + low: '#28a745' + }; + const locationBgColors = { + high: '#ffebee', + medium: '#fff3e0', + low: '#e8f5e9' + }; + const locationIcon = result.type === 'error' ? 'fa-map-marker-alt' : result.type === 'warning' ? 'fa-exclamation-circle' : 'fa-info-circle'; + const locationColor = locationColors[riskLevel] || '#666'; + const locationBg = locationBgColors[riskLevel] || '#f5f5f5'; + resultItem.innerHTML = `
@@ -500,20 +518,30 @@ function createResultItem(result) { ${riskBadge}
-
${location}
+
+ + ${location} +
${result.suggestion ? `
💡 修改建议: ${result.suggestion}
` : ''}
- +
`; const desc = resultItem.querySelector('.result-description'); desc.innerHTML = `规则: ${result.rule || '未知规则'} | 严重程度: ${result.severity || 'medium'} | 检测工具: ${detectedBy}${result.confidence ? ` | 置信度: ${result.confidence}` : ''}${result.risk_score !== undefined ? ` | 风险评分: ${result.risk_score}` : ''}`; - const [detailBtn, fixBtn] = resultItem.querySelectorAll('.result-actions .btn'); - detailBtn.addEventListener('click', () => viewIssueDetail(result.rule || '', result.message || '', result.suggestion)); + const [locateBtn, fixBtn] = resultItem.querySelectorAll('.result-actions .btn'); + locateBtn.addEventListener('click', () => { + const filePath = locateBtn.getAttribute('data-file'); + const line = parseInt(locateBtn.getAttribute('data-line')); + const column = parseInt(locateBtn.getAttribute('data-column') || '0'); + locateToCode(filePath, line, column); + }); fixBtn.addEventListener('click', () => showFixSuggestion(result.rule || '', result.message || '', result.suggestion)); return resultItem; } @@ -524,7 +552,7 @@ function createResultsSummary(results, resultData = {}) { 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; @@ -978,13 +1006,13 @@ function showProjectDetail(project) { // 运行项目检查 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 requestBody = { @@ -999,11 +1027,11 @@ async function runProjectCheck(projectId, ruleSetName = null, tools = ['pylint', } 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: { @@ -1011,18 +1039,18 @@ async function runProjectCheck(projectId, ruleSetName = null, tools = ['pylint', }, 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); @@ -1281,15 +1309,15 @@ if (downloadReportBtn) { showErrorMessage('请先选择一个项目'); return; } - + if (!currentCheckResultData) { showErrorMessage('没有可下载的检查结果,请先运行检查'); return; } - + try { showLoadingOverlay('正在生成报告...'); - + const response = await fetch(`${API_BASE_URL}/projects/${currentProject.id}/export-report`, { method: 'POST', headers: { @@ -1299,12 +1327,12 @@ if (downloadReportBtn) { resultData: currentCheckResultData }) }); - + if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '下载失败'); } - + // 获取文件名(从响应头) const contentDisposition = response.headers.get('Content-Disposition'); let filename = `${currentProject.name}_报告_${new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)}.md`; @@ -1314,7 +1342,7 @@ if (downloadReportBtn) { filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, '')); } } - + // 获取文件内容并下载 const blob = await response.blob(); const url = window.URL.createObjectURL(blob); @@ -1325,7 +1353,7 @@ if (downloadReportBtn) { a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); - + hideLoadingOverlay(); showSuccessMessage('报告下载成功'); } catch (error) { @@ -1345,20 +1373,20 @@ async function showRuleSetSelectDialog(projectId) { 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; @@ -1369,25 +1397,25 @@ async function showRuleSetSelectDialog(projectId) { 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 = ''; if (data.success && data.data && data.data.length > 0) { @@ -1411,7 +1439,7 @@ async function showRuleSetSelectDialog(projectId) { console.error('[检查配置] 加载规则集列表失败:', error); showErrorMessage('加载规则集列表失败'); } - + modal.style.display = 'block'; console.log('[检查配置] 对话框已显示'); } @@ -1424,7 +1452,7 @@ document.addEventListener('DOMContentLoaded', () => { const closeRuleSetModal = document.getElementById('closeRuleSetModal'); const cancelRuleSetBtn = document.getElementById('cancelRuleSetBtn'); const confirmRuleSetBtn = document.getElementById('confirmRuleSetBtn'); - + console.log('[检查配置] 找到的元素:', { useCustomCheckbox: !!useCustomCheckbox, ruleSetSelectGroup: !!ruleSetSelectGroup, @@ -1432,7 +1460,7 @@ document.addEventListener('DOMContentLoaded', () => { cancelRuleSetBtn: !!cancelRuleSetBtn, confirmRuleSetBtn: !!confirmRuleSetBtn }); - + if (useCustomCheckbox) { useCustomCheckbox.addEventListener('change', (e) => { console.log('[检查配置] 使用自定义规则集复选框状态改变:', e.target.checked); @@ -1445,14 +1473,14 @@ document.addEventListener('DOMContentLoaded', () => { } }); } - + function closeRuleSetModalFunc() { console.log('[检查配置] 关闭对话框'); const modal = document.getElementById('ruleSetSelectModal'); if (modal) modal.style.display = 'none'; pendingCheckProjectId = null; } - + if (closeRuleSetModal) { closeRuleSetModal.addEventListener('click', () => { console.log('[检查配置] 点击关闭按钮'); @@ -1461,7 +1489,7 @@ document.addEventListener('DOMContentLoaded', () => { } else { console.warn('[检查配置] closeRuleSetModal 元素未找到'); } - + if (cancelRuleSetBtn) { cancelRuleSetBtn.addEventListener('click', () => { console.log('[检查配置] 点击取消按钮'); @@ -1470,7 +1498,7 @@ document.addEventListener('DOMContentLoaded', () => { } else { console.warn('[检查配置] cancelRuleSetBtn 元素未找到'); } - + if (confirmRuleSetBtn) { confirmRuleSetBtn.addEventListener('click', () => { console.log('[检查配置] 确认按钮被点击'); @@ -1479,53 +1507,53 @@ document.addEventListener('DOMContentLoaded', () => { 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:', { @@ -1547,7 +1575,7 @@ document.addEventListener('DOMContentLoaded', () => { } else { console.error('[检查配置] confirmRuleSetBtn 元素未找到'); } - + // 点击模态框外部关闭 const ruleSetModal = document.getElementById('ruleSetSelectModal'); if (ruleSetModal) { @@ -1774,9 +1802,18 @@ function showCodeEditor(filePath, content) { editorPanel.style.display = 'block'; + // 更新行号 + updateLineNumbers(editor); + + // 等待DOM更新后初始化滚动同步 + setTimeout(() => { + initLineNumbersScrollSync(editor); + }, 100); + // 监听内容变化 editor.oninput = () => { isFileModified = true; + updateLineNumbers(editor); }; } @@ -2078,6 +2115,355 @@ function bindFileBrowserEvents() { }); } +// ===== 代码编辑器行号功能 ===== + +/** + * 更新代码编辑器的行号 + */ +function updateLineNumbers(editor) { + const lineNumbers = document.getElementById('lineNumbers'); + if (!lineNumbers || !editor) { + console.log('[行号更新] 行号区域或编辑器不存在'); + return; + } + + const lines = editor.value.split('\n'); + const lineCount = lines.length || 1; + console.log('[行号更新] 开始更新行号,行数:', lineCount); + + // 保存当前滚动位置 + const currentScrollTop = editor.scrollTop; + console.log('[行号更新] 保存当前滚动位置:', currentScrollTop); + + // 生成行号HTML + let lineNumbersHTML = ''; + for (let i = 1; i <= lineCount; i++) { + lineNumbersHTML += `
${i}
`; + } + lineNumbers.innerHTML = lineNumbersHTML; + console.log('[行号更新] 行号HTML已生成'); + + // 同步行号区域的高度,确保可以滚动 + // 使用双重 requestAnimationFrame 确保 DOM 完全更新 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const editorHeight = editor.scrollHeight; + const editorClientHeight = editor.clientHeight; + const lineNumbersHeight = lineCount * 22.4; // 每行22.4px + const totalLineNumbersHeight = lineNumbersHeight + 30; // 加上上下padding (15px * 2) + + console.log('[行号更新] 计算高度 - 编辑器 scrollHeight:', editorHeight, 'clientHeight:', editorClientHeight); + console.log('[行号更新] 计算高度 - 行号总高度:', totalLineNumbersHeight); + + // 关键:行号区域的内容高度必须大于其可视高度才能滚动 + // 设置行号区域的内容高度(不是容器高度) + const targetContentHeight = Math.max(editorHeight, totalLineNumbersHeight); + + // 先设置 overflow,再设置高度 + lineNumbers.style.overflowY = 'scroll'; + lineNumbers.style.overflowX = 'hidden'; + + // 设置行号区域的最小高度,确保内容可以超出可视区域 + // 使用 minHeight 而不是 height,让内容自然撑开 + lineNumbers.style.minHeight = '100%'; + lineNumbers.style.height = 'auto'; + + // 强制设置行号区域内部内容的高度 + const lineNumbersContent = lineNumbers; + if (lineNumbersContent) { + // 确保行号区域的内容高度足够 + lineNumbersContent.style.display = 'block'; + } + + // 等待样式应用后再检查 + setTimeout(() => { + const actualScrollHeight = lineNumbers.scrollHeight; + const actualClientHeight = lineNumbers.clientHeight; + const maxScrollTop = actualScrollHeight - actualClientHeight; + + console.log('[行号更新] 行号区域实际尺寸 - scrollHeight:', actualScrollHeight, 'clientHeight:', actualClientHeight); + console.log('[行号更新] 行号区域最大可滚动距离:', maxScrollTop); + + // 验证是否可以滚动 + if (actualScrollHeight <= actualClientHeight) { + console.error('[行号更新] 错误:行号区域无法滚动!scrollHeight <= clientHeight'); + console.error('[行号更新] 尝试强制设置内容高度...'); + + // 强制设置内容高度 + const lineNumberDivs = lineNumbers.querySelectorAll('div'); + if (lineNumberDivs.length > 0) { + const lastDiv = lineNumberDivs[lineNumberDivs.length - 1]; + const lastDivBottom = lastDiv.offsetTop + lastDiv.offsetHeight; + console.log('[行号更新] 最后一个行号div的底部位置:', lastDivBottom); + + // 如果内容高度不够,强制设置一个更大的高度 + if (lastDivBottom < editorHeight) { + lineNumbers.style.height = editorHeight + 'px'; + console.log('[行号更新] 强制设置行号区域高度为:', editorHeight); + } + } + } + + // 恢复并同步滚动位置 + editor.scrollTop = currentScrollTop; + + // 重新检查是否可以滚动 + const finalScrollHeight = lineNumbers.scrollHeight; + const finalClientHeight = lineNumbers.clientHeight; + const finalMaxScrollTop = finalScrollHeight - finalClientHeight; + + if (finalMaxScrollTop > 0) { + // 可以滚动,设置滚动位置 + const targetScrollTop = Math.min(currentScrollTop, finalMaxScrollTop); + lineNumbers.scrollTop = targetScrollTop; + console.log('[行号更新] 恢复滚动位置 - 编辑器:', editor.scrollTop, '行号区域:', lineNumbers.scrollTop, '(最大:', finalMaxScrollTop, ')'); + } else { + console.warn('[行号更新] 警告:行号区域仍然无法滚动!scrollHeight:', finalScrollHeight, 'clientHeight:', finalClientHeight); + } + }, 10); + }); + }); +} + +/** + * 同步行号滚动 + */ +function syncLineNumbersScroll(editor) { + const lineNumbers = document.getElementById('lineNumbers'); + if (!lineNumbers || !editor) return; + // 同步滚动位置 + lineNumbers.scrollTop = editor.scrollTop; +} + +/** + * 初始化行号滚动同步 + */ +function initLineNumbersScrollSync(editor) { + if (!editor) { + console.log('[行号同步] 编辑器不存在'); + return; + } + + const lineNumbers = document.getElementById('lineNumbers'); + if (!lineNumbers) { + console.log('[行号同步] 行号区域不存在'); + return; + } + + console.log('[行号同步] 开始初始化滚动同步'); + console.log('[行号同步] 编辑器 scrollHeight:', editor.scrollHeight, 'clientHeight:', editor.clientHeight); + console.log('[行号同步] 行号区域 scrollHeight:', lineNumbers.scrollHeight, 'clientHeight:', lineNumbers.clientHeight); + + // 清除之前的定时器 + if (editor._lineNumberSyncInterval) { + clearInterval(editor._lineNumberSyncInterval); + console.log('[行号同步] 清除之前的定时器'); + } + + // 移除之前的事件监听器(如果存在) + if (editor._lineNumberScrollHandler) { + editor.removeEventListener('scroll', editor._lineNumberScrollHandler); + editor.removeEventListener('input', editor._lineNumberScrollHandler); + editor.removeEventListener('wheel', editor._lineNumberScrollHandler); + console.log('[行号同步] 移除之前的事件监听器'); + } + if (lineNumbers._editorScrollHandler) { + lineNumbers.removeEventListener('scroll', lineNumbers._editorScrollHandler); + console.log('[行号同步] 移除行号区域的事件监听器'); + } + + // 创建滚动同步函数 + const syncScroll = () => { + const editorScrollTop = editor.scrollTop; + const lineNumbersScrollTop = lineNumbers.scrollTop; + + // 检查行号区域是否可以滚动 + const lineNumbersMaxScroll = lineNumbers.scrollHeight - lineNumbers.clientHeight; + + if (lineNumbersMaxScroll <= 0) { + // 如果无法滚动,尝试修复 + if (lineNumbers.scrollHeight <= lineNumbers.clientHeight) { + console.warn('[行号同步] 行号区域无法滚动 - scrollHeight:', lineNumbers.scrollHeight, 'clientHeight:', lineNumbers.clientHeight); + // 尝试强制设置高度 + const editorHeight = editor.scrollHeight; + if (lineNumbers.scrollHeight < editorHeight) { + lineNumbers.style.height = editorHeight + 'px'; + console.log('[行号同步] 尝试修复:设置行号区域高度为:', editorHeight); + } + return; // 暂时跳过同步 + } + } + + if (lineNumbersScrollTop !== editorScrollTop) { + // 限制滚动范围 + const targetScrollTop = Math.min(editorScrollTop, lineNumbersMaxScroll); + + console.log('[行号同步] 同步滚动 - 编辑器:', editorScrollTop, '行号区域:', lineNumbersScrollTop, '最大:', lineNumbersMaxScroll, '-> 设置为:', targetScrollTop); + lineNumbers.scrollTop = targetScrollTop; + + // 验证是否设置成功 + setTimeout(() => { + const actualScrollTop = lineNumbers.scrollTop; + if (Math.abs(actualScrollTop - targetScrollTop) > 1) { + console.error('[行号同步] 警告:设置 scrollTop 失败!期望:', targetScrollTop, '实际:', actualScrollTop); + console.error('[行号同步] 调试信息 - scrollHeight:', lineNumbers.scrollHeight, 'clientHeight:', lineNumbers.clientHeight, 'maxScroll:', lineNumbersMaxScroll); + } + }, 0); + } + }; + + // 创建反向同步函数(行号滚动时同步编辑器) + const syncEditorScroll = () => { + const editorScrollTop = editor.scrollTop; + const lineNumbersScrollTop = lineNumbers.scrollTop; + + if (editorScrollTop !== lineNumbersScrollTop) { + console.log('[行号同步] 反向同步 - 行号区域:', lineNumbersScrollTop, '编辑器:', editorScrollTop, '-> 设置为:', lineNumbersScrollTop); + editor.scrollTop = lineNumbersScrollTop; + } + }; + + // 保存处理器引用以便后续清理 + editor._lineNumberScrollHandler = syncScroll; + lineNumbers._editorScrollHandler = syncEditorScroll; + + // 监听编辑器滚动事件(使用多种方式确保捕获) + editor.addEventListener('scroll', () => { + console.log('[行号同步] 编辑器 scroll 事件触发, scrollTop:', editor.scrollTop); + syncScroll(); + }, { passive: true }); + + editor.addEventListener('input', () => { + console.log('[行号同步] 编辑器 input 事件触发, scrollTop:', editor.scrollTop); + syncScroll(); + }, { passive: true }); + + editor.addEventListener('wheel', () => { + console.log('[行号同步] 编辑器 wheel 事件触发, scrollTop:', editor.scrollTop); + syncScroll(); + }, { passive: true }); + + // 监听行号区域滚动事件 + lineNumbers.addEventListener('scroll', () => { + console.log('[行号同步] 行号区域 scroll 事件触发, scrollTop:', lineNumbers.scrollTop); + syncEditorScroll(); + }, { passive: true }); + + console.log('[行号同步] 事件监听器已添加'); + + // 使用高频定时器持续同步(每16ms,约60fps) + let syncCount = 0; + editor._lineNumberSyncInterval = setInterval(() => { + syncCount++; + if (syncCount % 60 === 0) { // 每60次(约1秒)打印一次 + console.log('[行号同步] 定时器同步 - 编辑器 scrollTop:', editor.scrollTop, '行号区域 scrollTop:', lineNumbers.scrollTop); + } + syncScroll(); + }, 16); + + console.log('[行号同步] 定时器已启动,每16ms同步一次'); + + // 立即同步一次 + syncScroll(); + console.log('[行号同步] 初始化完成,当前编辑器 scrollTop:', editor.scrollTop, '行号区域 scrollTop:', lineNumbers.scrollTop); +} + +/** + * 定位到代码位置 + */ +function locateToCode(filePath, line, column) { + if (!currentProject) { + showErrorMessage('请先选择一个项目'); + return; + } + + // 打开编辑器模态框 + const modal = document.getElementById('editorModal'); + if (modal) { + modal.style.display = 'block'; + } + + // 打开文件 + openFileModal(filePath).then(() => { + // 等待文件加载完成后定位 + setTimeout(() => { + const editor = document.getElementById('modalCodeEditor'); + if (editor) { + // 计算目标行的位置 + const lines = editor.value.split('\n'); + let position = 0; + for (let i = 0; i < Math.min(line - 1, lines.length); i++) { + position += lines[i].length + 1; // +1 for newline + } + + // 设置光标位置 + editor.focus(); + editor.setSelectionRange(position, position); + + // 滚动到目标位置 + const lineHeight = 22.4; // 与CSS中的line-height一致 + const targetScrollTop = (line - 1) * lineHeight - editor.clientHeight / 2; + editor.scrollTop = Math.max(0, targetScrollTop); + + // 同步行号滚动 + syncLineNumbersScroll(editor); + + // 高亮当前行(通过添加临时样式) + highlightLineInEditor(editor, line); + + showSuccessMessage(`已定位到 ${filePath}:${line}:${column}`); + } + }, 100); + }).catch(error => { + console.error('定位失败:', error); + showErrorMessage('定位失败:' + error.message); + }); +} + +/** + * 高亮编辑器中的指定行 + */ +function highlightLineInEditor(editor, lineNumber) { + // 移除之前的高亮 + const existingHighlight = document.getElementById('editor-line-highlight'); + if (existingHighlight) { + existingHighlight.remove(); + } + + // 创建高亮元素 + const wrapper = document.getElementById('codeEditorWrapper'); + if (!wrapper) return; + + const highlight = document.createElement('div'); + highlight.id = 'editor-line-highlight'; + highlight.style.position = 'absolute'; + highlight.style.left = '50px'; // 行号宽度 + highlight.style.right = '0'; + highlight.style.height = '22.4px'; // 行高 + highlight.style.background = 'rgba(255, 235, 59, 0.3)'; + highlight.style.borderLeft = '3px solid #ffc107'; + highlight.style.pointerEvents = 'none'; + highlight.style.zIndex = '1'; + highlight.style.transition = 'opacity 0.3s'; + + // 计算位置 + const lineHeight = 22.4; + const topOffset = (lineNumber - 1) * lineHeight + 15; // 15px是padding + highlight.style.top = (topOffset - editor.scrollTop) + 'px'; + + wrapper.appendChild(highlight); + + // 3秒后淡出 + setTimeout(() => { + highlight.style.opacity = '0'; + setTimeout(() => { + if (highlight.parentNode) { + highlight.remove(); + } + }, 300); + }, 3000); +} + // ===== 文件编辑模态框逻辑 ===== const openEditorModalBtn = document.getElementById('openEditorModalBtn'); const closeEditorModal = document.getElementById('closeEditorModal'); @@ -2098,7 +2484,11 @@ if (openEditorModalBtn) { document.getElementById('modalCurrentPath').textContent = '/'; document.getElementById('modalEditorTitle').textContent = '代码编辑器'; document.getElementById('modalCurrentFilePath').textContent = '未选择文件'; - document.getElementById('modalCodeEditor').value = ''; + const editor = document.getElementById('modalCodeEditor'); + if (editor) { + editor.value = ''; + updateLineNumbers(editor); + } const modal = document.getElementById('editorModal'); if (modal) modal.style.display = 'block'; loadProjectFilesModal(''); @@ -2211,7 +2601,9 @@ function handleModalFileClick(file) { } async function openFileModal(filePath) { - if (!currentProject) return; + if (!currentProject) { + return Promise.reject(new Error('请先选择一个项目')); + } try { const response = await fetch(`${API_BASE_URL}/projects/${currentProject.id}/files/content?path=${encodeURIComponent(filePath)}`); const data = await response.json(); @@ -2219,13 +2611,30 @@ async function openFileModal(filePath) { modalCurrentFilePath = filePath; document.getElementById('modalEditorTitle').textContent = `编辑文件 - ${filePath.split('/').pop()}`; document.getElementById('modalCurrentFilePath').textContent = filePath; - document.getElementById('modalCodeEditor').value = data.data.content || ''; + const editor = document.getElementById('modalCodeEditor'); + editor.value = data.data.content || ''; + + // 更新行号 + updateLineNumbers(editor); + + // 初始化行号滚动同步 + initLineNumbersScrollSync(editor); + + // 监听内容变化 + editor.oninput = () => { + updateLineNumbers(editor); + }; + + return Promise.resolve(); } else { - showErrorMessage('打开文件失败:' + data.error); + const error = new Error(data.error || '打开文件失败'); + showErrorMessage('打开文件失败:' + error.message); + return Promise.reject(error); } } catch (e) { console.error('打开文件失败:', e); showErrorMessage('打开文件失败,请检查网络连接!'); + return Promise.reject(e); } } @@ -2399,4 +2808,387 @@ window.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; } +}); + +// ==================== 配置中心功能 ==================== + +// 加载配置中心数据 +async function loadSettings() { + console.log('[配置中心] 加载配置中心数据'); + await loadAiConfig(); + await loadLocalModels(); +} + +// 加载AI配置 +async function loadAiConfig() { + try { + const response = await fetch(`${API_BASE_URL}/config`); + if (response.ok) { + const data = await response.json(); + if (data.success) { + const config = data.config || {}; + const useLocalAi = config.useLocalAi || false; + const selectedModelId = config.selectedModelId || ''; + + document.getElementById('useLocalAi').checked = useLocalAi; + document.getElementById('localModelSelectGroup').style.display = useLocalAi ? 'block' : 'none'; + + // 加载云端AI提供商信息 + const cloudProvider = data.cloudProvider || '未配置'; + document.getElementById('currentCloudProvider').textContent = cloudProvider; + + // 加载本地模型列表并设置选中项 + await loadLocalModelsList(); + if (selectedModelId) { + document.getElementById('localModelSelect').value = selectedModelId; + } + } + } + } catch (error) { + console.error('[配置中心] 加载AI配置失败:', error); + } +} + +// 加载本地模型列表 +async function loadLocalModelsList() { + try { + const response = await fetch(`${API_BASE_URL}/local-models`); + if (response.ok) { + const data = await response.json(); + if (data.success) { + const models = data.models || []; + const select = document.getElementById('localModelSelect'); + select.innerHTML = ''; + + models.forEach(model => { + const option = document.createElement('option'); + option.value = model.id; + option.textContent = `${model.name} (${model.apiUrl})`; + select.appendChild(option); + }); + } + } + } catch (error) { + console.error('[配置中心] 加载本地模型列表失败:', error); + } +} + +// 加载本地模型(用于模型管理面板) +async function loadLocalModels() { + try { + const response = await fetch(`${API_BASE_URL}/local-models`); + if (response.ok) { + const data = await response.json(); + if (data.success) { + const models = data.models || []; + renderLocalModelsList(models); + } + } + } catch (error) { + console.error('[配置中心] 加载本地模型失败:', error); + showErrorMessage('加载本地模型列表失败: ' + error.message); + } +} + +// 渲染本地模型列表 +function renderLocalModelsList(models) { + const container = document.getElementById('localModelsList'); + if (!container) return; + + if (models.length === 0) { + container.innerHTML = ` +
+ +

暂无本地模型,请先上传模型

+
+ `; + return; + } + + container.innerHTML = models.map(model => ` +
+
+
+

+ + ${model.name} +

+ ${model.description ? `

${model.description}

` : ''} +

+ + API地址: ${model.apiUrl} +

+

+ + 创建时间: ${new Date(model.createdAt).toLocaleString('zh-CN')} +

+
+
+ + +
+
+
+ `).join(''); +} + +// 测试本地模型 +async function testLocalModel(modelId) { + try { + showLoadingOverlay('正在测试模型连接...'); + const response = await fetch(`${API_BASE_URL}/local-models/${modelId}/test`, { + method: 'POST' + }); + const data = await response.json(); + hideLoadingOverlay(); + + if (data.success) { + showSuccessMessage('模型连接测试成功!'); + } else { + showErrorMessage('模型连接测试失败: ' + (data.error || '未知错误')); + } + } catch (error) { + hideLoadingOverlay(); + console.error('[配置中心] 测试模型失败:', error); + showErrorMessage('测试模型失败: ' + error.message); + } +} + +// 删除本地模型 +async function deleteLocalModel(modelId) { + if (!confirm('确定要删除这个本地模型吗?此操作不可恢复。')) { + return; + } + + try { + showLoadingOverlay('正在删除模型...'); + const response = await fetch(`${API_BASE_URL}/local-models/${modelId}`, { + method: 'DELETE' + }); + const data = await response.json(); + hideLoadingOverlay(); + + if (data.success) { + showSuccessMessage('模型删除成功'); + await loadLocalModels(); + await loadLocalModelsList(); // 更新选择列表 + } else { + showErrorMessage('删除模型失败: ' + (data.error || '未知错误')); + } + } catch (error) { + hideLoadingOverlay(); + console.error('[配置中心] 删除模型失败:', error); + showErrorMessage('删除模型失败: ' + error.message); + } +} + +// 配置中心标签页切换 +document.addEventListener('DOMContentLoaded', () => { + const settingsTabs = document.querySelectorAll('.settings-tab'); + const settingsPanels = document.querySelectorAll('.settings-panel'); + + settingsTabs.forEach(tab => { + tab.addEventListener('click', () => { + const targetTab = tab.getAttribute('data-tab'); + + // 更新标签页状态 + settingsTabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // 更新面板显示 + settingsPanels.forEach(panel => { + panel.classList.remove('active'); + if (panel.id === `${targetTab}-panel`) { + panel.classList.add('active'); + } + }); + }); + }); + + // 使用本地AI复选框 + const useLocalAiCheckbox = document.getElementById('useLocalAi'); + if (useLocalAiCheckbox) { + useLocalAiCheckbox.addEventListener('change', (e) => { + const localModelSelectGroup = document.getElementById('localModelSelectGroup'); + if (localModelSelectGroup) { + localModelSelectGroup.style.display = e.target.checked ? 'block' : 'none'; + } + }); + } + + // 保存AI配置 + const saveAiConfigBtn = document.getElementById('saveAiConfigBtn'); + if (saveAiConfigBtn) { + saveAiConfigBtn.addEventListener('click', async () => { + const useLocalAi = document.getElementById('useLocalAi').checked; + const selectedModelId = document.getElementById('localModelSelect').value; + + if (useLocalAi && !selectedModelId) { + showErrorMessage('请先选择一个本地模型'); + return; + } + + try { + showLoadingOverlay('正在保存配置...'); + const response = await fetch(`${API_BASE_URL}/config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + useLocalAi: useLocalAi, + selectedModelId: useLocalAi ? selectedModelId : null + }) + }); + const data = await response.json(); + hideLoadingOverlay(); + + if (data.success) { + showSuccessMessage('配置保存成功'); + } else { + showErrorMessage('保存配置失败: ' + (data.error || '未知错误')); + } + } catch (error) { + hideLoadingOverlay(); + console.error('[配置中心] 保存配置失败:', error); + showErrorMessage('保存配置失败: ' + error.message); + } + }); + } + + // 测试AI配置 + const testAiConfigBtn = document.getElementById('testAiConfigBtn'); + if (testAiConfigBtn) { + testAiConfigBtn.addEventListener('click', async () => { + const useLocalAi = document.getElementById('useLocalAi').checked; + const selectedModelId = document.getElementById('localModelSelect').value; + + if (useLocalAi && !selectedModelId) { + showErrorMessage('请先选择一个本地模型'); + return; + } + + try { + showLoadingOverlay('正在测试连接...'); + const response = await fetch(`${API_BASE_URL}/config/test`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + useLocalAi: useLocalAi, + selectedModelId: useLocalAi ? selectedModelId : null + }) + }); + const data = await response.json(); + hideLoadingOverlay(); + + if (data.success) { + showSuccessMessage('连接测试成功!'); + } else { + showErrorMessage('连接测试失败: ' + (data.error || '未知错误')); + } + } catch (error) { + hideLoadingOverlay(); + console.error('[配置中心] 测试连接失败:', error); + showErrorMessage('测试连接失败: ' + error.message); + } + }); + } + + // 上传模型按钮 + const uploadModelBtn = document.getElementById('uploadModelBtn'); + if (uploadModelBtn) { + uploadModelBtn.addEventListener('click', () => { + const modal = document.getElementById('uploadModelModal'); + if (modal) { + modal.style.display = 'block'; + } + }); + } + + // 刷新模型列表 + const refreshModelsBtn = document.getElementById('refreshModelsBtn'); + if (refreshModelsBtn) { + refreshModelsBtn.addEventListener('click', () => { + loadLocalModels(); + loadLocalModelsList(); + }); + } + + // 关闭上传模型模态框 + const closeUploadModelModal = document.getElementById('closeUploadModelModal'); + const cancelUploadModel = document.getElementById('cancelUploadModel'); + if (closeUploadModelModal) { + closeUploadModelModal.addEventListener('click', () => { + document.getElementById('uploadModelModal').style.display = 'none'; + }); + } + if (cancelUploadModel) { + cancelUploadModel.addEventListener('click', () => { + document.getElementById('uploadModelModal').style.display = 'none'; + }); + } + + // 确认上传模型 + const confirmUploadModel = document.getElementById('confirmUploadModel'); + if (confirmUploadModel) { + confirmUploadModel.addEventListener('click', async () => { + const modelName = document.getElementById('modelName').value.trim(); + const modelDescription = document.getElementById('modelDescription').value.trim(); + const modelApiUrl = document.getElementById('modelApiUrl').value.trim(); + const modelFile = document.getElementById('modelFile').files[0]; + + if (!modelName) { + showErrorMessage('请输入模型名称'); + return; + } + + if (!modelApiUrl) { + showErrorMessage('请输入API地址'); + return; + } + + try { + showLoadingOverlay('正在上传模型...'); + + const formData = new FormData(); + formData.append('name', modelName); + formData.append('description', modelDescription); + formData.append('apiUrl', modelApiUrl); + if (modelFile) { + formData.append('file', modelFile); + } + + const response = await fetch(`${API_BASE_URL}/local-models`, { + method: 'POST', + body: formData + }); + + const data = await response.json(); + hideLoadingOverlay(); + + if (data.success) { + showSuccessMessage('模型上传成功'); + document.getElementById('uploadModelModal').style.display = 'none'; + document.getElementById('modelName').value = ''; + document.getElementById('modelDescription').value = ''; + document.getElementById('modelApiUrl').value = 'http://localhost:11434/v1'; + document.getElementById('modelFile').value = ''; + await loadLocalModels(); + await loadLocalModelsList(); + } else { + showErrorMessage('上传模型失败: ' + (data.error || '未知错误')); + } + } catch (error) { + hideLoadingOverlay(); + console.error('[配置中心] 上传模型失败:', error); + showErrorMessage('上传模型失败: ' + error.message); + } + }); + } }); \ No newline at end of file diff --git a/src/out/report_1758176934803.md b/src/out/report_1758176934803.md deleted file mode 100644 index 50481ed..0000000 --- a/src/out/report_1758176934803.md +++ /dev/null @@ -1,48 +0,0 @@ -# 代码质量检查报告 - -**文件:** test_sample.py -**检查时间:** 2025/9/18 14:28:54 - -## flake8 检查结果 -### 输出: -``` -D:\软件工程\代码质量检查\src\temp_check_1758176934520.py:3:1: E302 expected 2 blank lines, found 1 -D:\软件工程\代码质量检查\src\temp_check_1758176934520.py:5:37: E261 at least two spaces before inline comment -D:\软件工程\代码质量检查\src\temp_check_1758176934520.py:7:1: E305 expected 2 blank lines after class or function definition, found 1 -D:\软件工程\代码质量检查\src\temp_check_1758176934520.py:7:12: E225 missing whitespace around operator -``` - -### 解析结果: -```json -[ - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758176934520.py", - "line": 3, - "column": 1, - "code": "E302", - "message": "expected 2 blank lines, found 1" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758176934520.py", - "line": 5, - "column": 37, - "code": "E261", - "message": "at least two spaces before inline comment" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758176934520.py", - "line": 7, - "column": 1, - "code": "E305", - "message": "expected 2 blank lines after class or function definition, found 1" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758176934520.py", - "line": 7, - "column": 12, - "code": "E225", - "message": "missing whitespace around operator" - } -] -``` - diff --git a/src/out/report_1758177201533.md b/src/out/report_1758177201533.md deleted file mode 100644 index 040e225..0000000 --- a/src/out/report_1758177201533.md +++ /dev/null @@ -1,370 +0,0 @@ -# 代码质量检查报告 - -**文件:** test_sample.py -**检查时间:** 2025/9/18 14:33:21 - -## bandit 检查结果 -### 输出: -``` -{ - "errors": [], - "generated_at": "2025-09-18T06:33:19Z", - "metrics": { - "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - }, - "_totals": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - } - }, - "results": [ - { - "code": "3 def bad_function():\n4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n", - "col_offset": 4, - "end_col_offset": 34, - "filename": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "MEDIUM", - "issue_text": "Use of exec detected.", - "line_number": 4, - "line_range": [ - 4 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b102_exec_used.html", - "test_id": "B102", - "test_name": "exec_used" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b605_start_process_with_a_shell.html", - "test_id": "B605", - "test_name": "start_process_with_a_shell" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a partial executable path", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b607_start_process_with_partial_path.html", - "test_id": "B607", - "test_name": "start_process_with_partial_path" - } - ] -} -``` - -### 错误: -``` -[main] INFO profile include tests: None -[main] INFO profile exclude tests: None -[main] INFO cli include tests: None -[main] INFO cli exclude tests: None -``` - -### 解析结果: -```json -{ - "errors": [], - "generated_at": "2025-09-18T06:33:19Z", - "metrics": { - "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - }, - "_totals": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - } - }, - "results": [ - { - "code": "3 def bad_function():\n4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n", - "col_offset": 4, - "end_col_offset": 34, - "filename": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "MEDIUM", - "issue_text": "Use of exec detected.", - "line_number": 4, - "line_range": [ - 4 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b102_exec_used.html", - "test_id": "B102", - "test_name": "exec_used" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b605_start_process_with_a_shell.html", - "test_id": "B605", - "test_name": "start_process_with_a_shell" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a partial executable path", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b607_start_process_with_partial_path.html", - "test_id": "B607", - "test_name": "start_process_with_partial_path" - } - ] -} -``` - -## flake8 检查结果 -### 输出: -``` -D:\软件工程\代码质量检查\src\temp_check_1758177199898.py:3:1: E302 expected 2 blank lines, found 1 -D:\软件工程\代码质量检查\src\temp_check_1758177199898.py:5:37: E261 at least two spaces before inline comment -D:\软件工程\代码质量检查\src\temp_check_1758177199898.py:7:1: E305 expected 2 blank lines after class or function definition, found 1 -D:\软件工程\代码质量检查\src\temp_check_1758177199898.py:7:12: E225 missing whitespace around operator -``` - -### 解析结果: -```json -[ - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177199898.py", - "line": 3, - "column": 1, - "code": "E302", - "message": "expected 2 blank lines, found 1" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177199898.py", - "line": 5, - "column": 37, - "code": "E261", - "message": "at least two spaces before inline comment" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177199898.py", - "line": 7, - "column": 1, - "code": "E305", - "message": "expected 2 blank lines after class or function definition, found 1" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177199898.py", - "line": 7, - "column": 12, - "code": "E225", - "message": "missing whitespace around operator" - } -] -``` - -## pylint 检查结果 -### 输出: -``` -[ - { - "type": "convention", - "module": "1758177199344-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "missing-module-docstring", - "message": "Missing module docstring", - "message-id": "C0114" - }, - { - "type": "convention", - "module": "1758177199344-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "invalid-name", - "message": "Module name \"1758177199344-test_sample\" doesn't conform to snake_case naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "1758177199344-test_sample", - "obj": "bad_function", - "line": 3, - "column": 0, - "endLine": 3, - "endColumn": 16, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "missing-function-docstring", - "message": "Missing function or method docstring", - "message-id": "C0116" - }, - { - "type": "warning", - "module": "1758177199344-test_sample", - "obj": "bad_function", - "line": 4, - "column": 4, - "endLine": 4, - "endColumn": 34, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "exec-used", - "message": "Use of exec", - "message-id": "W0122" - } -] -``` - -### 解析结果: -```json -[ - { - "type": "convention", - "module": "1758177199344-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "missing-module-docstring", - "message": "Missing module docstring", - "message-id": "C0114" - }, - { - "type": "convention", - "module": "1758177199344-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "invalid-name", - "message": "Module name \"1758177199344-test_sample\" doesn't conform to snake_case naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "1758177199344-test_sample", - "obj": "bad_function", - "line": 3, - "column": 0, - "endLine": 3, - "endColumn": 16, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "missing-function-docstring", - "message": "Missing function or method docstring", - "message-id": "C0116" - }, - { - "type": "warning", - "module": "1758177199344-test_sample", - "obj": "bad_function", - "line": 4, - "column": 4, - "endLine": 4, - "endColumn": 34, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177199344-test_sample.py", - "symbol": "exec-used", - "message": "Use of exec", - "message-id": "W0122" - } -] -``` - diff --git a/src/out/report_1758177353371.md b/src/out/report_1758177353371.md deleted file mode 100644 index 67b8ac2..0000000 --- a/src/out/report_1758177353371.md +++ /dev/null @@ -1,370 +0,0 @@ -# 代码质量检查报告 - -**文件:** test_sample.py -**检查时间:** 2025/9/18 14:35:53 - -## bandit 检查结果 -### 输出: -``` -{ - "errors": [], - "generated_at": "2025-09-18T06:35:52Z", - "metrics": { - "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - }, - "_totals": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - } - }, - "results": [ - { - "code": "3 def bad_function():\n4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n", - "col_offset": 4, - "end_col_offset": 34, - "filename": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "MEDIUM", - "issue_text": "Use of exec detected.", - "line_number": 4, - "line_range": [ - 4 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b102_exec_used.html", - "test_id": "B102", - "test_name": "exec_used" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b605_start_process_with_a_shell.html", - "test_id": "B605", - "test_name": "start_process_with_a_shell" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a partial executable path", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b607_start_process_with_partial_path.html", - "test_id": "B607", - "test_name": "start_process_with_partial_path" - } - ] -} -``` - -### 错误: -``` -[main] INFO profile include tests: None -[main] INFO profile exclude tests: None -[main] INFO cli include tests: None -[main] INFO cli exclude tests: None -``` - -### 解析结果: -```json -{ - "errors": [], - "generated_at": "2025-09-18T06:35:52Z", - "metrics": { - "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - }, - "_totals": { - "CONFIDENCE.HIGH": 3, - "CONFIDENCE.LOW": 0, - "CONFIDENCE.MEDIUM": 0, - "CONFIDENCE.UNDEFINED": 0, - "SEVERITY.HIGH": 0, - "SEVERITY.LOW": 2, - "SEVERITY.MEDIUM": 1, - "SEVERITY.UNDEFINED": 0, - "loc": 6, - "nosec": 0, - "skipped_tests": 0 - } - }, - "results": [ - { - "code": "3 def bad_function():\n4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n", - "col_offset": 4, - "end_col_offset": 34, - "filename": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "MEDIUM", - "issue_text": "Use of exec detected.", - "line_number": 4, - "line_range": [ - 4 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b102_exec_used.html", - "test_id": "B102", - "test_name": "exec_used" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b605_start_process_with_a_shell.html", - "test_id": "B605", - "test_name": "start_process_with_a_shell" - }, - { - "code": "4 exec(\"print('Hello, world!')\") # Potential security risk\n5 os.system(\"echo This is a test\")# Missing space after comment\n6 \n", - "col_offset": 4, - "end_col_offset": 36, - "filename": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "issue_confidence": "HIGH", - "issue_cwe": { - "id": 78, - "link": "https://cwe.mitre.org/data/definitions/78.html" - }, - "issue_severity": "LOW", - "issue_text": "Starting a process with a partial executable path", - "line_number": 5, - "line_range": [ - 5 - ], - "more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b607_start_process_with_partial_path.html", - "test_id": "B607", - "test_name": "start_process_with_partial_path" - } - ] -} -``` - -## flake8 检查结果 -### 输出: -``` -D:\软件工程\代码质量检查\src\temp_check_1758177352260.py:3:1: E302 expected 2 blank lines, found 1 -D:\软件工程\代码质量检查\src\temp_check_1758177352260.py:5:37: E261 at least two spaces before inline comment -D:\软件工程\代码质量检查\src\temp_check_1758177352260.py:7:1: E305 expected 2 blank lines after class or function definition, found 1 -D:\软件工程\代码质量检查\src\temp_check_1758177352260.py:7:12: E225 missing whitespace around operator -``` - -### 解析结果: -```json -[ - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177352260.py", - "line": 3, - "column": 1, - "code": "E302", - "message": "expected 2 blank lines, found 1" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177352260.py", - "line": 5, - "column": 37, - "code": "E261", - "message": "at least two spaces before inline comment" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177352260.py", - "line": 7, - "column": 1, - "code": "E305", - "message": "expected 2 blank lines after class or function definition, found 1" - }, - { - "file": "D:\\软件工程\\代码质量检查\\src\\temp_check_1758177352260.py", - "line": 7, - "column": 12, - "code": "E225", - "message": "missing whitespace around operator" - } -] -``` - -## pylint 检查结果 -### 输出: -``` -[ - { - "type": "convention", - "module": "1758177351895-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "missing-module-docstring", - "message": "Missing module docstring", - "message-id": "C0114" - }, - { - "type": "convention", - "module": "1758177351895-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "invalid-name", - "message": "Module name \"1758177351895-test_sample\" doesn't conform to snake_case naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "1758177351895-test_sample", - "obj": "bad_function", - "line": 3, - "column": 0, - "endLine": 3, - "endColumn": 16, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "missing-function-docstring", - "message": "Missing function or method docstring", - "message-id": "C0116" - }, - { - "type": "warning", - "module": "1758177351895-test_sample", - "obj": "bad_function", - "line": 4, - "column": 4, - "endLine": 4, - "endColumn": 34, - "path": "C:\\Users\\\u5f20\u6d0b\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "exec-used", - "message": "Use of exec", - "message-id": "W0122" - } -] -``` - -### 解析结果: -```json -[ - { - "type": "convention", - "module": "1758177351895-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "missing-module-docstring", - "message": "Missing module docstring", - "message-id": "C0114" - }, - { - "type": "convention", - "module": "1758177351895-test_sample", - "obj": "", - "line": 1, - "column": 0, - "endLine": null, - "endColumn": null, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "invalid-name", - "message": "Module name \"1758177351895-test_sample\" doesn't conform to snake_case naming style", - "message-id": "C0103" - }, - { - "type": "convention", - "module": "1758177351895-test_sample", - "obj": "bad_function", - "line": 3, - "column": 0, - "endLine": 3, - "endColumn": 16, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "missing-function-docstring", - "message": "Missing function or method docstring", - "message-id": "C0116" - }, - { - "type": "warning", - "module": "1758177351895-test_sample", - "obj": "bad_function", - "line": 4, - "column": 4, - "endLine": 4, - "endColumn": 34, - "path": "C:\\Users\\张洋\\AppData\\Local\\Temp\\1758177351895-test_sample.py", - "symbol": "exec-used", - "message": "Use of exec", - "message-id": "W0122" - } -] -``` - diff --git a/src/server/services/aiAnalyzer.js b/src/server/services/aiAnalyzer.js index 856635d..0fc7b48 100644 --- a/src/server/services/aiAnalyzer.js +++ b/src/server/services/aiAnalyzer.js @@ -15,7 +15,7 @@ class AIAnalyzer { 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) // 注意:HTTP API使用APIpassword(Bearer Token),不是apikey和apisecret // 获取地址:https://console.xfyun.cn/services/bmx1 @@ -24,13 +24,20 @@ class AIAnalyzer { this.xfApiSecret = process.env.XF_API_SECRET || ''; // 兼容旧配置(WebSocket使用) this.xfModel = process.env.XF_MODEL || 'spark-x'; // Spark X1.5 模型,使用spark-x 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') { + + // 本地模型配置 + this.useLocalAi = false; + this.localModel = null; + this.loadLocalAiConfig(); + + console.log(`[AI分析] 使用提供商: ${this.useLocalAi ? '本地模型' : this.provider}`); + if (this.useLocalAi && this.localModel) { + console.log(`[AI分析] 本地模型: ${this.localModel.name} (${this.localModel.apiUrl})`); + } else if (this.provider === 'xf') { if (this.xfApiPassword) { console.log(`[AI分析] 科大讯飞 HTTP API Password: ${this.xfApiPassword.substring(0, 10)}... (已配置)`); console.log(`[AI分析] 使用模型: ${this.xfModel}`); @@ -43,6 +50,36 @@ class AIAnalyzer { } } + /** + * 加载本地AI配置 + */ + loadLocalAiConfig() { + try { + const configPath = path.join(__dirname, '..', '..', 'config', 'ai_config.json'); + if (fs.existsSync(configPath)) { + const configData = fs.readFileSync(configPath, 'utf8'); + const config = JSON.parse(configData); + this.useLocalAi = config.useLocalAi || false; + + if (this.useLocalAi && config.selectedModelId) { + const modelsPath = path.join(__dirname, '..', '..', 'config', 'local_models.json'); + if (fs.existsSync(modelsPath)) { + const modelsData = fs.readFileSync(modelsPath, 'utf8'); + const models = JSON.parse(modelsData); + this.localModel = models.find(m => m.id === config.selectedModelId); + if (!this.localModel) { + console.warn(`[AI分析] 配置的本地模型 ${config.selectedModelId} 不存在,回退到云端AI`); + this.useLocalAi = false; + } + } + } + } + } catch (error) { + console.error('[AI分析] 加载本地AI配置失败:', error); + this.useLocalAi = false; + } + } + /** * 分析检查结果 * @param {Array} issues - 检查结果列表 @@ -56,13 +93,22 @@ class AIAnalyzer { } try { + // 重新加载配置(可能已更新) + this.loadLocalAiConfig(); + // 先进行基础去重 const deduplicated = this.basicDeduplication(issues); - - // 检查是否有可用的API配置 - const hasApiConfig = (this.provider === 'openai' && this.openaiApiKey) || - (this.provider === 'xf' && this.xfApiPassword); - + + // 检查是否使用本地AI + if (this.useLocalAi && this.localModel) { + console.log('[AI分析] 使用本地AI模型进行分析'); + return await this.aiAnalysis(deduplicated.issues, projectPath); + } + + // 检查是否有可用的云端API配置 + const hasApiConfig = (this.provider === 'openai' && this.openaiApiKey) || + (this.provider === 'xf' && this.xfApiPassword); + if (hasApiConfig) { return await this.aiAnalysis(deduplicated.issues, projectPath); } else { @@ -87,7 +133,7 @@ class AIAnalyzer { 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); @@ -95,7 +141,7 @@ class AIAnalyzer { issue.message || '', existing.message || '' ); - + if (similarity < 0.7) { // 消息差异较大,可能是不同的问题 uniqueIssues.push(issue); @@ -120,7 +166,7 @@ class AIAnalyzer { issues: uniqueIssues, total_issues: uniqueIssues.length, duplicates_removed: duplicates.length, - deduplication_rate: issues.length > 0 + deduplication_rate: issues.length > 0 ? ((duplicates.length / issues.length) * 100).toFixed(2) + '%' : '0%' }; @@ -145,7 +191,7 @@ class AIAnalyzer { const analyzed = issues.map(issue => { const risk = this.assessRisk(issue); const suggestion = this.generateSuggestion(issue); - + return { ...issue, risk_level: risk.level, @@ -175,10 +221,17 @@ class AIAnalyzer { try { // 一次性发送所有问题,不再分批处理 console.log(`[AI分析] 开始AI分析,共 ${issues.length} 个问题,一次性处理`); - - const analyzedIssues = this.provider === 'xf' - ? await this.analyzeBatchXf(issues, projectPath) - : await this.analyzeBatch(issues, projectPath); + + let analyzedIssues; + + // 根据配置选择AI服务 + if (this.useLocalAi && this.localModel) { + analyzedIssues = await this.analyzeBatchLocal(issues, projectPath); + } else if (this.provider === 'xf') { + analyzedIssues = await this.analyzeBatchXf(issues, projectPath); + } else { + analyzedIssues = await this.analyzeBatch(issues, projectPath); + } // 按风险评分排序 analyzedIssues.sort((a, b) => b.risk_score - a.risk_score); @@ -189,7 +242,7 @@ class AIAnalyzer { 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' + analysis_method: this.useLocalAi ? 'local-ai' : 'ai-powered' }; } catch (error) { console.error('[AI分析] AI分析失败:', error); @@ -203,7 +256,7 @@ class AIAnalyzer { */ async analyzeBatch(issues, projectPath) { const prompt = await this.buildAnalysisPrompt(issues, projectPath); - + try { const requestData = JSON.stringify({ model: this.openaiModel, @@ -270,7 +323,7 @@ class AIAnalyzer { console.log(`[AI分析-OpenAI] 收到响应,长度: ${aiContent.length} 字符`); console.log(`[AI分析-OpenAI] 响应内容预览: ${aiContent.substring(0, 300)}`); - + // 解析AI响应中的JSON let analysisResult; try { @@ -295,21 +348,21 @@ class AIAnalyzer { throw new Error('AI响应中未找到有效的JSON格式'); } } - + // 验证分析结果格式 if (!analysisResult.issues || !Array.isArray(analysisResult.issues)) { console.warn('[AI分析-OpenAI] AI返回的格式不正确,使用规则基础分析'); console.warn('[AI分析-OpenAI] 返回的数据结构:', JSON.stringify(analysisResult).substring(0, 500)); throw new Error('AI返回格式不正确'); } - + console.log(`[AI分析-OpenAI] 成功解析,包含 ${analysisResult.issues.length} 个问题的分析结果`); - + // 合并分析结果到原始问题 return issues.map((issue, index) => { const analysis = analysisResult.issues && analysisResult.issues[index] ? analysisResult.issues[index] : {}; const aiSuggestion = analysis.suggestion || ''; - + // 验证AI返回的建议是否有效 if (!aiSuggestion || aiSuggestion.trim().length < 20) { console.warn(`[AI分析-OpenAI] 问题 ${index + 1} 的AI建议过短或为空,使用规则基础建议`); @@ -317,7 +370,7 @@ class AIAnalyzer { } else { console.log(`[AI分析-OpenAI] 问题 ${index + 1} 获得AI建议,长度: ${aiSuggestion.length} 字符`); } - + return { ...issue, risk_level: analysis.risk_level || this.assessRisk(issue).level, @@ -349,7 +402,7 @@ class AIAnalyzer { */ async analyzeBatchXf(issues, projectPath) { const prompt = await this.buildAnalysisPrompt(issues, projectPath); - + try { // 构建请求数据 - 使用类似OpenAI的格式 const requestData = JSON.stringify({ @@ -370,13 +423,13 @@ class AIAnalyzer { // 解析API URL const url = new URL(this.xfApiUrl); - + // 根据官方文档,HTTP API使用Bearer Token认证(APIpassword) // 文档:https://www.xfyun.cn/doc/spark/X1http.html if (!this.xfApiPassword) { throw new Error('未配置XF_API_PASSWORD(HTTP协议的APIpassword),请在控制台获取:https://console.xfyun.cn/services/bmx1'); } - + const options = { hostname: url.hostname, port: url.port || 443, @@ -438,12 +491,12 @@ class AIAnalyzer { console.log(`[AI分析-讯飞] 收到响应,长度: ${aiContent.length} 字符`); console.log(`[AI分析-讯飞] 响应内容预览: ${aiContent.substring(0, 500)}`); - + // 如果响应太长,也打印末尾部分 if (aiContent.length > 1000) { console.log(`[AI分析-讯飞] 响应内容末尾: ...${aiContent.substring(aiContent.length - 500)}`); } - + // 解析AI响应中的JSON let analysisResult; try { @@ -453,7 +506,7 @@ class AIAnalyzer { } catch (e) { console.log('[AI分析-讯飞] 直接解析失败,尝试提取JSON部分'); console.log('[AI分析-讯飞] 错误信息:', e.message); - + // 方法1: 尝试提取markdown代码块中的JSON let jsonText = ''; const codeBlockMatch = aiContent.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); @@ -468,7 +521,7 @@ class AIAnalyzer { console.log('[AI分析-讯飞] 从响应中提取JSON对象'); } } - + if (jsonText) { try { // 尝试清理JSON文本(移除可能的控制字符) @@ -478,44 +531,44 @@ class AIAnalyzer { } catch (e2) { console.error('[AI分析-讯飞] 提取的JSON解析失败:', e2.message); console.error('[AI分析-讯飞] 错误位置:', e2.message.match(/position (\d+)/)?.[1] || '未知'); - + // 尝试修复常见的JSON错误 try { // 先尝试找到JSON的起始和结束位置 let startPos = jsonText.indexOf('{'); let endPos = jsonText.lastIndexOf('}'); - + if (startPos >= 0 && endPos > startPos) { jsonText = jsonText.substring(startPos, endPos + 1); } - + // 尝试修复未转义的引号(在字符串值中) // 使用更智能的方法:识别JSON结构,只修复字符串值中的引号 // 策略:找到 "key": "value" 模式,修复value中的引号 - + // 使用状态机方法修复JSON中的未转义引号 // 策略:遍历JSON,识别字符串值,转义其中的未转义引号 let fixedJson = ''; let inString = false; let escapeNext = false; - + for (let i = 0; i < jsonText.length; i++) { const char = jsonText[i]; - + if (escapeNext) { // 当前字符是转义序列的一部分 fixedJson += char; escapeNext = false; continue; } - + if (char === '\\') { // 遇到反斜杠,下一个字符是转义字符 fixedJson += char; escapeNext = true; continue; } - + if (char === '"') { if (!inString) { // 字符串开始 @@ -529,7 +582,7 @@ class AIAnalyzer { while (j < jsonText.length && /\s/.test(jsonText[j])) { j++; } - + if (j >= jsonText.length) { // 到达末尾,这是字符串结束 inString = false; @@ -550,15 +603,15 @@ class AIAnalyzer { fixedJson += char; } } - + jsonText = fixedJson; - + // 尝试解析修复后的JSON analysisResult = JSON.parse(jsonText); console.log('[AI分析-讯飞] 修复后成功解析JSON'); } catch (e3) { console.error('[AI分析-讯飞] 修复后仍然失败:', e3.message); - + // 打印错误位置的上下文 const errorPos = parseInt(e3.message.match(/position (\d+)/)?.[1] || '0'); if (errorPos > 0) { @@ -567,12 +620,12 @@ class AIAnalyzer { console.error('[AI分析-讯飞] 错误位置上下文:', jsonText.substring(start, end)); console.error('[AI分析-讯飞] 错误位置标记:', ' '.repeat(Math.min(100, errorPos - start)) + '^'); } - + console.error('[AI分析-讯飞] 尝试解析的JSON文本(前1000字符):', jsonText.substring(0, 1000)); if (jsonText.length > 1000) { console.error('[AI分析-讯飞] 尝试解析的JSON文本(后1000字符):', jsonText.substring(Math.max(0, jsonText.length - 1000))); } - + // 最后尝试:使用eval(不安全,但作为最后的尝试) // 实际上,我们应该避免使用eval,而是抛出错误让用户知道 throw new Error(`无法解析AI响应为JSON: ${e2.message}。错误位置: ${errorPos}。请检查AI返回的JSON格式是否正确。`); @@ -584,34 +637,34 @@ class AIAnalyzer { throw new Error('AI响应中未找到有效的JSON格式'); } } - + // 验证分析结果格式 if (!analysisResult.issues || !Array.isArray(analysisResult.issues)) { console.warn('[AI分析-讯飞] AI返回的格式不正确,使用规则基础分析'); console.warn('[AI分析-讯飞] 返回的数据结构:', JSON.stringify(analysisResult).substring(0, 500)); throw new Error('AI返回格式不正确'); } - + console.log(`[AI分析-讯飞] 成功解析,包含 ${analysisResult.issues.length} 个问题的分析结果`); console.log(`[AI分析-讯飞] 原始问题数量: ${issues.length}`); - + // 检查返回的issues数组长度 if (analysisResult.issues.length < issues.length) { console.warn(`[AI分析-讯飞] AI返回的issues数组长度不足: ${analysisResult.issues.length} < ${issues.length}`); console.warn(`[AI分析-讯飞] 将使用规则基础分析补充缺失的问题`); - + // 打印AI返回的每个问题的建议长度,用于调试 analysisResult.issues.forEach((item, idx) => { const suggestion = item.suggestion || ''; console.log(`[AI分析-讯飞] 问题 ${idx + 1}: suggestion长度=${suggestion.length}, risk_level=${item.risk_level}, risk_score=${item.risk_score}`); }); } - + // 合并分析结果到原始问题 return issues.map((issue, index) => { const analysis = analysisResult.issues && analysisResult.issues[index] ? analysisResult.issues[index] : {}; const aiSuggestion = analysis.suggestion || ''; - + // 如果AI返回的数组长度不够,或者建议为空/过短,使用规则基础建议 if (index >= analysisResult.issues.length) { console.warn(`[AI分析-讯飞] 问题 ${index + 1} 超出AI返回范围,使用规则基础建议`); @@ -625,7 +678,7 @@ class AIAnalyzer { ai_analyzed: false }; } - + // 验证AI返回的建议是否有效 if (!aiSuggestion || aiSuggestion.trim().length < 20) { console.warn(`[AI分析-讯飞] 问题 ${index + 1} 的AI建议过短或为空,使用规则基础建议`); @@ -667,6 +720,362 @@ class AIAnalyzer { } } + /** + * 使用本地AI模型批量分析问题 + */ + async analyzeBatchLocal(issues, projectPath) { + const prompt = await this.buildAnalysisPrompt(issues, projectPath); + + try { + if (!this.localModel) { + throw new Error('本地模型未配置'); + } + + const requestData = JSON.stringify({ + model: this.localModel.name, + messages: [ + { + role: 'system', + content: '你是一个专业的Python代码质量分析专家,擅长分析代码问题,评估风险并提供详细、具体的修改建议。你的建议必须包含问题分析、修复步骤、代码示例和最佳实践。' + }, + { + role: 'user', + content: prompt + } + ], + temperature: 0.3, + max_tokens: 8000 + }); + + const apiUrl = new URL(this.localModel.apiUrl); + const http = require('http'); + const https = require('https'); + const protocol = apiUrl.protocol === 'https:' ? https : http; + + // 确保使用正确的OpenAI兼容端点 + let apiPath = apiUrl.pathname; + if (!apiPath.endsWith('/chat/completions')) { + // 如果路径是 /v1,自动添加 /chat/completions + if (apiPath.endsWith('/v1') || apiPath === '/v1') { + apiPath = '/v1/chat/completions'; + } else if (apiPath.endsWith('/v1/')) { + apiPath = '/v1/chat/completions'; + } else if (!apiPath || apiPath === '/') { + apiPath = '/v1/chat/completions'; + } + } + + const options = { + hostname: apiUrl.hostname, + port: apiUrl.port || (apiUrl.protocol === 'https:' ? 443 : 80), + path: apiPath + (apiUrl.search || ''), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestData) + }, + timeout: 120000 // 本地模型可能需要更长时间 + }; + + console.log(`[AI分析-本地] 发送HTTP请求到: ${this.localModel.apiUrl}`); + console.log(`[AI分析-本地] 使用模型: ${this.localModel.name}`); + + const data = await new Promise((resolve, reject) => { + const req = protocol.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(`本地AI 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.on('timeout', () => { + req.destroy(); + reject(new Error('请求超时')); + }); + req.write(requestData); + req.end(); + }); + + 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} 字符`); + console.log(`[AI分析-本地] 响应内容预览: ${aiContent.substring(0, 1000)}`); + + // 解析JSON响应(增强的解析逻辑) + let analysisResult; + try { + analysisResult = JSON.parse(aiContent); + console.log('[AI分析-本地] 直接解析JSON成功'); + } catch (e) { + console.log('[AI分析-本地] 直接解析失败,尝试提取并修复JSON部分'); + let jsonText = ''; + + // 尝试从markdown代码块中提取 + const codeBlockMatch = aiContent.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); + if (codeBlockMatch) { + jsonText = codeBlockMatch[1]; + console.log('[AI分析-本地] 从代码块中提取JSON'); + } else { + // 尝试提取第一个完整的JSON对象 + const jsonMatch = aiContent.match(/\{[\s\S]*\}/); + if (jsonMatch) { + jsonText = jsonMatch[0]; + console.log('[AI分析-本地] 从文本中提取JSON'); + } + } + + if (jsonText) { + try { + // 清理控制字符 + jsonText = jsonText.replace(/[\x00-\x1F\x7F]/g, ''); + + // 尝试修复常见的JSON格式错误 + // 1. 修复 "issues": 后面缺少数组括号的情况 + jsonText = jsonText.replace(/"issues"\s*:\s*([^{])/g, '"issues": [$1'); + + // 2. 修复未闭合的字符串 + jsonText = jsonText.replace(/("suggestion"\s*:\s*"[^"]*?)(\n|$)/g, '$1"'); + + // 3. 确保 issues 是数组 + if (jsonText.includes('"issues"') && !jsonText.includes('"issues"[') && !jsonText.includes('"issues": [')) { + jsonText = jsonText.replace(/"issues"\s*:\s*\{/g, '"issues": [{'); + // 如果最后一个issue对象后面没有闭合,添加闭合 + if (!jsonText.includes(']')) { + jsonText = jsonText.replace(/(\})\s*$/, '$1]'); + } + } + + // 4. 修复未转义的换行符 + jsonText = jsonText.replace(/([^\\])\n/g, '$1\\n'); + + // 5. 尝试修复未闭合的对象 + const openBraces = (jsonText.match(/\{/g) || []).length; + const closeBraces = (jsonText.match(/\}/g) || []).length; + if (openBraces > closeBraces) { + jsonText += '}'.repeat(openBraces - closeBraces); + } + + console.log('[AI分析-本地] 修复后的JSON预览:', jsonText.substring(0, 500)); + + analysisResult = JSON.parse(jsonText); + console.log('[AI分析-本地] 成功提取并解析JSON'); + } catch (e2) { + console.error('[AI分析-本地] 提取的JSON解析失败:', e2.message); + console.error('[AI分析-本地] 错误位置:', e2.message.match(/position (\d+)/)?.[1] || '未知'); + console.error('[AI分析-本地] 错误位置上下文:', jsonText.substring(Math.max(0, parseInt(e2.message.match(/position (\d+)/)?.[1] || '0') - 50), parseInt(e2.message.match(/position (\d+)/)?.[1] || '0') + 50)); + + // 如果修复失败,尝试手动解析部分数据 + console.log('[AI分析-本地] 尝试手动解析部分数据...'); + analysisResult = this.parsePartialJSON(jsonText, issues.length); + } + } else { + throw new Error('AI响应中未找到有效的JSON格式'); + } + } + + if (!analysisResult || !analysisResult.issues || !Array.isArray(analysisResult.issues)) { + console.warn('[AI分析-本地] AI返回的格式不正确,issues不是数组'); + console.warn('[AI分析-本地] analysisResult类型:', typeof analysisResult); + if (analysisResult) { + console.warn('[AI分析-本地] analysisResult keys:', Object.keys(analysisResult)); + console.warn('[AI分析-本地] analysisResult.issues类型:', typeof analysisResult.issues); + } + + // 如果issues存在但不是数组,尝试转换 + if (analysisResult && analysisResult.issues && typeof analysisResult.issues === 'object' && !Array.isArray(analysisResult.issues)) { + console.log('[AI分析-本地] 尝试将issues对象转换为数组'); + analysisResult.issues = Object.values(analysisResult.issues); + } + + // 如果仍然不是数组,抛出错误 + if (!analysisResult || !Array.isArray(analysisResult.issues)) { + throw new Error('AI返回格式不正确:issues不是数组'); + } + } + + console.log(`[AI分析-本地] 成功解析,包含 ${analysisResult.issues.length} 个问题的分析结果`); + + // 检查返回的issues数组长度 + if (analysisResult.issues.length < issues.length) { + console.warn(`[AI分析-本地] AI返回的issues数组长度不足: ${analysisResult.issues.length} < ${issues.length}`); + console.warn(`[AI分析-本地] 将为缺失的问题补充空分析结果`); + // 补充缺失的问题 + while (analysisResult.issues.length < issues.length) { + analysisResult.issues.push({ + risk_level: 'medium', + risk_score: 50, + suggestion: '' + }); + } + } else if (analysisResult.issues.length > issues.length) { + console.warn(`[AI分析-本地] AI返回的issues数组长度超出: ${analysisResult.issues.length} > ${issues.length}`); + console.warn(`[AI分析-本地] 将截取前 ${issues.length} 个结果`); + analysisResult.issues = analysisResult.issues.slice(0, issues.length); + } + + return issues.map((issue, index) => { + const analysis = analysisResult.issues && analysisResult.issues[index] ? analysisResult.issues[index] : {}; + const aiSuggestion = (analysis.suggestion || '').trim(); + + // 如果AI返回了建议(即使较短),优先使用AI的建议 + if (index < analysisResult.issues.length && aiSuggestion && aiSuggestion.length > 0) { + // 如果建议太短,补充一些基础信息 + let finalSuggestion = aiSuggestion; + if (aiSuggestion.length < 30) { + const risk = this.assessRisk(issue); + const basicSuggestion = this.generateSuggestion(issue); + finalSuggestion = `${aiSuggestion}\n\n补充建议:${basicSuggestion}`; + console.log(`[AI分析-本地] 问题 ${index + 1} 的AI建议较短(${aiSuggestion.length}字符),已补充基础建议`); + } else { + console.log(`[AI分析-本地] 问题 ${index + 1} 获得AI建议,长度: ${aiSuggestion.length} 字符`); + } + + return { + ...issue, + risk_level: analysis.risk_level || this.assessRisk(issue).level, + risk_score: analysis.risk_score !== undefined ? analysis.risk_score : this.assessRisk(issue).score, + suggestion: finalSuggestion, + ai_analyzed: true + }; + } else { + // 如果AI没有返回建议,使用规则基础建议 + console.warn(`[AI分析-本地] 问题 ${index + 1} 的AI建议为空或缺失,使用规则基础建议`); + const risk = this.assessRisk(issue); + const suggestion = this.generateSuggestion(issue); + return { + ...issue, + risk_level: analysis.risk_level || risk.level, + risk_score: analysis.risk_score !== undefined ? analysis.risk_score : risk.score, + suggestion: suggestion, + ai_analyzed: false + }; + } + }); + } 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 + }; + }); + } + } + + /** + * 手动解析部分JSON数据(当JSON格式不完整时) + */ + parsePartialJSON(jsonText, expectedCount) { + console.log('[AI分析-本地] 开始手动解析部分JSON数据'); + const parsedIssues = []; + + try { + // 尝试提取每个issue对象 - 使用更宽松的正则 + const lines = jsonText.split('\n'); + let currentIssue = null; + let currentSuggestion = ''; + let inSuggestion = false; + + for (let i = 0; i < lines.length && parsedIssues.length < expectedCount; i++) { + const line = lines[i].trim(); + + // 匹配 risk_level + const riskLevelMatch = line.match(/"risk_level"\s*:\s*"([^"]+)"/); + if (riskLevelMatch) { + if (currentIssue && currentIssue.suggestion) { + parsedIssues.push(currentIssue); + } + currentIssue = { + risk_level: riskLevelMatch[1], + risk_score: 50, + suggestion: '' + }; + inSuggestion = false; + currentSuggestion = ''; + continue; + } + + // 匹配 risk_score + const riskScoreMatch = line.match(/"risk_score"\s*:\s*(\d+)/); + if (riskScoreMatch && currentIssue) { + currentIssue.risk_score = parseInt(riskScoreMatch[1]); + continue; + } + + // 匹配 suggestion 开始 + const suggestionStartMatch = line.match(/"suggestion"\s*:\s*"([^"]*)/); + if (suggestionStartMatch) { + currentSuggestion = suggestionStartMatch[1]; + inSuggestion = true; + // 检查是否在同一行结束 + if (line.endsWith('"') && !line.endsWith('\\"')) { + if (currentIssue) { + currentIssue.suggestion = currentSuggestion; + } + currentSuggestion = ''; + inSuggestion = false; + } + continue; + } + + // 如果在suggestion中,继续收集 + if (inSuggestion && currentIssue) { + // 检查是否结束 + if (line.endsWith('"') && !line.endsWith('\\"')) { + currentSuggestion += ' ' + line.slice(0, -1); + currentIssue.suggestion = currentSuggestion; + currentSuggestion = ''; + inSuggestion = false; + } else { + currentSuggestion += ' ' + line; + } + } + } + + // 添加最后一个issue + if (currentIssue && currentIssue.suggestion) { + parsedIssues.push(currentIssue); + } + + console.log(`[AI分析-本地] 手动解析出 ${parsedIssues.length} 个问题`); + + return { issues: parsedIssues }; + } catch (error) { + console.error('[AI分析-本地] 手动解析也失败:', error); + // 返回空数组,让系统使用规则基础分析 + return { issues: [] }; + } + } /** * 构建AI分析提示词 @@ -709,12 +1118,12 @@ class AIAnalyzer { // 构建问题列表(包含完整文件内容) let issuesText = ''; let issueIndex = 1; - + for (const [filePath, fileData] of filesData.entries()) { issuesText += `\n## 文件: ${filePath}\n`; issuesText += `完整代码(共 ${fileData.lineCount} 行):\n\`\`\`python\n${fileData.content}\n\`\`\`\n\n`; issuesText += `该文件中的问题:\n`; - + for (const issue of fileData.issues) { issuesText += `${issueIndex}. 行号: ${issue.line}, 列号: ${issue.column || 0}\n`; issuesText += ` 规则: ${issue.rule || '未知'}\n`; @@ -788,11 +1197,12 @@ ${issuesText} ] } -**重要提示:** +**重要提示(最后提醒):** - 只返回JSON对象,不要包含任何解释文字 - 不要使用markdown代码块包裹JSON - JSON字符串中的换行符、引号等特殊字符必须正确转义 -- 必须返回 ${issues.length} 个issues,每个都必须有完整的suggestion!**`; +- **必须返回 ${issues.length} 个issues,每个都必须有完整的suggestion!** +- **这是硬性要求,不能违反!**如果返回的issues数量不是 ${issues.length},系统将无法正常工作!**`; } /** @@ -816,8 +1226,8 @@ ${issuesText} 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 => + + if (securityKeywords.some(keyword => ruleLower.includes(keyword) || messageLower.includes(keyword) )) { score += 30; @@ -844,28 +1254,28 @@ ${issuesText} 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') || + + 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),说明其功能、参数和返回值。'; } diff --git a/src/test_code_sample.py b/src/test_code_sample.py deleted file mode 100644 index 8893fff..0000000 --- a/src/test_code_sample.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -测试代码样例 - 用于测试 FortifyCode 系统 - -这个文件包含了各种常见的代码问题,用于测试检查工具是否正常工作。 -包括:代码风格问题、安全问题、代码质量问题等。 -""" - -import os -import sys - -# 缺少空行 (Flake8: E302) -def test_function(): - """测试函数""" - x=1+2 # 缺少空格 (Flake8: E225) - password = "hardcoded_password" # 硬编码密码 (Bandit: B105) - - # SQL注入风险 (Bandit: B608) - query = "SELECT * FROM users WHERE id = " + str(x) - - # 使用 eval 存在安全风险 (Bandit: B307) - result = eval("1 + 1") - - # 行长度超过限制 (Flake8: E501) - very_long_variable_name = "这是一个非常非常非常非常非常非常非常非常非常非常长的字符串,用于测试行长度检查" - - return result - -# 缺少空行 (Flake8: E305) -class TestClass: - """测试类""" - def __init__(self): - """初始化方法""" - self.value = 42 - - def method(self, unused_param): # 未使用的参数 (Pylint: W0613) - """测试方法""" - print(self.value) # 可以改用 logging - -# 全局变量使用不当 (Pylint: C0103) -myVariable = "should_use_snake_case" - -# 未使用的导入 (Pylint: W0611) -import json - -# 主函数 -if __name__ == "__main__": - test_function() - obj = TestClass() - obj.method("unused") - diff --git a/src/test_flake8.js b/src/test_flake8.js deleted file mode 100644 index 492ebaa..0000000 --- a/src/test_flake8.js +++ /dev/null @@ -1,80 +0,0 @@ -const { exec } = require('child_process'); -const path = require('path'); - -// 测试Flake8的实际输出 -function testFlake8() { - const testFile = path.join(__dirname, 'test_sample.py'); - - console.log('测试文件路径:', testFile); - - // 测试1: 默认格式 - console.log('\n=== 测试1: flake8 默认格式 ==='); - exec(`flake8 "${testFile}"`, (error, stdout, stderr) => { - console.log('默认格式输出:'); - console.log('stdout:', stdout); - console.log('stderr:', stderr); - }); - - // 测试2: JSON格式 - console.log('\n=== 测试2: flake8 --format=json ==='); - exec(`flake8 --format=json "${testFile}"`, (error, stdout, stderr) => { - console.log('JSON格式输出:'); - console.log('stdout:', stdout); - console.log('stderr:', stderr); - - // 尝试解析JSON - try { - if (stdout.trim()) { - const parsed = JSON.parse(stdout); - console.log('JSON解析成功:', parsed); - } else { - console.log('输出为空'); - } - } catch (e) { - console.log('JSON解析失败:', e.message); - console.log('原始输出长度:', stdout.length); - console.log('输出前50个字符:', stdout.substring(0, 50)); - } - }); - - // 测试3: quiet格式(不使用JSON) - console.log('\n=== 测试3: flake8 --quiet ==='); - exec(`flake8 --quiet "${testFile}"`, (error, stdout, stderr) => { - console.log('quiet格式输出:'); - console.log('stdout:', stdout); - console.log('stderr:', stderr); - }); - - // 测试4: 使用临时文件路径(模拟实际使用场景) - const tempFile = 'C:\\Users\\张洋\\AppData\\Local\\Temp\\test_temp_file.py'; - console.log('\n=== 测试4: 使用临时文件路径 ==='); - exec(`flake8 --quiet "${testFile}"`, (error, stdout, stderr) => { - console.log('临时文件格式输出:'); - console.log('stdout:', stdout); - console.log('stderr:', stderr); - - // 模拟我们的解析逻辑 - if (stdout.trim()) { - const lines = stdout.trim().split('\n').filter(line => line.trim()); - console.log('解析后的行数:', lines.length); - - for (const line of lines) { - console.log('处理行:', line); - const match = line.match(/^(.+?):(\d+):(\d+):\s*([A-Z]\d+)\s*(.+)$/); - if (match) { - console.log('解析成功:', { - file: match[1], - line: parseInt(match[2]), - column: parseInt(match[3]), - code: match[4], - message: match[5] - }); - } else { - console.log('解析失败:', line); - } - } - } - }); -} - -testFlake8(); diff --git a/src/test_path.js b/src/test_path.js deleted file mode 100644 index e553522..0000000 --- a/src/test_path.js +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path'); -const fs = require('fs'); - -// 模拟backend.js中的路径计算 -function testPathCalculation() { - // 模拟__dirname的值 (view目录) - const mockDirname = path.join(__dirname, 'view'); - - console.log('当前目录:', __dirname); - console.log('模拟的__dirname:', mockDirname); - - // 计算out目录路径 - const outDir = path.join(mockDirname, '..', 'out'); - console.log('计算出的out目录路径:', outDir); - - // 规范化路径 - const normalizedOutDir = path.resolve(outDir); - console.log('规范化后的out目录路径:', normalizedOutDir); - - // 检查目录是否存在 - if (fs.existsSync(normalizedOutDir)) { - console.log('目录存在'); - // 列出目录内容 - const files = fs.readdirSync(normalizedOutDir); - console.log('目录内容:', files); - } else { - console.log('目录不存在'); - } -} - -testPathCalculation(); diff --git a/src/test_sample.py b/src/test_sample.py deleted file mode 100644 index 206c56c..0000000 --- a/src/test_sample.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - -def bad_function(): - exec("print('Hello, world!')") # Potential security risk - os.system("echo This is a test")# Missing space after comment - -if __name__=="__main__": # Missing spaces around == - bad_function() diff --git a/src/创新点说明.md b/src/创新点说明.md new file mode 100644 index 0000000..ca941a3 --- /dev/null +++ b/src/创新点说明.md @@ -0,0 +1,169 @@ +# 系统创新点与差异化优势说明 + +## 一、与华为CodeArts的差异化分析 + +### 华为CodeArts的主要特点 +- 企业级DevOps平台,集成代码检查、CI/CD、项目管理等 +- 主要面向云端部署,依赖华为云服务 +- 商业产品,需要付费使用 +- 功能全面但相对封闭,定制化程度有限 + +### 本系统的核心创新点 + +#### 1. **本地AI模型支持与数据安全创新** ⭐⭐⭐ +**创新性**:这是本系统最重要的创新点 + +- **本地AI模型集成**:支持Ollama等本地AI服务,实现完全离线的代码质量分析 +- **数据安全保护**:敏感代码(如军工、金融)无需上传到云端,数据完全本地处理 +- **云端/本地无缝切换**:通过统一接口实现本地AI和云端AI(科大讯飞Spark、OpenAI)的灵活切换 +- **多AI服务适配**:支持多种AI服务提供商,不绑定单一厂商 + +**与CodeArts的差异**: +- CodeArts主要依赖云端AI服务,数据需要上传到华为云 +- 本系统提供本地AI选项,满足数据不出域的安全要求 +- 特别适合军工、金融等对数据安全要求极高的场景 + +#### 2. **多工具结果智能去重算法创新** ⭐⭐ +**创新性**:算法层面的创新 + +- **多层次去重机制**: + - 精确匹配:基于文件路径、行号、规则ID + - 模糊匹配:基于Jaccard相似度算法,识别语义相似的问题 + - 跨工具去重:自动识别Pylint、Flake8、Bandit检测到的重复问题 + +- **智能合并策略**:不仅识别重复,还能智能合并相似问题的分析结果 + +**与CodeArts的差异**: +- CodeArts可能只是简单的结果展示,缺乏深度的智能去重 +- 本系统的Jaccard相似度算法能够识别语义相似但表述不同的问题 +- 减少开发者需要处理的重复问题,提高效率 + +#### 3. **批量AI分析与上下文感知创新** ⭐⭐ +**创新性**:AI应用方式的创新 + +- **批量处理优化**:一次性将所有问题发送给AI,而不是逐个处理,大幅提升分析速度 +- **完整代码上下文**:AI分析时提供完整文件内容,而非仅代码片段,提供更准确的上下文感知分析 +- **统一分析策略**:AI先合并重复问题,再统一分析,避免重复建议 + +**与CodeArts的差异**: +- 传统方式逐个问题分析,效率低且缺乏全局视角 +- 本系统批量分析+完整上下文,提供更准确、更一致的建议 +- 减少AI调用次数,降低成本 + +#### 4. **军工代码合规性定制化创新** ⭐ +**创新性**:应用场景的创新 + +- **规则集灵活配置**:支持.pylintrc、setup.cfg、.flake8等多种配置文件格式 +- **行业定制化**:特别针对军工领域对代码质量的特殊要求,提供定制化的代码合规性检查 +- **规则文件管理**:支持规则文件的上传、编辑、应用和管理 + +**与CodeArts的差异**: +- CodeArts是通用平台,难以满足特定行业的特殊合规要求 +- 本系统提供灵活的规则定制能力,适应不同行业标准 +- 特别适合有严格合规要求的场景 + +#### 5. **轻量级Web平台与开源特性** ⭐ +**创新性**:架构和部署的创新 + +- **轻量级架构**:前后端分离,Node.js+Express,部署简单 +- **开源可定制**:代码开源,可根据需求自由定制和扩展 +- **本地部署**:支持完全本地部署,不依赖云服务 + +**与CodeArts的差异**: +- CodeArts是商业产品,需要付费,定制化受限 +- 本系统开源免费,可自由修改和扩展 +- 适合中小团队、教育机构、个人开发者使用 + +--- + +## 二、回答策略建议 + +### 回答模板1:强调数据安全和本地AI创新 + +**老师,您提到的华为CodeArts确实是一个优秀的企业级平台。但我们的系统在以下几个方面有独特的创新价值:** + +**首先,最重要的是本地AI模型支持。** CodeArts主要依赖云端AI服务,数据需要上传到华为云。而我们的系统支持Ollama等本地AI模型,可以实现完全离线的代码质量分析。这对于军工、金融等对数据安全要求极高的场景非常重要,因为敏感代码可以完全在本地处理,数据不出域。 + +**其次,我们实现了云端和本地AI的无缝切换。** 通过统一接口,用户可以根据需求选择使用本地AI(保证数据安全)或云端AI(获得更强的分析能力),这种灵活性是CodeArts所不具备的。 + +**第三,我们在多工具结果去重方面有算法创新。** 我们不仅做简单的精确匹配去重,还使用Jaccard相似度算法进行模糊匹配,能够识别语义相似但表述不同的问题,这是传统工具所缺乏的。 + +**最后,我们的系统是开源轻量级的。** CodeArts是商业产品,需要付费且定制化受限。我们的系统开源免费,特别适合中小团队、教育机构使用,也更容易根据特定行业需求进行定制化开发。 + +**因此,我们的系统不是CodeArts的简单复制,而是在数据安全、本地AI支持、智能去重算法等方面有独特创新,面向的是不同的应用场景和用户群体。** + +--- + +### 回答模板2:强调算法创新和应用场景创新 + +**老师,我理解您的关注。让我从创新角度来说明我们系统的独特价值:** + +**1. 算法层面的创新:** +- **智能去重算法**:我们使用Jaccard相似度算法进行模糊匹配,能够识别不同工具检测到的语义相似问题,这是传统工具简单去重所不具备的 +- **批量AI分析策略**:我们不是逐个问题分析,而是批量处理+完整代码上下文,让AI有全局视角,提供更准确、更一致的建议 + +**2. 应用场景的创新:** +- **本地AI支持**:这是CodeArts等商业平台所不具备的。我们支持Ollama等本地AI,满足数据不出域的安全要求,特别适合军工、金融等敏感领域 +- **行业定制化**:我们提供灵活的规则集管理,可以针对军工等特定行业的合规要求进行定制,而CodeArts作为通用平台难以满足这些特殊需求 + +**3. 技术架构的创新:** +- **多AI服务适配**:通过统一接口支持科大讯飞Spark、OpenAI、本地Ollama等多种AI服务,不绑定单一厂商 +- **轻量级开源架构**:相比CodeArts的商业化、重量级平台,我们的系统更轻量、更灵活,适合中小团队和教育场景 + +**因此,我们的创新点不在于"有没有AI",而在于"如何更好地应用AI"和"如何满足特定场景需求"。我们的系统在数据安全、算法优化、场景定制等方面有独特价值。** + +--- + +### 回答模板3:从研究价值角度回答 + +**老师,从学术研究的角度,我们的系统在以下方面有研究价值:** + +**1. 多工具协同的智能去重算法研究:** +- 我们提出了基于Jaccard相似度的跨工具问题去重算法,这在学术上是有研究价值的 +- 如何准确识别不同工具检测到的语义相似问题,是一个值得研究的算法问题 + +**2. 本地AI与云端AI的统一接口设计:** +- 我们设计了一个统一接口,能够无缝切换本地AI和云端AI,这在架构设计上有创新 +- 如何平衡数据安全和AI能力,是一个实际应用中的重要问题 + +**3. 批量AI分析与上下文感知的优化:** +- 我们研究了如何通过批量处理和完整上下文来提升AI分析的准确性和效率 +- 这是一个AI应用优化的研究问题 + +**4. 特定领域的代码合规性检查:** +- 我们针对军工等特定领域进行了定制化研究,这是应用层面的创新 +- 如何将通用工具适配到特定行业需求,是一个有价值的研究方向 + +**因此,虽然CodeArts在商业应用上很成熟,但我们的系统在算法研究、架构设计、应用创新等方面有独特的学术价值。** + +--- + +## 三、关键数据支撑(如果可能,准备一些数据) + +1. **去重效果**:我们的智能去重算法能够减少X%的重复问题 +2. **分析效率**:批量AI分析比逐个分析快X倍 +3. **数据安全**:本地AI模式确保100%的数据不出域 +4. **成本对比**:开源免费 vs CodeArts的商业费用 + +--- + +## 四、总结 + +**核心创新点排序(按重要性):** +1. **本地AI模型支持** - 数据安全创新,满足敏感场景需求 +2. **智能去重算法** - 算法创新,提升分析效率 +3. **批量AI分析** - AI应用优化,提升准确性和效率 +4. **行业定制化** - 应用场景创新,满足特定需求 +5. **开源轻量架构** - 架构创新,降低使用门槛 + +**与CodeArts的定位差异:** +- CodeArts:企业级、商业化、云端为主、通用平台 +- 本系统:开源、轻量级、本地AI支持、行业定制、教育友好 + +**回答要点:** +- 承认CodeArts的优秀,但强调我们的差异化创新 +- 重点突出本地AI支持(数据安全) +- 强调算法创新(智能去重) +- 说明应用场景的不同(军工、教育等) +- 从研究价值角度说明学术贡献 + diff --git a/src/清理说明.md b/src/清理说明.md new file mode 100644 index 0000000..0bbdcf6 --- /dev/null +++ b/src/清理说明.md @@ -0,0 +1,83 @@ +# 项目清理说明 + +## 已删除的文件 + +### 1. 测试文件(根目录) +- ✅ `test_code_sample.py` - 测试代码样例 +- ✅ `test_flake8.js` - Flake8测试脚本 +- ✅ `test_path.js` - 路径测试脚本 +- ✅ `test_sample.py` - 简单测试样例 + +### 2. 设计文档 +- ✅ `配置中心设计.md` - 配置中心设计文档(功能已实现) +- ✅ `项目集成说明.md` - 项目集成说明文档(功能已实现) + +## 保留的文档 + +### 核心项目文档 +- ✅ `README.md` - 项目说明文档 +- ✅ `项目功能说明.md` - 项目功能说明 +- ✅ `研究背景.md` - 研究背景 +- ✅ `快速开始指南.md` - 快速开始指南 +- ✅ `创新点说明.md` - 创新点说明(用于答辩) + +## 建议进一步清理的内容 + +### 1. 第三方工具的文档和测试(可选) +以下目录包含第三方工具(pylint、flake8、bandit)的源码、文档和测试,如果不需要可以删除: +- `pylint-main/doc/` - Pylint文档目录 +- `pylint-main/tests/` - Pylint测试目录 +- `flake8-main/flake8-main/docs/` - Flake8文档目录 +- `flake8-main/flake8-main/tests/` - Flake8测试目录 +- `bandit-main/bandit-main/tests/` - Bandit测试目录 +- `bandit-main/bandit-main/examples/` - Bandit示例目录 +- `bandit-main/bandit-main/doc/` - Bandit文档目录 + +**注意**:这些是第三方工具的源码,删除后不影响系统运行,但会减少项目体积。 + +### 2. 输出文件(可选) +- `out/` 目录下的旧报告文件可以定期清理,保留最新的即可 + +### 3. 空目录 +- `scripts/` 目录为空,可以删除(如果存在) + +## 清理后的项目结构 + +``` +src/ +├── backend.js # 后端主文件 +├── frontend/ # 前端文件 +├── server/ # 服务器模块 +├── config/ # 配置文件目录 +├── projects_data/ # 项目数据 +├── out/ # 输出报告 +├── rule/ # 规则文件 +├── pylint-main/ # Pylint源码(仅保留核心代码) +├── flake8-main/ # Flake8源码(仅保留核心代码) +├── bandit-main/ # Bandit源码(仅保留核心代码) +├── README.md # 项目说明 +├── 项目功能说明.md # 功能说明 +├── 研究背景.md # 研究背景 +├── 快速开始指南.md # 快速开始 +├── 创新点说明.md # 创新点说明 +└── package.json # 依赖配置 +``` + +## 注意事项 + +1. **不要删除**: + - `pylint-main/pylint/` - Pylint核心代码 + - `flake8-main/flake8-main/src/flake8/` - Flake8核心代码 + - `bandit-main/bandit-main/bandit/` - Bandit核心代码 + - 所有配置文件(`package.json`, `requirements.txt`等) + +2. **可以删除**: + - 第三方工具的文档和测试目录 + - 旧的项目报告文件 + - 空的目录 + +3. **建议保留**: + - 所有项目文档(README、功能说明等) + - 核心代码文件 + - 配置文件 + diff --git a/src/研究背景.md b/src/研究背景.md new file mode 100644 index 0000000..867e7ad --- /dev/null +++ b/src/研究背景.md @@ -0,0 +1,6 @@ +# 基于AI智能分析与多工具协同的Python代码质量检查系统 + +## 研究背景 + +随着Python在军工、金融等关键领域的广泛应用,代码质量检查的重要性日益凸显。传统的静态分析工具(如Pylint、Flake8、Bandit)虽然能有效检测代码问题,但存在工具分散、结果重复、缺乏智能分析和详细修复建议等局限性。同时,现有研究多集中在单一工具或单一AI模型的应用,缺乏多工具协同与AI智能分析的深度融合。因此,本研究旨在构建一个基于AI智能分析与多工具协同的Python代码质量检查系统,通过集成Pylint、Flake8、Bandit等主流工具实现统一检查平台,利用大语言模型进行智能去重、风险评估和详细修复建议生成,并支持本地AI模型(Ollama)和云端AI服务(科大讯飞、OpenAI)的灵活切换,特别针对军工等特殊领域提供定制化的代码合规性检查方案,以提升代码质量、提高开发效率、降低安全风险,为代码质量检查领域提供新的理论支撑和实践价值。 + diff --git a/src/配置中心设计.md b/src/配置中心设计.md deleted file mode 100644 index 405f358..0000000 --- a/src/配置中心设计.md +++ /dev/null @@ -1,424 +0,0 @@ -# 配置中心设计文档 - -## 概述 - -配置中心用于集中管理系统中的所有配置项,支持通过环境变量、配置文件或Web界面进行配置管理。 - ---- - -## 一、服务器配置 - -### 1.1 基础服务配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 服务端口 | `PORT` | `5000` | 后端服务器监听端口 | -| 主机地址 | `HOST` | `0.0.0.0` | 服务器绑定地址(0.0.0.0表示所有接口) | -| 环境模式 | `NODE_ENV` | `development` | 运行环境:development/production/test | -| API前缀 | `API_PREFIX` | `/api` | API接口统一前缀 | - -### 1.2 CORS跨域配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 允许来源 | `CORS_ORIGIN` | `*` | 允许的跨域来源(生产环境建议设置为具体域名) | -| 允许方法 | `CORS_METHODS` | `GET,POST,PUT,DELETE,OPTIONS` | 允许的HTTP方法 | -| 允许请求头 | `CORS_HEADERS` | `Content-Type,Authorization` | 允许的请求头 | -| 允许凭证 | `CORS_CREDENTIALS` | `true` | 是否允许携带凭证 | - -### 1.3 请求体限制 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| JSON大小限制 | `JSON_LIMIT` | `10mb` | JSON请求体大小限制 | -| URL编码限制 | `URL_ENCODED_LIMIT` | `10mb` | URL编码请求体大小限制 | - ---- - -## 二、AI分析服务配置 - -### 2.1 服务开关 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| AI分析启用 | `AI_ANALYZER_ENABLED` | `true` | 是否启用AI分析功能 | -| AI提供商 | `AI_PROVIDER` | `xf` | 使用的AI服务:`openai` 或 `xf` | - -### 2.2 OpenAI配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| API密钥 | `OPENAI_API_KEY` | - | OpenAI API密钥(必需) | -| API基础URL | `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI API基础地址 | -| 模型名称 | `OPENAI_MODEL` | `gpt-3.5-turbo` | 使用的模型(如:gpt-3.5-turbo, gpt-4) | -| 最大Token数 | `OPENAI_MAX_TOKENS` | `8000` | 单次请求最大Token数 | -| 温度参数 | `OPENAI_TEMPERATURE` | `0.7` | 生成文本的随机性(0-1) | -| 请求超时 | `OPENAI_TIMEOUT` | `60000` | API请求超时时间(毫秒) | - -### 2.3 科大讯飞(iFlytek)配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| API密码 | `XF_API_PASSWORD` | - | HTTP API的APIpassword(Bearer Token) | -| API密钥 | `XF_API_KEY` | - | WebSocket API的API Key(兼容旧配置) | -| API密钥 | `XF_API_SECRET` | - | WebSocket API的API Secret(兼容旧配置) | -| 模型名称 | `XF_MODEL` | `spark-x` | 使用的模型(spark-x表示Spark X1.5) | -| API地址 | `XF_API_URL` | `https://spark-api-open.xf-yun.com/v2/chat/completions` | HTTP API地址 | -| 最大Token数 | `XF_MAX_TOKENS` | `8000` | 单次请求最大Token数 | -| 请求超时 | `XF_TIMEOUT` | `60000` | API请求超时时间(毫秒) | - -### 2.4 AI分析参数 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 批量大小 | `AI_BATCH_SIZE` | `50` | 单次AI分析的问题数量 | -| 最小建议长度 | `AI_MIN_SUGGESTION_LENGTH` | `20` | AI建议的最小字符数(低于此值将使用规则建议) | -| 去重相似度阈值 | `AI_DEDUP_THRESHOLD` | `0.8` | 问题去重的相似度阈值(0-1) | - ---- - -## 三、代码检查工具配置 - -### 3.1 工具启用配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| Pylint启用 | `TOOL_PYLINT_ENABLED` | `true` | 是否启用Pylint检查 | -| Flake8启用 | `TOOL_FLAKE8_ENABLED` | `true` | 是否启用Flake8检查 | -| Bandit启用 | `TOOL_BANDIT_ENABLED` | `true` | 是否启用Bandit检查 | - -### 3.2 工具执行配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 命令超时 | `TOOL_TIMEOUT` | `60000` | 单个工具执行超时时间(毫秒) | -| 并发检查数 | `TOOL_CONCURRENT` | `3` | 同时执行的工具数量 | -| Python路径 | `PYTHON_PATH` | `python` | Python解释器路径 | - -### 3.3 Pylint配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| Pylint命令 | `PYLINT_CMD` | `python -m pylint` | Pylint执行命令 | -| 默认配置文件 | `PYLINT_DEFAULT_RC` | - | 默认的pylintrc配置文件路径 | -| 输出格式 | `PYLINT_FORMAT` | `json` | 输出格式(json/text) | - -### 3.4 Flake8配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| Flake8命令 | `FLAKE8_CMD` | `python -m flake8` | Flake8执行命令 | -| 默认配置文件 | `FLAKE8_DEFAULT_CONFIG` | - | 默认的setup.cfg或tox.ini配置文件路径 | -| 输出格式 | `FLAKE8_FORMAT` | `default` | 输出格式 | - -### 3.5 Bandit配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| Bandit命令 | `BANDIT_CMD` | `python -m bandit` | Bandit执行命令 | -| 默认配置文件 | `BANDIT_DEFAULT_CONFIG` | - | 默认的bandit配置文件路径 | -| 输出格式 | `BANDIT_FORMAT` | `json` | 输出格式(json/txt/csv) | -| 安全级别 | `BANDIT_LEVEL` | `1` | 最低报告的安全级别(1-3) | - ---- - -## 四、文件上传配置 - -### 4.1 上传限制 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 单文件大小限制 | `UPLOAD_MAX_FILE_SIZE` | `100MB` | 单个文件最大大小 | -| 总大小限制 | `UPLOAD_MAX_TOTAL_SIZE` | `500MB` | 单次上传总大小限制 | -| 文件数量限制 | `UPLOAD_MAX_FILES` | `1000` | 单次上传最大文件数 | -| 允许的文件类型 | `UPLOAD_ALLOWED_EXT` | `.py,.pyx,.pyi` | 允许的文件扩展名(逗号分隔) | - -### 4.2 存储配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 临时目录 | `UPLOAD_TEMP_DIR` | `os.tmpdir()/fortifycode_uploads` | 文件上传临时存储目录 | -| 自动清理 | `UPLOAD_AUTO_CLEANUP` | `true` | 是否自动清理临时文件 | -| 清理间隔 | `UPLOAD_CLEANUP_INTERVAL` | `3600000` | 临时文件清理间隔(毫秒,1小时) | -| 文件保留时间 | `UPLOAD_RETENTION_HOURS` | `24` | 临时文件保留时间(小时) | - ---- - -## 五、存储路径配置 - -### 5.1 目录配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 项目数据目录 | `PROJECTS_DIR` | `./projects_data` | 项目数据存储根目录 | -| 规则集目录 | `RULE_DIR` | `./rule` | 规则集配置文件存储目录 | -| 报告输出目录 | `OUT_DIR` | `./out` | 检查报告输出目录 | -| 日志目录 | `LOG_DIR` | `./logs` | 日志文件存储目录 | - -### 5.2 数据持久化 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 数据存储方式 | `DATA_STORAGE_TYPE` | `file` | 存储方式:`file`(文件)或 `database`(数据库) | -| 数据库连接 | `DATABASE_URL` | - | 数据库连接字符串(当使用数据库时) | -| 自动备份 | `DATA_AUTO_BACKUP` | `true` | 是否自动备份数据 | -| 备份间隔 | `DATA_BACKUP_INTERVAL` | `86400000` | 数据备份间隔(毫秒,24小时) | - ---- - -## 六、日志配置 - -### 6.1 日志级别 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 日志级别 | `LOG_LEVEL` | `info` | 日志级别:debug/info/warn/error | -| 控制台日志 | `LOG_CONSOLE` | `true` | 是否输出到控制台 | -| 文件日志 | `LOG_FILE` | `true` | 是否输出到文件 | -| 日志文件路径 | `LOG_FILE_PATH` | `./logs/app.log` | 日志文件路径 | - -### 6.2 日志格式 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 日志格式 | `LOG_FORMAT` | `json` | 日志格式:json/text | -| 时间格式 | `LOG_TIME_FORMAT` | `ISO8601` | 时间格式 | -| 包含堆栈 | `LOG_INCLUDE_STACK` | `false` | 错误日志是否包含堆栈信息 | - -### 6.3 日志轮转 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 日志轮转 | `LOG_ROTATE` | `true` | 是否启用日志轮转 | -| 最大文件大小 | `LOG_MAX_SIZE` | `10MB` | 单个日志文件最大大小 | -| 保留文件数 | `LOG_MAX_FILES` | `10` | 保留的日志文件数量 | - ---- - -## 七、安全配置 - -### 7.1 认证授权 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 启用认证 | `AUTH_ENABLED` | `false` | 是否启用用户认证(生产环境建议启用) | -| JWT密钥 | `JWT_SECRET` | - | JWT令牌签名密钥 | -| Token过期时间 | `JWT_EXPIRES_IN` | `24h` | Token过期时间 | -| 密码加密算法 | `PASSWORD_HASH_ALGO` | `bcrypt` | 密码加密算法 | - -### 7.2 API安全 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| API限流 | `API_RATE_LIMIT` | `false` | 是否启用API限流 | -| 限流窗口 | `API_RATE_WINDOW` | `60000` | 限流时间窗口(毫秒) | -| 最大请求数 | `API_RATE_MAX` | `100` | 时间窗口内最大请求数 | -| 请求验证 | `API_VALIDATE_REQUEST` | `true` | 是否验证请求格式 | - -### 7.3 文件安全 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 文件类型验证 | `FILE_TYPE_VALIDATION` | `true` | 是否验证文件类型 | -| 文件内容扫描 | `FILE_CONTENT_SCAN` | `false` | 是否扫描文件内容(防病毒) | -| 路径遍历防护 | `PATH_TRAVERSAL_PROTECTION` | `true` | 是否防护路径遍历攻击 | - ---- - -## 八、性能配置 - -### 8.1 缓存配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 启用缓存 | `CACHE_ENABLED` | `true` | 是否启用缓存 | -| 缓存类型 | `CACHE_TYPE` | `memory` | 缓存类型:memory/redis | -| Redis连接 | `REDIS_URL` | - | Redis连接字符串(当使用Redis时) | -| 缓存TTL | `CACHE_TTL` | `3600` | 缓存过期时间(秒) | - -### 8.2 并发配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 最大并发检查 | `MAX_CONCURRENT_CHECKS` | `5` | 同时运行的最大检查任务数 | -| 工作线程数 | `WORKER_THREADS` | `4` | 工作线程数量 | -| 任务队列大小 | `TASK_QUEUE_SIZE` | `100` | 任务队列最大大小 | - ---- - -## 九、前端配置 - -### 9.1 UI配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 主题 | `UI_THEME` | `light` | 界面主题:light/dark | -| 语言 | `UI_LANGUAGE` | `zh-CN` | 界面语言:zh-CN/en-US | -| 每页显示数 | `UI_ITEMS_PER_PAGE` | `20` | 列表每页显示的项目数 | -| 自动刷新 | `UI_AUTO_REFRESH` | `false` | 是否自动刷新数据 | -| 刷新间隔 | `UI_REFRESH_INTERVAL` | `30000` | 自动刷新间隔(毫秒) | - -### 9.2 编辑器配置 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 编辑器主题 | `EDITOR_THEME` | `vs` | 代码编辑器主题 | -| 字体大小 | `EDITOR_FONT_SIZE` | `14` | 编辑器字体大小 | -| 显示行号 | `EDITOR_LINE_NUMBERS` | `true` | 是否显示行号 | -| 自动换行 | `EDITOR_WORD_WRAP` | `false` | 是否自动换行 | -| Tab大小 | `EDITOR_TAB_SIZE` | `4` | Tab键缩进空格数 | - ---- - -## 十、通知配置 - -### 10.1 邮件通知 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 启用邮件 | `EMAIL_ENABLED` | `false` | 是否启用邮件通知 | -| SMTP服务器 | `SMTP_HOST` | - | SMTP服务器地址 | -| SMTP端口 | `SMTP_PORT` | `587` | SMTP端口 | -| 发件人邮箱 | `SMTP_FROM` | - | 发件人邮箱地址 | -| 发件人密码 | `SMTP_PASSWORD` | - | 发件人邮箱密码 | -| 收件人列表 | `EMAIL_RECIPIENTS` | - | 收件人邮箱列表(逗号分隔) | - -### 10.2 Webhook通知 - -| 配置项 | 环境变量 | 默认值 | 说明 | -|--------|---------|--------|------| -| 启用Webhook | `WEBHOOK_ENABLED` | `false` | 是否启用Webhook通知 | -| Webhook URL | `WEBHOOK_URL` | - | Webhook回调地址 | -| Webhook密钥 | `WEBHOOK_SECRET` | - | Webhook签名密钥 | - ---- - -## 十一、配置管理方式 - -### 11.1 环境变量(推荐) - -在启动脚本或系统环境变量中设置: - -```bash -# Windows (start_server.bat) -set PORT=5000 -set AI_PROVIDER=xf -set XF_API_PASSWORD=your_api_password - -# Linux/Mac (start_server.sh) -export PORT=5000 -export AI_PROVIDER=xf -export XF_API_PASSWORD=your_api_password -``` - -### 11.2 配置文件(.env) - -创建 `.env` 文件(需要安装 `dotenv` 包): - -```env -PORT=5000 -AI_PROVIDER=xf -XF_API_PASSWORD=your_api_password -TOOL_TIMEOUT=60000 -UPLOAD_MAX_FILE_SIZE=100MB -``` - -### 11.3 Web配置界面(未来功能) - -- 提供Web界面进行配置管理 -- 支持配置的导入/导出 -- 配置变更历史记录 -- 配置验证和测试 - ---- - -## 十二、配置优先级 - -配置项的优先级(从高到低): - -1. **环境变量** - 最高优先级 -2. **配置文件(.env)** - 中等优先级 -3. **代码默认值** - 最低优先级 - ---- - -## 十三、配置验证 - -系统启动时应验证: - -1. ✅ 必需的配置项是否存在(如AI API密钥) -2. ✅ 配置值的格式是否正确(如端口号范围) -3. ✅ 路径配置是否存在且可访问 -4. ✅ 外部服务连接是否正常(如AI API、数据库) - ---- - -## 十四、配置示例 - -### 14.1 开发环境配置 - -```env -NODE_ENV=development -PORT=5000 -LOG_LEVEL=debug -AI_ANALYZER_ENABLED=true -AI_PROVIDER=xf -XF_API_PASSWORD=your_dev_password -TOOL_TIMEOUT=30000 -UPLOAD_MAX_FILE_SIZE=50MB -``` - -### 14.2 生产环境配置 - -```env -NODE_ENV=production -PORT=5000 -LOG_LEVEL=info -CORS_ORIGIN=https://yourdomain.com -AI_ANALYZER_ENABLED=true -AI_PROVIDER=xf -XF_API_PASSWORD=your_prod_password -TOOL_TIMEOUT=120000 -UPLOAD_MAX_FILE_SIZE=200MB -AUTH_ENABLED=true -API_RATE_LIMIT=true -DATA_AUTO_BACKUP=true -``` - ---- - -## 十五、配置迁移建议 - -1. **从硬编码迁移到配置中心** - - 识别所有硬编码的配置值 - - 逐步迁移到环境变量或配置文件 - - 保持向后兼容 - -2. **配置文档化** - - 维护配置项说明文档 - - 提供配置示例 - - 记录配置变更历史 - -3. **配置安全** - - 敏感配置(如API密钥)使用环境变量 - - 不要将敏感配置提交到版本控制 - - 使用 `.gitignore` 排除配置文件 - ---- - -## 总结 - -配置中心应包含以上所有配置项,支持: -- ✅ 环境变量配置 -- ✅ 配置文件管理 -- ✅ 配置验证 -- ✅ 配置优先级 -- ✅ 配置文档化 -- ✅ 敏感信息保护 - -通过集中管理配置,可以: -- 提高系统的可维护性 -- 便于环境切换(开发/测试/生产) -- 增强系统安全性 -- 简化部署流程 - diff --git a/src/项目功能说明.md b/src/项目功能说明.md new file mode 100644 index 0000000..405a49a --- /dev/null +++ b/src/项目功能说明.md @@ -0,0 +1,172 @@ +# 项目功能说明 + +## 基于AI智能分析与多工具协同的Python代码质量检查系统 + +### 核心功能 + +本项目开发了四个核心功能模块:**多工具协同检查**集成Pylint、Flake8、Bandit三大主流Python代码检查工具,实现并行执行和结果统一,全面覆盖代码质量、编码规范和安全漏洞检测;**AI智能分析模块**利用大语言模型对检查结果进行智能去重(基于文件路径、行号、规则ID和消息相似度匹配)、风险评估(划分高、中、低风险等级并给出0-100分评分)和详细修复建议生成(包含问题分析、修复步骤、代码示例和最佳实践);**AI服务支持**提供灵活的AI服务选择,支持云端AI服务(科大讯飞Spark、OpenAI)和本地AI模型(Ollama等),通过统一接口实现本地与云端AI的无缝切换,满足数据安全和离线使用需求;**规则集管理**支持自定义规则集配置(.pylintrc、setup.cfg、.flake8等),实现规则文件的上传、编辑、应用和管理,满足不同行业和项目的特殊合规性要求。这四个核心功能模块协同工作,形成了从代码检查、智能分析到规则定制的完整代码质量检查解决方案。 + +#### 5. 项目管理 + +**功能描述**:提供完整的项目生命周期管理,支持项目创建、导入、查看和删除。 + +**主要特性**: +- **项目创建**: + - 支持从本地文件夹导入项目 + - 支持GitHub/Gitee仓库克隆 + - 支持文件上传创建项目 + +- **项目信息**: + - 项目基本信息展示(名称、路径、创建时间等) + - 项目文件树浏览 + - 项目检查历史记录 + +- **项目操作**: + - 项目删除 + - 项目文件管理 + - 项目配置管理 + +#### 6. 在线代码编辑 + +**功能描述**:提供Web端的代码编辑器,支持在线查看、编辑和保存代码文件。 + +**主要特性**: +- **文件浏览**: + - 树形文件结构展示 + - 文件路径导航 + - 文件类型过滤 + +- **代码编辑**: + - 在线代码编辑器 + - 语法高亮(支持Python) + - 代码保存功能 + +- **问题定位**: + - 检查结果一键定位到代码位置 + - 高亮显示问题代码行 + - 快速跳转到问题文件 + +#### 7. 检查结果展示 + +**功能描述**:提供直观、详细的检查结果展示,帮助开发者快速理解问题。 + +**主要特性**: +- **结果分类**: + - 按问题类型分类(错误、警告、信息) + - 按风险等级分类(高、中、低风险) + - 按文件分组显示 + +- **详细信息**: + - 问题位置(文件、行号、列号) + - 问题规则和消息 + - 风险等级和评分 + - AI生成的详细修复建议 + +- **结果筛选**: + - 按类型筛选(全部/错误/警告/信息) + - 按风险等级筛选 + - 按文件筛选 + +#### 8. 报告导出 + +**功能描述**:支持将检查结果导出为Markdown格式的报告文件。 + +**主要特性**: +- **报告生成**: + - 自动生成Markdown格式报告 + - 包含项目信息、检查摘要、问题详情 + - 包含AI分析结果和修复建议 + +- **报告内容**: + - 检查结果统计(错误、警告、信息数量) + - AI分析结果(去重数量、风险分布) + - 详细问题列表(按文件分组) + - 每个问题的完整信息和修复建议 + +- **报告下载**: + - 一键下载报告文件 + - 文件名自动包含时间戳 + - 支持本地保存和分享 + +#### 9. 配置中心 + +**功能描述**:集中管理系统配置,包括AI配置和本地模型管理。 + +**主要特性**: +- **AI配置管理**: + - 选择使用本地AI或云端AI + - 本地模型选择 + - 云端AI服务信息显示 + - 配置保存和测试连接 + +- **本地模型管理**: + - 本地模型上传和添加 + - 模型信息管理(名称、描述、API地址) + - 模型列表查看 + - 模型连接测试 + - 模型删除 + +#### 10. Web可视化平台 + +**功能描述**:提供友好的Web界面,支持所有功能的可视化操作。 + +**主要特性**: +- **仪表板**: + - 项目统计信息展示 + - 合规率、待修复问题、高危漏洞统计 + - 最近项目列表 + - 快速开始指南 + +- **用户界面**: + - 响应式设计,适配不同屏幕尺寸 + - 现代化UI设计,操作直观 + - 实时进度显示 + - 错误提示和成功提示 + +- **导航系统**: + - 顶部导航栏 + - 页面切换 + - 功能模块快速访问 + +### 二、技术特性 + +#### 1. 系统架构 +- **前后端分离**:前端使用HTML5/CSS3/JavaScript,后端使用Node.js/Express +- **模块化设计**:功能模块独立,便于维护和扩展 +- **RESTful API**:标准化的API接口设计 + +#### 2. 性能优化 +- **并行检查**:多个工具同时运行,提高检查效率 +- **批量处理**:AI分析支持批量处理所有问题 +- **结果缓存**:检查结果缓存,避免重复计算 + +#### 3. 错误处理 +- **容错机制**:工具执行失败时自动回退 +- **错误提示**:友好的错误信息提示 +- **日志记录**:详细的日志记录,便于问题排查 + +#### 4. 安全性 +- **路径验证**:防止路径遍历攻击 +- **文件类型验证**:限制上传文件类型 +- **输入验证**:API参数验证和过滤 + +### 三、应用场景 + +1. **军工代码合规性检查**:针对军工领域对代码质量的特殊要求,提供定制化的代码合规性检查 +2. **企业代码质量管控**:帮助企业建立代码质量检查流程,提升代码质量 +3. **开源项目维护**:帮助开源项目维护者快速发现和修复代码问题 +4. **教学和培训**:作为代码质量检查的教学工具,帮助学生理解代码规范 +5. **CI/CD集成**:可集成到持续集成流程中,自动化代码质量检查 + +### 四、系统优势 + +1. **多工具协同**:集成多个工具,覆盖代码质量、风格、安全等各个方面 +2. **AI智能分析**:利用AI技术提供智能去重、风险评估和详细建议 +3. **灵活配置**:支持自定义规则集和AI服务选择 +4. **用户友好**:直观的Web界面,操作简单便捷 +5. **功能完整**:从项目导入到结果导出,提供完整的代码检查流程 + +--- + +*本文档基于"基于AI智能分析与多工具协同的Python代码质量检查系统"项目编写* + diff --git a/src/项目集成说明.md b/src/项目集成说明.md deleted file mode 100644 index 6804827..0000000 --- a/src/项目集成说明.md +++ /dev/null @@ -1,405 +0,0 @@ -# FortifyCode 项目集成说明 - -## 修改概述 - -本次修改将前端界面与项目后端进行了完整集成,使系统能够正常运行。 - -## 主要修改内容 - -### 1. 创建完整的后端服务器 (`backend.js`) - -**位置**: `src/backend.js` - -**主要功能**: -- 运行在端口 5000(与前端期望的端口一致) -- 提供所有前端需要的 API 接口(添加了 `/api` 前缀) -- 集成三个代码检查工具(Pylint、Flake8、Bandit) -- 实现项目管理功能 -- 实现文件上传和管理功能 -- 提供静态文件服务(前端页面) - -**新增 API 端点**: -``` -GET /api/health - 健康检查 -POST /api/upload - 文件上传 -POST /api/check - 代码检查 -GET /api/projects - 获取所有项目 -POST /api/projects - 创建新项目 -GET /api/projects/:id - 获取项目详情 -DELETE /api/projects/:id - 删除项目 -POST /api/projects/:id/check - 运行项目检查 -POST /api/projects/:id/upload-files - 上传文件到项目 -GET /api/projects/:id/files - 获取项目文件列表 -GET /api/projects/:id/files/content - 获取文件内容 -PUT /api/projects/:id/files/content - 保存文件内容 -``` - -### 2. 修改前端 API 配置 (`frontend/js/app.js`) - -**修改内容**: -```javascript -// 修改前 -const API_BASE_URL = 'http://localhost:5000/api'; - -// 修改后 -const API_BASE_URL = window.location.origin + '/api'; -``` - -**优势**: 自动适配部署环境,无需手动修改配置 - -### 3. 创建启动脚本 - -#### Windows 启动脚本 (`start_server.bat`) -- 自动检查 Node.js 和 Python 环境 -- 自动安装缺失的依赖 -- 自动安装代码检查工具 -- 提供友好的启动提示 - -#### Linux/Mac 启动脚本 (`start_server.sh`) -- 同上,适配 Unix 系统 - -### 4. 更新项目配置 (`package.json`) - -**修改内容**: -- 更新项目名称和描述 -- 添加启动脚本 -- 设置正确的主入口文件 - -### 5. 创建文档 - -#### README.md -- 完整的项目说明 -- 安装和使用指南 -- API 文档 -- 常见问题解答 - -#### 快速开始指南.md -- 快速上手教程 -- 使用示例 -- 问题排查 - -#### requirements.txt -- Python 依赖列表 -- 便于一键安装 - -## 项目结构变化 - -### 之前的结构问题 - -``` -src/ -├── frontend/ # 前端代码 -│ ├── index.html -│ ├── css/style.css -│ └── js/app.js # API指向 localhost:5000/api -├── view/ -│ └── backend.js # 后端运行在 localhost:3000 -│ # 只有 /check 端点,没有 /api 前缀 -└── ... - -❌ 问题: -1. 端口不匹配 (前端5000, 后端3000) -2. API路径不匹配 (前端需要/api前缀) -3. 缺少大量前端需要的API端点 -``` - -### 修改后的结构 - -``` -src/ -├── backend.js # ✅ 新的完整后端服务器 -│ # - 端口5000 -│ # - /api 前缀 -│ # - 所有必要的API端点 -├── frontend/ # 前端代码(已集成) -│ ├── index.html -│ ├── css/style.css -│ └── js/app.js # ✅ API自动适配当前域名 -├── start_server.bat # ✅ Windows启动脚本 -├── start_server.sh # ✅ Linux/Mac启动脚本 -├── package.json # ✅ 更新的配置 -├── README.md # ✅ 完整文档 -├── 快速开始指南.md # ✅ 快速入门 -├── requirements.txt # ✅ Python依赖 -├── projects_data/ # ✅ 项目数据目录(自动创建) -└── view/ # 旧的后端(保留,可选择性使用) - └── backend.js - -✅ 优势: -1. 端口统一为5000 -2. API路径统一 -3. 功能完整 -4. 易于部署和使用 -``` - -## 技术实现细节 - -### 1. 代码检查工具集成 - -```javascript -// 工具配置 -const TOOL_CONFIG = { - bandit: { ... }, // 安全漏洞检查 - flake8: { ... }, // 代码规范检查 - pylint: { ... } // 代码质量检查 -}; - -// 执行检查 -async function runCodeCheck(filePath) { - // 并行执行三个工具 - // 收集和统一处理结果 - // 返回统一格式的问题列表 -} -``` - -### 2. 文件上传处理 - -```javascript -// 使用 Multer 中间件处理文件上传 -const upload = multer({ - storage: storage, - limits: { fileSize: 100 * 1024 * 1024 }, // 100MB - fileFilter: (req, file, cb) => { - // 只接受 Python 文件 - if (file.originalname.match(/\.(py|pyx|pyi)$/)) { - cb(null, true); - } - } -}); -``` - -### 3. 项目数据持久化 - -```javascript -// 使用 JSON 文件存储项目数据 -// 生产环境建议使用数据库 -const PROJECTS_FILE = path.join(PROJECTS_DIR, 'projects.json'); - -function saveProjects() { - fs.writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2)); -} -``` - -### 4. CORS 配置 - -```javascript -// 允许跨域请求 -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true -})); -``` - -## 与原有项目的兼容性 - -### 保留的内容 - -1. **前端代码** (`frontend/`) - - 完全保留,只修改了 API_BASE_URL - - HTML、CSS、JavaScript 功能不变 - -2. **原后端** (`view/backend.js`) - - 保留不动,可作为参考 - - 如需使用,可独立运行在3000端口 - -3. **代码检查工具** (bandit-main, flake8-main, pylint-main) - - 保留不动 - - 新后端通过 Python 命令调用这些工具 - -4. **文档和配置** (doc/, model/) - - 完全保留 - -### 新增的内容 - -1. `backend.js` - 完整的后端服务器 -2. `start_server.bat` - Windows 启动脚本 -3. `start_server.sh` - Linux/Mac 启动脚本 -4. `README.md` - 项目文档 -5. `快速开始指南.md` - 快速入门 -6. `requirements.txt` - Python 依赖 -7. `项目集成说明.md` - 本文档 - -## 使用说明 - -### 启动系统 - -**方式 1**: 使用启动脚本(推荐) -```cmd -# Windows -start_server.bat - -# Linux/Mac -./start_server.sh -``` - -**方式 2**: 使用 npm -```bash -npm start -``` - -**方式 3**: 直接运行 -```bash -node backend.js -``` - -### 访问系统 - -浏览器打开: http://localhost:5000 - -### 系统流程 - -``` -1. 用户访问 http://localhost:5000 - ↓ -2. Express 服务器返回前端页面 - ↓ -3. 前端调用 /api/* 接口 - ↓ -4. 后端处理请求,调用 Python 检查工具 - ↓ -5. 返回检查结果给前端 - ↓ -6. 前端展示结果 -``` - -## 测试建议 - -### 1. 环境测试 - -```bash -# 检查 Node.js -node --version - -# 检查 Python -python --version - -# 检查检查工具 -python -m pylint --version -python -m flake8 --version -python -m bandit --version -``` - -### 2. 功能测试 - -1. **文件上传检查** - - 上传一个 Python 文件 - - 检查是否正常显示结果 - -2. **项目管理** - - 创建一个新项目 - - 上传文件到项目 - - 运行项目检查 - -3. **文件浏览器** - - 浏览项目文件 - - 编辑文件内容 - - 保存文件 - -### 3. API 测试 - -使用 curl 或 Postman 测试 API 端点: - -```bash -# 健康检查 -curl http://localhost:5000/api/health - -# 获取项目列表 -curl http://localhost:5000/api/projects -``` - -## 部署建议 - -### 开发环境 -- 使用 `node backend.js` 直接运行 -- 使用 `nodemon` 实现自动重启 - -### 生产环境 -- 使用 PM2 管理进程 -- 配置反向代理(Nginx/Apache) -- 使用数据库替代 JSON 文件存储 -- 添加用户认证和权限管理 - -## 后续优化建议 - -### 功能优化 - -1. **数据库集成** - - 使用 SQLite/MySQL/PostgreSQL 存储项目数据 - - 更好的并发性能 - -2. **用户系统** - - 添加用户注册和登录 - - 多用户项目隔离 - -3. **检查任务队列** - - 使用消息队列处理大量检查任务 - - 支持异步检查和结果推送 - -4. **结果缓存** - - 缓存检查结果 - - 避免重复检查相同代码 - -5. **Git 集成增强** - - 实际实现 GitHub/Gitee 克隆功能 - - 支持分支和版本管理 - -### 性能优化 - -1. **并发处理** - - 多个文件并行检查 - - Worker 线程池 - -2. **增量检查** - - 只检查变更的文件 - - 智能差异分析 - -3. **前端优化** - - 代码分割和懒加载 - - 虚拟滚动优化大量结果显示 - -## 常见问题 - -### Q: 为什么有两个 backend.js? - -A: -- `src/backend.js` - 新的完整后端(推荐使用) -- `src/view/backend.js` - 原有后端(保留作为参考) - -建议使用新的 `src/backend.js`。 - -### Q: 可以删除旧的 view/backend.js 吗? - -A: 可以。新的 backend.js 已经包含了所有功能。但建议保留一段时间作为备份。 - -### Q: 如何修改端口? - -A: 编辑 `backend.js` 第 10 行: -```javascript -const PORT = process.env.PORT || 5000; -``` -或设置环境变量: -```bash -PORT=8080 node backend.js -``` - -### Q: Python 检查工具安装在哪里? - -A: 通过 pip 安装到系统或虚拟环境。后端通过 Python 命令行调用这些工具。 - -## 总结 - -本次集成: -- ✅ 统一了前后端接口 -- ✅ 完善了所有缺失的功能 -- ✅ 提供了完整的文档和启动脚本 -- ✅ 保持了代码的可维护性 -- ✅ 易于部署和使用 - -现在可以直接运行 `start_server.bat`(Windows)或 `./start_server.sh`(Linux/Mac)来启动完整的系统。 - ---- - -集成完成时间: 2025年10月21日 - -- 2.34.1