gitlab-ai-review 1.0.3 → 1.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
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { getConfig, validateConfig } from './lib/config.js';
7
7
  import { GitLabClient } from './lib/gitlab-client.js';
8
+ import { AIClient } from './lib/ai-client.js';
9
+ import * as PromptTools from './lib/prompt-tools.js';
8
10
 
9
11
  /**
10
12
  * GitLab AI Review SDK 主类
@@ -12,7 +14,7 @@ import { GitLabClient } from './lib/gitlab-client.js';
12
14
  export class GitLabAIReview {
13
15
  constructor(options = {}) {
14
16
  this.name = 'GitLab AI Review SDK';
15
- this.version = '1.0.0';
17
+ this.version = '1.2.0';
16
18
 
17
19
  // 如果传入了配置,使用手动配置;否则使用自动检测
18
20
  if (options.token || options.gitlab) {
@@ -26,6 +28,10 @@ export class GitLabAIReview {
26
28
  projectId: options.projectId || options.project?.projectId,
27
29
  mergeRequestIid: options.mergeRequestIid || options.project?.mergeRequestIid,
28
30
  },
31
+ ai: {
32
+ arkApiKey: options.arkApiKey || options.ai?.arkApiKey,
33
+ guardConfig: options.guardConfig || options.ai?.guardConfig,
34
+ },
29
35
  };
30
36
  } else {
31
37
  // 自动检测模式(CI/CD)
@@ -37,6 +43,14 @@ export class GitLabAIReview {
37
43
  token: this.config.gitlab.token,
38
44
  host: this.config.gitlab.url,
39
45
  });
46
+
47
+ // 创建 AI 客户端(如果配置了 API Key)
48
+ if (this.config.ai?.arkApiKey) {
49
+ this.aiClient = new AIClient({
50
+ apiKey: this.config.ai.arkApiKey,
51
+ ...options.aiConfig,
52
+ });
53
+ }
40
54
  }
41
55
 
42
56
  /**
@@ -112,6 +126,106 @@ export class GitLabAIReview {
112
126
  );
113
127
  }
114
128
 
129
+ /**
130
+ * 获取 AI 客户端
131
+ */
132
+ getAIClient() {
133
+ if (!this.aiClient) {
134
+ throw new Error('AI 客户端未初始化,请确保配置了 ARK_API_KEY');
135
+ }
136
+ return this.aiClient;
137
+ }
138
+
139
+ /**
140
+ * AI 审查单个文件的代码变更
141
+ * @param {Object} change - 代码变更对象
142
+ * @returns {Promise<Object>} 审查结果
143
+ */
144
+ async reviewCodeChange(change) {
145
+ const aiClient = this.getAIClient();
146
+ const guardConfig = this.config.ai?.guardConfig?.content || '';
147
+
148
+ // 使用 PromptTools 构建消息
149
+ const messages = PromptTools.buildReviewMessages({
150
+ diff: change.diff,
151
+ fileName: change.new_path || change.old_path,
152
+ guardConfig,
153
+ });
154
+
155
+ // 调用 AI
156
+ return aiClient.sendMessage(messages);
157
+ }
158
+
159
+ /**
160
+ * AI 审查 MR 的所有代码变更
161
+ * @returns {Promise<Array>} 审查结果数组
162
+ */
163
+ async reviewMergeRequest() {
164
+ const changes = await this.getMergeRequestChanges();
165
+ const aiClient = this.getAIClient();
166
+ const guardConfig = this.config.ai?.guardConfig?.content || '';
167
+
168
+ const reviews = [];
169
+
170
+ for (const change of changes) {
171
+ try {
172
+ // 使用 PromptTools 构建消息
173
+ const messages = PromptTools.buildReviewMessages({
174
+ diff: change.diff,
175
+ fileName: change.new_path || change.old_path,
176
+ guardConfig,
177
+ });
178
+
179
+ // 调用 AI
180
+ const result = await aiClient.sendMessage(messages);
181
+
182
+ reviews.push({
183
+ fileName: change.new_path || change.old_path,
184
+ status: 'success',
185
+ ...result,
186
+ });
187
+ } catch (error) {
188
+ reviews.push({
189
+ fileName: change.new_path || change.old_path,
190
+ status: 'error',
191
+ error: error.message,
192
+ });
193
+ }
194
+ }
195
+
196
+ return reviews;
197
+ }
198
+
199
+ /**
200
+ * AI 审查 MR 并自动添加评论
201
+ */
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
+ }
214
+
215
+ comment += `### 📄 ${review.fileName}\n\n`;
216
+
217
+ if (review.reasoning) {
218
+ comment += `**思考过程**:\n${review.reasoning}\n\n`;
219
+ }
220
+
221
+ comment += `**审查意见**:\n${review.content}\n\n`;
222
+ comment += `---\n\n`;
223
+ }
224
+
225
+ // 添加评论到 MR
226
+ return this.addComment(comment);
227
+ }
228
+
115
229
  /**
116
230
  * 测试方法
117
231
  */
