job51-gitlab-cr-node-jt-1 3.1.2 → 3.1.3

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.
Files changed (4) hide show
  1. package/README.md +19 -0
  2. package/index.js +202 -3
  3. package/package.json +1 -1
  4. package/utils.js +24 -0
package/README.md CHANGED
@@ -46,10 +46,29 @@ gitlab-cr
46
46
  - 使用 Claude AI 进行代码审查
47
47
  - 生成结构化的审查报告
48
48
  - 将审查结果发布到 GitLab MR
49
+ - **分阶段审查**:快速扫描过滤低价值变更,减少 token 消耗
49
50
  - 自动跳过测试文件(如 `*Test.java`、`*.test.js` 等)
50
51
  - 自动跳过 DTO/VO/Query 文件(如 `*Dto.java`、`*VO.java`、`*Query.java`、`*Request.java` 等)
51
52
  - 自动跳过非代码文件(如配置文件、文档文件、资源文件等)
52
53
 
54
+ ## Token 优化
55
+
56
+ ### 快速扫描机制
57
+
58
+ 在调用 AI 完整审查前,会先进行本地快速扫描,过滤以下低价值变更:
59
+
60
+ | 跳过类型 | 示例 |
61
+ |---------|------|
62
+ | 纯 import 变更 | `import java.util.List;` |
63
+ | 纯注释变更 | `// 注释内容`、`/* 多行注释 */` |
64
+ | 纯空白/格式变更 | 空行、缩进调整 |
65
+ | 纯注解变更 | `@Override`、`@Autowired` |
66
+ | 纯 getter/setter | `getXxx()`、`setXxx()` |
67
+ | 纯日志打印 | `log.info()`、`console.log()` |
68
+ | 纯常量定义 | `private static final String XXX = "xxx";` |
69
+
70
+ **预计效果**:减少 30-50% 的 AI 调用,显著降低 token 消耗。
71
+
53
72
  ## 依赖要求
54
73
 
55
74
  - Node.js 10+
package/index.js CHANGED
@@ -312,6 +312,181 @@ class GitLabCodeReviewer {
312
312
  return false;
313
313
  }
314
314
 
