@ -0,0 +1,51 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# 项目特定
|
||||
projects_data/
|
||||
out/
|
||||
*.log
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
temp_*.py
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
.env.local
|
||||
|
||||
@ -0,0 +1,751 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const cors = require('cors');
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// 创建Express应用
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// 配置CORS
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// 解析JSON请求体
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// 请求日志中间件
|
||||
app.use((req, res, next) => {
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// 配置Multer文件上传
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const tempDir = path.join(os.tmpdir(), 'fortifycode_uploads');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
cb(null, tempDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, `${Date.now()}-${file.originalname}`);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: 100 * 1024 * 1024 }, // 100MB
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.originalname.match(/\.(py|pyx|pyi)$/)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('只支持 Python 文件 (.py, .pyx, .pyi)'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 项目存储目录
|
||||
const PROJECTS_DIR = path.join(__dirname, 'projects_data');
|
||||
if (!fs.existsSync(PROJECTS_DIR)) {
|
||||
fs.mkdirSync(PROJECTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 项目数据存储(简化版,生产环境应使用数据库)
|
||||
let projects = [];
|
||||
const PROJECTS_FILE = path.join(PROJECTS_DIR, 'projects.json');
|
||||
|
||||
// 加载项目数据
|
||||
function loadProjects() {
|
||||
try {
|
||||
if (fs.existsSync(PROJECTS_FILE)) {
|
||||
const data = fs.readFileSync(PROJECTS_FILE, 'utf8');
|
||||
projects = JSON.parse(data);
|
||||
console.log(`加载了 ${projects.length} 个项目`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载项目数据失败:', error);
|
||||
projects = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 保存项目数据
|
||||
function saveProjects() {
|
||||
try {
|
||||
fs.writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2));
|
||||
} catch (error) {
|
||||
console.error('保存项目数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadProjects();
|
||||
|
||||
// 工具配置
|
||||
const TOOL_CONFIG = {
|
||||
bandit: {
|
||||
command: 'python',
|
||||
args: (filePath) => `-m bandit -r -f json ${filePath}`,
|
||||
parseResult: (stdout) => {
|
||||
try {
|
||||
if (!stdout || stdout.trim() === '') return [];
|
||||
const result = JSON.parse(stdout);
|
||||
return (result.results || []).map(item => ({
|
||||
file: item.filename,
|
||||
line: item.line_number,
|
||||
column: 0,
|
||||
rule: item.test_id,
|
||||
message: item.issue_text,
|
||||
severity: item.issue_severity.toLowerCase(),
|
||||
confidence: item.issue_confidence.toLowerCase(),
|
||||
type: item.issue_severity === 'HIGH' ? 'error' : 'warning'
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Bandit解析失败:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
flake8: {
|
||||
command: 'python',
|
||||
args: (filePath) => `-m flake8 ${filePath}`,
|
||||
parseResult: (stdout) => {
|
||||
try {
|
||||
if (!stdout || stdout.trim() === '') return [];
|
||||
|
||||
const lines = stdout.trim().split('\n').filter(line => line.trim());
|
||||
const issues = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(.+?):(\d+):(\d+):\s*([A-Z]\d+)\s*(.+)$/);
|
||||
if (match) {
|
||||
const code = match[4];
|
||||
issues.push({
|
||||
file: match[1],
|
||||
line: parseInt(match[2]),
|
||||
column: parseInt(match[3]),
|
||||
rule: code,
|
||||
message: match[5],
|
||||
severity: code.startsWith('E') ? 'high' : 'medium',
|
||||
type: code.startsWith('E') ? 'error' : 'warning'
|
||||
});
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
} catch (e) {
|
||||
console.error('Flake8解析失败:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
pylint: {
|
||||
command: 'python',
|
||||
args: (filePath) => `-m pylint --output-format=json ${filePath}`,
|
||||
parseResult: (stdout) => {
|
||||
try {
|
||||
if (!stdout || stdout.trim() === '') return [];
|
||||
const result = JSON.parse(stdout);
|
||||
return result.map(item => ({
|
||||
file: item.path,
|
||||
line: item.line,
|
||||
column: item.column,
|
||||
rule: item.symbol,
|
||||
message: item.message,
|
||||
severity: item.type === 'error' ? 'high' : item.type === 'warning' ? 'medium' : 'low',
|
||||
type: item.type === 'error' ? 'error' : item.type === 'warning' ? 'warning' : 'info'
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Pylint解析失败:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 运行单个工具检查
|
||||
async function runTool(tool, filePath) {
|
||||
return new Promise((resolve) => {
|
||||
const config = TOOL_CONFIG[tool];
|
||||
if (!config) {
|
||||
return resolve({ status: 'error', issues: [] });
|
||||
}
|
||||
|
||||
const command = `${config.command} ${config.args(filePath)}`;
|
||||
console.log(`执行命令: ${command}`);
|
||||
|
||||
exec(command, { shell: true, timeout: 60000 }, (error, stdout, stderr) => {
|
||||
console.log(`${tool} 执行完成`);
|
||||
|
||||
try {
|
||||
const issues = config.parseResult(stdout || '');
|
||||
resolve({
|
||||
status: 'completed',
|
||||
issues: issues,
|
||||
raw_output: stdout
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`${tool} 处理结果失败:`, e);
|
||||
resolve({
|
||||
status: 'error',
|
||||
issues: [],
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 运行代码检查
|
||||
async function runCodeCheck(filePath) {
|
||||
const tools = ['pylint', 'flake8', 'bandit'];
|
||||
const toolsStatus = {};
|
||||
const allIssues = [];
|
||||
|
||||
for (const tool of tools) {
|
||||
try {
|
||||
const result = await runTool(tool, filePath);
|
||||
toolsStatus[tool] = result.status;
|
||||
allIssues.push(...result.issues);
|
||||
} catch (error) {
|
||||
console.error(`${tool} 执行失败:`, error);
|
||||
toolsStatus[tool] = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// 统计问题
|
||||
const errorCount = allIssues.filter(i => i.type === 'error').length;
|
||||
const warningCount = allIssues.filter(i => i.type === 'warning').length;
|
||||
const infoCount = allIssues.filter(i => i.type === 'info').length;
|
||||
|
||||
return {
|
||||
tools_status: toolsStatus,
|
||||
all_issues: allIssues,
|
||||
total_issues: allIssues.length,
|
||||
error_count: errorCount,
|
||||
warning_count: warningCount,
|
||||
info_count: infoCount
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== API 端点 ====================
|
||||
|
||||
// 健康检查
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// 文件上传端点
|
||||
app.post('/api/upload', upload.array('files'), (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '没有上传文件'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建临时目录
|
||||
const tempDir = path.join(os.tmpdir(), `fortifycode_${Date.now()}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
// 复制文件到临时目录
|
||||
req.files.forEach(file => {
|
||||
const targetPath = path.join(tempDir, file.originalname);
|
||||
fs.copyFileSync(file.path, targetPath);
|
||||
// 删除原始上传文件
|
||||
fs.unlinkSync(file.path);
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
temp_path: tempDir,
|
||||
files: req.files.map(f => f.originalname)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 代码检查端点
|
||||
app.post('/api/check', async (req, res) => {
|
||||
try {
|
||||
const { temp_path } = req.body;
|
||||
|
||||
if (!temp_path || !fs.existsSync(temp_path)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '无效的临时路径'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有Python文件
|
||||
const files = fs.readdirSync(temp_path).filter(f => f.endsWith('.py'));
|
||||
|
||||
if (files.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
tools_status: { pylint: 'completed', flake8: 'completed', bandit: 'completed' },
|
||||
all_issues: [],
|
||||
total_issues: 0,
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
info_count: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查第一个文件(简化版,可以扩展为检查所有文件)
|
||||
const firstFile = path.join(temp_path, files[0]);
|
||||
const result = await runCodeCheck(firstFile);
|
||||
|
||||
// 清理临时文件
|
||||
setTimeout(() => {
|
||||
try {
|
||||
fs.rmSync(temp_path, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
console.error('清理临时文件失败:', e);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('代码检查失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有项目
|
||||
app.get('/api/projects', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: projects
|
||||
});
|
||||
});
|
||||
|
||||
// 创建新项目
|
||||
app.post('/api/projects', (req, res) => {
|
||||
try {
|
||||
const { name, description, source_type, source_url } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '项目名称不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const project = {
|
||||
id: Date.now(),
|
||||
name,
|
||||
description: description || '',
|
||||
source_type: source_type || 'upload',
|
||||
source_url: source_url || '',
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
latest_check: null
|
||||
};
|
||||
|
||||
// 创建项目目录
|
||||
const projectDir = path.join(PROJECTS_DIR, `project_${project.id}`);
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
project.path = projectDir;
|
||||
|
||||
projects.push(project);
|
||||
saveProjects();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: project
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建项目失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取项目详情
|
||||
app.get('/api/projects/:id', (req, res) => {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: project
|
||||
});
|
||||
});
|
||||
|
||||
// 删除项目
|
||||
app.delete('/api/projects/:id', (req, res) => {
|
||||
try {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const projectIndex = projects.findIndex(p => p.id === projectId);
|
||||
|
||||
if (projectIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const project = projects[projectIndex];
|
||||
|
||||
// 删除项目目录
|
||||
if (project.path && fs.existsSync(project.path)) {
|
||||
fs.rmSync(project.path, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 从列表中删除
|
||||
projects.splice(projectIndex, 1);
|
||||
saveProjects();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '项目删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除项目失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 运行项目检查
|
||||
app.post('/api/projects/:id/check', async (req, res) => {
|
||||
try {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取项目中的所有Python文件
|
||||
const projectPath = project.path;
|
||||
const files = [];
|
||||
|
||||
function getAllPythonFiles(dir) {
|
||||
const items = fs.readdirSync(dir);
|
||||
items.forEach(item => {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
getAllPythonFiles(fullPath);
|
||||
} else if (item.endsWith('.py')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAllPythonFiles(projectPath);
|
||||
|
||||
if (files.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
check_id: Date.now(),
|
||||
status: 'completed',
|
||||
total_issues: 0,
|
||||
message: '项目中没有Python文件'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查第一个文件作为示例
|
||||
const result = await runCodeCheck(files[0]);
|
||||
|
||||
// 更新项目的最新检查记录
|
||||
project.latest_check = {
|
||||
id: Date.now(),
|
||||
status: 'completed',
|
||||
started_at: new Date().toISOString(),
|
||||
completed_at: new Date().toISOString(),
|
||||
total_issues: result.total_issues,
|
||||
error_count: result.error_count,
|
||||
warning_count: result.warning_count,
|
||||
info_count: result.info_count
|
||||
};
|
||||
project.status = 'completed';
|
||||
project.updated_at = new Date().toISOString();
|
||||
saveProjects();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
check_id: project.latest_check.id,
|
||||
...result
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('项目检查失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上传文件到项目
|
||||
app.post('/api/projects/:id/upload-files', upload.array('files'), (req, res) => {
|
||||
try {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '没有上传文件'
|
||||
});
|
||||
}
|
||||
|
||||
// 复制文件到项目目录
|
||||
let uploadedCount = 0;
|
||||
req.files.forEach(file => {
|
||||
const targetPath = path.join(project.path, file.originalname);
|
||||
fs.copyFileSync(file.path, targetPath);
|
||||
fs.unlinkSync(file.path);
|
||||
uploadedCount++;
|
||||
});
|
||||
|
||||
project.updated_at = new Date().toISOString();
|
||||
saveProjects();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
uploaded_count: uploadedCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取项目文件列表
|
||||
app.get('/api/projects/:id/files', (req, res) => {
|
||||
try {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const targetPath = req.query.path || '';
|
||||
const fullPath = path.join(project.path, targetPath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(fullPath);
|
||||
const files = items.map(item => {
|
||||
const itemPath = path.join(fullPath, item);
|
||||
const stat = fs.statSync(itemPath);
|
||||
return {
|
||||
name: item,
|
||||
path: path.join(targetPath, item),
|
||||
is_directory: stat.isDirectory(),
|
||||
size: stat.size,
|
||||
modified_at: stat.mtime.toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: files
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取文件内容
|
||||
app.get('/api/projects/:id/files/content', (req, res) => {
|
||||
try {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const filePath = req.query.path;
|
||||
if (!filePath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '文件路径不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const fullPath = path.join(project.path, filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '文件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: filePath,
|
||||
content: content
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('读取文件失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 保存文件内容
|
||||
app.put('/api/projects/:id/files/content', (req, res) => {
|
||||
try {
|
||||
const projectId = parseInt(req.params.id);
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '项目不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const { path: filePath, content } = req.body;
|
||||
if (!filePath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '文件路径不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const fullPath = path.join(project.path, filePath);
|
||||
const dirPath = path.dirname(fullPath);
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(fullPath, content || '');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件保存成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存文件失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 静态文件服务 - 前端
|
||||
app.use(express.static(path.join(__dirname, 'frontend')));
|
||||
|
||||
// 默认路由 - 返回前端页面
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'frontend', 'index.html'));
|
||||
});
|
||||
|
||||
// 404处理
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: '接口不存在'
|
||||
});
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('服务器错误:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message || '服务器内部错误'
|
||||
});
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, () => {
|
||||
console.log('=================================');
|
||||
console.log('FortifyCode 后端服务器已启动');
|
||||
console.log(`服务器地址: http://localhost:${PORT}`);
|
||||
console.log(`API 地址: http://localhost:${PORT}/api`);
|
||||
console.log(`前端地址: http://localhost:${PORT}`);
|
||||
console.log(`项目数据目录: ${PROJECTS_DIR}`);
|
||||
console.log('=================================');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,361 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>基于多agent协同的军工Python代码合规性检查系统 - FortifyCode</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<div class="logo">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
FortifyCode
|
||||
</div>
|
||||
<ul class="nav-menu">
|
||||
<li><a href="#dashboard" class="nav-link active">仪表板</a></li>
|
||||
<li><a href="#projects" class="nav-link">项目管理</a></li>
|
||||
<li><a href="#rules" class="nav-link">规则集管理</a></li>
|
||||
<li><a href="#settings" class="nav-link">配置中心</a></li>
|
||||
</ul>
|
||||
<div class="user-info">
|
||||
<div class="notification">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notification-badge">3</span>
|
||||
</div>
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user-circle" style="font-size: 24px;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 仪表板页面 -->
|
||||
<div id="dashboard-page" class="page-content active">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header fade-in">
|
||||
<h1 class="page-title">基于多agent协同的军工Python代码合规性检查系统</h1>
|
||||
<p class="page-subtitle">基于多 Agent 协同的智能代码安全检测平台</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid fade-in">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-code"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="totalFiles">1,247</h3>
|
||||
<p>已检查文件</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="complianceRate">98.5%</h3>
|
||||
<p>合规率</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="pendingIssues">23</h3>
|
||||
<p>待修复问题</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-bug"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="highRiskIssues">5</h3>
|
||||
<p>高危漏洞</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="function-grid fade-in">
|
||||
<!-- 代码上传和检查区域 -->
|
||||
<div class="main-panel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">快捷代码检查</h2>
|
||||
<div class="filter-tabs">
|
||||
<span class="filter-tab active">Python</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<div class="upload-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<div class="upload-text">拖拽文件到此处或点击上传</div>
|
||||
<div class="upload-hint">支持 .py, .pyx, .pyi 文件,最大 100MB</div>
|
||||
<input type="file" class="file-input" id="fileInput" multiple accept=".py,.pyx,.pyi">
|
||||
</div>
|
||||
|
||||
<div class="progress-container" id="progressContainer" style="display: none;">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progressText">准备检查...</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<button class="btn btn-primary" id="startCheckBtn" style="display: none;">
|
||||
<i class="fas fa-play"></i> 开始检查
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="stopCheckBtn" style="display: none;">
|
||||
<i class="fas fa-stop"></i> 停止检查
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 多Agent状态面板 -->
|
||||
<div class="agent-status">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Agent 状态</h2>
|
||||
</div>
|
||||
<div class="agent-list">
|
||||
<div class="agent-item">
|
||||
<div class="agent-avatar">A1</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">pylint Agent</div>
|
||||
<span class="agent-status-badge status-idle">空闲</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-item">
|
||||
<div class="agent-avatar">A2</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">flake8 Agent</div>
|
||||
<span class="agent-status-badge status-idle">空闲</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-item">
|
||||
<div class="agent-avatar">A3</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">bandit Agent</div>
|
||||
<span class="agent-status-badge status-idle">空闲</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检查结果区域 -->
|
||||
<div class="results-section fade-in" id="resultsSection" style="display: none;">
|
||||
<div class="results-header">
|
||||
<h2 class="results-title">检查结果</h2>
|
||||
<div class="filter-tabs">
|
||||
<span class="filter-tab active" data-filter="all">全部</span>
|
||||
<span class="filter-tab" data-filter="error">错误</span>
|
||||
<span class="filter-tab" data-filter="warning">警告</span>
|
||||
<span class="filter-tab" data-filter="info">信息</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results-content" id="resultsContent">
|
||||
<!-- 结果项将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目管理页面 -->
|
||||
<div id="projects-page" class="page-content">
|
||||
<div class="page-header fade-in">
|
||||
<h1 class="page-title">项目管理</h1>
|
||||
<p class="page-subtitle">管理您的代码检查项目和任务</p>
|
||||
</div>
|
||||
|
||||
<div class="projects-container">
|
||||
<div class="projects-header">
|
||||
<div class="projects-actions">
|
||||
<button class="btn btn-primary" id="createProjectBtn">
|
||||
<i class="fas fa-plus"></i> 新建项目
|
||||
</button>
|
||||
</div>
|
||||
<div class="projects-search">
|
||||
<input type="text" placeholder="搜索项目..." class="search-input" id="projectSearch">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="projects-grid" id="projectsGrid">
|
||||
<!-- 项目卡片将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目详情页面 -->
|
||||
<div id="project-detail-page" class="page-content">
|
||||
<div class="page-header fade-in">
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<button class="btn btn-secondary" id="backToProjectsBtn">
|
||||
<i class="fas fa-arrow-left"></i> 返回项目列表
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="page-title" id="projectDetailTitle">项目详情</h1>
|
||||
<p class="page-subtitle" id="projectDetailSubtitle">查看项目信息和运行代码检查</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-detail-container">
|
||||
<div class="project-info-panel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">项目信息</h2>
|
||||
<div class="project-actions">
|
||||
<button class="btn btn-primary" id="runCheckBtn">
|
||||
<i class="fas fa-play"></i> 运行检查
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="editProjectBtn">
|
||||
<i class="fas fa-edit"></i> 编辑项目
|
||||
</button>
|
||||
<button class="btn btn-danger" id="deleteProjectBtn">
|
||||
<i class="fas fa-trash"></i> 删除项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content" id="projectInfoContent">
|
||||
<!-- 项目信息将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="check-history-panel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">检查历史</h2>
|
||||
</div>
|
||||
<div class="panel-content" id="checkHistoryContent">
|
||||
<!-- 检查历史将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件浏览器面板 -->
|
||||
<div class="file-browser-panel">
|
||||
<div class="panel-header">
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<h2 class="panel-title">文件浏览器</h2>
|
||||
<div class="file-actions">
|
||||
<button class="btn btn-primary" id="uploadFileBtn">
|
||||
<i class="fas fa-upload"></i> 上传文件
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="createFileBtn">
|
||||
<i class="fas fa-plus"></i> 新建文件
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="createFolderBtn">
|
||||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-path">
|
||||
<span id="currentPath">/</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-browser-content">
|
||||
<div class="file-tree" id="fileTree">
|
||||
<!-- 文件树将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代码编辑器面板 -->
|
||||
<div class="code-editor-panel" id="codeEditorPanel" style="display: none;">
|
||||
<div class="panel-header">
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<h2 class="panel-title" id="editorTitle">代码编辑器</h2>
|
||||
<div class="editor-actions">
|
||||
<button class="btn btn-success" id="saveFileBtn">
|
||||
<i class="fas fa-save"></i> 保存
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="closeEditorBtn">
|
||||
<i class="fas fa-times"></i> 关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span id="currentFilePath">未选择文件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-content">
|
||||
<textarea id="codeEditor" placeholder="选择文件开始编辑..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检查结果详情 -->
|
||||
<div class="check-results-panel" id="checkResultsPanel" style="display: none;">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">检查结果详情</h2>
|
||||
<div class="filter-tabs">
|
||||
<span class="filter-tab active" data-filter="all">全部</span>
|
||||
<span class="filter-tab" data-filter="error">错误</span>
|
||||
<span class="filter-tab" data-filter="warning">警告</span>
|
||||
<span class="filter-tab" data-filter="info">信息</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content" id="checkResultsContent">
|
||||
<!-- 检查结果将通过 JavaScript 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建项目模态框 -->
|
||||
<div id="createProjectModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>新建项目</h2>
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="createProjectForm">
|
||||
<div class="form-group">
|
||||
<label for="projectName">项目名称 *</label>
|
||||
<input type="text" id="projectName" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="projectDescription">项目描述</label>
|
||||
<textarea id="projectDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>项目来源 *</label>
|
||||
<div class="source-type-tabs">
|
||||
<input type="radio" id="sourceGithub" name="source_type" value="github" checked>
|
||||
<label for="sourceGithub">GitHub</label>
|
||||
|
||||
<input type="radio" id="sourceGitee" name="source_type" value="gitee">
|
||||
<label for="sourceGitee">Gitee</label>
|
||||
|
||||
<input type="radio" id="sourceUpload" name="source_type" value="upload">
|
||||
<label for="sourceUpload">文件上传</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="gitUrlGroup">
|
||||
<label for="gitUrl">Git URL *</label>
|
||||
<input type="url" id="gitUrl" name="source_url" placeholder="https://github.com/username/repository.git">
|
||||
</div>
|
||||
<div class="form-group" id="fileUploadGroup" style="display: none;">
|
||||
<label for="fileUpload">选择文件或文件夹</label>
|
||||
<input type="file" id="fileUpload" name="files" multiple webkitdirectory>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="cancelCreateProject">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmCreateProject">创建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
# FortifyCode Python 依赖
|
||||
|
||||
# 代码质量检查工具
|
||||
pylint>=2.17.0
|
||||
flake8>=6.0.0
|
||||
bandit>=1.7.5
|
||||
|
||||
# 可选:增强功能
|
||||
pycodestyle>=2.10.0
|
||||
mccabe>=0.7.0
|
||||
|
||||
@ -0,0 +1,217 @@
|
||||
# FortifyCode 快速开始指南
|
||||
|
||||
## 一键启动
|
||||
|
||||
### Windows 用户
|
||||
双击运行 `start_server.bat` 文件
|
||||
|
||||
### Linux/Mac 用户
|
||||
```bash
|
||||
chmod +x start_server.sh
|
||||
./start_server.sh
|
||||
```
|
||||
|
||||
### 使用 npm
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## 首次使用前的准备
|
||||
|
||||
### 1. 安装 Node.js
|
||||
访问 https://nodejs.org/ 下载并安装 Node.js (推荐 LTS 版本)
|
||||
|
||||
验证安装:
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
### 2. 安装 Python
|
||||
访问 https://www.python.org/ 下载并安装 Python 3.7+
|
||||
|
||||
**重要**: 安装时勾选 "Add Python to PATH"
|
||||
|
||||
验证安装:
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
### 3. 安装项目依赖
|
||||
|
||||
#### Node.js 依赖
|
||||
```bash
|
||||
cd src
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Python 代码检查工具
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
或手动安装:
|
||||
```bash
|
||||
pip install pylint flake8 bandit
|
||||
```
|
||||
|
||||
## 启动服务器
|
||||
|
||||
选择以下任一方式:
|
||||
|
||||
### 方式 1: 使用启动脚本(推荐)
|
||||
- Windows: 双击 `start_server.bat`
|
||||
- Linux/Mac: 运行 `./start_server.sh`
|
||||
|
||||
### 方式 2: 使用 npm
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### 方式 3: 直接运行
|
||||
```bash
|
||||
node backend.js
|
||||
```
|
||||
|
||||
## 访问系统
|
||||
|
||||
启动成功后,浏览器访问:
|
||||
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
你会看到 FortifyCode 的主界面。
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1: 快速检查单个文件
|
||||
|
||||
1. 准备一个 Python 文件,例如 `test.py`:
|
||||
```python
|
||||
import os
|
||||
def test():
|
||||
x=1+2
|
||||
print(x)
|
||||
test()
|
||||
```
|
||||
|
||||
2. 在主页点击或拖拽文件到上传区域
|
||||
3. 点击"开始检查"按钮
|
||||
4. 查看检查结果
|
||||
|
||||
### 示例 2: 创建项目
|
||||
|
||||
1. 点击顶部导航栏的"项目管理"
|
||||
2. 点击"新建项目"按钮
|
||||
3. 填写项目信息:
|
||||
- 项目名称: 我的第一个项目
|
||||
- 项目描述: 测试项目
|
||||
- 来源类型: 选择"文件上传"
|
||||
4. 选择要上传的文件或文件夹
|
||||
5. 点击"创建项目"
|
||||
|
||||
### 示例 3: 查看项目详情
|
||||
|
||||
1. 在项目列表中点击项目卡片
|
||||
2. 查看项目信息和文件浏览器
|
||||
3. 点击"运行检查"进行代码检查
|
||||
4. 在"检查历史"中查看历史记录
|
||||
|
||||
## 常见问题快速解决
|
||||
|
||||
### ❌ 提示 "未找到 Node.js"
|
||||
**解决**: 安装 Node.js 并确保添加到系统 PATH
|
||||
|
||||
### ❌ 提示 "未找到 Python"
|
||||
**解决**: 安装 Python 并确保添加到系统 PATH
|
||||
|
||||
### ❌ 提示 "pylint 未安装"
|
||||
**解决**:
|
||||
```bash
|
||||
pip install pylint
|
||||
```
|
||||
|
||||
### ❌ 提示 "端口 5000 被占用"
|
||||
**解决**:
|
||||
- 方案 1: 关闭占用端口的程序
|
||||
- 方案 2: 修改端口号
|
||||
编辑 `backend.js` 第 10 行:
|
||||
```javascript
|
||||
const PORT = process.env.PORT || 8080; // 改为其他端口
|
||||
```
|
||||
|
||||
### ❌ 上传文件失败
|
||||
**解决**:
|
||||
- 确保文件是 .py, .pyx, .pyi 格式
|
||||
- 确保文件大小不超过 100MB
|
||||
- 检查磁盘空间是否充足
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 浏览器 │ http://localhost:5000
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Express 服务器 (Node.js) │
|
||||
│ - 提供前端页面 │
|
||||
│ - 处理API请求 │
|
||||
│ - 文件管理 │
|
||||
└──────┬──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────┐
|
||||
│ 代码检查工具 (Python) │
|
||||
│ - Pylint (代码质量) │
|
||||
│ - Flake8 (代码规范) │
|
||||
│ - Bandit (安全检查) │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
## 功能概览
|
||||
|
||||
### 🏠 仪表板
|
||||
- 查看系统统计信息
|
||||
- 快捷文件上传和检查
|
||||
- Agent 状态监控
|
||||
|
||||
### 📁 项目管理
|
||||
- 创建和管理多个项目
|
||||
- 支持 GitHub/Gitee 克隆
|
||||
- 支持本地文件上传
|
||||
|
||||
### 📝 文件浏览器
|
||||
- 浏览项目文件
|
||||
- 在线编辑代码
|
||||
- 文件上传和管理
|
||||
|
||||
### 🔍 代码检查
|
||||
- 多工具协同检查
|
||||
- 问题分类显示
|
||||
- 修复建议提供
|
||||
|
||||
### 📊 检查报告
|
||||
- 详细的问题列表
|
||||
- 问题统计分析
|
||||
- 历史记录查看
|
||||
|
||||
## 下一步
|
||||
|
||||
- 📖 阅读完整的 [README.md](README.md)
|
||||
- 📚 查看 [API 文档](view/API.md)
|
||||
- 🔧 了解 [军工软件Python编码指南](../doc/军工软件python编码指南.docx)
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果遇到问题:
|
||||
1. 检查控制台输出的错误信息
|
||||
2. 查看浏览器开发者工具的 Console 选项卡
|
||||
3. 参考 README.md 中的"常见问题"部分
|
||||
|
||||
---
|
||||
|
||||
祝使用愉快! 🚀
|
||||
|
||||
Loading…
Reference in new issue