gitlab-ai-review 2.4.0 → 2.5.1
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/index.js +52 -5
- package/lib/diff-parser.js +31 -0
- package/lib/prompt-tools.js +71 -30
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as DiffParser from './lib/diff-parser.js';
|
|
|
15
15
|
export class GitLabAIReview {
|
|
16
16
|
constructor(options = {}) {
|
|
17
17
|
this.name = 'GitLab AI Review SDK';
|
|
18
|
-
this.version = '2.
|
|
18
|
+
this.version = '2.5.1';
|
|
19
19
|
|
|
20
20
|
// 如果传入了配置,使用手动配置;否则使用自动检测
|
|
21
21
|
if (options.token || options.gitlab) {
|
|
@@ -166,14 +166,35 @@ export class GitLabAIReview {
|
|
|
166
166
|
continue;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
const additionsCount = meaningfulChanges.filter(c => c.type === 'addition').length;
|
|
170
|
+
const deletionsCount = meaningfulChanges.filter(c => c.type === 'deletion').length;
|
|
171
|
+
console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更(新增 ${additionsCount} 行,删除 ${deletionsCount} 行)`);
|
|
170
172
|
|
|
171
173
|
// 调用 AI 一次性审查整个文件的所有变更(按文件批量)
|
|
172
174
|
const fileReview = await this.reviewFileChanges(change, meaningfulChanges);
|
|
173
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
184
|
for (const review of fileReview.reviews) {
|
|
176
185
|
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
|
+
|
|
177
198
|
try {
|
|
178
199
|
const commentResult = await this.gitlabClient.createLineComment(
|
|
179
200
|
this.config.project.projectId,
|
|
@@ -238,8 +259,11 @@ export class GitLabAIReview {
|
|
|
238
259
|
const projectPrompt = this.config.ai?.guardConfig?.content || '';
|
|
239
260
|
const fileName = change.new_path || change.old_path;
|
|
240
261
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
262
|
+
// 按 hunk 分组变更
|
|
263
|
+
const hunkGroups = DiffParser.groupChangesByHunk(meaningfulChanges);
|
|
264
|
+
|
|
265
|
+
// 构建整个文件的批量审查消息(按 hunk 组织)
|
|
266
|
+
const messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, hunkGroups, projectPrompt);
|
|
243
267
|
|
|
244
268
|
// 调用 AI(一次调用审查整个文件)
|
|
245
269
|
const response = await aiClient.sendMessage(messages);
|
|
@@ -255,6 +279,9 @@ export class GitLabAIReview {
|
|
|
255
279
|
jsonStr = jsonStr.slice(3, -3).trim();
|
|
256
280
|
}
|
|
257
281
|
|
|
282
|
+
// 清理可能的格式错误(如多余的括号)
|
|
283
|
+
jsonStr = this.cleanJsonString(jsonStr);
|
|
284
|
+
|
|
258
285
|
const result = JSON.parse(jsonStr);
|
|
259
286
|
return result;
|
|
260
287
|
} catch (error) {
|
|
@@ -264,6 +291,26 @@ export class GitLabAIReview {
|
|
|
264
291
|
}
|
|
265
292
|
}
|
|
266
293
|
|
|
294
|
+
/**
|
|
295
|
+
* 清理 JSON 字符串中的常见格式错误
|
|
296
|
+
* @param {string} jsonStr - 待清理的 JSON 字符串
|
|
297
|
+
* @returns {string} 清理后的 JSON 字符串
|
|
298
|
+
*/
|
|
299
|
+
cleanJsonString(jsonStr) {
|
|
300
|
+
// 修复常见的 JSON 格式错误
|
|
301
|
+
|
|
302
|
+
// 1. 移除末尾多余的 ]}(如 ]}]} 改为 ]})
|
|
303
|
+
jsonStr = jsonStr.replace(/\]\}\]\}$/g, ']}');
|
|
304
|
+
|
|
305
|
+
// 2. 移除末尾多余的 ](如 }]} 改为 })
|
|
306
|
+
jsonStr = jsonStr.replace(/\}\]\}$/g, '}');
|
|
307
|
+
|
|
308
|
+
// 3. 移除其他常见的多余闭合符号
|
|
309
|
+
jsonStr = jsonStr.replace(/\]\]$/g, ']');
|
|
310
|
+
|
|
311
|
+
return jsonStr.trim();
|
|
312
|
+
}
|
|
313
|
+
|
|
267
314
|
/**
|
|
268
315
|
* 测试方法
|
|
269
316
|
*/
|
package/lib/diff-parser.js
CHANGED
|
@@ -187,11 +187,42 @@ 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
|
+
|
|
190
220
|
export default {
|
|
191
221
|
parseHunkHeader,
|
|
192
222
|
parseDiff,
|
|
193
223
|
calculateNewLineNumber,
|
|
194
224
|
extractMeaningfulChanges,
|
|
195
225
|
generateDiffSummary,
|
|
226
|
+
groupChangesByHunk,
|
|
196
227
|
};
|
|
197
228
|
|
package/lib/prompt-tools.js
CHANGED
|
@@ -28,51 +28,90 @@ export function buildSystemPrompt(projectPrompt = '') {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* 构建整个文件所有变更行的批量审查提示词(按 hunk 组织)
|
|
32
32
|
* @param {string} fileName - 文件名
|
|
33
33
|
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
34
|
+
* @param {Array} hunkGroups - 按 hunk 分组的变更数组
|
|
34
35
|
* @param {string} projectPrompt - 项目配置的 prompt
|
|
35
36
|
* @returns {string} 批量审查提示词
|
|
36
37
|
*/
|
|
37
|
-
export function buildFileReviewPrompt(fileName, meaningfulChanges) {
|
|
38
|
-
|
|
38
|
+
export function buildFileReviewPrompt(fileName, meaningfulChanges, hunkGroups) {
|
|
39
|
+
const totalAdditions = meaningfulChanges.filter(c => c.type === 'addition').length;
|
|
40
|
+
const totalDeletions = meaningfulChanges.filter(c => c.type === 'deletion').length;
|
|
41
|
+
|
|
42
|
+
let prompt = `请审查以下文件的代码变更,只对**有问题的新增行**提出审查意见。
|
|
39
43
|
|
|
40
44
|
**文件名**: ${fileName}
|
|
41
|
-
|
|
45
|
+
**变更统计**:
|
|
46
|
+
- 共 ${hunkGroups.length} 个代码块(hunk)
|
|
47
|
+
- 删除了 ${totalDeletions} 行(旧代码)
|
|
48
|
+
- 新增了 ${totalAdditions} 行(新代码)
|
|
49
|
+
|
|
50
|
+
## 重要说明
|
|
51
|
+
- **每个代码块都明确标注了删除内容和新增内容**
|
|
52
|
+
- **删除的行**:标注为【删除】,是被移除的旧代码,**无需审查**
|
|
53
|
+
- **新增的行**:标注为【新增】,是新加入的代码,**只审查这些行**
|
|
54
|
+
- 行号是指新增行在新文件中的位置
|
|
55
|
+
|
|
56
|
+
---
|
|
42
57
|
|
|
43
58
|
`;
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
prompt +=
|
|
48
|
-
prompt += `**内容**:\n\`\`\`\n`;
|
|
60
|
+
// 按 hunk 展示变更
|
|
61
|
+
hunkGroups.forEach((hunk, hunkIndex) => {
|
|
62
|
+
prompt += `## 代码块 ${hunkIndex + 1}:${hunk.header}\n\n`;
|
|
49
63
|
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
// 展示删除的内容
|
|
65
|
+
if (hunk.deletions.length > 0) {
|
|
66
|
+
prompt += `### 📝 该代码块中删除的内容(无需审查)\n\n`;
|
|
67
|
+
hunk.deletions.forEach((change, index) => {
|
|
68
|
+
prompt += `\`\`\`\n`;
|
|
69
|
+
// 上下文
|
|
70
|
+
if (change.context?.before?.length > 0 && index === 0) {
|
|
71
|
+
change.context.before.forEach(line => {
|
|
72
|
+
prompt += ` ${line}\n`;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 删除的行
|
|
76
|
+
prompt += `【删除】${change.content}\n`;
|
|
77
|
+
prompt += `\`\`\`\n\n`;
|
|
54
78
|
});
|
|
55
79
|
}
|
|
56
80
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
// 展示新增的内容
|
|
82
|
+
if (hunk.additions.length > 0) {
|
|
83
|
+
prompt += `### ✨ 该代码块中新增的内容(**请审查**)\n\n`;
|
|
84
|
+
hunk.additions.forEach((change, index) => {
|
|
85
|
+
prompt += `\`\`\`\n`;
|
|
86
|
+
// 上下文
|
|
87
|
+
if (change.context?.before?.length > 0 && index === 0) {
|
|
88
|
+
change.context.before.forEach(line => {
|
|
89
|
+
prompt += ` ${line}\n`;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// 新增的行
|
|
93
|
+
prompt += `【新增 - 第 ${change.lineNumber} 行】${change.content}\n`;
|
|
94
|
+
prompt += `\`\`\`\n\n`;
|
|
64
95
|
});
|
|
65
96
|
}
|
|
66
97
|
|
|
67
|
-
|
|
98
|
+
// 如果该 hunk 没有删除也没有新增
|
|
99
|
+
if (hunk.deletions.length === 0 && hunk.additions.length === 0) {
|
|
100
|
+
prompt += `*该代码块无实质性变更*\n\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
prompt += `---\n\n`;
|
|
68
104
|
});
|
|
69
105
|
|
|
70
|
-
prompt +=
|
|
106
|
+
prompt += `## 审查要求
|
|
71
107
|
|
|
72
|
-
1.
|
|
73
|
-
2.
|
|
74
|
-
3.
|
|
75
|
-
4.
|
|
108
|
+
1. **只审查上面标注为【新增】的代码行**
|
|
109
|
+
2. **不要审查标注为【删除】的代码行**
|
|
110
|
+
3. 每个代码块的删除和新增内容已明确标注,请关注新增内容的质量
|
|
111
|
+
4. 判断新增的代码是否存在问题(安全、性能、逻辑、最佳实践等)
|
|
112
|
+
5. **只对有问题的新增行提出审查意见**,没有问题的行不需要评论
|
|
113
|
+
6. **必须使用中文**返回审查意见
|
|
114
|
+
7. 返回 JSON 格式的结果,格式如下:
|
|
76
115
|
|
|
77
116
|
\`\`\`json
|
|
78
117
|
{
|
|
@@ -90,9 +129,10 @@ export function buildFileReviewPrompt(fileName, meaningfulChanges) {
|
|
|
90
129
|
}
|
|
91
130
|
\`\`\`
|
|
92
131
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
8. lineNumber 必须是新增行的行号(在【新增 - 第 X 行】中标注的行号)
|
|
133
|
+
9. 如果所有新增代码都没有问题,返回空的 reviews 数组:\`{"reviews": []}\`
|
|
134
|
+
10. 只返回 JSON,不要包含其他文字说明
|
|
135
|
+
11. comment 字段必须使用中文,要简洁明了`;
|
|
96
136
|
|
|
97
137
|
return prompt;
|
|
98
138
|
}
|
|
@@ -101,13 +141,14 @@ export function buildFileReviewPrompt(fileName, meaningfulChanges) {
|
|
|
101
141
|
* 构建整个文件批量审查的完整消息数组
|
|
102
142
|
* @param {string} fileName - 文件名
|
|
103
143
|
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
144
|
+
* @param {Array} hunkGroups - 按 hunk 分组的变更数组
|
|
104
145
|
* @param {string} projectPrompt - 项目配置的 prompt
|
|
105
146
|
* @returns {Array} 消息数组
|
|
106
147
|
*/
|
|
107
|
-
export function buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt = '') {
|
|
148
|
+
export function buildFileReviewMessages(fileName, meaningfulChanges, hunkGroups, projectPrompt = '') {
|
|
108
149
|
return [
|
|
109
150
|
{ role: 'system', content: buildSystemPrompt(projectPrompt) },
|
|
110
|
-
{ role: 'user', content: buildFileReviewPrompt(fileName, meaningfulChanges) },
|
|
151
|
+
{ role: 'user', content: buildFileReviewPrompt(fileName, meaningfulChanges, hunkGroups) },
|
|
111
152
|
];
|
|
112
153
|
}
|
|
113
154
|
|