gitlab-ai-review 1.2.0 → 2.1.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
@@ -7,6 +7,7 @@ import { getConfig, validateConfig } from './lib/config.js';
7
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
+ import * as DiffParser from './lib/diff-parser.js';
10
11
 
11
12
  /**
12
13
  * GitLab AI Review SDK 主类
@@ -14,7 +15,7 @@ import * as PromptTools from './lib/prompt-tools.js';
14
15
  export class GitLabAIReview {
15
16
  constructor(options = {}) {
16
17
  this.name = 'GitLab AI Review SDK';
17
- this.version = '1.2.0';
18
+ this.version = '2.1.0';
18
19
 
19
20
  // 如果传入了配置,使用手动配置;否则使用自动检测
20
21
  if (options.token || options.gitlab) {
@@ -137,93 +138,170 @@ export class GitLabAIReview {
137
138
  }
138
139
 
139
140
  /**
140
- * AI 审查单个文件的代码变更
141
+ * AI 审查特定行的变更并在该行添加评论(附带项目 prompt)
141
142
  * @param {Object} change - 代码变更对象
142
- * @returns {Promise<Object>} 审查结果
143
+ * @param {Object} changeInfo - 具体的行变更信息
144
+ * @returns {Promise<Object>} 评论结果
143
145
  */
