gitlab-ai-review 4.2.4 → 6.3.9

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
@@ -3,12 +3,13 @@
3
3
  * 支持 CI/CD 自动配置和手动配置
4
4
  */
5
5
 
6
- import { getConfig, validateConfig, loadGuardConfig } from './lib/config.js';
6
+ import { getConfig, validateConfig, loadGuardConfig, generateAIReviewGuardConfigFromGitLab, checkAndEnhanceReviewGuard } 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
10
  import * as DiffParser from './lib/diff-parser.js';
11
- import * as ImpactAnalyzer from './lib/impact-analyzer.js';
11
+ import * as ExportAnalyzer from './lib/export-analyzer.js';
12
+ import { loadProjectDocuments, formatDocumentsInfo } from './lib/document-loader.js';
12
13
 
13
14
  /**
14
15
  * GitLab AI Review SDK 主类
@@ -53,6 +54,9 @@ export class GitLabAIReview {
53
54
  ...options.aiConfig,
54
55
  });
55
56
  }
57
+
58
+ // 项目文档缓存
59
+ this.projectDocuments = null;
56
60
  }
57
61
 
58
62
  /**
@@ -63,26 +67,308 @@ export class GitLabAIReview {
63
67
  }
64
68
 
65
69
  /**
66
- * 初始化并加载 Guard 配置(支持 AI 自动生成)
67
- * 如果项目中没有 reviewguard.md,会自动分析项目并生成
70
+ * 初始化并加载 Guard 配置(支持 AI 自动生成和智能补充)
71
+ *
72
+ * 工作流程:
73
+ * 1. 如果有现有的 reviewguard.md:
74
+ * - enableEnhance=true: 结合项目代码检查是否完整,不完整则补充
75
+ * - enableEnhance=false: 直接使用现有配置
76
+ * 2. 如果没有 reviewguard.md:
77
+ * - useGitLabAPI=true: 通过 GitLab API 获取整个项目代码生成
78
+ * - useGitLabAPI=false: 使用本地文件系统分析生成
79
+ *
80
+ * @param {Object} options - 选项
81
+ * @param {boolean} options.useGitLabAPI - 是否使用 GitLab API 获取整个项目(默认 true)
82
+ * @param {boolean} options.enableEnhance - 是否启用智能补充模式(默认 true)
83
+ * @param {string} options.ref - 分支名(默认使用 MR 的目标分支)
68
84
  */
69
- async initGuardConfig() {
85
+ async initGuardConfig(options = {}) {
86
+ const { useGitLabAPI = true, enableEnhance = true } = options;
87
+
70
88
  if (this.config.ai?.guardConfig) {
71
89
  return this.config.ai.guardConfig;
72
90
  }
73
91
 
74
- const guardConfig = await loadGuardConfig(this.aiClient);
92
+ // 获取目标分支
93
+ let ref = options.ref || 'main';
94
+ try {
95
+ const mrInfo = await this.getMergeRequest();
96
+ ref = mrInfo.target_branch || ref;
97
+ } catch (e) {
98
+ // 使用默认分支
99
+ }
100
+
101
+ // 1. 先尝试从本地加载现有的 reviewguard.md
102
+ let guardConfig = await loadGuardConfig(null); // 不传 aiClient,只检查本地文件
75
103
 
76
104
  if (guardConfig) {
105
+ console.log(`📄 发现现有的 Guard 配置: ${guardConfig.filename}`);
106
+
107
+ // 🎯 如果启用智能补充模式,检查并补充 reviewguard.md
108
+ if (enableEnhance && this.aiClient && useGitLabAPI && this.config.project.projectId) {
109
+ console.log('🔍 启用智能补充模式,检查 reviewguard.md 是否完整...');
110
+
111
+ guardConfig = await checkAndEnhanceReviewGuard(
112
+ this.aiClient,
113
+ this.gitlabClient,
114
+ this.config.project.projectId,
115
+ ref,
116
+ guardConfig
117
+ );
118
+
119
+ if (guardConfig.enhanced) {
120
+ console.log(`✅ Guard 配置已增强: ${guardConfig.filename}`);
121
+ } else if (guardConfig.isComplete) {
122
+ console.log(`✅ Guard 配置完整,无需补充: ${guardConfig.filename}`);
123
+ }
124
+ } else {
125
+ console.log(`✅ Guard 配置已加载: ${guardConfig.filename}`);
126
+ }
127
+
77
128
  this.config.ai.guardConfig = guardConfig;
78
- console.log(`✅ Guard 配置已加载: ${guardConfig.filename}${guardConfig.generated ? ' (AI自动生成)' : ''}`);
129
+ return guardConfig;
130
+ }
131
+
132
+ // 2. 本地没有找到,使用 AI 生成
133
+ console.log('📭 未找到现有的 reviewguard.md,将自动生成...');
134
+
135
+ if (this.aiClient) {
136
+ if (useGitLabAPI && this.config.project.projectId) {
137
+ // 🎯 使用 GitLab API 获取整个项目来生成
138
+ console.log('📡 使用 GitLab API 模式获取整个项目...');
139
+ console.log(` 目标分支: ${ref}`);
140
+
141
+ guardConfig = await generateAIReviewGuardConfigFromGitLab(
142
+ this.aiClient,
143
+ this.gitlabClient,
144
+ this.config.project.projectId,
145
+ ref
146
+ );
147
+ } else {
148
+ // 使用本地模式(读取本地文件)
149
+ console.log('📂 使用本地模式分析项目...');
150
+ guardConfig = await loadGuardConfig(this.aiClient);
151
+ }
152
+ }
153
+
154
+ if (guardConfig) {
155
+ this.config.ai.guardConfig = guardConfig;
156
+ console.log(`✅ Guard 配置已生成: ${guardConfig.filename}`);
79
157
  } else {
80
- console.log('⚠️ 未找到 Guard 配置,将使用默认审查规则');
158
+ console.log('⚠️ 未能生成 Guard 配置,将使用默认审查规则');
81
159
  }
82
160
 
83
161
  return guardConfig;
84
162
  }
85
163
 
164
+ /**
165
+ * 加载项目文档(PRD.docx 和 TD.docx)
166
+ *
167
+ * 从项目根目录的 documents 文件夹加载产品需求文档和技术设计文档,
168
+ * 使用 AI 生成总结,在后续代码审查时提供上下文。
169
+ *
170
+ * @param {Object} options - 选项
171
+ * @param {string} options.documentsPath - documents 文件夹路径(默认 'documents')
172
+ * @param {boolean} options.enableSummary - 是否使用 AI 生成文档总结(默认 true)
173
+ * @param {string} options.ref - 分支名(默认使用 MR 的目标分支)
174
+ * @returns {Promise<Object>} 文档对象 { prd, td, context }
175
+ */
176
+ async loadProjectDocuments(options = {}) {
177
+ const {
178
+ documentsPath = 'documents',
179
+ enableSummary = true,
180
+ ref: refOption,
181
+ } = options;
182
+
183
+ // 如果已经加载过,直接返回缓存
184
+ if (this.projectDocuments) {
185
+ console.log('📚 使用已缓存的项目文档');
186
+ return this.projectDocuments;
187
+ }
188
+
189
+ // 获取源分支(文档通常在 MR 的源分支上)
190
+ let ref = refOption || 'main';
191
+ try {
192
+ const mrInfo = await this.getMergeRequest();
193
+ ref = mrInfo.source_branch || ref; // 改为从源分支读取
194
+ } catch (e) {
195
+ // 使用默认分支
196
+ }
197
+
198
+ // 加载文档
199
+ const documents = await loadProjectDocuments({
200
+ gitlabClient: this.gitlabClient,
201
+ projectId: this.config.project.projectId,
202
+ ref,
203
+ aiClient: enableSummary ? this.aiClient : null,
204
+ documentsPath,
205
+ });
206
+
207
+ // 缓存结果
208
+ this.projectDocuments = documents;
209
+
210
+ // 输出加载信息
211
+ if (documents.prd || documents.td) {
212
+ console.log('\n' + '='.repeat(80));
213
+ console.log('📚 项目文档加载详情');
214
+ console.log('='.repeat(80));
215
+ console.log(formatDocumentsInfo(documents));
216
+ console.log('='.repeat(80) + '\n');
217
+ }
218
+
219
+ return documents;
220
+ }
221
+
222
+ /**
223
+ * 获取项目文档的上下文字符串(用于审查)
224
+ * @returns {string} 文档上下文
225
+ */
226
+ getDocumentsContext() {
227
+ if (!this.projectDocuments || !this.projectDocuments.context) {
228
+ return '';
229
+ }
230
+ return this.projectDocuments.context;
231
+ }
232
+
233
+ /**
234
+ * 从 Package Registry 加载历史反馈样本
235
+ * 区分正样本(up > down)和负样本(down > up)
236
+ *
237
+ * @param {Object} options - 选项
238
+ * @param {string} options.packageName - 包名(默认 'ai-review-data')
239
+ * @param {string} options.packageVersion - 版本(默认 'latest')
240
+ * @param {string} options.fileName - 文件名(默认 'review-history.json')
241
+ * @param {number} options.maxSamples - 最大样本数(默认每种 5 个)
242
+ * @returns {Promise<Object>} { positive: [], negative: [] }
243
+ */
244
+ async loadFeedbackSamples(options = {}) {
245
+ const {
246
+ packageName = 'ai-review-feedback',
247
+ packageVersion = 'latest',
248
+ fileName = 'review-history.json',
249
+ maxSamples = 5
250
+ } = options;
251
+
252
+ const result = {
253
+ positive: [], // 正样本(up > down)
254
+ negative: [], // 负样本(down > up)
255
+ loaded: false
256
+ };
257
+
258
+ if (!this.config.project.projectId) {
259
+ console.log('⚠️ 未配置项目 ID,跳过加载反馈样本');
260
+ return result;
261
+ }
262
+
263
+ console.log('\n📦 从 Package Registry 加载历史反馈样本...');
264
+
265
+ try {
266
+ // 1. 从 Package Registry 读取 review-history.json
267
+ const feedbackHistory = await this.gitlabClient.getPackageFile(
268
+ this.config.project.projectId,
269
+ packageName,
270
+ packageVersion,
271
+ fileName
272
+ );
273
+
274
+ if (!feedbackHistory || !Array.isArray(feedbackHistory) || feedbackHistory.length === 0) {
275
+ console.log('📭 没有历史反馈数据');
276
+ return result;
277
+ }
278
+
279
+ console.log(`📊 找到 ${feedbackHistory.length} 条历史反馈记录`);
280
+
281
+ // 2. 区分正负样本
282
+ const positiveFeedback = feedbackHistory.filter(f => f.up > f.down).slice(0, maxSamples * 2);
283
+ const negativeFeedback = feedbackHistory.filter(f => f.down > f.up).slice(0, maxSamples * 2);
284
+
285
+ console.log(` - 正样本候选: ${positiveFeedback.length} 条`);
286
+ console.log(` - 负样本候选: ${negativeFeedback.length} 条`);
287
+
288
+ // 3. 获取评论内容
289
+ for (const feedback of positiveFeedback) {
290
+ if (result.positive.length >= maxSamples) break;
291
+
292
+ const note = await this.gitlabClient.getMergeRequestNote(
293
+ this.config.project.projectId,
294
+ feedback.mr,
295
+ feedback.note
296
+ );
297
+
298
+ if (note && note.body) {
299
+ result.positive.push({
300
+ mrIid: feedback.mr,
301
+ noteId: feedback.note,
302
+ content: note.body,
303
+ up: feedback.up,
304
+ down: feedback.down
305
+ });
306
+ }
307
+ }
308
+
309
+ for (const feedback of negativeFeedback) {
310
+ if (result.negative.length >= maxSamples) break;
311
+
312
+ const note = await this.gitlabClient.getMergeRequestNote(
313
+ this.config.project.projectId,
314
+ feedback.mr,
315
+ feedback.note
316
+ );
317
+
318
+ if (note && note.body) {
319
+ result.negative.push({
320
+ mrIid: feedback.mr,
321
+ noteId: feedback.note,
322
+ content: note.body,
323
+ up: feedback.up,
324
+ down: feedback.down
325
+ });
326
+ }
327
+ }
328
+
329
+ result.loaded = true;
330
+ console.log(`\n✅ 成功加载样本: 正样本 ${result.positive.length} 条, 负样本 ${result.negative.length} 条`);
331
+
332
+ // 输出正样本详情
333
+ if (result.positive.length > 0) {
334
+ console.log('\n' + '='.repeat(80));
335
+ console.log('📗 正样本详情(获得好评的审查意见):');
336
+ console.log('='.repeat(80));
337
+ result.positive.forEach((sample, index) => {
338
+ console.log(`\n【正样本 ${index + 1}】MR !${sample.mrIid} | Note #${sample.noteId} | 👍${sample.up} 👎${sample.down}`);
339
+ console.log('-'.repeat(60));
340
+ // 截断过长的内容,只显示前 500 字符
341
+ const content = sample.content.length > 500
342
+ ? sample.content.substring(0, 500) + '\n... (内容过长,已截断)'
343
+ : sample.content;
344
+ console.log(content);
345
+ });
346
+ console.log('\n' + '='.repeat(80));
347
+ }
348
+
349
+ // 输出负样本详情
350
+ if (result.negative.length > 0) {
351
+ console.log('\n' + '='.repeat(80));
352
+ console.log('📕 负样本详情(获得差评的审查意见):');
353
+ console.log('='.repeat(80));
354
+ result.negative.forEach((sample, index) => {
355
+ console.log(`\n【负样本 ${index + 1}】MR !${sample.mrIid} | Note #${sample.noteId} | 👍${sample.up} 👎${sample.down}`);
356
+ console.log('-'.repeat(60));
357
+ const content = sample.content.length > 500
358
+ ? sample.content.substring(0, 500) + '\n... (内容过长,已截断)'
359
+ : sample.content;
360
+ console.log(content);
361
+ });
362
+ console.log('\n' + '='.repeat(80));
363
+ }
364
+
365
+ return result;
366
+ } catch (error) {
367
+ console.warn(`⚠️ 加载反馈样本失败: ${error.message}`);
368
+ return result;
369
+ }
370
+ }
371
+
86
372
  /**
87
373
  * 获取当前配置
88
374
  */
@@ -192,313 +478,271 @@ export class GitLabAIReview {
192
478
  }
193
479
 
194
480
  /**
195
- * AI 审查 MR 的所有有意义的变更并自动添加行级评论(按文件批量处理)
196
- * @param {Object} options - 选项
197
- * @param {number} options.maxFiles - 最大审查文件数量(默认不限制)
198
- * @returns {Promise<Array>} 评论结果数组
481
+ * 🎯 分析导出变更的影响(调用链分析)
482
+ * 1. 获取变更文件的新旧版本
483
+ * 2. 使用 TypeScript Compiler API 分析导出变更
484
+ * 3. 搜索其他文件对这些导出的引用
485
+ * @param {Array} changes - 变更列表
486
+ * @returns {Promise<Object>} 影响信息 Map
199
487
  */
200
- async reviewAndCommentOnLines(options = {}) {
201
- // 确保 Guard 配置已加载
202
- await this.initGuardConfig();
203
-
204
- const { maxFiles = Infinity } = options;
205
- const allChanges = await this.getMergeRequestChanges();
206
- const changes = this.filterReviewableFiles(allChanges);
207
- const results = [];
488
+ async analyzeExportImpact(changes) {
489
+ console.log('\n' + '='.repeat(80));
490
+ console.log('🔍 分析导出变更影响(调用链分析)...');
491
+ console.log('='.repeat(80));
208
492
 
209
- const filesToReview = maxFiles === Infinity ? changes.length : Math.min(maxFiles, changes.length);
210
- console.log(`共 ${changes.length} 个文件需要审查${maxFiles === Infinity ? '(不限制数量)' : `(最多审查 ${maxFiles} 个)`}(已过滤 ${allChanges.length - changes.length} 个文件)`);
493
+ const mrInfo = await this.getMergeRequest();
494
+ const sourceBranch = mrInfo.source_branch;
495
+ const targetBranch = mrInfo.target_branch;
496
+ const impactMap = {};
211
497
 
212
- for (const change of changes.slice(0, filesToReview)) {
498
+ console.log(`\n📌 源分支: ${sourceBranch}`);
499
+ console.log(`📌 目标分支: ${targetBranch}\n`);
500
+
501
+ // 只分析支持的文件类型
502
+ const supportedExtensions = ['.js', '.ts', '.tsx', '.jsx', '.vue'];
503
+ const analyzableChanges = changes.filter(c => {
504
+ const path = c.new_path || c.old_path;
505
+ return supportedExtensions.some(ext => path.endsWith(ext));
506
+ });
507
+
508
+ console.log(`📂 共 ${analyzableChanges.length} 个文件可分析导出\n`);
509
+
510
+ for (const change of analyzableChanges) {
213
511
  const fileName = change.new_path || change.old_path;
512
+ const isNewFile = change.new_file;
513
+ const isDeletedFile = change.deleted_file;
514
+
515
+ console.log(`\n${'─'.repeat(60)}`);
516
+ console.log(`📄 分析文件: ${fileName}`);
214
517
 
215
518
  try {
216
- console.log(`\n审查文件: ${fileName}`);
519
+ let oldExports = [];
520
+ let newExports = [];
521
+
522
+ // 获取旧版本内容(如果不是新文件)
523
+ if (!isNewFile && change.old_path) {
524
+ try {
525
+ console.log(` 📥 获取旧版本 (${targetBranch})...`);
526
+ const oldContent = await this.gitlabClient.getProjectFile(
527
+ this.config.project.projectId,
528
+ change.old_path,
529
+ targetBranch
530
+ );
531
+ if (oldContent) {
532
+ oldExports = ExportAnalyzer.extractExports(oldContent, change.old_path);
533
+ console.log(` ✓ 旧版本导出: ${oldExports.length} 个 [${oldExports.map(e => e.name).join(', ')}]`);
534
+ }
535
+ } catch (e) {
536
+ console.log(` ⚠️ 获取旧版本失败: ${e.message}`);
537
+ }
538
+ }
539
+
540
+ // 获取新版本内容(如果不是删除文件)
541
+ if (!isDeletedFile && change.new_path) {
542
+ try {
543
+ console.log(` 📥 获取新版本 (${sourceBranch})...`);
544
+ const newContent = await this.gitlabClient.getProjectFile(
545
+ this.config.project.projectId,
546
+ change.new_path,
547
+ sourceBranch
548
+ );
549
+ if (newContent) {
550
+ newExports = ExportAnalyzer.extractExports(newContent, change.new_path);
551
+ console.log(` ✓ 新版本导出: ${newExports.length} 个 [${newExports.map(e => e.name).join(', ')}]`);
552
+ }
553
+ } catch (e) {
554
+ console.log(` ⚠️ 获取新版本失败: ${e.message}`);
555
+ }
556
+ }
557
+
558
+ // 对比找出变更的导出
559
+ const changedExports = ExportAnalyzer.findChangedExports(oldExports, newExports);
217
560
 
218
- // 解析 diff,提取有意义的变更
219
- const hunks = DiffParser.parseDiff(change.diff);
220
- const meaningfulChanges = DiffParser.extractMeaningfulChanges(hunks);
561
+ const totalChanges = changedExports.added.length +
562
+ changedExports.removed.length +
563
+ changedExports.modified.length;
221
564
 
222
- if (meaningfulChanges.length === 0) {
223
- console.log(` 跳过:没有有意义的变更`);
565
+ if (totalChanges === 0) {
566
+ console.log(` ℹ️ 没有导出变更`);
224
567
  continue;
225
568
  }
226
569
 
227
- console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
570
+ console.log(`\n 📊 导出变更统计:`);
571
+ if (changedExports.added.length > 0) {
572
+ console.log(` 🆕 新增: ${changedExports.added.map(e => e.name).join(', ')}`);
573
+ }
574
+ if (changedExports.removed.length > 0) {
575
+ console.log(` 🗑️ 删除: ${changedExports.removed.map(e => e.name).join(', ')}`);
576
+ }
577
+ if (changedExports.modified.length > 0) {
578
+ console.log(` ✏️ 修改: ${changedExports.modified.map(e =>
579
+ `${e.name}(${e.changeType})`).join(', ')}`);
580
+ }
228
581
 
229
- // 调用 AI 一次性审查整个文件的所有变更(按文件批量)
230
- const fileReview = await this.reviewFileChanges(change, meaningfulChanges);
231
-
232
- // 智能合并修改块的评论
233
- const mergedReviews = this.mergeModificationReviews(fileReview.reviews, meaningfulChanges);
234
-
235
- // 根据 AI 返回的结果,只对有问题的行添加评论
236
- for (const review of mergedReviews) {
237
- if (review.hasIssue) {
582
+ // 搜索受影响的文件(删除和修改的导出需要检查引用)
583
+ const symbolsToSearch = [
584
+ ...changedExports.removed,
585
+ ...changedExports.modified,
586
+ ];
587
+
588
+ const references = [];
589
+
590
+ if (symbolsToSearch.length > 0) {
591
+ console.log(`\n 🔎 搜索符号引用...`);
592
+
593
+ for (const symbol of symbolsToSearch) {
238
594
  try {
239
- // 构建评论位置参数 - 使用实际的变更行号
240
- const positionParams = {
241
- filePath: fileName,
242
- oldPath: change.old_path,
243
- };
244
-
245
- // 如果是修改块(同时有 oldLine 和 newLine),同时指定两个行号
246
- if (review.oldLine && review.newLine) {
247
- positionParams.oldLine = review.oldLine;
248
- positionParams.newLine = review.newLine;
249
- } else if (review.lineNumber) {
250
- // 单独的删除或新增
251
- const relatedChange = meaningfulChanges.find(c => c.lineNumber === review.lineNumber);
252
- const isDeletion = relatedChange && relatedChange.type === 'deletion';
253
- if (isDeletion) {
254
- positionParams.oldLine = review.lineNumber;
255
- } else {
256
- positionParams.newLine = review.lineNumber;
257
- }
258
- }
259
-
260
- // 直接使用 AI 审查意见作为评论内容
261
- const commentBody = `🤖 **AI 代码审查**\n\n${review.comment}`;
262
-
263
- const commentResult = await this.gitlabClient.createLineComment(
595
+ console.log(` 搜索: ${symbol.name}`);
596
+ const searchResults = await this.gitlabClient.searchInProject(
264
597
  this.config.project.projectId,
265
- this.config.project.mergeRequestIid,
266
- commentBody,
267
- positionParams
598
+ symbol.name,
599
+ sourceBranch
268
600
  );
269
601
 
270
- results.push({
271
- status: 'success',
272
- fileName,
273
- lineNumber: review.lineNumber || review.oldLine,
274
- comment: review.comment,
275
- commentResult,
276
- });
602
+ // 过滤搜索结果
603
+ for (const result of searchResults) {
604
+ // 跳过当前文件
605
+ if (result.path === fileName) continue;
606
+
607
+ const data = result.data || '';
608
+
609
+ // 检查是否真的 import 了这个符号
610
+ const hasImport = data.includes(`import`) && data.includes(symbol.name);
611
+ const hasUsage = new RegExp(`\\b${symbol.name}\\b`).test(data);
612
+
613
+ if (hasImport || hasUsage) {
614
+ // 获取引用文件的完整内容以提取上下文
615
+ let usageContext = data;
616
+ try {
617
+ const refFileContent = await this.gitlabClient.getProjectFile(
618
+ this.config.project.projectId,
619
+ result.path,
620
+ sourceBranch
621
+ );
622
+ if (refFileContent) {
623
+ usageContext = ExportAnalyzer.extractUsageContext(
624
+ refFileContent,
625
+ symbol.name,
626
+ 2 // 上下文行数
627
+ );
628
+ }
629
+ } catch (e) {
630
+ // 使用搜索结果的数据
631
+ }
277
632
 
278
- const lineDesc = review.oldLine && review.newLine
279
- ? `第 ${review.oldLine}-${review.newLine} 行(修改块)`
280
- : `第 ${review.lineNumber} 行`;
281
- console.log(` ✓ ${lineDesc}:已添加评论`);
282
- } catch (error) {
283
- results.push({
284
- status: 'error',
285
- fileName,
286
- lineNumber: review.lineNumber || review.oldLine,
287
- error: error.message,
288
- });
289
- const lineDesc = review.oldLine && review.newLine
290
- ? `第 ${review.oldLine}-${review.newLine} 行`
291
- : `第 ${review.lineNumber} 行`;
292
- console.log(` ✗ ${lineDesc}:评论失败 - ${error.message}`);
633
+ // 检查是否已添加这个文件
634
+ const existing = references.find(r => r.file === result.path);
635
+ if (existing) {
636
+ if (!existing.symbols.includes(symbol.name)) {
637
+ existing.symbols.push(symbol.name);
638
+ }
639
+ } else {
640
+ references.push({
641
+ file: result.path,
642
+ symbols: [symbol.name],
643
+ usageContext,
644
+ startLine: result.startline || 1,
645
+ });
646
+ }
647
+ }
648
+ }
649
+ } catch (e) {
650
+ console.log(` ⚠️ 搜索失败: ${e.message}`);
293
651
  }
294
- } else {
295
- const lineDesc = review.oldLine && review.newLine
296
- ? `第 ${review.oldLine}-${review.newLine} 行`
297
- : `第 ${review.lineNumber} 行`;
298
- console.log(` ✓ ${lineDesc}:代码质量良好`);
299
652
  }
300
653
  }
301
654
 
302
- // 如果没有问题
303
- if (fileReview.reviews.length === 0 || fileReview.reviews.every(r => !r.hasIssue)) {
304
- console.log(` ✓ 所有代码质量良好,无需评论`);
655
+ if (references.length > 0) {
656
+ console.log(`\n ✅ 发现 ${references.length} 个文件引用了变更的导出:`);
657
+ references.forEach(ref => {
658
+ console.log(` - ${ref.file} (使用: ${ref.symbols.join(', ')})`);
659
+ });
305
660
  }
306
661
 
662
+ // 存储影响信息
663
+ impactMap[fileName] = {
664
+ changedExports: {
665
+ added: changedExports.added.map(e => e.name),
666
+ removed: changedExports.removed.map(e => e.name),
667
+ modified: changedExports.modified.map(e => ({
668
+ name: e.name,
669
+ changeType: e.changeType,
670
+ oldSignature: e.oldSignature,
671
+ newSignature: e.newSignature,
672
+ })),
673
+ },
674
+ references,
675
+ };
676
+
307
677
  } catch (error) {
308
- results.push({
309
- status: 'error',
310
- fileName,
311
- error: error.message,
312
- });
313
- console.log(` ✗ 文件审查失败: ${error.message}`);
678
+ console.log(` ❌ 分析失败: ${error.message}`);
314
679
  }
315
680
  }
316
681
 
317
- return results;
318
- }
319
-
320
- /**
321
- * 智能合并修改块的评论
322
- * 如果相邻的删除和新增是同一个修改,合并为一个评论
323
- * @param {Array} reviews - AI 返回的审查结果
324
- * @param {Array} meaningfulChanges - 有意义的变更列表
325
- * @returns {Array} 合并后的审查结果
326
- */
327
- mergeModificationReviews(reviews, meaningfulChanges) {
328
- if (!reviews || reviews.length === 0) return reviews;
329
-
330
- const merged = [];
331
- const processed = new Set();
332
-
333
- reviews.forEach((review, index) => {
334
- if (processed.has(index)) return;
335
-
336
- const currentChange = meaningfulChanges.find(c => c.lineNumber === review.lineNumber);
337
- if (!currentChange) {
338
- merged.push(review);
339
- return;
340
- }
341
-
342
- // 如果是删除,查找是否有对应的新增(可能是修改)
343
- if (currentChange.type === 'deletion') {
344
- // 查找相近行号的新增
345
- const nextReviewIndex = reviews.findIndex((r, i) => {
346
- if (i <= index || processed.has(i)) return false;
347
- const nextChange = meaningfulChanges.find(c => c.lineNumber === r.lineNumber);
348
- // 相差不超过 5 行,且是新增类型
349
- return nextChange &&
350
- nextChange.type === 'addition' &&
351
- Math.abs(nextChange.lineNumber - currentChange.lineNumber) <= 5;
352
- });
353
-
354
- if (nextReviewIndex !== -1) {
355
- const nextReview = reviews[nextReviewIndex];
356
- const nextChange = meaningfulChanges.find(c => c.lineNumber === nextReview.lineNumber);
357
-
358
- // 合并为一个修改块的评论
359
- merged.push({
360
- hasIssue: review.hasIssue || nextReview.hasIssue,
361
- oldLine: currentChange.lineNumber, // 删除行
362
- newLine: nextChange.lineNumber, // 新增行
363
- comment: review.hasIssue ? review.comment : nextReview.comment, // 使用有问题的那个评论
364
- });
365
-
366
- processed.add(index);
367
- processed.add(nextReviewIndex);
368
- return;
369
- }
370
- }
371
-
372
- // 如果不是修改块,保持原样
373
- merged.push(review);
374
- processed.add(index);
375
- });
376
-
377
- return merged;
378
- }
379
-
380
- /**
381
- * 审查单个文件的所有变更(一次 API 调用)
382
- * @param {Object} change - 代码变更对象
383
- * @param {Array} meaningfulChanges - 有意义的变更数组
384
- * @returns {Promise<Object>} 审查结果 { reviews: [{lineNumber, hasIssue, comment}] }
385
- */
386
- async reviewFileChanges(change, meaningfulChanges) {
387
- const aiClient = this.getAIClient();
388
- const projectPrompt = this.config.ai?.guardConfig?.content || '';
389
- const fileName = change.new_path || change.old_path;
390
-
391
- // 构建整个文件的批量审查消息
392
- const messages = PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
393
-
394
- // 🔍 输出完整的 Prompt(用于调试)
682
+ // 打印总结
395
683
  console.log('\n' + '='.repeat(80));
396
- console.log('🔍 发送给 AI 的完整 Prompt:');
397
- console.log('='.repeat(80));
398
- messages.forEach((msg, index) => {
399
- console.log(`\n📝 消息 ${index + 1} [${msg.role}]:`);
400
- console.log('-'.repeat(80));
401
- console.log(msg.content);
402
- });
403
- console.log('\n' + '='.repeat(80));
404
- console.log('⏳ 等待 AI 响应...\n');
405
-
406
- // 调用 AI(一次调用审查整个文件)
407
- const response = await aiClient.sendMessage(messages);
408
-
409
- // 🔍 输出 AI 的原始返回(用于调试)
410
- console.log('\n' + '='.repeat(80));
411
- console.log('📥 AI 原始返回:');
412
- console.log('='.repeat(80));
413
- console.log(response.content);
684
+ const filesWithChanges = Object.keys(impactMap).length;
685
+ const totalRefs = Object.values(impactMap).reduce(
686
+ (sum, info) => sum + info.references.length, 0
687
+ );
688
+ console.log(`✅ 调用链分析完成:`);
689
+ console.log(` - ${filesWithChanges} 个文件有导出变更`);
690
+ console.log(` - 共发现 ${totalRefs} 个文件受影响`);
414
691
  console.log('='.repeat(80) + '\n');
415
692
 
416
- // 解析 AI 返回的 JSON
417
- try {
418
- // 提取 JSON(可能被包裹在 ```json ``` 中)
419
- let jsonStr = response.content.trim();
420
- const jsonMatch = jsonStr.match(/```json\s*([\s\S]*?)\s*```/);
421
- if (jsonMatch) {
422
- jsonStr = jsonMatch[1];
423
- } else if (jsonStr.startsWith('```') && jsonStr.endsWith('```')) {
424
- jsonStr = jsonStr.slice(3, -3).trim();
425
- }
426
-
427
- // 清理非 JSON 标准的值
428
- jsonStr = jsonStr
429
- .replace(/:\s*undefined/g, ': null') // undefined → null
430
- .replace(/:\s*NaN/g, ': null') // NaN → null
431
- .replace(/:\s*Infinity/g, ': null'); // Infinity → null
432
-
433
- const result = JSON.parse(jsonStr);
434
-
435
- // 验证并过滤无效的 reviews
436
- if (result.reviews && Array.isArray(result.reviews)) {
437
- result.reviews = result.reviews.filter(review => {
438
- // 过滤掉 lineNumber 无效的项
439
- if (typeof review.lineNumber !== 'number' || isNaN(review.lineNumber)) {
440
- console.warn(` ⚠️ 跳过无效的审查结果:lineNumber = ${review.lineNumber}`);
441
- return false;
442
- }
443
- return true;
444
- });
445
- }
446
-
447
- return result;
448
- } catch (error) {
449
- console.error('解析 AI 返回的 JSON 失败:', error.message);
450
- console.error('AI 原始返回:', response.content);
451
- return { reviews: [] };
693
+ // 调试:输出完整的 impactMap
694
+ if (Object.keys(impactMap).length > 0) {
695
+ console.log('📋 影响分析详情:');
696
+ console.log(JSON.stringify(impactMap, null, 2));
697
+ console.log();
452
698
  }
699
+
700
+ return impactMap;
453
701
  }
454
702
 
455
703
  /**
456
- * AI 审查 MR 的所有变更(包含影响分析)
704
+ * AI 审查 MR 的所有有意义的变更并自动添加行级评论(按文件批量处理)
457
705
  * @param {Object} options - 选项
458
706
  * @param {number} options.maxFiles - 最大审查文件数量(默认不限制)
459
- * @param {number} options.maxAffectedFiles - 每个文件最多分析的受影响文件数量(默认 10
460
- * @param {boolean} options.enableImpactAnalysis - 是否启用影响分析(默认 true)
461
- * @param {boolean} options.useCallChainAnalysis - 是否使用 TypeScript 调用链分析(默认 true)
707
+ * @param {boolean} options.loadFeedback - 是否加载历史反馈样本(默认 true
462
708
  * @returns {Promise<Array>} 评论结果数组
463
709
  */
464
- async reviewWithImpactAnalysis(options = {}) {
710
+ async reviewAndCommentOnLines(options = {}) {
465
711
  // 确保 Guard 配置已加载
466
712
  await this.initGuardConfig();
467
713
 
468
- const {
469
- maxFiles = Infinity,
470
- maxAffectedFiles = 10,
471
- enableImpactAnalysis = true,
472
- useCallChainAnalysis = true
473
- } = options;
474
-
714
+ const { maxFiles = Infinity, loadFeedback = true } = options;
475
715
  const allChanges = await this.getMergeRequestChanges();
476
716
  const changes = this.filterReviewableFiles(allChanges);
477
- const mrInfo = await this.getMergeRequest();
478
- const ref = mrInfo.target_branch || 'main';
479
717
  const results = [];
480
718
 
481
719
  const filesToReview = maxFiles === Infinity ? changes.length : Math.min(maxFiles, changes.length);
482
720
  console.log(`共 ${changes.length} 个文件需要审查${maxFiles === Infinity ? '(不限制数量)' : `(最多审查 ${maxFiles} 个)`}(已过滤 ${allChanges.length - changes.length} 个文件)`);
483
- console.log(`影响分析: ${enableImpactAnalysis ? '已启用' : '已禁用'}`);
484
- console.log(`调用链分析: ${useCallChainAnalysis ? '已启用(TypeScript)' : '已禁用(使用 GitLab Search)'}`);
485
-
486
- // 🎯 新增:如果启用了调用链分析,先对所有变更进行一次性 TypeScript 分析
487
- let callChainResult = null;
488
- if (enableImpactAnalysis && useCallChainAnalysis) {
489
- console.log('\n🔗 正在进行增量调用链分析(TypeScript Compiler API)...');
490
- callChainResult = await ImpactAnalyzer.analyzeWithCallChain(changes.slice(0, filesToReview));
491
-
492
- if (callChainResult) {
493
- console.log(`✅ 调用链分析完成:`);
494
- console.log(` - 分析文件数: ${callChainResult.filesAnalyzed}`);
495
- console.log(` - 构建调用链: ${callChainResult.callChains?.length || 0} 个`);
496
- console.log(` - 发现问题: ${callChainResult.codeContext?.totalIssues || 0} 个`);
497
- } else {
498
- console.log('ℹ️ 调用链分析不可用,将降级使用 GitLab Search API');
721
+
722
+ // 🎯 0 阶段:加载历史反馈样本(正负样本学习)
723
+ let feedbackSamples = null;
724
+ if (loadFeedback) {
725
+ try {
726
+ feedbackSamples = await this.loadFeedbackSamples();
727
+ } catch (error) {
728
+ console.warn(`⚠️ 加载反馈样本失败: ${error.message}`);
499
729
  }
500
730
  }
501
731
 
732
+ // 🎯 第一阶段:分析导出变更影响(调用链分析)
733
+ let impactMap = {};
734
+ try {
735
+ impactMap = await this.analyzeExportImpact(changes.slice(0, filesToReview));
736
+ } catch (error) {
737
+ console.error('❌ 调用链分析失败:', error.message);
738
+ console.log('⚠️ 将继续进行审查,但不包含调用链信息\n');
739
+ }
740
+
741
+ // 🎯 第二阶段:逐文件审查
742
+ console.log('\n' + '='.repeat(80));
743
+ console.log('📝 开始逐文件代码审查');
744
+ console.log('='.repeat(80));
745
+
502
746
  for (const change of changes.slice(0, filesToReview)) {
503
747
  const fileName = change.new_path || change.old_path;
504
748
 
@@ -514,56 +758,46 @@ export class GitLabAIReview {
514
758
  continue;
515
759
  }
516
760
 
517
- console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
518
-
519
- // 影响分析(传入调用链结果)
520
- let impactAnalysis = null;
521
- if (enableImpactAnalysis) {
522
- impactAnalysis = await ImpactAnalyzer.analyzeImpact({
523
- gitlabClient: this.gitlabClient,
524
- projectId: this.config.project.projectId,
525
- ref: ref,
526
- change: change,
527
- maxAffectedFiles: maxAffectedFiles,
528
- callChainResult: callChainResult, // 🎯 传入调用链结果
529
- });
761
+ console.log(` 发现 ${meaningfulChanges.length} 个变更块`);
762
+
763
+ // 获取该文件的调用链影响信息
764
+ const impactInfo = impactMap[fileName] || null;
765
+ if (impactInfo && impactInfo.references.length > 0) {
766
+ console.log(` ✓ 已获取调用链信息: ${impactInfo.references.length} 个文件引用了变更的导出`);
530
767
  }
531
768
 
532
- // 调用 AI 审查(包含影响分析)
533
- const fileReview = await this.reviewFileChangesWithImpact(
534
- change,
535
- meaningfulChanges,
536
- impactAnalysis
537
- );
538
-
539
- // 智能合并修改块的评论
540
- const mergedReviews = this.mergeModificationReviews(fileReview.reviews, meaningfulChanges);
769
+ // 调用 AI 一次性审查整个文件的所有变更(携带调用链信息和历史反馈样本)
770
+ const fileReview = await this.reviewFileChanges(change, meaningfulChanges, impactInfo, feedbackSamples);
541
771
 
542
- // 根据 AI 返回的结果,只对有问题的行添加评论
543
- for (const review of mergedReviews) {
772
+ // 根据 AI 返回的结果,直接使用 AI 提供的行号添加评论
773
+ for (const review of (fileReview.reviews || [])) {
544
774
  if (review.hasIssue) {
545
775
  try {
546
- // 构建评论位置参数 - 使用实际的变更行号
776
+ // 验证 AI 返回的行号
777
+ if (typeof review.lineNumber !== 'number' || review.lineNumber <= 0) {
778
+ console.warn(` ⚠️ 无效的行号: ${review.lineNumber}`);
779
+ continue;
780
+ }
781
+
782
+ // 构建评论位置参数 - 直接使用 AI 返回的行号
547
783
  const positionParams = {
548
784
  filePath: fileName,
549
785
  oldPath: change.old_path,
550
786
  };
551
787
 
552
- // 如果是修改块(同时有 oldLine newLine),同时指定两个行号
553
- if (review.oldLine && review.newLine) {
554
- positionParams.oldLine = review.oldLine;
555
- positionParams.newLine = review.newLine;
556
- } else if (review.lineNumber) {
557
- // 单独的删除或新增
558
- const relatedChange = meaningfulChanges.find(c => c.lineNumber === review.lineNumber);
559
- const isDeletion = relatedChange && relatedChange.type === 'deletion';
560
- if (isDeletion) {
561
- positionParams.oldLine = review.lineNumber;
562
- } else {
563
- positionParams.newLine = review.lineNumber;
564
- }
788
+ // 根据 AI 返回的 isOldLine 决定使用哪种行号
789
+ if (review.isOldLine === true) {
790
+ // 删除类型:使用旧文件行号
791
+ positionParams.oldLine = review.lineNumber;
792
+ } else {
793
+ // 新增或修改类型:使用新文件行号
794
+ positionParams.newLine = review.lineNumber;
565
795
  }
566
796
 
797
+ // 打印行号信息用于调试
798
+ console.log(` 📍 AI 返回行号: ${review.lineNumber}, isOldLine: ${review.isOldLine || false}`);
799
+ console.log(` 使用参数: oldLine=${positionParams.oldLine || 'N/A'}, newLine=${positionParams.newLine || 'N/A'}`);
800
+
567
801
  // 直接使用 AI 审查意见作为评论内容
568
802
  const commentBody = `🤖 **AI 代码审查**\n\n${review.comment}`;
569
803
 
@@ -577,15 +811,13 @@ export class GitLabAIReview {
577
811
  results.push({
578
812
  status: 'success',
579
813
  fileName,
580
- lineNumber: review.lineNumber || review.newLine || review.oldLine,
814
+ lineNumber: review.lineNumber,
815
+ isOldLine: review.isOldLine || false,
581
816
  comment: review.comment,
582
817
  commentResult,
583
818
  });
584
819
 
585
- const lineDesc = review.oldLine && review.newLine
586
- ? `第 ${review.oldLine}-${review.newLine} 行(修改块)`
587
- : `第 ${review.lineNumber} 行`;
588
- console.log(` ✓ ${lineDesc}:已添加评论`);
820
+ console.log(` ✓ ${review.lineNumber} 行:已添加评论`);
589
821
  } catch (error) {
590
822
  results.push({
591
823
  status: 'error',
@@ -615,29 +847,96 @@ export class GitLabAIReview {
615
847
  }
616
848
  }
617
849
 
850
+ // 🎯 生成并发布 MR 总结评论
851
+ if (options.enableSummary !== false) {
852
+ try {
853
+ await this.generateAndPostSummary(results, changes.slice(0, filesToReview));
854
+ } catch (error) {
855
+ console.error('❌ 生成 MR 总结失败:', error.message);
856
+ }
857
+ }
858
+
618
859
  return results;
619
860
  }
620
861
 
621
862
  /**
622
- * 审查单个文件的所有变更(包含影响分析)
863
+ * 处理 AI 返回的审查结果
864
+ * 验证 lineNumber 是否有效
865
+ * @param {Array} reviews - AI 返回的审查结果
866
+ * @param {Array} meaningfulChanges - 有意义的变更列表(用于参考)
867
+ * @returns {Array} 处理后的审查结果
868
+ */
869
+ mergeModificationReviews(reviews, meaningfulChanges) {
870
+ if (!reviews || reviews.length === 0) return reviews;
871
+
872
+ // 验证 lineNumber 是否有效
873
+ return reviews.filter(review => {
874
+ if (typeof review.lineNumber !== 'number' || review.lineNumber <= 0) {
875
+ console.warn(` ⚠️ 跳过无效的审查结果:lineNumber = ${review.lineNumber}`);
876
+ return false;
877
+ }
878
+ return true;
879
+ });
880
+ }
881
+
882
+ /**
883
+ * 审查单个文件的所有变更(一次 API 调用)
623
884
  * @param {Object} change - 代码变更对象
624
885
  * @param {Array} meaningfulChanges - 有意义的变更数组
625
- * @param {Object} impactAnalysis - 影响分析结果
886
+ * @param {Object} impactInfo - 调用链影响信息(可选)
887
+ * @param {Object} feedbackSamples - 历史反馈样本(可选)
626
888
  * @returns {Promise<Object>} 审查结果 { reviews: [{lineNumber, hasIssue, comment}] }
627
889
  */
628
- async reviewFileChangesWithImpact(change, meaningfulChanges, impactAnalysis) {
890
+ async reviewFileChanges(change, meaningfulChanges, impactInfo = null, feedbackSamples = null) {
629
891
  const aiClient = this.getAIClient();
630
892
  const projectPrompt = this.config.ai?.guardConfig?.content || '';
631
893
  const fileName = change.new_path || change.old_path;
632
894
 
633
- // 构建包含影响分析的批量审查消息
634
- const messages = impactAnalysis
635
- ? PromptTools.buildFileReviewWithImpactMessages(fileName, meaningfulChanges, impactAnalysis, projectPrompt)
636
- : PromptTools.buildFileReviewMessages(fileName, meaningfulChanges, projectPrompt);
895
+ // 🎯 获取完整文件内容
896
+ let fullFileContent = null;
897
+ try {
898
+ const mrInfo = await this.getMergeRequest();
899
+ const ref = mrInfo.target_branch || 'main';
900
+
901
+ // 优先获取新文件内容,如果不存在则获取旧文件
902
+ if (change.new_path && !change.deleted_file) {
903
+ fullFileContent = await this.gitlabClient.getProjectFile(
904
+ this.config.project.projectId,
905
+ change.new_path,
906
+ ref
907
+ );
908
+ } else if (change.old_path) {
909
+ fullFileContent = await this.gitlabClient.getProjectFile(
910
+ this.config.project.projectId,
911
+ change.old_path,
912
+ ref
913
+ );
914
+ }
915
+
916
+ if (fullFileContent) {
917
+ console.log(` ✓ 已获取完整文件内容(${fullFileContent.split('\n').length} 行)`);
918
+ }
919
+ } catch (error) {
920
+ console.warn(` ⚠️ 获取完整文件内容失败: ${error.message}`);
921
+ }
922
+
923
+ // 📚 获取项目文档上下文
924
+ const documentsContext = this.getDocumentsContext();
925
+
926
+ // 构建整个文件的批量审查消息(包含完整文件内容、调用链信息、历史反馈样本和文档上下文)
927
+ const messages = PromptTools.buildFileReviewMessages(
928
+ fileName,
929
+ meaningfulChanges,
930
+ projectPrompt,
931
+ fullFileContent,
932
+ impactInfo,
933
+ feedbackSamples,
934
+ documentsContext // 新增文档上下文参数
935
+ );
637
936
 
638
937
  // 🔍 输出完整的 Prompt(用于调试)
639
938
  console.log('\n' + '='.repeat(80));
640
- console.log('🔍 发送给 AI 的完整 Prompt (包含影响分析):');
939
+ console.log('🔍 发送给 AI 的完整 Prompt:');
641
940
  console.log('='.repeat(80));
642
941
  messages.forEach((msg, index) => {
643
942
  console.log(`\n📝 消息 ${index + 1} [${msg.role}]:`);
@@ -645,42 +944,76 @@ export class GitLabAIReview {
645
944
  console.log(msg.content);
646
945
  });
647
946
  console.log('\n' + '='.repeat(80));
648
- console.log('⏳ 等待 AI 响应...\n');
947
+ console.log('⏳ AI 流式响应中...\n');
649
948
 
650
- // 调用 AI(一次调用审查整个文件)
651
- const response = await aiClient.sendMessage(messages);
652
-
653
- // 🔍 输出 AI 的原始返回(用于调试)
654
- console.log('\n' + '='.repeat(80));
655
- console.log('📥 AI 原始返回 (包含影响分析):');
656
- console.log('='.repeat(80));
657
- console.log(response.content);
949
+ // 🎯 使用流式调用 AI(实时输出响应)
950
+ console.log('📥 AI 实时响应:');
951
+ console.log('-'.repeat(80));
952
+
953
+ const response = await aiClient.sendMessageStream(messages, (chunk) => {
954
+ // 实时打印 AI 的响应内容
955
+ if (chunk.content) {
956
+ process.stdout.write(chunk.content);
957
+ }
958
+ });
959
+
960
+ console.log('\n' + '-'.repeat(80));
961
+ console.log('✅ AI 响应完成');
658
962
  console.log('='.repeat(80) + '\n');
659
963
 
660
964
  // 解析 AI 返回的 JSON
661
965
  try {
662
966
  // 提取 JSON(可能被包裹在 ```json ``` 中)
663
967
  let jsonStr = response.content.trim();
968
+
969
+ // 🔍 增强 JSON 提取逻辑
664
970
  const jsonMatch = jsonStr.match(/```json\s*([\s\S]*?)\s*```/);
665
971
  if (jsonMatch) {
666
972
  jsonStr = jsonMatch[1];
667
973
  } else if (jsonStr.startsWith('```') && jsonStr.endsWith('```')) {
668
974
  jsonStr = jsonStr.slice(3, -3).trim();
975
+ } else {
976
+ const bracketMatch = jsonStr.match(/\{[\s\S]*\}/);
977
+ if (bracketMatch) {
978
+ jsonStr = bracketMatch[0];
979
+ }
669
980
  }
670
981
 
671
- // 清理非 JSON 标准的值
982
+ // 🧹 清理非 JSON 标准的值和尾部垃圾
672
983
  jsonStr = jsonStr
673
- .replace(/:\s*undefined/g, ': null') // undefined → null
674
- .replace(/:\s*NaN/g, ': null') // NaN → null
675
- .replace(/:\s*Infinity/g, ': null'); // Infinity → null
984
+ .replace(/:\s*undefined/g, ': null')
985
+ .replace(/:\s*NaN/g, ': null')
986
+ .replace(/:\s*Infinity/g, ': null')
987
+ .replace(/,(\s*[}\]])/g, '$1')
988
+ .trim();
989
+
990
+ // 🔍 检查 JSON 是否完整(基本的括号匹配)
991
+ const openBraces = (jsonStr.match(/\{/g) || []).length;
992
+ const closeBraces = (jsonStr.match(/\}/g) || []).length;
993
+ const openBrackets = (jsonStr.match(/\[/g) || []).length;
994
+ const closeBrackets = (jsonStr.match(/\]/g) || []).length;
995
+
996
+ if (openBraces !== closeBraces || openBrackets !== closeBrackets) {
997
+ console.warn('⚠️ 检测到不完整的 JSON(括号不匹配)');
998
+ console.warn(` - 花括号: 开 ${openBraces}, 闭 ${closeBraces}`);
999
+ console.warn(` - 方括号: 开 ${openBrackets}, 闭 ${closeBrackets}`);
1000
+
1001
+ if (openBraces > closeBraces) {
1002
+ jsonStr += '}'.repeat(openBraces - closeBraces);
1003
+ console.warn(` - 已自动添加 ${openBraces - closeBraces} 个闭花括号`);
1004
+ }
1005
+ if (openBrackets > closeBrackets) {
1006
+ jsonStr += ']'.repeat(openBrackets - closeBrackets);
1007
+ console.warn(` - 已自动添加 ${openBrackets - closeBrackets} 个闭方括号`);
1008
+ }
1009
+ }
676
1010
 
677
1011
  const result = JSON.parse(jsonStr);
678
1012
 
679
- // 验证并过滤无效的 reviews
1013
+ // 验证并过滤无效的 reviews(现在使用 lineNumber 而不是 blockIndex)
680
1014
  if (result.reviews && Array.isArray(result.reviews)) {
681
1015
  result.reviews = result.reviews.filter(review => {
682
- // 过滤掉 lineNumber 无效的项
683
- if (typeof review.lineNumber !== 'number' || isNaN(review.lineNumber)) {
1016
+ if (typeof review.lineNumber !== 'number' || isNaN(review.lineNumber) || review.lineNumber <= 0) {
684
1017
  console.warn(` ⚠️ 跳过无效的审查结果:lineNumber = ${review.lineNumber}`);
685
1018
  return false;
686
1019
  }
@@ -690,12 +1023,85 @@ export class GitLabAIReview {
690
1023
 
691
1024
  return result;
692
1025
  } catch (error) {
693
- console.error('解析 AI 返回的 JSON 失败:', error.message);
694
- console.error('AI 原始返回:', response.content);
1026
+ console.error('解析 AI 返回的 JSON 失败:', error.message);
1027
+ const position = parseInt(error.message.match(/position (\d+)/)?.[1] || '0');
1028
+ if (position > 0) {
1029
+ const start = Math.max(0, position - 50);
1030
+ const end = Math.min(response.content.length, position + 50);
1031
+ console.error('\n出错位置附近的内容:');
1032
+ console.error(response.content.substring(start, end));
1033
+ console.error(' '.repeat(Math.min(50, position - start)) + '^^^');
1034
+ }
1035
+ console.error('\nAI 原始返回(前 2000 字符):', response.content.substring(0, 2000));
695
1036
  return { reviews: [] };
696
1037
  }
697
1038
  }
698
1039
 
1040
+ /**
1041
+ * 生成并发布 MR 总结评论
1042
+ * @param {Array} results - 审查结果数组
1043
+ * @param {Array} changes - 代码变更数组
1044
+ */
1045
+ async generateAndPostSummary(results, changes) {
1046
+ console.log('\n' + '='.repeat(80));
1047
+ console.log('📝 正在生成 MR 总结评论...');
1048
+ console.log('='.repeat(80));
1049
+
1050
+ const aiClient = this.getAIClient();
1051
+
1052
+ // 统计信息
1053
+ const stats = {
1054
+ totalFiles: changes.length,
1055
+ reviewedFiles: results.filter(r => r.status === 'success' || r.status === 'error').length,
1056
+ successComments: results.filter(r => r.status === 'success').length,
1057
+ failedComments: results.filter(r => r.status === 'error').length,
1058
+ filesList: changes.map(c => c.new_path || c.old_path),
1059
+ };
1060
+
1061
+ // 按文件分组评论
1062
+ const commentsByFile = {};
1063
+ results.forEach(result => {
1064
+ if (result.status === 'success' && result.fileName) {
1065
+ if (!commentsByFile[result.fileName]) {
1066
+ commentsByFile[result.fileName] = [];
1067
+ }
1068
+ commentsByFile[result.fileName].push({
1069
+ lineNumber: result.lineNumber,
1070
+ isOldLine: result.isOldLine,
1071
+ comment: result.comment,
1072
+ });
1073
+ }
1074
+ });
1075
+
1076
+ // 构建总结 prompt
1077
+ const summaryPrompt = PromptTools.buildMRSummaryPrompt(stats, commentsByFile);
1078
+ const messages = [
1079
+ { role: 'system', content: '你是一个专业的代码审查助手,负责总结 Merge Request 的审查结果。' },
1080
+ { role: 'user', content: summaryPrompt },
1081
+ ];
1082
+
1083
+ // 🎯 使用流式调用 AI 生成总结
1084
+ console.log('📥 AI 生成总结中...');
1085
+ console.log('-'.repeat(80));
1086
+
1087
+ const response = await aiClient.sendMessageStream(messages, (chunk) => {
1088
+ // 实时打印 AI 的响应内容
1089
+ if (chunk.content) {
1090
+ process.stdout.write(chunk.content);
1091
+ }
1092
+ });
1093
+
1094
+ console.log('\n' + '-'.repeat(80));
1095
+ const summary = response.content.trim();
1096
+
1097
+ // 发布总结评论
1098
+ console.log('\n📤 发布 MR 总结评论...\n');
1099
+ await this.addComment(summary);
1100
+
1101
+ console.log('✅ MR 总结评论已发布');
1102
+ console.log('='.repeat(80) + '\n');
1103
+ }
1104
+
699
1105
  /**
700
1106
  * 测试方法
701
1107
  */
@@ -708,7 +1114,7 @@ export class GitLabAIReview {
708
1114
  export { getConfig, validateConfig, loadGuardConfig } from './lib/config.js';
709
1115
  export { GitLabClient } from './lib/gitlab-client.js';
710
1116
  export { AIClient } from './lib/ai-client.js';
711
- export { PromptTools, DiffParser, ImpactAnalyzer };
1117
+ export { PromptTools, DiffParser };
712
1118
 
713
1119
  // 默认导出
714
1120
  export default GitLabAIReview;