315
+ /**
316
+ * 快速扫描判断 diff 内容是否值得审查(本地分析,不调用 AI)
317
+ * 用于过滤低价值变更,减少 token 消耗
318
+ * @param {string} diffContent diff 内容
319
+ * @param {string} filePath 文件路径(用于日志)
320
+ * @returns {{ worthReviewing: boolean, reason: string }} 是否值得审查及原因
321
+ */
322
+ quickScanDiffContent(diffContent, filePath = '') {
323
+ if (!diffContent || diffContent.trim() === '') {
324
+ return { worthReviewing: false, reason: 'diff 内容为空' };
325
+ }
326
+
327
+ // 解析 diff 行
328
+ const lines = diffContent.split('\n');
329
+ // 只关注新增行(+ 开头)和删除行(- 开头)
330
+ const changedLines = lines.filter(line =>
331
+ line.startsWith('+') || line.startsWith('-')
332
+ );
333
+
334
+ // 如果没有实际变更行,跳过
335
+ if (changedLines.length === 0) {
336
+ return { worthReviewing: false, reason: '没有实际变更行' };
337
+ }
338
+
339
+ // 统计各类变更
340
+ const stats = {
341
+ importChanges: 0, // import 语句变更
342
+ commentChanges: 0, // 注释变更
343
+ whitespaceChanges: 0, // 空白/格式变更
344
+ getterSetterChanges: 0, // getter/setter 变更
345
+ logChanges: 0, // 日志打印变更
346
+ constantChanges: 0, // 常量定义变更
347
+ annotationChanges: 0, // 注解变更(如 @Override, @Autowired)
348
+ meaningfulChanges: 0, // 有意义的代码变更
349
+ };
350
+
351
+ // 分析每行变更
352
+ for (const line of changedLines) {
353
+ // 跳过 diff 头信息行(如 +++ b/file.txt)
354
+ if (line.startsWith('+++') || line.startsWith('---')) {
355
+ continue;
356
+ }
357
+
358
+ // 获取实际内容(去掉 +/- 前缀)
359
+ const content = line.substring(1).trim();
360
+
361
+ // 空行或纯空白
362
+ if (content === '' || /^\s*$/.test(content)) {
363
+ stats.whitespaceChanges++;
364
+ continue;
365
+ }
366
+
367
+ // import 语句(Java/JavaScript/Python 等)
368
+ if (/^import\s/.test(content) ||
369
+ /^from\s+['"]/.test(content) ||
370
+ /^require\s*\(/.test(content)) {
371
+ stats.importChanges++;
372
+ continue;
373
+ }
374
+
375
+ // 注释(单行或多行注释开始)
376
+ if (/^\/\/.*/.test(content) ||
377
+ /^\/\*.*\*\/$/.test(content) ||
378
+ /^\/\*[\s\S]*/.test(content) ||
379
+ /^\*[\s\S]*/.test(content) ||
380
+ /^#.*$/.test(content) ||
381
+ content.startsWith('*') ||
382
+ content.startsWith('//')) {
383
+ stats.commentChanges++;
384
+ continue;
385
+ }
386
+
387
+ // 注解(Java/Kotlin)
388
+ if (/^@\w+/.test(content) ||
389
+ /^@\w+\(/.test(content) ||
390
+ /^@Override/.test(content) ||
391
+ /^@Autowired/.test(content) ||
392
+ /^@Resource/.test(content) ||
393
+ /^@Inject/.test(content)) {
394
+ stats.annotationChanges++;
395
+ continue;
396
+ }
397
+
398
+ // getter/setter 方法
399
+ if (/^\s*(public|private|protected)?\s*\w+\s+get\w+\s*\(/.test(content) ||
400
+ /^\s*(public|private|protected)?\s*\w+\s+set\w+\s*\(/.test(content) ||
401
+ /^\s*public\s+\w+\s+is\w+\s*\(/.test(content) ||
402
+ /\.get\w+\(\)/.test(content) ||
403
+ /\.set\w+\(/.test(content)) {
404
+ // 只有 getter/setter 声明才算,调用不算
405
+ if (/^\s*(public|private|protected)?\s*\w+\s+(get|set|is)\w+\s*\(/.test(content)) {
406
+ stats.getterSetterChanges++;
407
+ continue;
408
+ }
409
+ }
410
+
411
+ // 日志打印语句
412
+ if (/^\s*(log|logger|Log|Logger|console)\.\w+\s*\(/.test(content) ||
413
+ /^\s*System\.out\.print/.test(content) ||
414
+ /^\s*System\.err\.print/.test(content) ||
415
+ /^\s*print\(/.test(content) ||
416
+ /^\s*println\(/.test(content) ||
417
+ /^\s*printf\(/.test(content) ||
418
+ /^\s*debugLog\(/.test(content) ||
419
+ /^\s*infoLog\(/.test(content) ||
420
+ /^\s*warnLog\(/.test(content) ||
421
+ /^\s*errorLog\(/.test(content)) {
422
+ stats.logChanges++;
423
+ continue;
424
+ }
425
+
426
+ // 常量定义(简单的静态常量)
427
+ if (/^\s*(public|private|protected)?\s*(static)?\s*(final)?\s*\w+\s+\w+\s*=\s*[^;]+;/.test(content) ||
428
+ /^\s*const\s+\w+\s*=/.test(content) ||
429
+ /^\s*static\s+final\s+\w+\s+\w+\s*=/.test(content)) {
430
+ stats.constantChanges++;
431
+ continue;
432
+ }
433
+
434
+ // 其他变更视为有意义的代码变更
435
+ stats.meaningfulChanges++;
436
+ }
437
+
438
+ // 判断是否值得审查
439
+ const totalChanges = changedLines.length;
440
+ const lowValueChanges = stats.importChanges + stats.commentChanges +
441
+ stats.whitespaceChanges + stats.annotationChanges;
442
+
443
+ // 统计低价值变更比例
444
+ const lowValueRatio = totalChanges > 0 ? lowValueChanges / totalChanges : 0;
445
+
446
+ debugLog(`[${filePath}] 快速扫描统计: 总变更=${totalChanges}, import=${stats.importChanges}, 注释=${stats.commentChanges}, 空白=${stats.whitespaceChanges}, 注解=${stats.annotationChanges}, getter/setter=${stats.getterSetterChanges}, 日志=${stats.logChanges}, 常量=${stats.constantChanges}, 有意义=${stats.meaningfulChanges}, 低价值比例=${lowValueRatio.toFixed(2)}`);
447
+
448
+ // 决策规则
449
+ // 1. 如果没有任何有意义变更,跳过
450
+ if (stats.meaningfulChanges === 0) {
451
+ // 进一步检查:如果只有 getter/setter、日志、常量变更,也跳过
452
+ const onlyHelperChanges = stats.getterSetterChanges + stats.logChanges + stats.constantChanges;
453
+ if (onlyHelperChanges === totalChanges - stats.importChanges - stats.whitespaceChanges) {
454
+ return {
455
+ worthReviewing: false,
456
+ reason: `仅有辅助性变更(getter/setter=${stats.getterSetterChanges}, 日志=${stats.logChanges}, 常量=${stats.constantChanges})`
457
+ };
458
+ }
459
+
460
+ return {
461
+ worthReviewing: false,
462
+ reason: `无有意义代码变更(import=${stats.importChanges}, 注释=${stats.commentChanges}, 空白=${stats.whitespaceChanges}, 注解=${stats.annotationChanges})`
463
+ };
464
+ }
465
+
466
+ // 2. 如果低价值变更占比超过 90%,且有意义变更少于 3 行,跳过
467
+ if (lowValueRatio > 0.9 && stats.meaningfulChanges < 3) {
468
+ return {
469
+ worthReviewing: false,
470
+ reason: `低价值变更占比过高(${(lowValueRatio * 100).toFixed(1)}%),有意义变更仅 ${stats.meaningfulChanges} 行`
471
+ };
472
+ }
473
+
474
+ // 3. 如果有意义变更少于 2 行,且主要是辅助性代码,跳过
475
+ if (stats.meaningfulChanges < 2 &&
476
+ (stats.getterSetterChanges > 0 || stats.logChanges > 0 || stats.constantChanges > 0)) {
477
+ return {
478
+ worthReviewing: false,
479
+ reason: `有意义变更太少(${stats.meaningfulChanges} 行),且包含辅助性代码`
480
+ };
481
+ }
482
+
483
+ // 其他情况值得审查
484
+ return {
485
+ worthReviewing: true,
486
+ reason: `有 ${stats.meaningfulChanges} 行有意义代码变更,值得审查`
487
+ };
488
+ }
489
+
315
490
  /**
316
491
  * 获取合并请求的diff信息
317
492
  * @param {number} projectId GitLab项目ID
@@ -497,6 +672,25 @@ class GitLabCodeReviewer {
497
672
  const diffLines = diffObject.diff.split('\n');
498
673
  const codeLines = diffLines.filter(line => !line.startsWith('@@')).join('\n');
499
674
 
675
+ // ========== 阶段1:快速扫描(本地分析,不调用 AI)==========
676
+ const quickScanResult = this.quickScanDiffContent(diffObject.diff, diffObject.new_path || diffObject.old_path);
677
+ if (!quickScanResult.worthReviewing) {
678
+ infoLog(`[跳过审查] ${diffObject.new_path || diffObject.old_path}#${blockIndex}: ${quickScanResult.reason}`);
679
+ // 记录快速扫描跳过统计
680
+ this.metrics.recordQuickScanSkip(quickScanResult.reason);
681
+ return {
682
+ diff_info: diffObject,
683
+ block_index: blockIndex,
684
+ review_result: { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n该变更无需审查。\n</REPORT>', lineInfo: '[]' },
685
+ temp_file_path: null,
686
+ hallucination_detected: false,
687
+ skipped_by_quick_scan: true,
688
+ skip_reason: quickScanResult.reason,
689
+ };
690
+ }
691
+ debugLog(`[快速扫描通过] ${diffObject.new_path || diffObject.old_path}#${blockIndex}: ${quickScanResult.reason}`);
692
+
693
+ // ========== 阶段2:完整审查(调用 AI)==========
500
694
  // 构造临时文件内容:纯代码在前,元数据在后
501
695
  const diffContentWithMetadata = `${codeLines}
502
696
 
@@ -664,10 +858,15 @@ class GitLabCodeReviewer {
664
858
  collectAllReviewReports(results) {
665
859
  let allReportsText = '';
666
860
 
667
- // 遍历所有审查结果,过滤掉检测到幻觉的结果
668
- const validResults = results.filter(result => !result.hallucination_detected);
861
+ // 遍历所有审查结果,过滤掉检测到幻觉的结果和快速扫描跳过的结果
862
+ const validResults = results.filter(result =>
863
+ !result.hallucination_detected && !result.skipped_by_quick_scan
864
+ );
865
+
866
+ const skippedCount = results.filter(r => r.skipped_by_quick_scan).length;
867
+ const hallucinationCount = results.filter(r => r.hallucination_detected).length;
669
868
 
670
- debugLog(`汇总报告过滤:总共 ${results.length} 个结果,过滤掉 ${results.length - validResults.length} 个幻觉检测结果,保留 ${validResults.length} 个有效结果`);
869
+ debugLog(`汇总报告过滤:总共 ${results.length} 个结果,快速扫描跳过 ${skippedCount} 个,幻觉检测跳过 ${hallucinationCount} 个,保留 ${validResults.length} 个有效结果`);
671
870
 
672
871
  for (let i = 0; i < validResults.length; i++) {
673
872
  const result = validResults[i];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job51-gitlab-cr-node-jt-1",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "GitLab merge request code review tool with AI-powered analysis and project context support",
5
5
  "main": "index.js",
6
6
  "bin": {
package/utils.js CHANGED
@@ -116,6 +116,10 @@ class MetricsCollector {
116
116
  totalProblemsFound: 0,
117
117
  seriousProblemsFound: 0,
118
118
 
119
+ // 快速扫描跳过统计
120
+ quickScanSkipped: 0, // 快速扫描跳过的 diff 块数
121
+ quickScanSkipReasons: {}, // 跳过原因统计
122
+
119
123
  // 耗时统计(毫秒)
120
124
  reviewStartTime: 0,
121
125
  reviewEndTime: 0,
@@ -271,11 +275,31 @@ class MetricsCollector {
271
275
  this.metrics.commentsPublished++;
272
276
  }
273
277
 
278
+ // 记录快速扫描跳过
279
+ recordQuickScanSkip(reason = '') {
280
+ this.metrics.quickScanSkipped++;
281
+ if (reason) {
282
+ // 简化原因,只保留关键信息
283
+ const simplifiedReason = reason.split('(')[0] || reason;
284
+ if (!this.metrics.quickScanSkipReasons[simplifiedReason]) {
285
+ this.metrics.quickScanSkipReasons[simplifiedReason] = 0;
286
+ }
287
+ this.metrics.quickScanSkipReasons[simplifiedReason]++;
288
+ }
289
+ }
290
+
274
291
  // 打印统计摘要
275
292
  printSummary() {
276
293
  console.log('\n========== 审查统计 ==========');
277
294
  console.log(`审查文件数:${this.metrics.totalFilesReviewed}`);
278
295
  console.log(`审查 diff 块数:${this.metrics.totalBlocksReviewed}`);
296
+ console.log(`快速扫描跳过:${this.metrics.quickScanSkipped} 个 diff 块(节省 AI 调用)`);
297
+ if (Object.keys(this.metrics.quickScanSkipReasons).length > 0) {
298
+ console.log(' 跳过原因统计:');
299
+ for (const [reason, count] of Object.entries(this.metrics.quickScanSkipReasons)) {
300
+ console.log(` - ${reason}: ${count} 个`);
301
+ }
302
+ }
279
303
  console.log(`发现问题总数:${this.metrics.totalProblemsFound}`);
280
304
  console.log(`严重问题数:${this.metrics.seriousProblemsFound}`);
281
305
  console.log(`发布评论数:${this.metrics.commentsPublished}`);