@@ -120,9 +234,11 @@ export class GitLabAIReview {
120
234
  }
121
235
  }
122
236
 
123
- // 导出工具函数
237
+ // 导出工具函数和类
124
238
  export { getConfig, validateConfig } from './lib/config.js';
125
239
  export { GitLabClient } from './lib/gitlab-client.js';
240
+ export { AIClient } from './lib/ai-client.js';
241
+ export { PromptTools };
126
242
 
127
243
  // 默认导出
128
244
  export default GitLabAIReview;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * AI 客户端 - 基于 ARK API (豆包大模型)
3
+ * 只负责调用 AI API,不处理业务逻辑
4
+ */
5
+
6
+ import OpenAI from 'openai';
7
+
8
+ /**
9
+ * AI 客户端类 - 纯粹的 API 调用封装
10
+ */
11
+ export class AIClient {
12
+ constructor(config = {}) {
13
+ const apiKey = config.apiKey || process.env.ARK_API_KEY;
14
+ const baseURL = config.baseURL || 'https://ark.cn-beijing.volces.com/api/v3';
15
+ const model = config.model || 'doubao-seed-1-6-251015';
16
+
17
+ if (!apiKey) {
18
+ throw new Error('ARK_API_KEY 未配置');
19
+ }
20
+
21
+ this.openai = new OpenAI({
22
+ apiKey,
23
+ baseURL,
24
+ });
25
+
26
+ this.model = model;
27
+ this.config = config;
28
+ }
29
+
30
+ /**
31
+ * 发送消息到 AI(非流式)
32
+ * @param {Array|string} prompt - 消息数组或单个提示词
33
+ * @param {Object} options - 可选参数
34
+ * @returns {Promise<Object>} AI 响应
35
+ */
36
+ async sendMessage(prompt, options = {}) {
37
+ // 如果传入的是字符串,转换为消息数组
38
+ const messages = typeof prompt === 'string'
39
+ ? [{ role: 'user', content: prompt }]
40
+ : prompt;
41
+
42
+ const completion = await this.openai.chat.completions.create({
43
+ messages,
44
+ model: options.model || this.model,
45
+ reasoning_effort: options.reasoningEffort || this.config.reasoningEffort || 'medium',
46
+ ...options,
47
+ });
48
+
49
+ return {
50
+ reasoning: completion.choices[0]?.message?.reasoning_content || '',
51
+ content: completion.choices[0]?.message?.content || '',
52
+ usage: completion.usage,
53
+ raw: completion,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * 发送消息到 AI(流式)
59
+ * @param {Array|string} prompt - 消息数组或单个提示词
60
+ * @param {Function} onChunk - 流式回调函数
61
+ * @param {Object} options - 可选参数
62
+ * @returns {Promise<Object>} 完整 AI 响应
63
+ */
64
+ async sendMessageStream(prompt, onChunk, options = {}) {
65
+ // 如果传入的是字符串,转换为消息数组
66
+ const messages = typeof prompt === 'string'
67
+ ? [{ role: 'user', content: prompt }]
68
+ : prompt;
69
+
70
+ const stream = await this.openai.chat.completions.create({
71
+ messages,
72
+ model: options.model || this.model,
73
+ reasoning_effort: options.reasoningEffort || this.config.reasoningEffort || 'medium',
74
+ stream: true,
75
+ ...options,
76
+ });
77
+
78
+ let fullReasoning = '';
79
+ let fullContent = '';
80
+
81
+ for await (const part of stream) {
82
+ const reasoning = part.choices[0]?.delta?.reasoning_content || '';
83
+ const content = part.choices[0]?.delta?.content || '';
84
+
85
+ fullReasoning += reasoning;
86
+ fullContent += content;
87
+
88
+ if (onChunk) {
89
+ onChunk({ reasoning, content });
90
+ }
91
+ }
92
+
93
+ return {
94
+ reasoning: fullReasoning,
95
+ content: fullContent,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * 获取原始 OpenAI 客户端(高级用法)
101
+ * @returns {OpenAI} OpenAI 客户端实例
102
+ */
103
+ getClient() {
104
+ return this.openai;
105
+ }
106
+
107
+ /**
108
+ * 获取当前配置
109
+ * @returns {Object} 配置对象
110
+ */
111
+ getConfig() {
112
+ return {
113
+ model: this.model,
114
+ ...this.config,
115
+ };
116
+ }
117
+ }
118
+
119
+ export default AIClient;
120
+
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Prompt 工具 - 用于拼接代码审查的提示词
3
+ */
4
+
5
+ /**
6
+ * 构建系统提示词
7
+ * @param {string} guardConfig - AI Review Guard 配置内容
8
+ * @returns {string} 系统提示词
9
+ */
10
+ export function buildSystemPrompt(guardConfig = '') {
11
+ let prompt = `你是一个专业的代码审查助手,负责审查 GitLab Merge Request 的代码变更。
12
+
13
+ 你的职责是:
14
+ 1. 识别代码中的潜在问题(安全漏洞、性能问题、逻辑错误等)
15
+ 2. 提供具体的改进建议
16
+ 3. 指出不符合最佳实践的代码
17
+ 4. 评估代码的可维护性和可读性
18
+
19
+ 请以专业、建设性的语气提供审查意见。`;
20
+
21
+ if (guardConfig) {
22
+ prompt += `\n\n项目特定的审查规则:\n${guardConfig}`;
23
+ }
24
+
25
+ return prompt;
26
+ }
27
+
28
+ /**
29
+ * 构建代码审查的用户提示词
30
+ * @param {string} diff - 代码差异
31
+ * @param {string} fileName - 文件名
32
+ * @returns {string} 用户提示词
33
+ */
34
+ export function buildReviewPrompt(diff, fileName) {
35
+ return `请审查以下代码变更:
36
+
37
+ **文件名**: ${fileName}
38
+
39
+ **代码差异**:
40
+ \`\`\`diff
41
+ ${diff}
42
+ \`\`\`
43
+
44
+ 请提供:
45
+ 1. 主要问题(如果有)
46
+ 2. 改进建议
47
+ 3. 优点(如果有)
48
+
49
+ 如果代码没有问题,请简要说明代码质量良好。`;
50
+ }
51
+
52
+ /**
53
+ * 构建完整的审查消息数组
54
+ * @param {Object} params - 参数
55
+ * @param {string} params.diff - 代码差异
56
+ * @param {string} params.fileName - 文件名
57
+ * @param {string} params.guardConfig - AI Review Guard 配置
58
+ * @returns {Array} 消息数组
59
+ */
60
+ export function buildReviewMessages({ diff, fileName, guardConfig = '' }) {
61
+ return [
62
+ { role: 'system', content: buildSystemPrompt(guardConfig) },
63
+ { role: 'user', content: buildReviewPrompt(diff, fileName) },
64
+ ];
65
+ }
66
+
67
+ /**
68
+ * 构建批量审查的提示词
69
+ * @param {Array} changes - 代码变更数组
70
+ * @param {string} guardConfig - AI Review Guard 配置
71
+ * @returns {string} 批量审查提示词
72
+ */
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 ? '删除' : '修改';
81
+
82
+ prompt += `## ${index + 1}. ${fileName} (${status})\n\n`;
83
+ prompt += '```diff\n';
84
+ prompt += change.diff || '(无差异内容)';
85
+ prompt += '\n```\n\n';
86
+ });
87
+
88
+ prompt += '\n请为每个文件提供审查意见,并在最后给出整体评价。';
89
+
90
+ return prompt;
91
+ }
92
+
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;
111
+ }
112
+
113
+ /**
114
+ * 从 change 对象中提取关键信息
115
+ * @param {Object} change - GitLab change 对象
116
+ * @returns {Object} 提取的信息
117
+ */
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
+ };
132
+ }
133
+
134
+ export default {
135
+ buildSystemPrompt,
136
+ buildReviewPrompt,
137
+ buildReviewMessages,
138
+ buildBatchReviewPrompt,
139
+ formatDiff,
140
+ extractChangeInfo,
141
+ };
142
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-ai-review",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "GitLab AI Review SDK - 支持 CI/CD 自动配置和 ARK API",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -25,5 +25,8 @@
25
25
  "license": "MIT",
26
26
  "engines": {
27
27
  "node": ">=18.0.0"
28
+ },
29
+ "dependencies": {
30
+ "openai": "^4.73.0"
28
31
  }
29
32
  }