gitlab-ai-review 2.5.4 → 3.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/README.md +46 -0
- package/cli.js +72 -93
- package/index.js +158 -48
- package/lib/diff-parser.js +0 -31
- package/lib/gitlab-client.js +69 -0
- package/lib/impact-analyzer.js +455 -0
- package/lib/prompt-tools.js +219 -64
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -9,6 +9,13 @@ GitLab AI Review SDK - 支持 CI/CD 自动配置和手动配置
|
|
|
9
9
|
- ✅ 支持手动传递配置
|
|
10
10
|
- ✅ 获取项目和 MR 信息
|
|
11
11
|
- ✅ 添加 MR 评论
|
|
12
|
+
- ✅ **全面影响分析** 🆕
|
|
13
|
+
- 检测删除符号的影响
|
|
14
|
+
- 检查文件内部冲突
|
|
15
|
+
- 追踪跨文件依赖关系
|
|
16
|
+
- 完整上下文分析
|
|
17
|
+
- ✅ 行级 AI 代码审查
|
|
18
|
+
- ✅ 批量审查和评论
|
|
12
19
|
|
|
13
20
|
## 安装
|
|
14
21
|
|
|
@@ -135,6 +142,45 @@ changes.forEach(change => {
|
|
|
135
142
|
await sdk.addComment('✅ 代码审查通过!');
|
|
136
143
|
```
|
|
137
144
|
|
|
145
|
+
#### `reviewWithImpactAnalysis(options)` 🆕
|
|
146
|
+
AI 审查 MR 的所有变更(包含影响分析)
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// 使用默认配置(启用影响分析)
|
|
150
|
+
const results = await sdk.reviewWithImpactAnalysis();
|
|
151
|
+
|
|
152
|
+
// 自定义配置
|
|
153
|
+
const results = await sdk.reviewWithImpactAnalysis({
|
|
154
|
+
maxFiles: 5, // 最多审查 5 个文件
|
|
155
|
+
maxAffectedFiles: 10, // 每个文件最多分析 10 个受影响的文件
|
|
156
|
+
enableImpactAnalysis: true // 启用影响分析(默认)
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log(`审查完成,共添加 ${results.length} 条评论`);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**参数:**
|
|
163
|
+
- `options.maxFiles` - 最大审查文件数量(默认不限制)
|
|
164
|
+
- `options.maxAffectedFiles` - 每个文件最多分析的受影响文件数量(默认 10)
|
|
165
|
+
- `options.enableImpactAnalysis` - 是否启用影响分析(默认 true)
|
|
166
|
+
|
|
167
|
+
**返回值:**
|
|
168
|
+
返回评论结果数组,每个元素包含:
|
|
169
|
+
- `status` - 'success' 或 'error'
|
|
170
|
+
- `fileName` - 文件名
|
|
171
|
+
- `lineNumber` - 行号
|
|
172
|
+
- `comment` - 评论内容
|
|
173
|
+
- `error` - 错误信息(如果失败)
|
|
174
|
+
|
|
175
|
+
#### `reviewAndCommentOnLines(options)`
|
|
176
|
+
AI 审查 MR 的所有变更(不包含影响分析)
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
const results = await sdk.reviewAndCommentOnLines({
|
|
180
|
+
maxFiles: 10 // 最多审查 10 个文件
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
138
184
|
## 在 GitLab CI/CD 中使用
|
|
139
185
|
|
|
140
186
|
`.gitlab-ci.yml` 示例:
|
package/cli.js
CHANGED
|
@@ -7,7 +7,68 @@
|
|
|
7
7
|
|
|
8
8
|
import { GitLabAIReview } from './index.js';
|
|
9
9
|
|
|
10
|
+
// 解析命令行参数
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const options = {
|
|
14
|
+
enableImpactAnalysis: true, // 默认启用影响分析
|
|
15
|
+
maxAffectedFiles: 10,
|
|
16
|
+
maxFiles: Infinity,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
|
|
22
|
+
if (arg === '--no-impact' || arg === '--disable-impact') {
|
|
23
|
+
options.enableImpactAnalysis = false;
|
|
24
|
+
} else if (arg === '--max-affected-files' && args[i + 1]) {
|
|
25
|
+
options.maxAffectedFiles = parseInt(args[i + 1], 10);
|
|
26
|
+
i++;
|
|
27
|
+
} else if (arg === '--max-files' && args[i + 1]) {
|
|
28
|
+
options.maxFiles = parseInt(args[i + 1], 10);
|
|
29
|
+
i++;
|
|
30
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
31
|
+
console.log(`
|
|
32
|
+
GitLab AI Review CLI
|
|
33
|
+
|
|
34
|
+
用法:
|
|
35
|
+
npx gitlab-ai-review [选项]
|
|
36
|
+
|
|
37
|
+
选项:
|
|
38
|
+
--no-impact, --disable-impact 禁用影响分析(默认启用)
|
|
39
|
+
--max-affected-files <数量> 每个文件最多分析的受影响文件数量(默认 10)
|
|
40
|
+
--max-files <数量> 最大审查文件数量(默认不限制)
|
|
41
|
+
--help, -h 显示帮助信息
|
|
42
|
+
|
|
43
|
+
环境变量:
|
|
44
|
+
GITLAB_TOKEN GitLab 访问令牌
|
|
45
|
+
CI_PROJECT_ID 项目 ID
|
|
46
|
+
CI_MERGE_REQUEST_IID MR IID
|
|
47
|
+
ARK_API_KEY AI API 密钥
|
|
48
|
+
|
|
49
|
+
示例:
|
|
50
|
+
# 默认模式(启用影响分析)
|
|
51
|
+
npx gitlab-ai-review
|
|
52
|
+
|
|
53
|
+
# 禁用影响分析
|
|
54
|
+
npx gitlab-ai-review --no-impact
|
|
55
|
+
|
|
56
|
+
# 自定义受影响文件数量
|
|
57
|
+
npx gitlab-ai-review --max-affected-files 5
|
|
58
|
+
|
|
59
|
+
# 限制审查文件数量
|
|
60
|
+
npx gitlab-ai-review --max-files 3
|
|
61
|
+
`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return options;
|
|
67
|
+
}
|
|
68
|
+
|
|
10
69
|
async function main() {
|
|
70
|
+
const cliOptions = parseArgs();
|
|
71
|
+
|
|
11
72
|
console.log('🚀 GitLab AI Review CLI 启动...\n');
|
|
12
73
|
|
|
13
74
|
try {
|
|
@@ -16,11 +77,15 @@ async function main() {
|
|
|
16
77
|
|
|
17
78
|
// 2. 验证配置
|
|
18
79
|
console.log('📋 配置信息:');
|
|
19
|
-
console.log(` - GitLab Host: ${sdk.config.gitlab.
|
|
80
|
+
console.log(` - GitLab Host: ${sdk.config.gitlab.url}`);
|
|
20
81
|
console.log(` - Project ID: ${sdk.config.project.projectId}`);
|
|
21
82
|
console.log(` - MR IID: ${sdk.config.project.mergeRequestIid}`);
|
|
22
83
|
console.log(` - AI API Key: ${sdk.config.ai?.arkApiKey ? '✅ 已配置' : '❌ 未配置'}`);
|
|
23
84
|
console.log(` - Review Guard: ${sdk.config.ai?.guardConfig?.content ? '✅ 已加载' : '⚠️ 未找到'}`);
|
|
85
|
+
console.log(` - 影响分析: ${cliOptions.enableImpactAnalysis ? '✅ 已启用' : '⚠️ 已禁用'}`);
|
|
86
|
+
if (cliOptions.enableImpactAnalysis) {
|
|
87
|
+
console.log(` - 最多分析受影响文件: ${cliOptions.maxAffectedFiles} 个`);
|
|
88
|
+
}
|
|
24
89
|
console.log();
|
|
25
90
|
|
|
26
91
|
// 3. 获取 MR 变更信息
|
|
@@ -28,105 +93,19 @@ async function main() {
|
|
|
28
93
|
const changes = await sdk.getMergeRequestChanges();
|
|
29
94
|
console.log(`✅ 共发现 ${changes.length} 个文件变更\n`);
|
|
30
95
|
|
|
31
|
-
// 3.1 输出详细的变更列表
|
|
32
|
-
if (changes.length > 0) {
|
|
33
|
-
console.log('📝 变更文件列表:');
|
|
34
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
35
|
-
|
|
36
|
-
changes.forEach((change, index) => {
|
|
37
|
-
const fileName = change.new_path || change.old_path;
|
|
38
|
-
let status = '';
|
|
39
|
-
let statusIcon = '';
|
|
40
|
-
|
|
41
|
-
// 判断文件状态
|
|
42
|
-
if (change.new_file) {
|
|
43
|
-
status = '新增';
|
|
44
|
-
statusIcon = '✨';
|
|
45
|
-
} else if (change.deleted_file) {
|
|
46
|
-
status = '删除';
|
|
47
|
-
statusIcon = '🗑️';
|
|
48
|
-
} else if (change.renamed_file) {
|
|
49
|
-
status = '重命名';
|
|
50
|
-
statusIcon = '📝';
|
|
51
|
-
} else {
|
|
52
|
-
status = '修改';
|
|
53
|
-
statusIcon = '✏️';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 计算变更行数
|
|
57
|
-
const diff = change.diff || '';
|
|
58
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
59
|
-
const deletedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
60
|
-
|
|
61
|
-
console.log(`${index + 1}. ${statusIcon} ${status} - ${fileName}`);
|
|
62
|
-
if (addedLines > 0 || deletedLines > 0) {
|
|
63
|
-
console.log(` 📊 +${addedLines} -${deletedLines} 行`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 如果是重命名,显示旧路径
|
|
67
|
-
if (change.renamed_file && change.old_path !== change.new_path) {
|
|
68
|
-
console.log(` 📂 ${change.old_path} → ${change.new_path}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 输出具体的 diff 内容
|
|
72
|
-
if (diff) {
|
|
73
|
-
console.log(`\n 📄 Diff 内容:`);
|
|
74
|
-
console.log(' ┌─────────────────────────────────────────────────────');
|
|
75
|
-
|
|
76
|
-
// 分行显示 diff,并添加颜色标记
|
|
77
|
-
const diffLines = diff.split('\n');
|
|
78
|
-
diffLines.forEach((line, lineIndex) => {
|
|
79
|
-
// 限制显示最多 100 行 diff
|
|
80
|
-
if (lineIndex >= 100) {
|
|
81
|
-
if (lineIndex === 100) {
|
|
82
|
-
console.log(' │ ... (diff 内容过长,已截断)');
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 为不同类型的行添加标记
|
|
88
|
-
let prefix = ' │ ';
|
|
89
|
-
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
90
|
-
prefix = ' │ 📁 '; // 文件路径
|
|
91
|
-
} else if (line.startsWith('@@')) {
|
|
92
|
-
prefix = ' │ 🔵 '; // hunk 标记
|
|
93
|
-
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
94
|
-
prefix = ' │ ➕ '; // 新增行
|
|
95
|
-
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
96
|
-
prefix = ' │ ➖ '; // 删除行
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log(prefix + line);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
console.log(' └─────────────────────────────────────────────────────\n');
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
96
|
// 4. 执行 AI 代码审查
|
|
110
97
|
if (sdk.config.ai?.arkApiKey && changes.length > 0) {
|
|
111
98
|
console.log('🤖 开始 AI 代码审查...\n');
|
|
112
99
|
|
|
113
|
-
|
|
100
|
+
// 使用新的影响分析方法
|
|
101
|
+
const results = await sdk.reviewWithImpactAnalysis(cliOptions);
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
const fileNames = new Set(results.map(r => r.fileName));
|
|
103
|
+
console.log('\n📊 审查结果汇总:');
|
|
117
104
|
const successCount = results.filter(r => r.status === 'success').length;
|
|
118
105
|
const errorCount = results.filter(r => r.status === 'error').length;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.log(
|
|
122
|
-
console.log(` - 审查文件数: ${fileNames.size}`);
|
|
123
|
-
console.log(` - 成功添加评论: ${successCount} 条`);
|
|
124
|
-
if (errorCount > 0) {
|
|
125
|
-
console.log(` - 失败: ${errorCount} 条`);
|
|
126
|
-
}
|
|
127
|
-
if (skippedCount > 0) {
|
|
128
|
-
console.log(` - 跳过: ${skippedCount} 条`);
|
|
129
|
-
}
|
|
106
|
+
console.log(` - 总评论数: ${results.length}`);
|
|
107
|
+
console.log(` - 成功: ${successCount}`);
|
|
108
|
+
console.log(` - 失败: ${errorCount}`);
|
|
130
109
|
|
|
131
110
|
if (errorCount > 0) {
|
|
132
111
|
console.log('\n⚠️ 部分评论添加失败,请检查上方日志');
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { GitLabClient } from './lib/gitlab-client.js';
|
|
|
8
8
|
import { AIClient } from './lib/ai-client.js';
|
|
9
9
|
import * as PromptTools from './lib/prompt-tools.js';
|
|
10
10
|
import * as DiffParser from './lib/diff-parser.js';
|
|
11
|
+
import * as ImpactAnalyzer from './lib/impact-analyzer.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* GitLab AI Review SDK 主类
|
|
@@ -15,7 +16,7 @@ import * as DiffParser from './lib/diff-parser.js';
|
|
|
15
16
|
export class GitLabAIReview {
|
|
16
17
|
constructor(options = {}) {
|
|
17
18
|
this.name = 'GitLab AI Review SDK';
|
|
18
|
-
this.version = '2.
|
|
19
|
+
this.version = '2.3.0';
|
|
19
20
|
|
|
20
21
|
// 如果传入了配置,使用手动配置;否则使用自动检测
|
|
21
22
|
if (options.token || options.gitlab) {
|
|
@@ -166,35 +167,14 @@ export class GitLabAIReview {
|
|
|
166
167
|
continue;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
const deletionsCount = meaningfulChanges.filter(c => c.type === 'deletion').length;
|
|
171
|
-
console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更(新增 ${additionsCount} 行,删除 ${deletionsCount} 行)`);
|
|
170
|
+
console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
|
|
172
171
|
|
|
173
172
|
// 调用 AI 一次性审查整个文件的所有变更(按文件批量)
|
|
174
173
|
const fileReview = await this.reviewFileChanges(change, meaningfulChanges);
|
|
175
174
|
|
|
176
|
-
//
|
|
177
|
-
const validAdditionLineNumbers = new Set(
|
|
178
|
-
meaningfulChanges
|
|
179
|
-
.filter(c => c.type === 'addition') // 只包含新增的行
|
|
180
|
-
.map(c => c.lineNumber)
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// 根据 AI 返回的结果,只对有问题的新增行添加评论
|
|
175
|
+
// 根据 AI 返回的结果,只对有问题的行添加评论
|
|
184
176
|
for (const review of fileReview.reviews) {
|
|
185
177
|
if (review.hasIssue) {
|
|
186
|
-
// 验证行号是否是新增行
|
|
187
|
-
if (!validAdditionLineNumbers.has(review.lineNumber)) {
|
|
188
|
-
console.log(` ⚠ 第 ${review.lineNumber} 行:不是新增行或不在变更范围内,跳过评论`);
|
|
189
|
-
results.push({
|
|
190
|
-
status: 'skipped',
|
|
191
|
-
fileName,
|
|
192
|
-
lineNumber: review.lineNumber,
|
|
193
|
-
reason: '不是新增行或行号不在 diff 变更范围内',
|
|
194
|
-
});
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
178
|
try {
|
|
199
179
|
const commentResult = await this.gitlabClient.createLineComment(
|
|
200
180
|
this.config.project.projectId,
|
|
@@ -259,11 +239,8 @@ export class GitLabAIReview {
|
|
|
259
239
|
const projectPrompt = this.config.ai?.guardConfig?.content || '';
|
|
260
240
|
const fileName = change.new_path || change.old_path;
|
|
261
241
|
|
|
262
|
-
//
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
// 构建整个文件的批量审查消息(按 hunk 组织)
|
|
266
|
-
const messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, hunkGroups, projectPrompt);
|
|
242
|
+
// 构建整个文件的批量审查消息
|
|
243
|
+
const messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
|
|
267
244
|
|
|
268
245
|
// 调用 AI(一次调用审查整个文件)
|
|
269
246
|
const response = await aiClient.sendMessage(messages);
|
|
@@ -279,9 +256,6 @@ export class GitLabAIReview {
|
|
|
279
256
|
jsonStr = jsonStr.slice(3, -3).trim();
|
|
280
257
|
}
|
|
281
258
|
|
|
282
|
-
// 清理可能的格式错误(如多余的括号)
|
|
283
|
-
jsonStr = this.cleanJsonString(jsonStr);
|
|
284
|
-
|
|
285
259
|
const result = JSON.parse(jsonStr);
|
|
286
260
|
return result;
|
|
287
261
|
} catch (error) {
|
|
@@ -292,23 +266,159 @@ export class GitLabAIReview {
|
|
|
292
266
|
}
|
|
293
267
|
|
|
294
268
|
/**
|
|
295
|
-
*
|
|
296
|
-
* @param {
|
|
297
|
-
* @
|
|
269
|
+
* AI 审查 MR 的所有变更(包含影响分析)
|
|
270
|
+
* @param {Object} options - 选项
|
|
271
|
+
* @param {number} options.maxFiles - 最大审查文件数量(默认不限制)
|
|
272
|
+
* @param {number} options.maxAffectedFiles - 每个文件最多分析的受影响文件数量(默认 10)
|
|
273
|
+
* @param {boolean} options.enableImpactAnalysis - 是否启用影响分析(默认 true)
|
|
274
|
+
* @returns {Promise<Array>} 评论结果数组
|
|
298
275
|
*/
|
|
299
|
-
|
|
300
|
-
|
|
276
|
+
async reviewWithImpactAnalysis(options = {}) {
|
|
277
|
+
const {
|
|
278
|
+
maxFiles = Infinity,
|
|
279
|
+
maxAffectedFiles = 10,
|
|
280
|
+
enableImpactAnalysis = true
|
|
281
|
+
} = options;
|
|
301
282
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
283
|
+
const changes = await this.getMergeRequestChanges();
|
|
284
|
+
const mrInfo = await this.getMergeRequest();
|
|
285
|
+
const ref = mrInfo.target_branch || 'main';
|
|
286
|
+
const results = [];
|
|
287
|
+
|
|
288
|
+
const filesToReview = maxFiles === Infinity ? changes.length : Math.min(maxFiles, changes.length);
|
|
289
|
+
console.log(`共 ${changes.length} 个文件需要审查${maxFiles === Infinity ? '(不限制数量)' : `(最多审查 ${maxFiles} 个)`}`);
|
|
290
|
+
console.log(`影响分析: ${enableImpactAnalysis ? '已启用' : '已禁用'}`);
|
|
291
|
+
|
|
292
|
+
for (const change of changes.slice(0, filesToReview)) {
|
|
293
|
+
const fileName = change.new_path || change.old_path;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
console.log(`\n审查文件: ${fileName}`);
|
|
297
|
+
|
|
298
|
+
// 解析 diff,提取有意义的变更
|
|
299
|
+
const hunks = DiffParser.parseDiff(change.diff);
|
|
300
|
+
const meaningfulChanges = DiffParser.extractMeaningfulChanges(hunks);
|
|
301
|
+
|
|
302
|
+
if (meaningfulChanges.length === 0) {
|
|
303
|
+
console.log(` 跳过:没有有意义的变更`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
|
|
308
|
+
|
|
309
|
+
// 影响分析
|
|
310
|
+
let impactAnalysis = null;
|
|
311
|
+
if (enableImpactAnalysis) {
|
|
312
|
+
impactAnalysis = await ImpactAnalyzer.analyzeImpact({
|
|
313
|
+
gitlabClient: this.gitlabClient,
|
|
314
|
+
projectId: this.config.project.projectId,
|
|
315
|
+
ref: ref,
|
|
316
|
+
change: change,
|
|
317
|
+
maxAffectedFiles: maxAffectedFiles,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 调用 AI 审查(包含影响分析)
|
|
322
|
+
const fileReview = await this.reviewFileChangesWithImpact(
|
|
323
|
+
change,
|
|
324
|
+
meaningfulChanges,
|
|
325
|
+
impactAnalysis
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// 根据 AI 返回的结果,只对有问题的行添加评论
|
|
329
|
+
for (const review of fileReview.reviews) {
|
|
330
|
+
if (review.hasIssue) {
|
|
331
|
+
try {
|
|
332
|
+
const commentResult = await this.gitlabClient.createLineComment(
|
|
333
|
+
this.config.project.projectId,
|
|
334
|
+
this.config.project.mergeRequestIid,
|
|
335
|
+
`🤖 **AI 代码审查**\n\n${review.comment}`,
|
|
336
|
+
{
|
|
337
|
+
filePath: fileName,
|
|
338
|
+
oldPath: change.old_path,
|
|
339
|
+
newLine: review.lineNumber,
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
results.push({
|
|
344
|
+
status: 'success',
|
|
345
|
+
fileName,
|
|
346
|
+
lineNumber: review.lineNumber,
|
|
347
|
+
comment: review.comment,
|
|
348
|
+
commentResult,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
console.log(` ✓ 第 ${review.lineNumber} 行:已添加评论`);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
results.push({
|
|
354
|
+
status: 'error',
|
|
355
|
+
fileName,
|
|
356
|
+
lineNumber: review.lineNumber,
|
|
357
|
+
error: error.message,
|
|
358
|
+
});
|
|
359
|
+
console.log(` ✗ 第 ${review.lineNumber} 行:评论失败 - ${error.message}`);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
console.log(` ✓ 第 ${review.lineNumber} 行:代码质量良好`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 如果没有问题
|
|
367
|
+
if (fileReview.reviews.length === 0 || fileReview.reviews.every(r => !r.hasIssue)) {
|
|
368
|
+
console.log(` ✓ 所有代码质量良好,无需评论`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
} catch (error) {
|
|
372
|
+
results.push({
|
|
373
|
+
status: 'error',
|
|
374
|
+
fileName,
|
|
375
|
+
error: error.message,
|
|
376
|
+
});
|
|
377
|
+
console.log(` ✗ 文件审查失败: ${error.message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return results;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 审查单个文件的所有变更(包含影响分析)
|
|
386
|
+
* @param {Object} change - 代码变更对象
|
|
387
|
+
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
388
|
+
* @param {Object} impactAnalysis - 影响分析结果
|
|
389
|
+
* @returns {Promise<Object>} 审查结果 { reviews: [{lineNumber, hasIssue, comment}] }
|
|
390
|
+
*/
|
|
391
|
+
async reviewFileChangesWithImpact(change, meaningfulChanges, impactAnalysis) {
|
|
392
|
+
const aiClient = this.getAIClient();
|
|
393
|
+
const projectPrompt = this.config.ai?.guardConfig?.content || '';
|
|
394
|
+
const fileName = change.new_path || change.old_path;
|
|
395
|
+
|
|
396
|
+
// 构建包含影响分析的批量审查消息
|
|
397
|
+
const messages = impactAnalysis
|
|
398
|
+
? PromptTools.buildFileReviewWithImpactMessages(fileName, meaningfulChanges, impactAnalysis, projectPrompt)
|
|
399
|
+
: PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
|
|
400
|
+
|
|
401
|
+
// 调用 AI(一次调用审查整个文件)
|
|
402
|
+
const response = await aiClient.sendMessage(messages);
|
|
403
|
+
|
|
404
|
+
// 解析 AI 返回的 JSON
|
|
405
|
+
try {
|
|
406
|
+
// 提取 JSON(可能被包裹在 ```json ``` 中)
|
|
407
|
+
let jsonStr = response.content.trim();
|
|
408
|
+
const jsonMatch = jsonStr.match(/```json\s*([\s\S]*?)\s*```/);
|
|
409
|
+
if (jsonMatch) {
|
|
410
|
+
jsonStr = jsonMatch[1];
|
|
411
|
+
} else if (jsonStr.startsWith('```') && jsonStr.endsWith('```')) {
|
|
412
|
+
jsonStr = jsonStr.slice(3, -3).trim();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = JSON.parse(jsonStr);
|
|
416
|
+
return result;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('解析 AI 返回的 JSON 失败:', error.message);
|
|
419
|
+
console.error('AI 原始返回:', response.content);
|
|
420
|
+
return { reviews: [] };
|
|
421
|
+
}
|
|
312
422
|
}
|
|
313
423
|
|
|
314
424
|
/**
|
|
@@ -323,7 +433,7 @@ export class GitLabAIReview {
|
|
|
323
433
|
export { getConfig, validateConfig } from './lib/config.js';
|
|
324
434
|
export { GitLabClient } from './lib/gitlab-client.js';
|
|
325
435
|
export { AIClient } from './lib/ai-client.js';
|
|
326
|
-
export { PromptTools, DiffParser };
|
|
436
|
+
export { PromptTools, DiffParser, ImpactAnalyzer };
|
|
327
437
|
|
|
328
438
|
// 默认导出
|
|
329
439
|
export default GitLabAIReview;
|
package/lib/diff-parser.js
CHANGED
|
@@ -187,42 +187,11 @@ export function generateDiffSummary(diff) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
/**
|
|
191
|
-
* 将有意义的变更按 hunk 分组
|
|
192
|
-
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
193
|
-
* @returns {Array} 按 hunk 分组的变更数组
|
|
194
|
-
*/
|
|
195
|
-
export function groupChangesByHunk(meaningfulChanges) {
|
|
196
|
-
const hunkMap = new Map();
|
|
197
|
-
|
|
198
|
-
meaningfulChanges.forEach(change => {
|
|
199
|
-
const hunkKey = change.hunk; // 使用 hunk header 作为 key
|
|
200
|
-
|
|
201
|
-
if (!hunkMap.has(hunkKey)) {
|
|
202
|
-
hunkMap.set(hunkKey, {
|
|
203
|
-
header: hunkKey,
|
|
204
|
-
additions: [],
|
|
205
|
-
deletions: [],
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const hunkGroup = hunkMap.get(hunkKey);
|
|
210
|
-
if (change.type === 'addition') {
|
|
211
|
-
hunkGroup.additions.push(change);
|
|
212
|
-
} else if (change.type === 'deletion') {
|
|
213
|
-
hunkGroup.deletions.push(change);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return Array.from(hunkMap.values());
|
|
218
|
-
}
|
|
219
|
-
|
|
220
190
|
export default {
|
|
221
191
|
parseHunkHeader,
|
|
222
192
|
parseDiff,
|
|
223
193
|
calculateNewLineNumber,
|
|
224
194
|
extractMeaningfulChanges,
|
|
225
195
|
generateDiffSummary,
|
|
226
|
-
groupChangesByHunk,
|
|
227
196
|
};
|
|
228
197
|
|
package/lib/gitlab-client.js
CHANGED
|
@@ -145,5 +145,74 @@ export class GitLabClient {
|
|
|
145
145
|
|
|
146
146
|
return results;
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取项目中的文件内容
|
|
151
|
+
* @param {string} projectId - 项目 ID
|
|
152
|
+
* @param {string} filePath - 文件路径
|
|
153
|
+
* @param {string} ref - 分支名或提交 SHA
|
|
154
|
+
* @returns {Promise<string|null>} 文件内容
|
|
155
|
+
*/
|
|
156
|
+
async getProjectFile(projectId, filePath, ref) {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(
|
|
159
|
+
`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(filePath)}/raw?ref=${ref}`,
|
|
160
|
+
{
|
|
161
|
+
headers: {
|
|
162
|
+
'PRIVATE-TOKEN': this.token,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
if (response.status === 404) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`获取文件失败: ${response.status}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return await response.text();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn(`获取文件 ${filePath} 失败:`, error.message);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 在项目中搜索代码
|
|
183
|
+
* @param {string} projectId - 项目 ID
|
|
184
|
+
* @param {string} search - 搜索关键词
|
|
185
|
+
* @param {string} ref - 分支名
|
|
186
|
+
* @returns {Promise<Array>} 搜索结果
|
|
187
|
+
*/
|
|
188
|
+
async searchInProject(projectId, search, ref) {
|
|
189
|
+
try {
|
|
190
|
+
const response = await this.request(
|
|
191
|
+
`/projects/${encodeURIComponent(projectId)}/search?scope=blobs&search=${encodeURIComponent(search)}&ref=${ref}`
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return response || [];
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.warn(`搜索 "${search}" 失败:`, error.message);
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 获取仓库树结构
|
|
203
|
+
* @param {string} projectId - 项目 ID
|
|
204
|
+
* @param {string} ref - 分支名
|
|
205
|
+
* @param {string} path - 路径(可选)
|
|
206
|
+
* @returns {Promise<Array>} 文件树
|
|
207
|
+
*/
|
|
208
|
+
async getRepositoryTree(projectId, ref, path = '') {
|
|
209
|
+
try {
|
|
210
|
+
const endpoint = `/projects/${encodeURIComponent(projectId)}/repository/tree?ref=${ref}&path=${encodeURIComponent(path)}&recursive=false`;
|
|
211
|
+
return await this.request(endpoint);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn(`获取仓库树失败:`, error.message);
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
148
217
|
}
|
|
149
218
|
|