gitlab-ai-review 1.1.0 → 2.0.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 +73 -39
- package/lib/ai-client.js +40 -127
- package/lib/diff-parser.js +197 -0
- package/lib/gitlab-client.js +64 -2
- package/lib/prompt-tools.js +101 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
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
|
+
import * as PromptTools from './lib/prompt-tools.js';
|
|
10
|
+
import * as DiffParser from './lib/diff-parser.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* GitLab AI Review SDK 主类
|
|
@@ -13,7 +15,7 @@ import { AIClient } from './lib/ai-client.js';
|
|
|
13
15
|
export class GitLabAIReview {
|
|
14
16
|
constructor(options = {}) {
|
|
15
17
|
this.name = 'GitLab AI Review SDK';
|
|
16
|
-
this.version = '
|
|
18
|
+
this.version = '2.0.0';
|
|
17
19
|
|
|
18
20
|
// 如果传入了配置,使用手动配置;否则使用自动检测
|
|
19
21
|
if (options.token || options.gitlab) {
|
|
@@ -136,58 +138,89 @@ export class GitLabAIReview {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
/**
|
|
139
|
-
* AI
|
|
141
|
+
* AI 审查特定行的变更并在该行添加评论(附带项目 prompt)
|
|
142
|
+
* @param {Object} change - 代码变更对象
|
|
143
|
+
* @param {Object} changeInfo - 具体的行变更信息
|
|
144
|
+
* @returns {Promise<Object>} 评论结果
|
|
140
145
|
*/
|
|
141
|
-
async
|
|
146
|
+
async reviewAndCommentOnLine(change, changeInfo) {
|
|
142
147
|
const aiClient = this.getAIClient();
|
|
143
|
-
|
|
148
|
+
|
|
149
|
+
// 获取项目配置的 prompt(来自 reviewguard.md)
|
|
150
|
+
const projectPrompt = this.config.ai?.guardConfig?.content || '';
|
|
144
151
|
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
// 构建针对特定行的审查消息(附带项目 prompt)
|
|
153
|
+
const messages = PromptTools.buildLineReviewMessages({
|
|
154
|
+
...changeInfo,
|
|
147
155
|
fileName: change.new_path || change.old_path,
|
|
148
|
-
|
|
149
|
-
});
|
|
150
|
-
}
|
|
156
|
+
}, projectPrompt);
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
};
|
|
159
167
|
|
|
160
|
-
|
|
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
|
+
);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
review,
|
|
177
|
+
comment: commentResult,
|
|
178
|
+
};
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
/**
|
|
164
|
-
* AI 审查 MR
|
|
182
|
+
* AI 审查 MR 的所有有意义的变更并自动添加行级评论
|
|
183
|
+
* @param {Object} options - 选项
|
|
184
|
+
* @param {number} options.maxComments - 最大评论数量(避免过多评论)
|
|
185
|
+
* @returns {Promise<Array>} 评论结果数组
|
|
165
186
|
*/
|
|
166
|
-
async
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
comment += `审查失败: ${review.error}\n\n`;
|
|
176
|
-
continue;
|
|
187
|
+
async reviewAndCommentOnLines(options = {}) {
|
|
188
|
+
const { maxComments = 10 } = options;
|
|
189
|
+
const changes = await this.getMergeRequestChanges();
|
|
190
|
+
const results = [];
|
|
191
|
+
|
|
192
|
+
for (const change of changes) {
|
|
193
|
+
if (results.length >= maxComments) {
|
|
194
|
+
console.log(`已达到最大评论数量限制 (${maxComments}),停止审查`);
|
|
195
|
+
break;
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
});
|
|
219
|
+
}
|
|
183
220
|
}
|
|
184
|
-
|
|
185
|
-
comment += `**审查意见**:\n${review.content}\n\n`;
|
|
186
|
-
comment += `---\n\n`;
|
|
187
221
|
}
|
|
188
222
|
|
|
189
|
-
|
|
190
|
-
return this.addComment(comment);
|
|
223
|
+
return results;
|
|
191
224
|
}
|
|
192
225
|
|
|
193
226
|
/**
|
|
@@ -198,10 +231,11 @@ export class GitLabAIReview {
|
|
|
198
231
|
}
|
|
199
232
|
}
|
|
200
233
|
|
|
201
|
-
//
|
|
234
|
+
// 导出工具函数和类
|
|
202
235
|
export { getConfig, validateConfig } from './lib/config.js';
|
|
203
236
|
export { GitLabClient } from './lib/gitlab-client.js';
|
|
204
237
|
export { AIClient } from './lib/ai-client.js';
|
|
238
|
+
export { PromptTools, DiffParser };
|
|
205
239
|
|
|
206
240
|
// 默认导出
|
|
207
241
|
export default GitLabAIReview;
|
package/lib/ai-client.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI 客户端 - 基于 ARK API (豆包大模型)
|
|
3
|
+
* 只负责调用 AI API,不处理业务逻辑
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import OpenAI from 'openai';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* AI 客户端类
|
|
9
|
+
* AI 客户端类 - 纯粹的 API 调用封装
|
|
9
10
|
*/
|
|
10
11
|
export class AIClient {
|
|
11
12
|
constructor(config = {}) {
|
|
@@ -27,54 +28,51 @@ export class AIClient {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param {
|
|
32
|
-
* @param {
|
|
33
|
-
* @
|
|
34
|
-
* @param {string} params.guardConfig - AI Review Guard 配置内容
|
|
35
|
-
* @returns {Promise<string>} 审查结果
|
|
31
|
+
* 发送消息到 AI(非流式)
|
|
32
|
+
* @param {Array|string} prompt - 消息数组或单个提示词
|
|
33
|
+
* @param {Object} options - 可选参数
|
|
34
|
+
* @returns {Promise<Object>} AI 响应
|
|
36
35
|
*/
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
const
|
|
36
|
+
async sendMessage(prompt, options = {}) {
|
|
37
|
+
// 如果传入的是字符串,转换为消息数组
|
|
38
|
+
const messages = typeof prompt === 'string'
|
|
39
|
+
? [{ role: 'user', content: prompt }]
|
|
40
|
+
: prompt;
|
|
40
41
|
|
|
41
42
|
const completion = await this.openai.chat.completions.create({
|
|
42
|
-
messages
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
model: this.model,
|
|
47
|
-
reasoning_effort: this.config.reasoningEffort || 'medium',
|
|
43
|
+
messages,
|
|
44
|
+
model: options.model || this.model,
|
|
45
|
+
reasoning_effort: options.reasoningEffort || this.config.reasoningEffort || 'medium',
|
|
46
|
+
...options,
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
return {
|
|
51
50
|
reasoning: completion.choices[0]?.message?.reasoning_content || '',
|
|
52
51
|
content: completion.choices[0]?.message?.content || '',
|
|
53
52
|
usage: completion.usage,
|
|
53
|
+
raw: completion,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* @param {
|
|
60
|
-
* @param {string} params.diff - 代码差异
|
|
61
|
-
* @param {string} params.fileName - 文件名
|
|
62
|
-
* @param {string} params.guardConfig - AI Review Guard 配置内容
|
|
58
|
+
* 发送消息到 AI(流式)
|
|
59
|
+
* @param {Array|string} prompt - 消息数组或单个提示词
|
|
63
60
|
* @param {Function} onChunk - 流式回调函数
|
|
64
|
-
* @
|
|
61
|
+
* @param {Object} options - 可选参数
|
|
62
|
+
* @returns {Promise<Object>} 完整 AI 响应
|
|
65
63
|
*/
|
|
66
|
-
async
|
|
67
|
-
|
|
68
|
-
const
|
|
64
|
+
async sendMessageStream(prompt, onChunk, options = {}) {
|
|
65
|
+
// 如果传入的是字符串,转换为消息数组
|
|
66
|
+
const messages = typeof prompt === 'string'
|
|
67
|
+
? [{ role: 'user', content: prompt }]
|
|
68
|
+
: prompt;
|
|
69
69
|
|
|
70
70
|
const stream = await this.openai.chat.completions.create({
|
|
71
|
-
messages
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
],
|
|
75
|
-
model: this.model,
|
|
76
|
-
reasoning_effort: this.config.reasoningEffort || 'medium',
|
|
71
|
+
messages,
|
|
72
|
+
model: options.model || this.model,
|
|
73
|
+
reasoning_effort: options.reasoningEffort || this.config.reasoningEffort || 'medium',
|
|
77
74
|
stream: true,
|
|
75
|
+
...options,
|
|
78
76
|
});
|
|
79
77
|
|
|
80
78
|
let fullReasoning = '';
|
|
@@ -99,107 +97,22 @@ export class AIClient {
|
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
/**
|
|
102
|
-
*
|
|
103
|
-
* @
|
|
104
|
-
* @param {Object} options - 可选参数
|
|
105
|
-
* @returns {Promise<Object>} AI 响应
|
|
100
|
+
* 获取原始 OpenAI 客户端(高级用法)
|
|
101
|
+
* @returns {OpenAI} OpenAI 客户端实例
|
|
106
102
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
messages,
|
|
110
|
-
model: options.model || this.model,
|
|
111
|
-
reasoning_effort: options.reasoningEffort || 'medium',
|
|
112
|
-
stream: options.stream || false,
|
|
113
|
-
...options,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (options.stream) {
|
|
117
|
-
return completion;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
reasoning: completion.choices[0]?.message?.reasoning_content || '',
|
|
122
|
-
content: completion.choices[0]?.message?.content || '',
|
|
123
|
-
usage: completion.usage,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 构建系统提示词
|
|
129
|
-
* @private
|
|
130
|
-
*/
|
|
131
|
-
_buildSystemPrompt(guardConfig) {
|
|
132
|
-
let prompt = `你是一个专业的代码审查助手,负责审查 GitLab Merge Request 的代码变更。
|
|
133
|
-
|
|
134
|
-
你的职责是:
|
|
135
|
-
1. 识别代码中的潜在问题(安全漏洞、性能问题、逻辑错误等)
|
|
136
|
-
2. 提供具体的改进建议
|
|
137
|
-
3. 指出不符合最佳实践的代码
|
|
138
|
-
4. 评估代码的可维护性和可读性
|
|
139
|
-
|
|
140
|
-
请以专业、建设性的语气提供审查意见。`;
|
|
141
|
-
|
|
142
|
-
if (guardConfig) {
|
|
143
|
-
prompt += `\n\n项目特定的审查规则:\n${guardConfig}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return prompt;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* 构建代码审查提示词
|
|
151
|
-
* @private
|
|
152
|
-
*/
|
|
153
|
-
_buildReviewPrompt(diff, fileName) {
|
|
154
|
-
return `请审查以下代码变更:
|
|
155
|
-
|
|
156
|
-
**文件名**: ${fileName}
|
|
157
|
-
|
|
158
|
-
**代码差异**:
|
|
159
|
-
\`\`\`diff
|
|
160
|
-
${diff}
|
|
161
|
-
\`\`\`
|
|
162
|
-
|
|
163
|
-
请提供:
|
|
164
|
-
1. 主要问题(如果有)
|
|
165
|
-
2. 改进建议
|
|
166
|
-
3. 优点(如果有)
|
|
167
|
-
|
|
168
|
-
如果代码没有问题,请简要说明代码质量良好。`;
|
|
103
|
+
getClient() {
|
|
104
|
+
return this.openai;
|
|
169
105
|
}
|
|
170
106
|
|
|
171
107
|
/**
|
|
172
|
-
*
|
|
173
|
-
* @
|
|
174
|
-
* @param {string} guardConfig - AI Review Guard 配置
|
|
175
|
-
* @returns {Promise<Array>} 审查结果数组
|
|
108
|
+
* 获取当前配置
|
|
109
|
+
* @returns {Object} 配置对象
|
|
176
110
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const result = await this.reviewCode({
|
|
183
|
-
diff: change.diff,
|
|
184
|
-
fileName: change.new_path || change.old_path,
|
|
185
|
-
guardConfig,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
reviews.push({
|
|
189
|
-
fileName: change.new_path || change.old_path,
|
|
190
|
-
status: 'success',
|
|
191
|
-
...result,
|
|
192
|
-
});
|
|
193
|
-
} catch (error) {
|
|
194
|
-
reviews.push({
|
|
195
|
-
fileName: change.new_path || change.old_path,
|
|
196
|
-
status: 'error',
|
|
197
|
-
error: error.message,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return reviews;
|
|
111
|
+
getConfig() {
|
|
112
|
+
return {
|
|
113
|
+
model: this.model,
|
|
114
|
+
...this.config,
|
|
115
|
+
};
|
|
203
116
|
}
|
|
204
117
|
}
|
|
205
118
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt 工具 - 用于拼接行级代码审查的提示词
|
|
3
|
+
* 简化版:只支持行级审查
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 构建系统提示词(附带项目 prompt)
|
|
8
|
+
* @param {string} projectPrompt - 项目配置的 prompt(来自 reviewguard.md)
|
|
9
|
+
* @returns {string} 系统提示词
|
|
10
|
+
*/
|
|
11
|
+
export function buildSystemPrompt(projectPrompt = '') {
|
|
12
|
+
let prompt = `你是一个专业的代码审查助手,负责审查 GitLab Merge Request 的代码变更。
|
|
13
|
+
|
|
14
|
+
你的职责是:
|
|
15
|
+
1. 识别代码中的潜在问题(安全漏洞、性能问题、逻辑错误等)
|
|
16
|
+
2. 提供具体的改进建议
|
|
17
|
+
3. 指出不符合最佳实践的代码
|
|
18
|
+
4. 评估代码的可维护性和可读性
|
|
19
|
+
|
|
20
|
+
请以专业、建设性的语气提供审查意见。`;
|
|
21
|
+
|
|
22
|
+
// 附带项目特定的审查规则
|
|
23
|
+
if (projectPrompt) {
|
|
24
|
+
prompt += `\n\n## 项目特定的审查规则\n\n${projectPrompt}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return prompt;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
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} 审查提示词
|
|
39
|
+
*/
|
|
40
|
+
export function buildLineReviewPrompt(changeInfo) {
|
|
41
|
+
const { content, type, lineNumber, fileName, context } = changeInfo;
|
|
42
|
+
|
|
43
|
+
let prompt = `请审查以下代码变更:
|
|
44
|
+
|
|
45
|
+
**文件**: ${fileName}
|
|
46
|
+
**行号**: ${lineNumber}
|
|
47
|
+
**变更类型**: ${type === 'addition' ? '新增' : '删除'}
|
|
48
|
+
|
|
49
|
+
**变更内容**:
|
|
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
|
+
}
|
|
70
|
+
|
|
71
|
+
prompt += `
|
|
72
|
+
请提供简洁的审查意见:
|
|
73
|
+
1. 如果有问题,指出具体问题
|
|
74
|
+
2. 如果有改进建议,提供具体的建议
|
|
75
|
+
3. 如果代码没有问题,简单说明"代码质量良好"
|
|
76
|
+
|
|
77
|
+
请直接给出审查意见,不要重复代码内容。`;
|
|
78
|
+
|
|
79
|
+
return prompt;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 构建针对特定行变更的完整消息数组(附带项目 prompt)
|
|
84
|
+
* @param {Object} changeInfo - 变更信息
|
|
85
|
+
* @param {string} projectPrompt - 项目配置的 prompt(来自 reviewguard.md)
|
|
86
|
+
* @returns {Array} 消息数组
|
|
87
|
+
*/
|
|
88
|
+
export function buildLineReviewMessages(changeInfo, projectPrompt = '') {
|
|
89
|
+
return [
|
|
90
|
+
{ role: 'system', content: buildSystemPrompt(projectPrompt) },
|
|
91
|
+
{ role: 'user', content: buildLineReviewPrompt(changeInfo) },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 只导出行级审查需要的函数
|
|
96
|
+
export default {
|
|
97
|
+
buildSystemPrompt,
|
|
98
|
+
buildLineReviewPrompt,
|
|
99
|
+
buildLineReviewMessages,
|
|
100
|
+
};
|
|
101
|
+
|
package/package.json
CHANGED