144
- async reviewCodeChange(change) {
146
+ async reviewAndCommentOnLine(change, changeInfo) {
145
147
  const aiClient = this.getAIClient();
146
- const guardConfig = this.config.ai?.guardConfig?.content || '';
148
+
149
+ // 获取项目配置的 prompt(来自 reviewguard.md)
150
+ const projectPrompt = this.config.ai?.guardConfig?.content || '';
147
151
 
148
- // 使用 PromptTools 构建消息
149
- const messages = PromptTools.buildReviewMessages({
150
- diff: change.diff,
152
+ // 构建针对特定行的审查消息(附带项目 prompt)
153
+ const messages = PromptTools.buildLineReviewMessages({
154
+ ...changeInfo,
151
155
  fileName: change.new_path || change.old_path,
152
- guardConfig,
153
- });
156
+ }, projectPrompt);
157
+
158
+ // 调用 AI 审查
159
+ const review = await aiClient.sendMessage(messages);
160
+
161
+ // 在该行添加评论
162
+ const lineInfo = {
163
+ filePath: change.new_path || change.old_path,
164
+ oldPath: change.old_path,
165
+ newLine: changeInfo.lineNumber,
166
+ };
167
+
168
+ const commentResult = await this.gitlabClient.createLineComment(
169
+ this.config.project.projectId,
170
+ this.config.project.mergeRequestIid,
171
+ `🤖 **AI 代码审查**\n\n${review.content}`,
172
+ lineInfo
173
+ );
154
174
 
155
- // 调用 AI
156
- return aiClient.sendMessage(messages);
175
+ return {
176
+ review,
177
+ comment: commentResult,
178
+ };
157
179
  }
158
180
 
159
181
  /**
160
- * AI 审查 MR 的所有代码变更
161
- * @returns {Promise<Array>} 审查结果数组
182
+ * AI 审查 MR 的所有有意义的变更并自动添加行级评论(按文件批量处理)
183
+ * @param {Object} options - 选项
184
+ * @param {number} options.maxFiles - 最大审查文件数量(默认 5)
185
+ * @returns {Promise<Array>} 评论结果数组
162
186
  */
163
- async reviewMergeRequest() {
187
+ async reviewAndCommentOnLines(options = {}) {
188
+ const { maxFiles = 5 } = options;
164
189
  const changes = await this.getMergeRequestChanges();
165
- const aiClient = this.getAIClient();
166
- const guardConfig = this.config.ai?.guardConfig?.content || '';
190
+ const results = [];
167
191
 
168
- const reviews = [];
192
+ console.log(`共 ${changes.length} 个文件需要审查(最多审查 ${maxFiles} 个)`);
169
193
 
170
- for (const change of changes) {
194
+ for (const change of changes.slice(0, maxFiles)) {
195
+ const fileName = change.new_path || change.old_path;
196
+
171
197
  try {
172
- // 使用 PromptTools 构建消息
173
- const messages = PromptTools.buildReviewMessages({
174
- diff: change.diff,
175
- fileName: change.new_path || change.old_path,
176
- guardConfig,
177
- });
198
+ console.log(`\n审查文件: ${fileName}`);
199
+
200
+ // 解析 diff,提取有意义的变更
201
+ const hunks = DiffParser.parseDiff(change.diff);
202
+ const meaningfulChanges = DiffParser.extractMeaningfulChanges(hunks);
178
203
 
179
- // 调用 AI
180
- const result = await aiClient.sendMessage(messages);
204
+ if (meaningfulChanges.length === 0) {
205
+ console.log(` 跳过:没有有意义的变更`);
206
+ continue;
207
+ }
208
+
209
+ console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
210
+
211
+ // 调用 AI 一次性审查整个文件的所有变更(按文件批量)
212
+ const fileReview = await this.reviewFileChanges(change, meaningfulChanges);
213
+
214
+ // 根据 AI 返回的结果,只对有问题的行添加评论
215
+ for (const review of fileReview.reviews) {
216
+ if (review.hasIssue) {
217
+ try {
218
+ const commentResult = await this.gitlabClient.createLineComment(
219
+ this.config.project.projectId,
220
+ this.config.project.mergeRequestIid,
221
+ `🤖 **AI 代码审查**\n\n${review.comment}`,
222
+ {
223
+ filePath: fileName,
224
+ oldPath: change.old_path,
225
+ newLine: review.lineNumber,
226
+ }
227
+ );
228
+
229
+ results.push({
230
+ status: 'success',
231
+ fileName,
232
+ lineNumber: review.lineNumber,
233
+ comment: review.comment,
234
+ commentResult,
235
+ });
236
+
237
+ console.log(` ✓ 第 ${review.lineNumber} 行:已添加评论`);
238
+ } catch (error) {
239
+ results.push({
240
+ status: 'error',
241
+ fileName,
242
+ lineNumber: review.lineNumber,
243
+ error: error.message,
244
+ });
245
+ console.log(` ✗ 第 ${review.lineNumber} 行:评论失败 - ${error.message}`);
246
+ }
247
+ } else {
248
+ console.log(` ✓ 第 ${review.lineNumber} 行:代码质量良好`);
249
+ }
250
+ }
251
+
252
+ // 如果没有问题
253
+ if (fileReview.reviews.length === 0 || fileReview.reviews.every(r => !r.hasIssue)) {
254
+ console.log(` ✓ 所有代码质量良好,无需评论`);
255
+ }
181
256
 
182
- reviews.push({
183
- fileName: change.new_path || change.old_path,
184
- status: 'success',
185
- ...result,
186
- });
187
257
  } catch (error) {
188
- reviews.push({
189
- fileName: change.new_path || change.old_path,
258
+ results.push({
190
259
  status: 'error',
260
+ fileName,
191
261
  error: error.message,
192
262
  });
263
+ console.log(` ✗ 文件审查失败: ${error.message}`);
193
264
  }
194
265
  }
195
266
 
196
- return reviews;
267
+ return results;
197
268
  }
198
269
 
199
270
  /**
200
- * AI 审查 MR 并自动添加评论
271
+ * 审查单个文件的所有变更(一次 API 调用)
272
+ * @param {Object} change - 代码变更对象
273
+ * @param {Array} meaningfulChanges - 有意义的变更数组
274
+ * @returns {Promise<Object>} 审查结果 { reviews: [{lineNumber, hasIssue, comment}] }
201
275
  */
202
- async reviewAndComment() {
203
- const reviews = await this.reviewMergeRequest();
204
-
205
- // 构建评论内容
206
- let comment = '## 🤖 AI 代码审查报告\n\n';
207
-
208
- for (const review of reviews) {
209
- if (review.status === 'error') {
210
- comment += `### ❌ ${review.fileName}\n`;
211
- comment += `审查失败: ${review.error}\n\n`;
212
- continue;
213
- }
276
+ async reviewFileChanges(change, meaningfulChanges) {
277
+ const aiClient = this.getAIClient();
278
+ const projectPrompt = this.config.ai?.guardConfig?.content || '';
279
+ const fileName = change.new_path || change.old_path;
214
280
 
215
- comment += `### 📄 ${review.fileName}\n\n`;
216
-
217
- if (review.reasoning) {
218
- comment += `**思考过程**:\n${review.reasoning}\n\n`;
281
+ // 构建整个文件的批量审查消息
282
+ const messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
283
+
284
+ // 调用 AI(一次调用审查整个文件)
285
+ const response = await aiClient.sendMessage(messages);
286
+
287
+ // 解析 AI 返回的 JSON
288
+ try {
289
+ // 提取 JSON(可能被包裹在 ```json ``` 中)
290
+ let jsonStr = response.content.trim();
291
+ const jsonMatch = jsonStr.match(/```json\s*([\s\S]*?)\s*```/);
292
+ if (jsonMatch) {
293
+ jsonStr = jsonMatch[1];
294
+ } else if (jsonStr.startsWith('```') && jsonStr.endsWith('```')) {
295
+ jsonStr = jsonStr.slice(3, -3).trim();
219
296
  }
220
-
221
- comment += `**审查意见**:\n${review.content}\n\n`;
222
- comment += `---\n\n`;
223
- }
224
297
 
225
- // 添加评论到 MR
226
- return this.addComment(comment);
298
+ const result = JSON.parse(jsonStr);
299
+ return result;
300
+ } catch (error) {
301
+ console.error('解析 AI 返回的 JSON 失败:', error.message);
302
+ console.error('AI 原始返回:', response.content);
303
+ return { reviews: [] };
304
+ }
227
305
  }
228
306
 
229
307
  /**
@@ -238,7 +316,7 @@ export class GitLabAIReview {
238
316
  export { getConfig, validateConfig } from './lib/config.js';
239
317
  export { GitLabClient } from './lib/gitlab-client.js';
240
318
  export { AIClient } from './lib/ai-client.js';
241
- export { PromptTools };
319
+ export { PromptTools, DiffParser };
242
320
 
243
321
  // 默认导出
244
322
  export default GitLabAIReview;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Diff 解析工具 - 解析 Git Diff 格式
3
+ */
4
+
5
+ /**
6
+ * 解析 diff 的 hunk 头部信息
7
+ * 例如: @@ -14,7 +14,7 @@
8
+ * @param {string} hunkHeader - hunk 头部字符串
9
+ * @returns {Object} 解析后的位置信息
10
+ */
11
+ export function parseHunkHeader(hunkHeader) {
12
+ const match = hunkHeader.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
13
+
14
+ if (!match) {
15
+ return null;
16
+ }
17
+
18
+ return {
19
+ oldStart: parseInt(match[1], 10),
20
+ oldLines: match[2] ? parseInt(match[2], 10) : 1,
21
+ newStart: parseInt(match[3], 10),
22
+ newLines: match[4] ? parseInt(match[4], 10) : 1,
23
+ };
24
+ }
25
+
26
+ /**
27
+ * 解析完整的 diff 内容,提取所有变更位置
28
+ * @param {string} diff - diff 内容
29
+ * @returns {Array} 变更块数组
30
+ */
31
+ export function parseDiff(diff) {
32
+ if (!diff) return [];
33
+
34
+ const lines = diff.split('\n');
35
+ const hunks = [];
36
+ let currentHunk = null;
37
+
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const line = lines[i];
40
+
41
+ // 检测 hunk 头部
42
+ if (line.startsWith('@@')) {
43
+ if (currentHunk) {
44
+ hunks.push(currentHunk);
45
+ }
46
+
47
+ const hunkInfo = parseHunkHeader(line);
48
+ currentHunk = {
49
+ ...hunkInfo,
50
+ header: line,
51
+ changes: [],
52
+ contextBefore: [],
53
+ contextAfter: [],
54
+ };
55
+ } else if (currentHunk) {
56
+ // 收集变更行
57
+ if (line.startsWith('+') && !line.startsWith('+++')) {
58
+ currentHunk.changes.push({
59
+ type: 'addition',
60
+ content: line.substring(1),
61
+ line: line,
62
+ });
63
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
64
+ currentHunk.changes.push({
65
+ type: 'deletion',
66
+ content: line.substring(1),
67
+ line: line,
68
+ });
69
+ } else if (line.startsWith(' ')) {
70
+ currentHunk.changes.push({
71
+ type: 'context',
72
+ content: line.substring(1),
73
+ line: line,
74
+ });
75
+ }
76
+ }
77
+ }
78
+
79
+ if (currentHunk) {
80
+ hunks.push(currentHunk);
81
+ }
82
+
83
+ return hunks;
84
+ }
85
+
86
+ /**
87
+ * 计算变更在新文件中的具体行号
88
+ * @param {Object} hunk - hunk 信息
89
+ * @param {number} changeIndex - 变更在 changes 数组中的索引
90
+ * @returns {number} 新文件中的行号
91
+ */
92
+ export function calculateNewLineNumber(hunk, changeIndex) {
93
+ let lineNumber = hunk.newStart;
94
+
95
+ for (let i = 0; i <= changeIndex; i++) {
96
+ const change = hunk.changes[i];
97
+ if (change.type === 'addition' || change.type === 'context') {
98
+ if (i === changeIndex) {
99
+ return lineNumber;
100
+ }
101
+ lineNumber++;
102
+ }
103
+ }
104
+
105
+ return lineNumber;
106
+ }
107
+
108
+ /**
109
+ * 提取有意义的变更(忽略纯空白变更)
110
+ * @param {Array} hunks - hunks 数组
111
+ * @returns {Array} 有意义的变更数组
112
+ */
113
+ export function extractMeaningfulChanges(hunks) {
114
+ const meaningfulChanges = [];
115
+
116
+ hunks.forEach(hunk => {
117
+ let currentNewLine = hunk.newStart;
118
+
119
+ hunk.changes.forEach((change, index) => {
120
+ if (change.type === 'addition') {
121
+ const content = change.content.trim();
122
+
123
+ // 过滤空行和仅包含符号的行
124
+ if (content.length > 0 && !/^[{}()\[\];,]*$/.test(content)) {
125
+ meaningfulChanges.push({
126
+ type: 'addition',
127
+ content: change.content,
128
+ lineNumber: currentNewLine,
129
+ hunk: hunk.header,
130
+ context: {
131
+ before: hunk.changes.slice(Math.max(0, index - 2), index).map(c => c.content),
132
+ after: hunk.changes.slice(index + 1, index + 3).map(c => c.content),
133
+ },
134
+ });
135
+ }
136
+ currentNewLine++;
137
+ } else if (change.type === 'deletion') {
138
+ const content = change.content.trim();
139
+
140
+ if (content.length > 0 && !/^[{}()\[\];,]*$/.test(content)) {
141
+ meaningfulChanges.push({
142
+ type: 'deletion',
143
+ content: change.content,
144
+ oldLineNumber: currentNewLine,
145
+ hunk: hunk.header,
146
+ context: {
147
+ before: hunk.changes.slice(Math.max(0, index - 2), index).map(c => c.content),
148
+ after: hunk.changes.slice(index + 1, index + 3).map(c => c.content),
149
+ },
150
+ });
151
+ }
152
+ } else if (change.type === 'context') {
153
+ currentNewLine++;
154
+ }
155
+ });
156
+ });
157
+
158
+ return meaningfulChanges;
159
+ }
160
+
161
+ /**
162
+ * 生成变更摘要
163
+ * @param {string} diff - diff 内容
164
+ * @returns {Object} 变更摘要
165
+ */
166
+ export function generateDiffSummary(diff) {
167
+ if (!diff) {
168
+ return { additions: 0, deletions: 0, changes: 0 };
169
+ }
170
+
171
+ const lines = diff.split('\n');
172
+ let additions = 0;
173
+ let deletions = 0;
174
+
175
+ lines.forEach(line => {
176
+ if (line.startsWith('+') && !line.startsWith('+++')) {
177
+ additions++;
178
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
179
+ deletions++;
180
+ }
181
+ });
182
+
183
+ return {
184
+ additions,
185
+ deletions,
186
+ changes: additions + deletions,
187
+ };
188
+ }
189
+
190
+ export default {
191
+ parseHunkHeader,
192
+ parseDiff,
193
+ calculateNewLineNumber,
194
+ extractMeaningfulChanges,
195
+ generateDiffSummary,
196
+ };
197
+
@@ -69,9 +69,37 @@ export class GitLabClient {
69
69
  }
70
70
 
71
71
  /**
72
- * 在 MR 上创建讨论(行内评论)
72
+ * 在 MR 的特定行上创建评论
73
+ * @param {string} projectId - 项目 ID
74
+ * @param {number} mergeRequestIid - MR IID
75
+ * @param {string} body - 评论内容
76
+ * @param {Object} lineInfo - 行信息
77
+ * @param {string} lineInfo.filePath - 文件路径
78
+ * @param {number} lineInfo.newLine - 新文件行号
79
+ * @param {number} lineInfo.oldLine - 旧文件行号(可选)
80
+ * @returns {Promise<Object>} 创建的讨论
73
81
  */
74
- async createMergeRequestDiscussion(projectId, mergeRequestIid, body, position) {
82
+ async createLineComment(projectId, mergeRequestIid, body, lineInfo) {
83
+ // 先获取 MR 信息以获取 SHA
84
+ const mr = await this.getMergeRequest(projectId, mergeRequestIid);
85
+
86
+ const position = {
87
+ base_sha: mr.diff_refs.base_sha,
88
+ head_sha: mr.diff_refs.head_sha,
89
+ start_sha: mr.diff_refs.start_sha,
90
+ position_type: 'text',
91
+ new_path: lineInfo.filePath,
92
+ old_path: lineInfo.oldPath || lineInfo.filePath,
93
+ };
94
+
95
+ // 设置行号
96
+ if (lineInfo.newLine !== undefined) {
97
+ position.new_line = lineInfo.newLine;
98
+ }
99
+ if (lineInfo.oldLine !== undefined) {
100
+ position.old_line = lineInfo.oldLine;
101
+ }
102
+
75
103
  return this.request(
76
104
  `/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions`,
77
105
  {
@@ -83,5 +111,39 @@ export class GitLabClient {
83
111
  }
84
112
  );
85
113
  }
114
+
115
+ /**
116
+ * 批量创建行级评论
117
+ * @param {string} projectId - 项目 ID
118
+ * @param {number} mergeRequestIid - MR IID
119
+ * @param {Array} comments - 评论数组
120
+ * @returns {Promise<Array>} 创建结果数组
121
+ */
122
+ async createMultipleLineComments(projectId, mergeRequestIid, comments) {
123
+ const results = [];
124
+
125
+ for (const comment of comments) {
126
+ try {
127
+ const result = await this.createLineComment(
128
+ projectId,
129
+ mergeRequestIid,
130
+ comment.body,
131
+ comment.lineInfo
132
+ );
133
+ results.push({
134
+ status: 'success',
135
+ result,
136
+ });
137
+ } catch (error) {
138
+ results.push({
139
+ status: 'error',
140
+ error: error.message,
141
+ comment,
142
+ });
143
+ }
144
+ }
145
+
146
+ return results;
147
+ }
86
148
  }
87
149
 
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Prompt 工具 - 用于拼接代码审查的提示词
2
+ * Prompt 工具 - 用于拼接行级代码审查的提示词
3
+ * 简化版:只支持行级审查
3
4
  */
4
5
 
5
6
  /**
6
- * 构建系统提示词
7
- * @param {string} guardConfig - AI Review Guard 配置内容
7
+ * 构建系统提示词(附带项目 prompt)
8
+ * @param {string} projectPrompt - 项目配置的 prompt(来自 reviewguard.md)
8
9
  * @returns {string} 系统提示词
9
10
  */
10
- export function buildSystemPrompt(guardConfig = '') {
11
+ export function buildSystemPrompt(projectPrompt = '') {
11
12
  let prompt = `你是一个专业的代码审查助手,负责审查 GitLab Merge Request 的代码变更。
12
13
 
13
14
  你的职责是:
@@ -18,125 +19,167 @@ export function buildSystemPrompt(guardConfig = '') {
18
19
 
19
20
  请以专业、建设性的语气提供审查意见。`;
20
21
 
21
- if (guardConfig) {
22
- prompt += `\n\n项目特定的审查规则:\n${guardConfig}`;
22
+ // 附带项目特定的审查规则
23
+ if (projectPrompt) {
24
+ prompt += `\n\n## 项目特定的审查规则\n\n${projectPrompt}`;
23
25
  }
24
26
 
25
27
  return prompt;
26
28
  }
27
29
 
28
30
  /**
29
- * 构建代码审查的用户提示词
30
- * @param {string} diff - 代码差异
31
- * @param {string} fileName - 文件名
32
- * @returns {string} 用户提示词
31
+ * 构建针对特定行变更的审查提示词(附带项目 prompt)
32
+ * @param {Object} changeInfo - 变更信息
33
+ * @param {string} changeInfo.content - 变更内容
34
+ * @param {string} changeInfo.type - 变更类型(addition/deletion)
35
+ * @param {number} changeInfo.lineNumber - 行号
36
+ * @param {string} changeInfo.fileName - 文件名
37
+ * @param {Object} changeInfo.context - 上下文
38
+ * @returns {string} 审查提示词
33
39
  */
34
- export function buildReviewPrompt(diff, fileName) {
35
- return `请审查以下代码变更:
40
+ export function buildLineReviewPrompt(changeInfo) {
41
+ const { content, type, lineNumber, fileName, context } = changeInfo;
42
+
43
+ let prompt = `请审查以下代码变更:
36
44
 
37
- **文件名**: ${fileName}
45
+ **文件**: ${fileName}
46
+ **行号**: ${lineNumber}
47
+ **变更类型**: ${type === 'addition' ? '新增' : '删除'}
38
48
 
39
- **代码差异**:
40
- \`\`\`diff
41
- ${diff}
49
+ **变更内容**:
42
50
  \`\`\`
51
+ ${type === 'addition' ? '+' : '-'} ${content}
52
+ \`\`\`
53
+ `;
54
+
55
+ if (context && (context.before.length > 0 || context.after.length > 0)) {
56
+ prompt += '\n**上下文**:\n```\n';
57
+ if (context.before.length > 0) {
58
+ context.before.forEach(line => {
59
+ prompt += ` ${line}\n`;
60
+ });
61
+ }
62
+ prompt += `${type === 'addition' ? '+' : '-'} ${content}\n`;
63
+ if (context.after.length > 0) {
64
+ context.after.forEach(line => {
65
+ prompt += ` ${line}\n`;
66
+ });
67
+ }
68
+ prompt += '```\n';
69
+ }
43
70
 
44
- 请提供:
45
- 1. 主要问题(如果有)
46
- 2. 改进建议
47
- 3. 优点(如果有)
71
+ prompt += `
72
+ 请提供简洁的审查意见:
73
+ 1. 如果有问题,指出具体问题
74
+ 2. 如果有改进建议,提供具体的建议
75
+ 3. 如果代码没有问题,简单说明"代码质量良好"
48
76
 
49
- 如果代码没有问题,请简要说明代码质量良好。`;
77
+ 请直接给出审查意见,不要重复代码内容。`;
78
+
79
+ return prompt;
50
80
  }
51
81
 
52
82
  /**
53
- * 构建完整的审查消息数组
54
- * @param {Object} params - 参数
55
- * @param {string} params.diff - 代码差异
56
- * @param {string} params.fileName - 文件名
57
- * @param {string} params.guardConfig - AI Review Guard 配置
83
+ * 构建针对特定行变更的完整消息数组(附带项目 prompt)
84
+ * @param {Object} changeInfo - 变更信息
85
+ * @param {string} projectPrompt - 项目配置的 prompt(来自 reviewguard.md)
58
86
  * @returns {Array} 消息数组
59
87
  */
60
- export function buildReviewMessages({ diff, fileName, guardConfig = '' }) {
88
+ export function buildLineReviewMessages(changeInfo, projectPrompt = '') {
61
89
  return [
62
- { role: 'system', content: buildSystemPrompt(guardConfig) },
63
- { role: 'user', content: buildReviewPrompt(diff, fileName) },
90
+ { role: 'system', content: buildSystemPrompt(projectPrompt) },
91
+ { role: 'user', content: buildLineReviewPrompt(changeInfo) },
64
92
  ];
65
93
  }
66
94
 
67
95
  /**
68
- * 构建批量审查的提示词
69
- * @param {Array} changes - 代码变更数组
70
- * @param {string} guardConfig - AI Review Guard 配置
96
+ * 构建整个文件所有变更行的批量审查提示词
97
+ * @param {string} fileName - 文件名
98
+ * @param {Array} meaningfulChanges - 有意义的变更数组
99
+ * @param {string} projectPrompt - 项目配置的 prompt
71
100
  * @returns {string} 批量审查提示词
72
101
  */
73
- export function buildBatchReviewPrompt(changes, guardConfig = '') {
74
- let prompt = buildSystemPrompt(guardConfig);
75
-
76
- prompt += '\n\n请审查以下 ' + changes.length + ' 个文件的代码变更:\n\n';
77
-
78
- changes.forEach((change, index) => {
79
- const fileName = change.new_path || change.old_path;
80
- const status = change.new_file ? '新增' : change.deleted_file ? '删除' : '修改';
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
+ }
81
131
 
82
- prompt += `## ${index + 1}. ${fileName} (${status})\n\n`;
83
- prompt += '```diff\n';
84
- prompt += change.diff || '(无差异内容)';
85
- prompt += '\n```\n\n';
132
+ prompt += `\`\`\`\n`;
86
133
  });
87
-
88
- prompt += '\n请为每个文件提供审查意见,并在最后给出整体评价。';
89
-
90
- return prompt;
134
+
135
+ prompt += `\n## 审查要求
136
+
137
+ 1. 仔细审查每一行变更,判断是否存在问题
138
+ 2. **只对有问题的行提出审查意见**,没有问题的行不需要评论
139
+ 3. 返回 JSON 格式的结果,格式如下:
140
+
141
+ \`\`\`json
142
+ {
143
+ "reviews": [
144
+ {
145
+ "lineNumber": 15,
146
+ "hasIssue": true,
147
+ "comment": "具体的问题描述和改进建议"
148
+ },
149
+ {
150
+ "lineNumber": 20,
151
+ "hasIssue": false
152
+ }
153
+ ]
91
154
  }
155
+ \`\`\`
92
156
 
93
- /**
94
- * 格式化 diff 内容(可选的预处理)
95
- * @param {string} diff - 原始 diff
96
- * @returns {string} 格式化后的 diff
97
- */
98
- export function formatDiff(diff) {
99
- if (!diff) return '(无变更内容)';
100
-
101
- // 移除过长的 diff(可选)
102
- const maxLines = 500;
103
- const lines = diff.split('\n');
104
-
105
- if (lines.length > maxLines) {
106
- return lines.slice(0, maxLines).join('\n') +
107
- `\n... (省略 ${lines.length - maxLines} 行)`;
108
- }
109
-
110
- return diff;
157
+ 4. 如果所有代码都没有问题,返回空的 reviews 数组:\`{"reviews": []}\`
158
+ 5. 只返回 JSON,不要包含其他文字说明`;
159
+
160
+ return prompt;
111
161
  }
112
162
 
113
163
  /**
114
- * 从 change 对象中提取关键信息
115
- * @param {Object} change - GitLab change 对象
116
- * @returns {Object} 提取的信息
164
+ * 构建整个文件批量审查的完整消息数组
165
+ * @param {string} fileName - 文件名
166
+ * @param {Array} meaningfulChanges - 有意义的变更数组
167
+ * @param {string} projectPrompt - 项目配置的 prompt
168
+ * @returns {Array} 消息数组
117
169
  */
118
- export function extractChangeInfo(change) {
119
- return {
120
- fileName: change.new_path || change.old_path,
121
- oldPath: change.old_path,
122
- newPath: change.new_path,
123
- status: change.new_file ? 'added' :
124
- change.deleted_file ? 'deleted' :
125
- change.renamed_file ? 'renamed' :
126
- 'modified',
127
- diff: change.diff || '',
128
- isNewFile: change.new_file,
129
- isDeleted: change.deleted_file,
130
- isRenamed: change.renamed_file,
131
- };
170
+ export function buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt = '') {
171
+ return [
172
+ { role: 'system', content: buildSystemPrompt(projectPrompt) },
173
+ { role: 'user', content: buildFileReviewPrompt(fileName, meaningfulChanges) },
174
+ ];
132
175
  }
133
176
 
177
+ // 导出函数
134
178
  export default {
135
179
  buildSystemPrompt,
136
- buildReviewPrompt,
137
- buildReviewMessages,
138
- buildBatchReviewPrompt,
139
- formatDiff,
140
- extractChangeInfo,
180
+ buildLineReviewPrompt,
181
+ buildLineReviewMessages,
182
+ buildFileReviewPrompt,
183
+ buildFileReviewMessages,
141
184
  };
142
185
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gitlab-ai-review",
3
- "version": "1.2.0",
4
- "description": "GitLab AI Review SDK - 支持 CI/CD 自动配置和 ARK API",
3
+ "version": "2.1.0",
4
+ "description": "GitLab AI Review SDK - 按文件批量审查,只评论有问题的代码",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {