@ -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) , 说明其功能、参数和返回值。' ;
}