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 +112 -30
- package/lib/prompt-tools.js +87 -1
- package/package.json +2 -2
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.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.
|
|
184
|
+
* @param {number} options.maxFiles - 最大审查文件数量(默认不限制)
|
|
185
185
|
* @returns {Promise<Array>} 评论结果数组
|
|
186
186
|
*/
|
|
187
187
|
async reviewAndCommentOnLines(options = {}) {
|
|
188
|
-
const {
|
|
188
|
+
const { maxFiles = Infinity } = options;
|
|
189
189
|
const changes = await this.getMergeRequestChanges();
|
|
190
190
|
const results = [];
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
*/
|
package/lib/prompt-tools.js
CHANGED
|
@@ -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