gitlab-ai-review 2.0.0 → 2.2.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/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.0.0';
18
+ this.version = '2.2.0';
19
19
 
20
20
  // 如果传入了配置,使用手动配置;否则使用自动检测
21
21
  if (options.token || options.gitlab) {
@@ -179,50 +179,132 @@ export class GitLabAIReview {
179
179
  }
180
180
 
181
181
  /**
182
- * AI 审查 MR 的所有有意义的变更并自动添加行级评论
182
+ * AI 审查 MR 的所有有意义的变更并自动添加行级评论(按文件批量处理)
183
183
  * @param {Object} options - 选项
184
- * @param {number} options.maxComments - 最大评论数量(避免过多评论)
184
+ * @param {number} options.maxFiles - 最大审查文件数量(默认不限制)
185
185
  * @returns {Promise<Array>} 评论结果数组
186
186
  */
187
187
  async reviewAndCommentOnLines(options = {}) {
188
- const { maxComments = 10 } = options;
188
+ const { maxFiles = Infinity } = options;
189
189
  const changes = await this.getMergeRequestChanges();
190
190
  const results = [];
191
191
 
192
- for (const change of changes) {
193
- if (results.length >= maxComments) {
194
- console.log(`已达到最大评论数量限制 (${maxComments}),停止审查`);
195
- break;
196
- }
192
+ const filesToReview = maxFiles === Infinity ? changes.length : Math.min(maxFiles, changes.length);
193
+ console.log(`共 ${changes.length} 个文件需要审查${maxFiles === Infinity ? '(不限制数量)' : `(最多审查 ${maxFiles} 个)`}`);
194
+
195
+ for (const change of changes.slice(0, filesToReview)) {
196
+ const fileName = change.new_path || change.old_path;
197
+
198
+ try {
199
+ console.log(`\n审查文件: ${fileName}`);
200
+
201
+ // 解析 diff,提取有意义的变更
202
+ const hunks = DiffParser.parseDiff(change.diff);
203
+ const meaningfulChanges = DiffParser.extractMeaningfulChanges(hunks);
204
+
205
+ if (meaningfulChanges.length === 0) {
206
+ console.log(` 跳过:没有有意义的变更`);
207
+ continue;
208
+ }
209
+
210
+ console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
211
+
212
+ // 调用 AI 一次性审查整个文件的所有变更(按文件批量)
213
+ const fileReview = await this.reviewFileChanges(change, meaningfulChanges);
214
+
215
+ // 根据 AI 返回的结果,只对有问题的行添加评论
216
+ for (const review of fileReview.reviews) {
217
+ if (review.hasIssue) {
218
+ try {
219
+ const commentResult = await this.gitlabClient.createLineComment(
220
+ this.config.project.projectId,
221
+ this.config.project.mergeRequestIid,
222
+ `🤖 **AI 代码审查**\n\n${review.comment}`,
223
+ {
224
+ filePath: fileName,
225
+ oldPath: change.old_path,
226
+ newLine: review.lineNumber,
227
+ }
228
+ );
197
229
 
198
- // 解析 diff,提取有意义的变更
199
- const hunks = DiffParser.parseDiff(change.diff);
200
- const meaningfulChanges = DiffParser.extractMeaningfulChanges(hunks);
201
-
202
- // 对每个有意义的变更进行审查
203
- for (const meaningfulChange of meaningfulChanges.slice(0, maxComments - results.length)) {
204
- try {
205
- const result = await this.reviewAndCommentOnLine(change, meaningfulChange);
206
- results.push({
207
- status: 'success',
208
- fileName: change.new_path || change.old_path,
209
- lineNumber: meaningfulChange.lineNumber,
210
- ...result,
211
- });
212
- } catch (error) {
213
- results.push({
214
- status: 'error',
215
- fileName: change.new_path || change.old_path,
216
- lineNumber: meaningfulChange.lineNumber,
217
- error: error.message,
218
- });
230
+ results.push({
231
+ status: 'success',
232
+ fileName,
233
+ lineNumber: review.lineNumber,
234
+ comment: review.comment,
235
+ commentResult,
236
+ });
237
+
238
+ console.log(` ✓ 第 ${review.lineNumber} 行:已添加评论`);
239
+ } catch (error) {
240
+ results.push({
241
+ status: 'error',
242
+ fileName,
243
+ lineNumber: review.lineNumber,
244
+ error: error.message,
245
+ });
246
+ console.log(` ✗ 第 ${review.lineNumber} 行:评论失败 - ${error.message}`);
247
+ }
248
+ } else {
249
+ console.log(` ✓ 第 ${review.lineNumber} 行:代码质量良好`);
250
+ }
251
+ }
252
+
253
+ // 如果没有问题
254
+ if (fileReview.reviews.length === 0 || fileReview.reviews.every(r => !r.hasIssue)) {
255
+ console.log(` ✓ 所有代码质量良好,无需评论`);
219
256
  }
257
+
258
+ } catch (error) {
259
+ results.push({
260
+ status: 'error',
261
+ fileName,
262
+ error: error.message,
263
+ });
264
+ console.log(` ✗ 文件审查失败: ${error.message}`);
220
265
  }
221
266
  }
222
267
 
223
268
  return results;
224
269
  }
225
270
 
271
+ /**
272
+ * 审查单个文件的所有变更(一次 API 调用)
273
+ * @param {Object} change - 代码变更对象
274
+ * @param {Array} meaningfulChanges - 有意义的变更数组
275
+ * @returns {Promise<Object>} 审查结果 { reviews: [{lineNumber, hasIssue, comment}] }
276
+ */
277
+ async reviewFileChanges(change, meaningfulChanges) {
278
+ const aiClient = this.getAIClient();
279
+ const projectPrompt = this.config.ai?.guardConfig?.content || '';
280
+ const fileName = change.new_path || change.old_path;
281
+
282
+ // 构建整个文件的批量审查消息
283
+ const messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
284
+
285
+ // 调用 AI(一次调用审查整个文件)
286
+ const response = await aiClient.sendMessage(messages);
287
+
288
+ // 解析 AI 返回的 JSON
289
+ try {
290
+ // 提取 JSON(可能被包裹在 ```json ``` 中)
291
+ let jsonStr = response.content.trim();
292
+ const jsonMatch = jsonStr.match(/```json\s*([\s\S]*?)\s*```/);
293
+ if (jsonMatch) {
294
+ jsonStr = jsonMatch[1];
295
+ } else if (jsonStr.startsWith('```') && jsonStr.endsWith('```')) {
296
+ jsonStr = jsonStr.slice(3, -3).trim();
297
+ }
298
+
299
+ const result = JSON.parse(jsonStr);
300
+ return result;
301
+ } catch (error) {
302
+ console.error('解析 AI 返回的 JSON 失败:', error.message);
303
+ console.error('AI 原始返回:', response.content);
304
+ return { reviews: [] };
305
+ }
306
+ }
307
+
226
308
  /**
227
309
  * 测试方法
228
310
  */
@@ -92,10 +92,96 @@ export function buildLineReviewMessages(changeInfo, projectPrompt = '') {
92
92
  ];
93
93
  }
94
94
 
95
- // 只导出行级审查需要的函数
95
+ /**
96
+ * 构建整个文件所有变更行的批量审查提示词
97
+ * @param {string} fileName - 文件名
98
+ * @param {Array} meaningfulChanges - 有意义的变更数组
99
+ * @param {string} projectPrompt - 项目配置的 prompt
100
+ * @returns {string} 批量审查提示词
101
+ */
102
+ export function buildFileReviewPrompt(fileName, meaningfulChanges) {
103
+ let prompt = `请审查以下文件的代码变更,只对**有问题的行**提出审查意见,代码没有问题的行不需要评论。
104
+
105
+ **文件名**: ${fileName}
106
+ **变更数量**: ${meaningfulChanges.length} 行
107
+
108
+ `;
109
+
110
+ meaningfulChanges.forEach((change, index) => {
111
+ prompt += `\n### 变更 ${index + 1} - 第 ${change.lineNumber} 行\n`;
112
+ prompt += `**类型**: ${change.type === 'addition' ? '新增' : '删除'}\n`;
113
+ prompt += `**内容**:\n\`\`\`\n`;
114
+
115
+ // 上下文
116
+ if (change.context?.before?.length > 0) {
117
+ change.context.before.forEach(line => {
118
+ prompt += ` ${line}\n`;
119
+ });
120
+ }
121
+
122
+ // 变更行
123
+ prompt += `${change.type === 'addition' ? '+' : '-'} ${change.content}\n`;
124
+
125
+ // 下文
126
+ if (change.context?.after?.length > 0) {
127
+ change.context.after.forEach(line => {
128
+ prompt += ` ${line}\n`;
129
+ });
130
+ }
131
+
132
+ prompt += `\`\`\`\n`;
133
+ });
134
+
135
+ prompt += `\n## 审查要求
136
+
137
+ 1. 仔细审查每一行变更,判断是否存在问题
138
+ 2. **只对有问题的行提出审查意见**,没有问题的行不需要评论
139
+ 3. **必须使用中文**返回审查意见
140
+ 4. 返回 JSON 格式的结果,格式如下:
141
+
142
+ \`\`\`json
143
+ {
144
+ "reviews": [
145
+ {
146
+ "lineNumber": 15,
147
+ "hasIssue": true,
148
+ "comment": "这里用中文描述具体的问题和改进建议"
149
+ },
150
+ {
151
+ "lineNumber": 20,
152
+ "hasIssue": false
153
+ }
154
+ ]
155
+ }
156
+ \`\`\`
157
+
158
+ 5. 如果所有代码都没有问题,返回空的 reviews 数组:\`{"reviews": []}\`
159
+ 6. 只返回 JSON,不要包含其他文字说明
160
+ 7. comment 字段必须使用中文,要简洁明了`;
161
+
162
+ return prompt;
163
+ }
164
+
165
+ /**
166
+ * 构建整个文件批量审查的完整消息数组
167
+ * @param {string} fileName - 文件名
168
+ * @param {Array} meaningfulChanges - 有意义的变更数组
169
+ * @param {string} projectPrompt - 项目配置的 prompt
170
+ * @returns {Array} 消息数组
171
+ */
172
+ export function buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt = '') {
173
+ return [
174
+ { role: 'system', content: buildSystemPrompt(projectPrompt) },
175
+ { role: 'user', content: buildFileReviewPrompt(fileName, meaningfulChanges) },
176
+ ];
177
+ }
178
+
179
+ // 导出函数
96
180
  export default {
97
181
  buildSystemPrompt,
98
182
  buildLineReviewPrompt,
99
183
  buildLineReviewMessages,
184
+ buildFileReviewPrompt,
185
+ buildFileReviewMessages,
100
186
  };
101
187
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gitlab-ai-review",
3
- "version": "2.0.0",
4
- "description": "GitLab AI Review SDK - 行级代码审查,附带项目 Prompt",
3
+ "version": "2.2.0",
4
+ "description": "GitLab AI Review SDK - 按文件批量审查,只评论有问题的代码",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {