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.
- package/README.md +19 -0
- package/index.js +202 -3
- package/package.json +1 -1
- 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 =>
|
|
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}
|
|
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
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}`);
|