@ttjl/ai-code-review 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/Readme.md +262 -0
- package/bin/ai-review +2 -0
- package/config/default.config.js +67 -0
- package/lefthook.yml +13 -0
- package/package.json +57 -0
- package/src/cli/index.js +193 -0
- package/src/core/ai-client.js +257 -0
- package/src/core/config-loader.js +102 -0
- package/src/core/file-collector.js +115 -0
- package/src/core/reviewer.js +163 -0
- package/src/formatters/console-formatter.js +146 -0
- package/src/formatters/json-formatter.js +189 -0
- package/src/utils/error-handler.js +183 -0
- package/src/utils/git.js +164 -0
- package/templates/.ai-reviewrc.json +66 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const retry = require('async-retry');
|
|
3
|
+
|
|
4
|
+
class AIClient {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
|
|
8
|
+
// 优先使用配置文件中的 API Key,否则使用环境变量
|
|
9
|
+
const apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
|
|
10
|
+
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
throw new Error('缺少 API Key 配置。请在配置文件中设置 ai.apiKey 或设置 DASHSCOPE_API_KEY 环境变量');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 阿里云百炼 API 配置
|
|
16
|
+
this.apiKey = apiKey;
|
|
17
|
+
this.baseUrl = config.ai?.baseUrl || 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
|
18
|
+
this.model = config.ai.model || 'qwen-max-latest';
|
|
19
|
+
console.log('使用模型:', this.model);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 审查代码
|
|
24
|
+
*/
|
|
25
|
+
async reviewCode(file, diff, rules) {
|
|
26
|
+
const prompt = this.buildPrompt(file, diff, rules);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = await retry(
|
|
30
|
+
async (bail) => {
|
|
31
|
+
try {
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
|
|
34
|
+
// 使用 OpenAI 兼容接口调用阿里云百炼
|
|
35
|
+
const response = await axios.post(
|
|
36
|
+
`${this.baseUrl}/chat/completions`,
|
|
37
|
+
{
|
|
38
|
+
model: this.model,
|
|
39
|
+
messages: [
|
|
40
|
+
{
|
|
41
|
+
role: 'user',
|
|
42
|
+
content: prompt
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
temperature: this.config.ai.temperature || 0.3,
|
|
46
|
+
max_tokens: this.config.ai.maxTokens || 4096
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
headers: {
|
|
50
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
51
|
+
'Content-Type': 'application/json'
|
|
52
|
+
},
|
|
53
|
+
timeout: this.config.ai.timeout || 30000
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const duration = Date.now() - startTime;
|
|
58
|
+
|
|
59
|
+
// 获取响应内容
|
|
60
|
+
const content = response.data.choices?.[0]?.message?.content || '';
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...this.parseResponse(content, file),
|
|
64
|
+
duration
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// 认证错误不需要重试
|
|
68
|
+
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
69
|
+
bail(
|
|
70
|
+
new Error(
|
|
71
|
+
'API 认证失败,请检查 DASHSCOPE_API_KEY 是否正确'
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
retries: this.config.ai.retry.times,
|
|
81
|
+
minTimeout: this.config.ai.retry.delay,
|
|
82
|
+
onRetry: (error, attempt) => {
|
|
83
|
+
console.log(` 重试第 ${attempt} 次...`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new Error(`AI 审查失败: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 构建 AI Prompt
|
|
96
|
+
*/
|
|
97
|
+
buildPrompt(file, diff, rules) {
|
|
98
|
+
return `
|
|
99
|
+
你是一个专业的代码审查专家。请审查以下代码变更,并根据指定的规则提供建议。
|
|
100
|
+
|
|
101
|
+
文件路径: ${file.path}
|
|
102
|
+
文件类型: ${file.ext}
|
|
103
|
+
|
|
104
|
+
代码差异:
|
|
105
|
+
\`\`\`diff
|
|
106
|
+
${diff}
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
审查规则:
|
|
110
|
+
${this.formatRules(rules)}
|
|
111
|
+
|
|
112
|
+
请以 JSON 格式返回审查结果,包含以下字段:
|
|
113
|
+
{
|
|
114
|
+
"summary": "审查摘要,简要描述发现的问题",
|
|
115
|
+
"issues": [
|
|
116
|
+
{
|
|
117
|
+
"severity": "error|warning|info",
|
|
118
|
+
"category": "quality|security|performance|naming|best-practices",
|
|
119
|
+
"line": 行号,
|
|
120
|
+
"message": "问题描述",
|
|
121
|
+
"suggestion": "改进建议",
|
|
122
|
+
"code": "相关代码片段(可选)"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"metrics": {
|
|
126
|
+
"complexity": 数字(1-10),
|
|
127
|
+
"maintainabilityIndex": 数字(0-100)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
注意事项:
|
|
132
|
+
1. 只报告真正的问题,避免误报
|
|
133
|
+
2. 提供具体的改进建议
|
|
134
|
+
3. 使用准确的严重级别:
|
|
135
|
+
- error: 必须修复的问题(安全漏洞、严重bug等)
|
|
136
|
+
- warning: 建议修复的问题(代码质量、性能等)
|
|
137
|
+
- info: 提示性建议(最佳实践、命名规范等)
|
|
138
|
+
4. 如果代码没有问题,返回空 issues 数组
|
|
139
|
+
`.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 格式化规则描述
|
|
144
|
+
*/
|
|
145
|
+
formatRules(rules) {
|
|
146
|
+
const enabledRules = [];
|
|
147
|
+
|
|
148
|
+
if (rules.codeQuality) enabledRules.push('代码质量');
|
|
149
|
+
if (rules.security) enabledRules.push('安全问题');
|
|
150
|
+
if (rules.bestPractices) enabledRules.push('最佳实践');
|
|
151
|
+
if (rules.naming) enabledRules.push('命名规范');
|
|
152
|
+
if (rules.complexity) enabledRules.push('代码复杂度');
|
|
153
|
+
if (rules.performance) enabledRules.push('性能优化');
|
|
154
|
+
|
|
155
|
+
return enabledRules.length > 0 ? enabledRules.join(', ') : '全面审查';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 解析 AI 响应
|
|
160
|
+
*/
|
|
161
|
+
parseResponse(text, file) {
|
|
162
|
+
try {
|
|
163
|
+
// 尝试提取 JSON 内容
|
|
164
|
+
let jsonStr = null;
|
|
165
|
+
|
|
166
|
+
// 方法1: 匹配 ```json...``` 代码块
|
|
167
|
+
const jsonCodeBlockMatch = text.match(/```json\n([\s\S]*?)\n```/);
|
|
168
|
+
if (jsonCodeBlockMatch) {
|
|
169
|
+
jsonStr = jsonCodeBlockMatch[1];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 方法2: 匹配 ```...``` 代码块
|
|
173
|
+
if (!jsonStr) {
|
|
174
|
+
const codeBlockMatch = text.match(/```\n([\s\S]*?)\n```/);
|
|
175
|
+
if (codeBlockMatch) {
|
|
176
|
+
jsonStr = codeBlockMatch[1];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 方法3: 匹配 JSON 对象
|
|
181
|
+
if (!jsonStr) {
|
|
182
|
+
const jsonObjectMatch = text.match(/\{[\s\S]*\}/);
|
|
183
|
+
if (jsonObjectMatch) {
|
|
184
|
+
jsonStr = jsonObjectMatch[0];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!jsonStr) {
|
|
189
|
+
throw new Error('无法从响应中提取 JSON');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 清理可能的 markdown 格式
|
|
193
|
+
jsonStr = jsonStr
|
|
194
|
+
.replace(/^```json\n/, '')
|
|
195
|
+
.replace(/^```\n/, '')
|
|
196
|
+
.replace(/\n```$/, '');
|
|
197
|
+
|
|
198
|
+
const result = JSON.parse(jsonStr);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
file: file.path,
|
|
202
|
+
summary: result.summary || '代码审查完成',
|
|
203
|
+
issues: this.normalizeIssues(result.issues || []),
|
|
204
|
+
metrics: result.metrics || { complexity: 0, maintainabilityIndex: 100 },
|
|
205
|
+
rawResponse: text
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// 解析失败,返回默认结构
|
|
209
|
+
return {
|
|
210
|
+
file: file.path,
|
|
211
|
+
summary: '审查完成(响应解析失败)',
|
|
212
|
+
issues: [],
|
|
213
|
+
error: error.message,
|
|
214
|
+
rawResponse: text,
|
|
215
|
+
metrics: { complexity: 0, maintainabilityIndex: 100 }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 标准化问题对象
|
|
222
|
+
*/
|
|
223
|
+
normalizeIssues(issues) {
|
|
224
|
+
return issues.map(issue => ({
|
|
225
|
+
severity: this.normalizeSeverity(issue.severity),
|
|
226
|
+
category: this.normalizeCategory(issue.category),
|
|
227
|
+
line: issue.line || 0,
|
|
228
|
+
message: issue.message || '未描述的问题',
|
|
229
|
+
suggestion: issue.suggestion || '',
|
|
230
|
+
code: issue.code || ''
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 标准化严重级别
|
|
236
|
+
*/
|
|
237
|
+
normalizeSeverity(severity) {
|
|
238
|
+
const validSeverities = ['error', 'warning', 'info'];
|
|
239
|
+
return validSeverities.includes(severity) ? severity : 'info';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 标准化问题类别
|
|
244
|
+
*/
|
|
245
|
+
normalizeCategory(category) {
|
|
246
|
+
const validCategories = [
|
|
247
|
+
'quality',
|
|
248
|
+
'security',
|
|
249
|
+
'performance',
|
|
250
|
+
'naming',
|
|
251
|
+
'best-practices'
|
|
252
|
+
];
|
|
253
|
+
return validCategories.includes(category) ? category : 'quality';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = AIClient;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const defaultConfig = require('../../config/default.config.js');
|
|
4
|
+
|
|
5
|
+
class ConfigLoader {
|
|
6
|
+
static async load(customConfigPath = null) {
|
|
7
|
+
let userConfig = {};
|
|
8
|
+
|
|
9
|
+
if (customConfigPath) {
|
|
10
|
+
userConfig = await this.loadConfigFile(customConfigPath);
|
|
11
|
+
} else {
|
|
12
|
+
// 尝试按优先级加载配置文件
|
|
13
|
+
const configFiles = [
|
|
14
|
+
'.ai-reviewrc.json',
|
|
15
|
+
'.ai-reviewrc.js',
|
|
16
|
+
'ai-review.config.js'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const file of configFiles) {
|
|
20
|
+
try {
|
|
21
|
+
const config = await this.loadConfigFile(file);
|
|
22
|
+
if (config) {
|
|
23
|
+
userConfig = config;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// 文件不存在,继续尝试下一个
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this.mergeConfig(defaultConfig, userConfig);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static async loadConfigFile(filename) {
|
|
37
|
+
const filePath = path.resolve(process.cwd(), filename);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
41
|
+
|
|
42
|
+
if (filename.endsWith('.json')) {
|
|
43
|
+
return JSON.parse(content);
|
|
44
|
+
} else if (filename.endsWith('.js')) {
|
|
45
|
+
// eslint-disable-next-line no-eval
|
|
46
|
+
return eval(`(${content})`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(`不支持的配置文件类型: ${filename}`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT') {
|
|
52
|
+
throw new Error(`配置文件不存在: ${filename}`);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`加载配置文件失败: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static mergeConfig(defaultConfig, userConfig) {
|
|
59
|
+
const merged = { ...defaultConfig };
|
|
60
|
+
|
|
61
|
+
for (const key in userConfig) {
|
|
62
|
+
if (typeof userConfig[key] === 'object' && !Array.isArray(userConfig[key])) {
|
|
63
|
+
merged[key] = {
|
|
64
|
+
...merged[key],
|
|
65
|
+
...userConfig[key]
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
merged[key] = userConfig[key];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static validate(config) {
|
|
76
|
+
const errors = [];
|
|
77
|
+
|
|
78
|
+
if (!config.review || typeof config.review.enabled !== 'boolean') {
|
|
79
|
+
errors.push('review.enabled 必须是布尔值');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!config.review || !['block', 'warn'].includes(config.review.onFail)) {
|
|
83
|
+
errors.push('review.onFail 必须是 "block" 或 "warn"');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!config.ai || !config.ai.model) {
|
|
87
|
+
errors.push('ai.model 是必需的');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!config.files || !Array.isArray(config.files.include)) {
|
|
91
|
+
errors.push('files.include 必须是数组');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
throw new Error(`配置验证失败:\n${errors.join('\n')}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = ConfigLoader;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const minimatch = require('minimatch');
|
|
4
|
+
const GitUtils = require('../utils/git.js');
|
|
5
|
+
|
|
6
|
+
class FileCollector {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 收集需要审查的文件
|
|
13
|
+
*/
|
|
14
|
+
async collect() {
|
|
15
|
+
const files = this.config.git.stagedOnly
|
|
16
|
+
? GitUtils.getStagedFiles()
|
|
17
|
+
: GitUtils.getChangedFiles(this.config.git.diffBase);
|
|
18
|
+
|
|
19
|
+
return this.filterFiles(files);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 过滤文件
|
|
24
|
+
*/
|
|
25
|
+
filterFiles(files) {
|
|
26
|
+
return files
|
|
27
|
+
.map(file => ({
|
|
28
|
+
path: file,
|
|
29
|
+
ext: path.extname(file),
|
|
30
|
+
relativePath: file
|
|
31
|
+
}))
|
|
32
|
+
.filter(file => this.isIncluded(file))
|
|
33
|
+
.filter(file => !this.isExcluded(file))
|
|
34
|
+
.filter(file => this.isWithinSizeLimit(file))
|
|
35
|
+
.slice(0, this.config.review.maxFiles);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 检查文件是否包含在审查范围内
|
|
40
|
+
*/
|
|
41
|
+
isIncluded(file) {
|
|
42
|
+
return this.config.files.include.some(pattern =>
|
|
43
|
+
minimatch(file.path, pattern)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 检查文件是否被排除
|
|
49
|
+
*/
|
|
50
|
+
isExcluded(file) {
|
|
51
|
+
return this.config.files.exclude.some(pattern =>
|
|
52
|
+
minimatch(file.path, pattern)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 检查文件大小是否在限制内
|
|
58
|
+
*/
|
|
59
|
+
async isWithinSizeLimit(file) {
|
|
60
|
+
try {
|
|
61
|
+
const stats = await fs.stat(file.path);
|
|
62
|
+
const sizeKB = stats.size / 1024;
|
|
63
|
+
return sizeKB <= this.config.review.maxFileSize;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// 文件不存在或无法访问
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取文件差异
|
|
72
|
+
*/
|
|
73
|
+
getDiff(file) {
|
|
74
|
+
const diff = this.config.git.stagedOnly
|
|
75
|
+
? GitUtils.getStagedFileDiff(file.path)
|
|
76
|
+
: GitUtils.getFileDiff(file.path, this.config.git.diffBase);
|
|
77
|
+
return this.truncateDiff(diff);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 截断过长的差异内容
|
|
82
|
+
*/
|
|
83
|
+
truncateDiff(diff) {
|
|
84
|
+
const lines = diff.split('\n');
|
|
85
|
+
if (lines.length > this.config.review.maxLineLength) {
|
|
86
|
+
return (
|
|
87
|
+
lines.slice(0, this.config.review.maxLineLength).join('\n') +
|
|
88
|
+
'\n... (内容过长,已截断)'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return diff;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取文件信息
|
|
96
|
+
*/
|
|
97
|
+
async getFileInfo(file) {
|
|
98
|
+
try {
|
|
99
|
+
const stats = await fs.stat(file.path);
|
|
100
|
+
const diff = this.getDiff(file);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
path: file.path,
|
|
104
|
+
ext: file.ext,
|
|
105
|
+
size: stats.size,
|
|
106
|
+
diff: diff,
|
|
107
|
+
lines: diff.split('\n').length
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new Error(`获取文件信息失败: ${file.path} - ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = FileCollector;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const AIClient = require('./ai-client');
|
|
3
|
+
const FileCollector = require('./file-collector');
|
|
4
|
+
const ConsoleFormatter = require('../formatters/console-formatter');
|
|
5
|
+
const JSONFormatter = require('../formatters/json-formatter');
|
|
6
|
+
const GitUtils = require('../utils/git');
|
|
7
|
+
const ErrorHandler = require('../utils/error-handler');
|
|
8
|
+
|
|
9
|
+
class CodeReviewer {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.aiClient = new AIClient(config);
|
|
13
|
+
this.fileCollector = new FileCollector(config);
|
|
14
|
+
this.consoleFormatter = new ConsoleFormatter(config);
|
|
15
|
+
this.jsonFormatter = new JSONFormatter(config);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 执行代码审查
|
|
20
|
+
*/
|
|
21
|
+
async review() {
|
|
22
|
+
try {
|
|
23
|
+
// 检查是否启用审查
|
|
24
|
+
if (!this.config.review.enabled) {
|
|
25
|
+
this.consoleFormatter.formatSuccess('代码审查已禁用');
|
|
26
|
+
return { success: true, issues: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 检查是否在 Git 仓库中
|
|
30
|
+
if (!GitUtils.isGitRepo()) {
|
|
31
|
+
throw new Error('当前目录不是 Git 仓库');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.consoleFormatter.formatProgress('🔍 开始收集文件...');
|
|
35
|
+
|
|
36
|
+
// 收集需要审查的文件
|
|
37
|
+
const files = await this.fileCollector.collect();
|
|
38
|
+
|
|
39
|
+
if (files.length === 0) {
|
|
40
|
+
this.consoleFormatter.formatSuccess('没有需要审查的文件');
|
|
41
|
+
return { success: true, issues: [] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.consoleFormatter.formatProgress(
|
|
45
|
+
`📁 找到 ${files.length} 个文件需要审查\n`
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const results = [];
|
|
49
|
+
const allIssues = [];
|
|
50
|
+
|
|
51
|
+
// 逐个审查文件
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
console.log(chalk.cyan(`正在审查: ${file.path}`));
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const fileInfo = await this.fileCollector.getFileInfo(file);
|
|
57
|
+
|
|
58
|
+
if (!fileInfo.diff || fileInfo.diff.trim().length === 0) {
|
|
59
|
+
console.log(' ⏭️ 跳过: 无差异内容\n');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 调用 AI 审查
|
|
64
|
+
const result = await this.aiClient.reviewCode(
|
|
65
|
+
fileInfo,
|
|
66
|
+
fileInfo.diff,
|
|
67
|
+
this.config.rules
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
results.push(result);
|
|
71
|
+
|
|
72
|
+
if (result.issues) {
|
|
73
|
+
allIssues.push(...result.issues);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 格式化输出
|
|
77
|
+
this.consoleFormatter.formatFile(result);
|
|
78
|
+
console.log('');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(` ❌ 错误: ${error.message}\n`);
|
|
81
|
+
results.push({
|
|
82
|
+
file: file.path,
|
|
83
|
+
error: error.message,
|
|
84
|
+
issues: []
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 生成报告
|
|
90
|
+
await this.generateReports(results, allIssues);
|
|
91
|
+
|
|
92
|
+
// 判断是否阻止提交
|
|
93
|
+
const shouldBlock = this.shouldBlockCommit(allIssues);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: !shouldBlock,
|
|
97
|
+
filesReviewed: results.length,
|
|
98
|
+
issues: allIssues,
|
|
99
|
+
summary: this.generateSummary(allIssues)
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const result = ErrorHandler.handle(error);
|
|
103
|
+
return {
|
|
104
|
+
success: !result.shouldExit,
|
|
105
|
+
issues: [],
|
|
106
|
+
error: error.message
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 生成报告
|
|
113
|
+
*/
|
|
114
|
+
async generateReports(results, issues) {
|
|
115
|
+
// 控制台输出
|
|
116
|
+
if (this.config.output.console.enabled) {
|
|
117
|
+
this.consoleFormatter.formatSummary(results, issues);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// JSON 报告
|
|
121
|
+
if (this.config.output.file.enabled) {
|
|
122
|
+
try {
|
|
123
|
+
await this.jsonFormatter.generate(results, issues);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(chalk.red(`生成报告失败: ${error.message}`));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 判断是否应该阻止提交
|
|
132
|
+
*/
|
|
133
|
+
shouldBlockCommit(issues) {
|
|
134
|
+
if (this.config.review.onFail !== 'block') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return issues.some(issue => issue.severity === 'error');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 生成总结
|
|
143
|
+
*/
|
|
144
|
+
generateSummary(issues) {
|
|
145
|
+
const bySeverity = {
|
|
146
|
+
error: issues.filter(i => i.severity === 'error').length,
|
|
147
|
+
warning: issues.filter(i => i.severity === 'warning').length,
|
|
148
|
+
info: issues.filter(i => i.severity === 'info').length
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const byCategory = {
|
|
152
|
+
quality: issues.filter(i => i.category === 'quality').length,
|
|
153
|
+
security: issues.filter(i => i.category === 'security').length,
|
|
154
|
+
performance: issues.filter(i => i.category === 'performance').length,
|
|
155
|
+
naming: issues.filter(i => i.category === 'naming').length,
|
|
156
|
+
bestPractices: issues.filter(i => i.category === 'best-practices').length
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return { bySeverity, byCategory };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = CodeReviewer;
|