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 +137 -59
- package/lib/diff-parser.js +197 -0
- package/lib/gitlab-client.js +64 -2
- package/lib/prompt-tools.js +131 -88
- package/package.json +2 -2
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.
|
|
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
|
-
* @
|
|
143
|
+
* @param {Object} changeInfo - 具体的行变更信息
|
|
144
|
+
* @returns {Promise<Object>} 评论结果
|
|
143
145
|
*/
|
|
144
|
-
async
|
|
146
|
+
async reviewAndCommentOnLine(change, changeInfo) {
|
|
145
147
|
const aiClient = this.getAIClient();
|
|
146
|
-
|
|
148
|
+
|
|
149
|
+
// 获取项目配置的 prompt(来自 reviewguard.md)
|
|
150
|
+
const projectPrompt = this.config.ai?.guardConfig?.content || '';
|
|
147
151
|
|
|
148
|
-
//
|
|
149
|
-
const messages = PromptTools.
|
|
150
|
-
|
|
152
|
+
// 构建针对特定行的审查消息(附带项目 prompt)
|
|
153
|
+
const messages = PromptTools.buildLineReviewMessages({
|
|
154
|
+
...changeInfo,
|
|
151
155
|
fileName: change.new_path || change.old_path,
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
175
|
+
return {
|
|
176
|
+
review,
|
|
177
|
+
comment: commentResult,
|
|
178
|
+
};
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
/**
|
|
160
|
-
* AI 审查 MR
|
|
161
|
-
* @
|
|
182
|
+
* AI 审查 MR 的所有有意义的变更并自动添加行级评论(按文件批量处理)
|
|
183
|
+
* @param {Object} options - 选项
|
|
184
|
+
* @param {number} options.maxFiles - 最大审查文件数量(默认 5)
|
|
185
|
+
* @returns {Promise<Array>} 评论结果数组
|
|
162
186
|
*/
|
|
163
|
-
async
|
|
187
|
+
async reviewAndCommentOnLines(options = {}) {
|
|
188
|
+
const { maxFiles = 5 } = options;
|
|
164
189
|
const changes = await this.getMergeRequestChanges();
|
|
165
|
-
const
|
|
166
|
-
const guardConfig = this.config.ai?.guardConfig?.content || '';
|
|
190
|
+
const results = [];
|
|
167
191
|
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
267
|
+
return results;
|
|
197
268
|
}
|
|
198
269
|
|
|
199
270
|
/**
|
|
200
|
-
*
|
|
271
|
+
* 审查单个文件的所有变更(一次 API 调用)
|
|
272
|
+
* @param {Object} change - 代码变更对象
|
|
273
|
+
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
274
|
+
* @returns {Promise<Object>} 审查结果 { reviews: [{lineNumber, hasIssue, comment}] }
|
|
201
275
|
*/
|
|
202
|
-
async
|
|
203
|
-
const
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
+
|
package/lib/gitlab-client.js
CHANGED
|
@@ -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
|
|
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
|
|
package/lib/prompt-tools.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt 工具 -
|
|
2
|
+
* Prompt 工具 - 用于拼接行级代码审查的提示词
|
|
3
|
+
* 简化版:只支持行级审查
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {string}
|
|
7
|
+
* 构建系统提示词(附带项目 prompt)
|
|
8
|
+
* @param {string} projectPrompt - 项目配置的 prompt(来自 reviewguard.md)
|
|
8
9
|
* @returns {string} 系统提示词
|
|
9
10
|
*/
|
|
10
|
-
export function buildSystemPrompt(
|
|
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
|
-
|
|
22
|
-
|
|
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 {
|
|
31
|
-
* @param {string}
|
|
32
|
-
* @
|
|
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
|
|
35
|
-
|
|
40
|
+
export function buildLineReviewPrompt(changeInfo) {
|
|
41
|
+
const { content, type, lineNumber, fileName, context } = changeInfo;
|
|
42
|
+
|
|
43
|
+
let prompt = `请审查以下代码变更:
|
|
36
44
|
|
|
37
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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}
|
|
55
|
-
* @param {string}
|
|
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
|
|
88
|
+
export function buildLineReviewMessages(changeInfo, projectPrompt = '') {
|
|
61
89
|
return [
|
|
62
|
-
{ role: 'system', content: buildSystemPrompt(
|
|
63
|
-
{ role: 'user', content:
|
|
90
|
+
{ role: 'system', content: buildSystemPrompt(projectPrompt) },
|
|
91
|
+
{ role: 'user', content: buildLineReviewPrompt(changeInfo) },
|
|
64
92
|
];
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
/**
|
|
68
|
-
*
|
|
69
|
-
* @param {
|
|
70
|
-
* @param {
|
|
96
|
+
* 构建整个文件所有变更行的批量审查提示词
|
|
97
|
+
* @param {string} fileName - 文件名
|
|
98
|
+
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
99
|
+
* @param {string} projectPrompt - 项目配置的 prompt
|
|
71
100
|
* @returns {string} 批量审查提示词
|
|
72
101
|
*/
|
|
73
|
-
export function
|
|
74
|
-
let prompt =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 +=
|
|
83
|
-
prompt += '```diff\n';
|
|
84
|
-
prompt += change.diff || '(无差异内容)';
|
|
85
|
-
prompt += '\n```\n\n';
|
|
132
|
+
prompt += `\`\`\`\n`;
|
|
86
133
|
});
|
|
87
|
-
|
|
88
|
-
prompt +=
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
*
|
|
115
|
-
* @param {
|
|
116
|
-
* @
|
|
164
|
+
* 构建整个文件批量审查的完整消息数组
|
|
165
|
+
* @param {string} fileName - 文件名
|
|
166
|
+
* @param {Array} meaningfulChanges - 有意义的变更数组
|
|
167
|
+
* @param {string} projectPrompt - 项目配置的 prompt
|
|
168
|
+
* @returns {Array} 消息数组
|
|
117
169
|
*/
|
|
118
|
-
export function
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
extractChangeInfo,
|
|
180
|
+
buildLineReviewPrompt,
|
|
181
|
+
buildLineReviewMessages,
|
|
182
|
+
buildFileReviewPrompt,
|
|
183
|
+
buildFileReviewMessages,
|
|
141
184
|
};
|
|
142
185
|
|
package/package.json
CHANGED