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

880 lines
41 KiB

This file contains ambiguous Unicode characters!

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

const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const crypto = require('crypto');
/**
* AI分析服务
* 用于对代码检查结果进行智能分析、去重、风险评估和生成修改建议
* 支持OpenAI和科大讯飞Spark API
*/
class AIAnalyzer {
constructor() {
// OpenAI配置
this.openaiApiKey = process.env.OPENAI_API_KEY || '';
this.openaiApiBase = process.env.OPENAI_API_BASE || 'https://api.openai.com/v1';
this.openaiModel = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';
// 科大讯飞配置HTTP API
// 注意HTTP API使用APIpasswordBearer Token不是apikey和apisecret
// 获取地址https://console.xfyun.cn/services/bmx1
this.xfApiPassword = process.env.XF_API_PASSWORD || process.env.XF_API_KEY || ''; // HTTP协议的APIpassword
this.xfApiKey = process.env.XF_API_KEY || ''; // 兼容旧配置
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') {
if (this.xfApiPassword) {
console.log(`[AI分析] 科大讯飞 HTTP API Password: ${this.xfApiPassword.substring(0, 10)}... (已配置)`);
console.log(`[AI分析] 使用模型: ${this.xfModel}`);
} else {
console.log(`[AI分析] 科大讯飞 HTTP API Password: 未配置`);
console.log(`[AI分析] 请在控制台获取APIpassword: https://console.xfyun.cn/services/bmx1`);
}
} else if (this.provider === 'openai') {
console.log(`[AI分析] OpenAI API Key: ${this.openaiApiKey ? '已配置' : '未配置'}`);
}
}
/**
* 分析检查结果
* @param {Array} issues - 检查结果列表
* @param {string} projectPath - 项目路径
* @returns {Promise<Object>} 分析结果
*/
async analyzeIssues(issues, projectPath = null) {
if (!this.enabled) {
console.log('[AI分析] AI分析功能已禁用使用基础去重');
return this.basicDeduplication(issues);
}
try {
// 先进行基础去重
const deduplicated = this.basicDeduplication(issues);
// 检查是否有可用的API配置
const hasApiConfig = (this.provider === 'openai' && this.openaiApiKey) ||
(this.provider === 'xf' && this.xfApiPassword);
if (hasApiConfig) {
return await this.aiAnalysis(deduplicated.issues, projectPath);
} else {
// 否则使用规则基础分析
console.log('[AI分析] 未配置API密钥使用规则基础分析');
return this.ruleBasedAnalysis(deduplicated.issues);
}
} catch (error) {
console.error('[AI分析] 分析失败,回退到基础分析:', error);
return this.basicDeduplication(issues);
}
}
/**
* 基础去重 - 基于文件、行号、规则和消息相似度
*/
basicDeduplication(issues) {
const seen = new Map();
const uniqueIssues = [];
const duplicates = [];
for (const issue of issues) {
// 生成唯一键:文件路径 + 行号 + 规则ID
const key = `${issue.relative_path || issue.file}:${issue.line}:${issue.rule || 'unknown'}`;
if (seen.has(key)) {
// 检查消息相似度
const existing = seen.get(key);
const similarity = this.calculateSimilarity(
issue.message || '',
existing.message || ''
);
if (similarity < 0.7) {
// 消息差异较大,可能是不同的问题
uniqueIssues.push(issue);
} else {
// 相似度高,合并工具来源
if (!existing.detected_by) {
existing.detected_by = [existing.tool || 'unknown'];
}
if (issue.tool && !existing.detected_by.includes(issue.tool)) {
existing.detected_by.push(issue.tool);
}
duplicates.push(issue);
}
} else {
issue.detected_by = issue.tool ? [issue.tool] : ['unknown'];
seen.set(key, issue);
uniqueIssues.push(issue);
}
}
return {
issues: uniqueIssues,
total_issues: uniqueIssues.length,
duplicates_removed: duplicates.length,
deduplication_rate: issues.length > 0
? ((duplicates.length / issues.length) * 100).toFixed(2) + '%'
: '0%'
};
}
/**
* 计算字符串相似度简单的Jaccard相似度
*/
calculateSimilarity(str1, str2) {
if (!str1 || !str2) return 0;
const words1 = new Set(str1.toLowerCase().split(/\s+/));
const words2 = new Set(str2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
/**
* 基于规则的分析不使用AI
*/
ruleBasedAnalysis(issues) {
const analyzed = issues.map(issue => {
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
});
// 按风险评分排序
analyzed.sort((a, b) => b.risk_score - a.risk_score);
return {
issues: analyzed,
total_issues: analyzed.length,
high_risk_count: analyzed.filter(i => i.risk_level === 'high').length,
medium_risk_count: analyzed.filter(i => i.risk_level === 'medium').length,
low_risk_count: analyzed.filter(i => i.risk_level === 'low').length,
analysis_method: 'rule-based'
};
}
/**
* 使用AI进行分析
*/
async aiAnalysis(issues, projectPath) {
try {
// 一次性发送所有问题,不再分批处理
console.log(`[AI分析] 开始AI分析${issues.length} 个问题,一次性处理`);
const analyzedIssues = this.provider === 'xf'
? await this.analyzeBatchXf(issues, projectPath)
: await this.analyzeBatch(issues, projectPath);
// 按风险评分排序
analyzedIssues.sort((a, b) => b.risk_score - a.risk_score);
return {
issues: analyzedIssues,
total_issues: analyzedIssues.length,
high_risk_count: analyzedIssues.filter(i => i.risk_level === 'high').length,
medium_risk_count: analyzedIssues.filter(i => i.risk_level === 'medium').length,
low_risk_count: analyzedIssues.filter(i => i.risk_level === 'low').length,
analysis_method: 'ai-powered'
};
} catch (error) {
console.error('[AI分析] AI分析失败:', error);
// 回退到规则基础分析
return this.ruleBasedAnalysis(issues);
}
}
/**
* 批量分析问题
*/
async analyzeBatch(issues, projectPath) {
const prompt = await this.buildAnalysisPrompt(issues, projectPath);
try {
const requestData = JSON.stringify({
model: this.openaiModel,
messages: [
{
role: 'system',
content: '你是一个专业的Python代码质量分析专家擅长分析代码问题评估风险并提供详细、具体的修改建议。你的建议必须包含问题分析、修复步骤、代码示例和最佳实践。'
},
{
role: 'user',
content: prompt
}
],
temperature: 0.3,
max_tokens: 8000 // 增加token数量以支持一次性处理所有问题和完整代码
});
const url = new URL(`${this.openaiApiBase}/chat/completions`);
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.openaiApiKey}`,
'Content-Length': Buffer.byteLength(requestData)
}
};
const data = await new Promise((resolve, reject) => {
const client = url.protocol === 'https:' ? https : http;
const req = client.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
reject(new Error(`AI API错误: ${res.statusCode} ${res.statusMessage}`));
return;
}
try {
resolve(JSON.parse(responseData));
} catch (e) {
reject(new Error(`解析AI响应失败: ${e.message}`));
}
});
});
req.on('error', reject);
req.write(requestData);
req.end();
});
// 提取AI返回的内容
let aiContent = '';
if (data.choices && data.choices.length > 0) {
aiContent = data.choices[0].message.content || '';
} else if (data.content) {
aiContent = data.content;
} else {
throw new Error('AI响应中未找到内容');
}
console.log(`[AI分析-OpenAI] 收到响应,长度: ${aiContent.length} 字符`);
console.log(`[AI分析-OpenAI] 响应内容预览: ${aiContent.substring(0, 300)}`);
// 解析AI响应中的JSON
let analysisResult;
try {
// 尝试直接解析
analysisResult = JSON.parse(aiContent);
} catch (e) {
// 如果不是纯JSON尝试提取JSON部分
console.log('[AI分析-OpenAI] 直接解析失败尝试提取JSON部分');
const jsonMatch = aiContent.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
analysisResult = JSON.parse(jsonMatch[0]);
console.log('[AI分析-OpenAI] 成功提取并解析JSON');
} catch (e2) {
console.error('[AI分析-OpenAI] 提取的JSON解析失败:', e2);
console.error('[AI分析-OpenAI] 响应内容:', aiContent.substring(0, 1000));
throw new Error('无法解析AI响应为JSON: ' + e2.message);
}
} else {
console.error('[AI分析-OpenAI] 响应中未找到JSON格式');
console.error('[AI分析-OpenAI] 响应内容:', aiContent.substring(0, 1000));
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建议过短或为空使用规则基础建议`);
console.warn(`[AI分析-OpenAI] 原始建议: ${aiSuggestion}`);
} else {
console.log(`[AI分析-OpenAI] 问题 ${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: (aiSuggestion && aiSuggestion.trim().length >= 20) ? aiSuggestion : this.generateSuggestion(issue),
ai_analyzed: (aiSuggestion && aiSuggestion.trim().length >= 20)
};
});
} catch (error) {
console.error('[AI分析] 批量分析失败:', error);
// 回退到规则基础分析
return issues.map(issue => {
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
});
}
}
/**
* 使用科大讯飞API批量分析问题HTTP方式
*/
async analyzeBatchXf(issues, projectPath) {
const prompt = await this.buildAnalysisPrompt(issues, projectPath);
try {
// 构建请求数据 - 使用类似OpenAI的格式
const requestData = JSON.stringify({
model: this.xfModel,
messages: [
{
role: 'system',
content: '你是一个专业的Python代码质量分析专家擅长分析代码问题评估风险并提供详细、具体的修改建议。你的建议必须包含问题分析、修复步骤、代码示例和最佳实践。'
},
{
role: 'user',
content: prompt
}
],
temperature: 0.3,
max_tokens: 8000 // 增加token数量以支持一次性处理所有问题和完整代码
});
// 解析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_PASSWORDHTTP协议的APIpassword请在控制台获取https://console.xfyun.cn/services/bmx1');
}
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Authorization': `Bearer ${this.xfApiPassword}`, // 使用Bearer Token
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestData)
}
};
console.log(`[AI分析-讯飞] 发送HTTP请求到: ${this.xfApiUrl}`);
console.log(`[AI分析-讯飞] 使用模型: ${this.xfModel}`);
console.log(`[AI分析-讯飞] 请求头部:`);
console.log(` Authorization: Bearer ${this.xfApiPassword.substring(0, 10)}...`);
console.log(` Content-Type: application/json`);
console.log(` Content-Length: ${Buffer.byteLength(requestData)}`);
const data = await new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
console.error(`[AI分析-讯飞] HTTP错误: ${res.statusCode} ${res.statusMessage}`);
console.error(`[AI分析-讯飞] 响应内容:`, responseData.substring(0, 500));
reject(new Error(`科大讯飞API错误: ${res.statusCode} ${res.statusMessage}`));
return;
}
try {
resolve(JSON.parse(responseData));
} catch (e) {
console.error('[AI分析-讯飞] 解析响应失败:', e);
console.error('[AI分析-讯飞] 响应内容:', responseData.substring(0, 500));
reject(new Error(`解析AI响应失败: ${e.message}`));
}
});
});
req.on('error', (error) => {
console.error('[AI分析-讯飞] 请求错误:', error);
reject(error);
});
req.write(requestData);
req.end();
});
// 提取AI返回的内容
let aiContent = '';
if (data.choices && data.choices.length > 0) {
aiContent = data.choices[0].message.content || '';
} else if (data.content) {
aiContent = data.content;
} else {
throw new Error('AI响应中未找到内容');
}
console.log(`[AI分析-讯飞] 收到响应,长度: ${aiContent.length} 字符`);
console.log(`[AI分析-讯飞] 响应内容预览: ${aiContent.substring(0, 500)}`);
// 如果响应太长,也打印末尾部分
if (aiContent.length > 1000) {
console.log(`[AI分析-讯飞] 响应内容末尾: ...${aiContent.substring(aiContent.length - 500)}`);
}
// 解析AI响应中的JSON
let analysisResult;
try {
// 尝试直接解析
analysisResult = JSON.parse(aiContent);
console.log('[AI分析-讯飞] 直接解析JSON成功');
} 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*```/);
if (codeBlockMatch) {
jsonText = codeBlockMatch[1];
console.log('[AI分析-讯飞] 从markdown代码块中提取JSON');
} else {
// 方法2: 尝试提取第一个完整的JSON对象
const jsonMatch = aiContent.match(/\{[\s\S]*\}/);
if (jsonMatch) {
jsonText = jsonMatch[0];
console.log('[AI分析-讯飞] 从响应中提取JSON对象');
}
}
if (jsonText) {
try {
// 尝试清理JSON文本移除可能的控制字符
jsonText = jsonText.replace(/[\x00-\x1F\x7F]/g, '');
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] || '未知');
// 尝试修复常见的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) {
// 字符串开始
inString = true;
fixedJson += char;
} else {
// 在字符串中遇到引号
// 检查后面的字符,判断这是字符串结束还是字符串值中的引号
let j = i + 1;
// 跳过空白字符
while (j < jsonText.length && /\s/.test(jsonText[j])) {
j++;
}
if (j >= jsonText.length) {
// 到达末尾,这是字符串结束
inString = false;
fixedJson += char;
} else {
const nextNonSpace = jsonText[j];
// 如果下一个非空白字符是:或,或}或],说明这是字符串结束
if (nextNonSpace === ':' || nextNonSpace === ',' || nextNonSpace === '}' || nextNonSpace === ']') {
inString = false;
fixedJson += char;
} else {
// 这是字符串值中的引号,需要转义
fixedJson += '\\"';
}
}
}
} else {
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) {
const start = Math.max(0, errorPos - 100);
const end = Math.min(jsonText.length, errorPos + 100);
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格式是否正确。`);
}
}
} else {
console.error('[AI分析-讯飞] 响应中未找到JSON格式');
console.error('[AI分析-讯飞] 完整响应内容:', aiContent);
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返回范围使用规则基础建议`);
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
}
// 验证AI返回的建议是否有效
if (!aiSuggestion || aiSuggestion.trim().length < 20) {
console.warn(`[AI分析-讯飞] 问题 ${index + 1} 的AI建议过短或为空使用规则基础建议`);
console.warn(`[AI分析-讯飞] 原始建议: "${aiSuggestion}" (长度: ${aiSuggestion.length})`);
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
};
} 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: aiSuggestion,
ai_analyzed: true
};
}
});
} catch (error) {
console.error('[AI分析-讯飞] 批量分析失败:', error);
// 回退到规则基础分析
return issues.map(issue => {
const risk = this.assessRisk(issue);
const suggestion = this.generateSuggestion(issue);
return {
...issue,
risk_level: risk.level,
risk_score: risk.score,
suggestion: suggestion,
ai_analyzed: false
};
});
}
}
/**
* 构建AI分析提示词
* 优化:一次性处理所有问题,按文件分组,读取完整文件内容
*/
async buildAnalysisPrompt(issues, projectPath) {
// 按文件分组问题
const issuesByFile = new Map();
for (const issue of issues) {
const filePath = issue.relative_path || issue.file;
if (!issuesByFile.has(filePath)) {
issuesByFile.set(filePath, []);
}
issuesByFile.get(filePath).push(issue);
}
console.log(`[AI分析] 问题分布在 ${issuesByFile.size} 个文件中`);
// 读取每个文件的完整内容
const filesData = new Map();
for (const [filePath, fileIssues] of issuesByFile.entries()) {
try {
if (filePath && projectPath) {
const fullPath = path.join(projectPath, filePath);
if (fs.existsSync(fullPath)) {
const content = fs.readFileSync(fullPath, 'utf8');
filesData.set(filePath, {
content: content,
issues: fileIssues,
lineCount: content.split('\n').length
});
console.log(`[AI分析] 读取文件: ${filePath} (${content.split('\n').length} 行)`);
}
}
} catch (error) {
console.warn(`[AI分析] 无法读取文件 ${filePath}: ${error.message}`);
}
}
// 构建问题列表(包含完整文件内容)
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`;
issuesText += ` 消息: ${issue.message || '无'}\n`;
issuesText += ` 类型: ${issue.type || 'unknown'}\n`;
issuesText += ` 严重程度: ${issue.severity || 'medium'}\n`;
issuesText += ` 检测工具: ${(issue.detected_by || []).join(', ')}\n\n`;
issueIndex++;
}
}
// 对于没有文件内容的问题,单独列出
for (const issue of issues) {
const filePath = issue.relative_path || issue.file;
if (!filesData.has(filePath)) {
issuesText += `${issueIndex}. 文件: ${filePath}\n`;
issuesText += ` 行号: ${issue.line}\n`;
issuesText += ` 规则: ${issue.rule || '未知'}\n`;
issuesText += ` 消息: ${issue.message || '无'}\n\n`;
issueIndex++;
}
}
return `你是一个专业的Python代码质量分析专家。请分析以下代码检查问题并提供统一的修复建议。
**重要任务:**
1. **合并重复问题**:首先识别并合并重复或相似的问题(不同工具可能检测到相同的问题)
2. **统一分析**:对所有问题(包括合并后的)进行统一分析
3. **提供详细建议**:为每个问题提供详细的修复建议
**分析要求:**
1. **风险等级**high/medium/low根据问题的严重程度和影响范围评估
2. **风险评分**0-100数值越高表示风险越大需要优先处理
3. **修改建议**:必须提供详细且具体的修复建议,包括:
- 问题原因分析
- 具体的修复步骤
- 修复前后的代码示例对比
- 最佳实践建议
- 避免类似问题的建议
**重要要求:**
- **必须为所有问题提供建议**返回的issues数组长度必须与原始问题数量完全一致${issues.length} 个问题)
- **每个问题都必须有suggestion字段**suggestion字段不能为空必须包含至少50字的详细建议
- **按顺序对应**issues数组中的第1个元素对应第1个问题第2个元素对应第2个问题以此类推
- **如果问题已合并**:在对应位置返回合并后的分析结果,但必须确保数组长度不变
**注意:**
- 请先合并重复问题,然后对所有问题(包括合并后的)进行分析
- 根据完整代码上下文给出针对性建议
- 建议要具体、可操作,避免空泛的描述
- **绝对不能省略任何问题**:即使问题相似或重复,也必须为每个问题提供独立的分析结果
问题列表(包含完整代码):
${issuesText}
**请严格按照以下JSON格式返回分析结果必须确保**
1. issues数组长度 = ${issues.length}(与原始问题数量完全一致)
2. 每个issue对象都包含完整的suggestion字段至少50字
3. 所有字段都必须有值,不能为空
4. **只返回纯JSON不要包含任何markdown代码块标记不要使用\`\`\`json\`\`\`包裹)**
5. **JSON中的字符串值必须正确转义**:换行符使用\\n引号使用\\",反斜杠使用\\\\
**返回格式纯JSON不要任何其他文本**
{
"issues": [
{
"risk_level": "high|medium|low",
"risk_score": 0-100,
"suggestion": "详细的问题分析、修复步骤、代码示例和最佳实践建议至少50字不能为空字符串中的特殊字符必须正确转义"
}
]
}
**重要提示:**
- 只返回JSON对象不要包含任何解释文字
- 不要使用markdown代码块包裹JSON
- JSON字符串中的换行符、引号等特殊字符必须正确转义
- 必须返回 ${issues.length} 个issues每个都必须有完整的suggestion**`;
}
/**
* 评估风险(规则基础)
*/
assessRisk(issue) {
let score = 0;
let level = 'low';
// 基于类型
if (issue.type === 'error') score += 50;
else if (issue.type === 'warning') score += 30;
else score += 10;
// 基于严重程度
if (issue.severity === 'high') score += 40;
else if (issue.severity === 'medium') score += 20;
else score += 10;
// 基于规则类型(安全相关规则风险更高)
const securityKeywords = ['security', 'injection', 'xss', 'csrf', 'sql', 'password', 'secret', 'key', 'token'];
const ruleLower = (issue.rule || '').toLowerCase();
const messageLower = (issue.message || '').toLowerCase();
if (securityKeywords.some(keyword =>
ruleLower.includes(keyword) || messageLower.includes(keyword)
)) {
score += 30;
}
// 确定风险等级
if (score >= 70) level = 'high';
else if (score >= 40) level = 'medium';
else level = 'low';
return { level, score };
}
/**
* 生成修改建议(规则基础)
*/
generateSuggestion(issue) {
const rule = issue.rule || '';
const message = issue.message || '';
const ruleLower = rule.toLowerCase();
const messageLower = message.toLowerCase();
// 基于规则类型生成建议
if (ruleLower.includes('import') || messageLower.includes('import')) {
return '检查导入语句确保导入的模块存在且路径正确。考虑使用相对导入或检查PYTHONPATH设置。';
}
if (ruleLower.includes('unused') || messageLower.includes('unused')) {
return '删除未使用的变量、函数或导入。如果确实需要保留,可以在变量名前加下划线(如 _unused_var。';
}
if (ruleLower.includes('naming') || messageLower.includes('naming')) {
return '遵循PEP 8命名规范类名使用驼峰命名CamelCase函数和变量使用下划线命名snake_case。';
}
if (ruleLower.includes('security') || messageLower.includes('security') ||
ruleLower.includes('bandit')) {
return '这是一个安全问题。请仔细检查代码,确保没有安全漏洞。考虑使用安全的替代方案,如使用参数化查询而不是字符串拼接。';
}
if (ruleLower.includes('complexity') || messageLower.includes('complexity')) {
return '代码复杂度过高。考虑将函数拆分为更小的函数,提高代码可读性和可维护性。';
}
if (messageLower.includes('line too long')) {
return '行长度超过限制。将长行拆分为多行,或使用括号、反斜杠进行换行。';
}
if (messageLower.includes('missing docstring')) {
return '添加函数或类的文档字符串docstring说明其功能、参数和返回值。';
}
// 默认建议
return `根据规则"${rule}"的建议:${message}。请参考相关代码规范文档进行修改。`;
}
}
module.exports = new AIAnalyzer();