job51-gitlab-cr-node-jt-1 3.1.1 → 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 +20 -1
  2. package/index.js +212 -6
  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
- - 自动跳过 DTO/VO 文件(如 `*Dto.java`、`*VO.java`、`*Request.java` 等)
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
@@ -98,12 +98,14 @@ class GitLabCodeReviewer {
98
98
  // 提取文件名(不含扩展名)
99
99
  const fileName = normalizedPath.split('/').pop() || '';
100
100
 
101
- // DTO/VO 目录模式(整个目录都是数据模型)
101
+ // DTO/VO/Query 目录模式(整个目录都是数据模型)
102
102
  const dtoVoDirPatterns = [
103
103
  '/dto/', // Java: com/example/dto/
104
104
  '/DTO/', // 大写形式
105
105
  '/vo/', // Java: com/example/vo/
106
106
  '/VO/', // 大写形式
107
+ '/query/', // Query 对象目录
108
+ '/Query/', // 大写形式
107
109
  '/model/', // 通用模型目录
108
110
  '/models/', // 通用模型目录
109
111
  '/entity/', // 实体类目录(有时也是纯数据)
@@ -119,13 +121,14 @@ class GitLabCodeReviewer {
119
121
  }
120
122
  }
121
123
 
122
- // DTO/VO 文件命名模式
124
+ // DTO/VO/Query 文件命名模式
123
125
  // 匹配文件名中包含或以这些后缀结尾的情况
124
126
  const dtoVoPatterns = [
125
127
  'Dto', // Java: XxxDto.java
126
128
  'DTO', // Java: XxxDTO.java
127
129
  'Vo', // Java: XxxVo.java
128
130
  'VO', // Java: XxxVO.java
131
+ 'Query', // Query 对象(纯数据查询条件)
129
132
  'Request', // Request 对象通常也是纯数据
130
133
  'Response', // Response 对象通常也是纯数据
131
134
  'Form', // Form 对象
@@ -143,12 +146,13 @@ class GitLabCodeReviewer {
143
146
  }
144
147
  }
145
148
 
146
- // Java 特殊处理:文件名以常见 DTO/VO 后缀结尾
149
+ // Java 特殊处理:文件名以常见 DTO/VO/Query 后缀结尾
147
150
  const dtoVoEndPatterns = [
148
151
  'Dto.java',
149
152
  'DTO.java',
150
153
  'Vo.java',
151
154
  'VO.java',
155
+ 'Query.java',
152
156
  'Request.java',
153
157
  'Response.java',
154
158
  'Form.java',
@@ -159,6 +163,7 @@ class GitLabCodeReviewer {
159
163
  'DTO.kt',
160
164
  'Vo.kt',
161
165
  'VO.kt',
166
+ 'Query.kt',
162
167
  'Request.kt',
163
168
  'Response.kt',
164
169
  // TypeScript/JavaScript
@@ -170,6 +175,8 @@ class GitLabCodeReviewer {
170
175
  'Vo.js',
171
176
  'VO.ts',
172
177
  'VO.js',
178
+ 'Query.ts',
179
+ 'Query.js',
173
180
  ];
174
181
 
175
182
  for (const pattern of dtoVoEndPatterns) {
@@ -305,6 +312,181 @@ class GitLabCodeReviewer {
305
312
  return false;
306
313
  }
307
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
+
308
490
  /**
309
491
  * 获取合并请求的diff信息
310
492
  * @param {number} projectId GitLab项目ID
@@ -490,6 +672,25 @@ class GitLabCodeReviewer {
490
672
  const diffLines = diffObject.diff.split('\n');
491
673
  const codeLines = diffLines.filter(line => !line.startsWith('@@')).join('\n');
492
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)==========
493
694
  // 构造临时文件内容:纯代码在前,元数据在后
494
695
  const diffContentWithMetadata = `${codeLines}
495
696
 
@@ -657,10 +858,15 @@ class GitLabCodeReviewer {
657
858
  collectAllReviewReports(results) {
658
859
  let allReportsText = '';
659
860
 
660
- // 遍历所有审查结果,过滤掉检测到幻觉的结果
661
- 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;
662
868
 
663
- debugLog(`汇总报告过滤:总共 ${results.length} 个结果,过滤掉 ${results.length - validResults.length} 个幻觉检测结果,保留 ${validResults.length} 个有效结果`);
869
+ debugLog(`汇总报告过滤:总共 ${results.length} 个结果,快速扫描跳过 ${skippedCount} 个,幻觉检测跳过 ${hallucinationCount} 个,保留 ${validResults.length} 个有效结果`);
664
870
 
665
871
  for (let i = 0; i < validResults.length; i++) {
666
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.1",
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}`);