gitlab-ai-review 2.4.0 → 2.5.2

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/cli.js CHANGED
@@ -34,15 +34,24 @@ async function main() {
34
34
 
35
35
  const results = await sdk.reviewAndCommentOnLines();
36
36
 
37
+ // 统计结果
38
+ const fileNames = new Set(results.map(r => r.fileName));
39
+ const successCount = results.filter(r => r.status === 'success').length;
40
+ const errorCount = results.filter(r => r.status === 'error').length;
41
+ const skippedCount = results.filter(r => r.status === 'skipped').length;
42
+
37
43
  console.log('\n📊 审查结果汇总:');
38
- console.log(` - 审查文件数: ${results.length}`);
39
- const successCount = results.filter(r => r.success).length;
40
- const commentCount = results.reduce((sum, r) => sum + (r.commentCount || 0), 0);
41
- console.log(` - 成功: ${successCount}`);
42
- console.log(` - 添加评论数: ${commentCount}`);
44
+ console.log(` - 审查文件数: ${fileNames.size}`);
45
+ console.log(` - 成功添加评论: ${successCount} 条`);
46
+ if (errorCount > 0) {
47
+ console.log(` - 失败: ${errorCount} 条`);
48
+ }
49
+ if (skippedCount > 0) {
50
+ console.log(` - 跳过: ${skippedCount} 条`);
51
+ }
43
52
 
44
- if (results.some(r => !r.success)) {
45
- console.log('\n⚠️ 部分文件审查失败,请检查上方日志');
53
+ if (errorCount > 0) {
54
+ console.log('\n⚠️ 部分评论添加失败,请检查上方日志');
46
55
  process.exit(1);
47
56
  } else {
48
57
  console.log('\n✅ 代码审查完成!');
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.3.0';
18
+ this.version = '2.5.2';
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
- console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
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
- // 根据 AI 返回的结果,只对有问题的行添加评论
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 messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
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
  */
@@ -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
 
@@ -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
- let prompt = `请审查以下文件的代码变更,只对**有问题的行**提出审查意见,代码没有问题的行不需要评论。
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
- **变更数量**: ${meaningfulChanges.length} 行
45
+ **变更统计**:
46
+ - 共 ${hunkGroups.length} 个代码块(hunk)
47
+ - 删除了 ${totalDeletions} 行(旧代码)
48
+ - 新增了 ${totalAdditions} 行(新代码)
49
+
50
+ ## 重要说明
51
+ - **每个代码块都明确标注了删除内容和新增内容**
52
+ - **删除的行**:标注为【删除】,是被移除的旧代码,**无需审查**
53
+ - **新增的行**:标注为【新增】,是新加入的代码,**只审查这些行**
54
+ - 行号是指新增行在新文件中的位置
55
+
56
+ ---
42
57
 
43
58
  `;
44
59
 
45
- meaningfulChanges.forEach((change, index) => {
46
- prompt += `\n### 变更 ${index + 1} - 第 ${change.lineNumber} 行\n`;
47
- prompt += `**类型**: ${change.type === 'addition' ? '新增' : '删除'}\n`;
48
- prompt += `**内容**:\n\`\`\`\n`;
60
+ // hunk 展示变更
61
+ hunkGroups.forEach((hunk, hunkIndex) => {
62
+ prompt += `## 代码块 ${hunkIndex + 1}:${hunk.header}\n\n`;
49
63
 
50
- // 上下文
51
- if (change.context?.before?.length > 0) {
52
- change.context.before.forEach(line => {
53
- prompt += ` ${line}\n`;
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
- prompt += `${change.type === 'addition' ? '+' : '-'} ${change.content}\n`;
59
-
60
- // 下文
61
- if (change.context?.after?.length > 0) {
62
- change.context.after.forEach(line => {
63
- prompt += ` ${line}\n`;
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
- prompt += `\`\`\`\n`;
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 += `\n## 审查要求
106
+ prompt += `## 审查要求
71
107
 
72
- 1. 仔细审查每一行变更,判断是否存在问题
73
- 2. **只对有问题的行提出审查意见**,没有问题的行不需要评论
74
- 3. **必须使用中文**返回审查意见
75
- 4. 返回 JSON 格式的结果,格式如下:
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
- 5. 如果所有代码都没有问题,返回空的 reviews 数组:\`{"reviews": []}\`
94
- 6. 只返回 JSON,不要包含其他文字说明
95
- 7. comment 字段必须使用中文,要简洁明了`;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-ai-review",
3
- "version": "2.4.0",
3
+ "version": "2.5.2",
4
4
  "description": "GitLab AI Review SDK - 支持 SDK 调用和 CLI 执行",
5
5
  "main": "index.js",
6
6
  "type": "module",