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.
- package/README.md +20 -1
- package/index.js +212 -6
- 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
|
-
- 自动跳过 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 =>
|
|
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}
|
|
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
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}`);
|