|
|
|
|
@ -17,10 +17,12 @@ class AIAnalyzer {
|
|
|
|
|
this.openaiModel = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';
|
|
|
|
|
|
|
|
|
|
// 科大讯飞配置(HTTP API)
|
|
|
|
|
this.xfAppId = process.env.XF_APP_ID || '96c72c9e';
|
|
|
|
|
this.xfApiKey = process.env.XF_API_KEY || 'mfxzmIRlAymtEmsgVojM';
|
|
|
|
|
this.xfApiSecret = process.env.XF_API_SECRET || 'ZZIvBMLpPhAHSeATcoDY';
|
|
|
|
|
this.xfModel = process.env.XF_MODEL || 'x1'; // Spark X1.5 模型
|
|
|
|
|
// 注意:HTTP API使用APIpassword(Bearer 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提供商
|
|
|
|
|
@ -29,7 +31,13 @@ class AIAnalyzer {
|
|
|
|
|
|
|
|
|
|
console.log(`[AI分析] 使用提供商: ${this.provider}`);
|
|
|
|
|
if (this.provider === 'xf') {
|
|
|
|
|
console.log(`[AI分析] 科大讯飞 APPID: ${this.xfAppId}`);
|
|
|
|
|
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 ? '已配置' : '未配置'}`);
|
|
|
|
|
}
|
|
|
|
|
@ -53,7 +61,7 @@ class AIAnalyzer {
|
|
|
|
|
|
|
|
|
|
// 检查是否有可用的API配置
|
|
|
|
|
const hasApiConfig = (this.provider === 'openai' && this.openaiApiKey) ||
|
|
|
|
|
(this.provider === 'xf' && this.xfApiKey && this.xfApiSecret);
|
|
|
|
|
(this.provider === 'xf' && this.xfApiPassword);
|
|
|
|
|
|
|
|
|
|
if (hasApiConfig) {
|
|
|
|
|
return await this.aiAnalysis(deduplicated.issues, projectPath);
|
|
|
|
|
@ -165,20 +173,12 @@ class AIAnalyzer {
|
|
|
|
|
*/
|
|
|
|
|
async aiAnalysis(issues, projectPath) {
|
|
|
|
|
try {
|
|
|
|
|
// 将问题分组,每批处理一定数量
|
|
|
|
|
const batchSize = this.provider === 'xf' ? 5 : 10; // 科大讯飞建议小批次
|
|
|
|
|
const batches = [];
|
|
|
|
|
for (let i = 0; i < issues.length; i += batchSize) {
|
|
|
|
|
batches.push(issues.slice(i, i + batchSize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const analyzedIssues = [];
|
|
|
|
|
for (const batch of batches) {
|
|
|
|
|
const analyzed = this.provider === 'xf'
|
|
|
|
|
? await this.analyzeBatchXf(batch, projectPath)
|
|
|
|
|
: await this.analyzeBatch(batch, projectPath);
|
|
|
|
|
analyzedIssues.push(...analyzed);
|
|
|
|
|
}
|
|
|
|
|
// 一次性发送所有问题,不再分批处理
|
|
|
|
|
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);
|
|
|
|
|
@ -202,15 +202,15 @@ class AIAnalyzer {
|
|
|
|
|
* 批量分析问题
|
|
|
|
|
*/
|
|
|
|
|
async analyzeBatch(issues, projectPath) {
|
|
|
|
|
const prompt = this.buildAnalysisPrompt(issues, projectPath);
|
|
|
|
|
const prompt = await this.buildAnalysisPrompt(issues, projectPath);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const requestData = JSON.stringify({
|
|
|
|
|
model: this.model,
|
|
|
|
|
model: this.openaiModel,
|
|
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: '你是一个专业的代码质量分析专家,擅长分析Python代码问题,评估风险并提供修改建议。'
|
|
|
|
|
content: '你是一个专业的Python代码质量分析专家,擅长分析代码问题,评估风险并提供详细、具体的修改建议。你的建议必须包含问题分析、修复步骤、代码示例和最佳实践。'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
role: 'user',
|
|
|
|
|
@ -218,10 +218,10 @@ class AIAnalyzer {
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
temperature: 0.3,
|
|
|
|
|
max_tokens: 2000
|
|
|
|
|
max_tokens: 8000 // 增加token数量以支持一次性处理所有问题和完整代码
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const url = new URL(`${this.apiBase}/chat/completions`);
|
|
|
|
|
const url = new URL(`${this.openaiApiBase}/chat/completions`);
|
|
|
|
|
const options = {
|
|
|
|
|
hostname: url.hostname,
|
|
|
|
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
|
|
|
@ -229,7 +229,7 @@ class AIAnalyzer {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
|
|
|
'Authorization': `Bearer ${this.openaiApiKey}`,
|
|
|
|
|
'Content-Length': Buffer.byteLength(requestData)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
@ -258,17 +258,72 @@ class AIAnalyzer {
|
|
|
|
|
req.end();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const analysisResult = JSON.parse(data.choices[0].message.content);
|
|
|
|
|
// 提取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[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 || this.assessRisk(issue).score,
|
|
|
|
|
suggestion: analysis.suggestion || this.generateSuggestion(issue),
|
|
|
|
|
ai_analyzed: true
|
|
|
|
|
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) {
|
|
|
|
|
@ -288,11 +343,12 @@ class AIAnalyzer {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 使用科大讯飞API批量分析问题(HTTP方式)
|
|
|
|
|
*/
|
|
|
|
|
async analyzeBatchXf(issues, projectPath) {
|
|
|
|
|
const prompt = this.buildAnalysisPrompt(issues, projectPath);
|
|
|
|
|
const prompt = await this.buildAnalysisPrompt(issues, projectPath);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 构建请求数据 - 使用类似OpenAI的格式
|
|
|
|
|
@ -301,32 +357,44 @@ class AIAnalyzer {
|
|
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: '你是一个专业的代码质量分析专家,擅长分析Python代码问题,评估风险并提供修改建议。'
|
|
|
|
|
content: '你是一个专业的Python代码质量分析专家,擅长分析代码问题,评估风险并提供详细、具体的修改建议。你的建议必须包含问题分析、修复步骤、代码示例和最佳实践。'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
role: 'user',
|
|
|
|
|
content: `${prompt}\n\n请以JSON格式返回分析结果,格式如下:\n{\n "issues": [\n {\n "risk_level": "high|medium|low",\n "risk_score": 0-100,\n "suggestion": "具体的修改建议"\n }\n ]\n}`
|
|
|
|
|
content: prompt
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
temperature: 0.3,
|
|
|
|
|
max_tokens: 2000
|
|
|
|
|
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_PASSWORD(HTTP协议的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',
|
|
|
|
|
'Authorization': `Basic ${Buffer.from(`${this.xfApiKey}:${this.xfApiSecret}`).toString('base64')}`,
|
|
|
|
|
'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) => {
|
|
|
|
|
@ -369,28 +437,150 @@ 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 {
|
|
|
|
|
// 尝试直接解析
|
|
|
|
|
analysisResult = JSON.parse(aiContent);
|
|
|
|
|
console.log('[AI分析-讯飞] 直接解析JSON成功');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 如果不是纯JSON,尝试提取JSON部分
|
|
|
|
|
console.log('[AI分析-讯飞] 直接解析失败,尝试提取JSON部分');
|
|
|
|
|
const jsonMatch = aiContent.match(/\{[\s\S]*\}/);
|
|
|
|
|
if (jsonMatch) {
|
|
|
|
|
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 {
|
|
|
|
|
analysisResult = JSON.parse(jsonMatch[0]);
|
|
|
|
|
// 尝试清理JSON文本(移除可能的控制字符)
|
|
|
|
|
jsonText = jsonText.replace(/[\x00-\x1F\x7F]/g, '');
|
|
|
|
|
analysisResult = JSON.parse(jsonText);
|
|
|
|
|
console.log('[AI分析-讯飞] 成功提取并解析JSON');
|
|
|
|
|
} catch (e2) {
|
|
|
|
|
console.error('[AI分析-讯飞] 提取的JSON解析失败:', e2);
|
|
|
|
|
console.error('[AI分析-讯飞] 响应内容:', aiContent.substring(0, 500));
|
|
|
|
|
throw new Error('无法解析AI响应为JSON: ' + e2.message);
|
|
|
|
|
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.substring(0, 500));
|
|
|
|
|
console.error('[AI分析-讯飞] 完整响应内容:', aiContent);
|
|
|
|
|
throw new Error('AI响应中未找到有效的JSON格式');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -398,21 +588,67 @@ class AIAnalyzer {
|
|
|
|
|
// 验证分析结果格式
|
|
|
|
|
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] : {};
|
|
|
|
|
return {
|
|
|
|
|
...issue,
|
|
|
|
|
risk_level: analysis.risk_level || this.assessRisk(issue).level,
|
|
|
|
|
risk_score: analysis.risk_score || this.assessRisk(issue).score,
|
|
|
|
|
suggestion: analysis.suggestion || this.generateSuggestion(issue),
|
|
|
|
|
ai_analyzed: true
|
|
|
|
|
};
|
|
|
|
|
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);
|
|
|
|
|
@ -434,37 +670,129 @@ class AIAnalyzer {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建AI分析提示词
|
|
|
|
|
* 优化:一次性处理所有问题,按文件分组,读取完整文件内容
|
|
|
|
|
*/
|
|
|
|
|
buildAnalysisPrompt(issues, projectPath) {
|
|
|
|
|
const issuesText = issues.map((issue, index) => {
|
|
|
|
|
return `${index + 1}. 文件: ${issue.relative_path || issue.file}
|
|
|
|
|
行号: ${issue.line}
|
|
|
|
|
列号: ${issue.column || 0}
|
|
|
|
|
规则: ${issue.rule || '未知'}
|
|
|
|
|
消息: ${issue.message || '无'}
|
|
|
|
|
类型: ${issue.type || 'unknown'}
|
|
|
|
|
严重程度: ${issue.severity || 'medium'}
|
|
|
|
|
检测工具: ${(issue.detected_by || []).join(', ')}`;
|
|
|
|
|
}).join('\n\n');
|
|
|
|
|
|
|
|
|
|
return `请分析以下Python代码检查问题,对每个问题提供:
|
|
|
|
|
1. 风险等级(high/medium/low)
|
|
|
|
|
2. 风险评分(0-100,数值越高风险越大)
|
|
|
|
|
3. 具体的修改建议
|
|
|
|
|
|
|
|
|
|
问题列表:
|
|
|
|
|
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格式返回,格式如下:
|
|
|
|
|
**请严格按照以下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": "具体的修改建议,包括代码示例"
|
|
|
|
|
"suggestion": "详细的问题分析、修复步骤、代码示例和最佳实践建议(至少50字,不能为空,字符串中的特殊字符必须正确转义)"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
**重要提示:**
|
|
|
|
|
- 只返回JSON对象,不要包含任何解释文字
|
|
|
|
|
- 不要使用markdown代码块包裹JSON
|
|
|
|
|
- JSON字符串中的换行符、引号等特殊字符必须正确转义
|
|
|
|
|
- 必须返回 ${issues.length} 个issues,每个都必须有完整的suggestion!**`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|