job51-gitlab-cr-node-jt-1 2.7.3 → 2.7.5
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.
|
@@ -47,7 +47,37 @@
|
|
|
47
47
|
> - **仅在校验风格与同一文件中其他接口明显不一致时**,才需要提示
|
|
48
48
|
|
|
49
49
|
0. **Diff 数据结构与上下文读取规则**:
|
|
50
|
-
> -
|
|
50
|
+
> - **⚠️ 重要变更:按文件合并审查(多块临时文件)**:
|
|
51
|
+
> - **临时文件可能包含同一文件的多个变更块**(解决了"同一方法被拆分成多个块"的问题)
|
|
52
|
+
> - **临时文件格式**:每个块有明确的边界标识和行号信息
|
|
53
|
+
> - **格式示例**:
|
|
54
|
+
```
|
|
55
|
+
# ===== 文件: UserService.java =====
|
|
56
|
+
# 变更块总数: 2
|
|
57
|
+
|
|
58
|
+
# ===== 变更块 0 =====
|
|
59
|
+
# 行号范围: 10 - 15
|
|
60
|
+
# New Start: 10
|
|
61
|
+
# New Count: 6
|
|
62
|
+
# 行号计算: 此块内第1行代码 = 10, 每往下一行行号+1
|
|
63
|
+
|
|
64
|
+
[上下文代码]
|
|
65
|
+
+ if (obj == null) return; ← 判空逻辑(块0)
|
|
66
|
+
|
|
67
|
+
# ===== 变更块 1 =====
|
|
68
|
+
# 行号范围: 50 - 53
|
|
69
|
+
# New Start: 50
|
|
70
|
+
# New Count: 4
|
|
71
|
+
|
|
72
|
+
[上下文代码]
|
|
73
|
+
+ obj.getXxx(); ← 使用逻辑(块1)
|
|
74
|
+
|
|
75
|
+
# ===== 文件信息总结 =====
|
|
76
|
+
# ⚠️ 行号计算规则:找到问题所在块,从该块第1行代码开始计数
|
|
77
|
+
```
|
|
78
|
+
> - **关键改进**:AI 能看到同一文件的所有变更块(块0判空 + 块1使用),**避免误报 NPE**
|
|
79
|
+
|
|
80
|
+
> - **临时文件格式说明(旧版兼容)**:
|
|
51
81
|
> - 文件分为两部分:`=== File Information ===` 和 `=== Diff Content ===`
|
|
52
82
|
> - **File Information 部分**:
|
|
53
83
|
> - `New Path`: 变更后的文件路径,格式如 `b/src/main/java/com/example/UserService.java`
|
|
@@ -7,47 +7,77 @@ description: 代码审查技能,审查变更代码并输出 REPORT 和 LINE_IN
|
|
|
7
7
|
|
|
8
8
|
1. **读取 diff 文件**:使用 Read 工具读取文件:$ARGUMENTS
|
|
9
9
|
|
|
10
|
-
2.
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
2. **识别临时文件类型**(关键步骤):
|
|
11
|
+
- **类型 A:多块合并文件**(新格式,推荐)
|
|
12
|
+
- 文件开头有 `# ===== 文件: [文件名] =====`
|
|
13
|
+
- 包含多个 `# ===== 变更块 N =====` 标记
|
|
14
|
+
- 每个块有独立的 `# New Start`、`# New Count`、`# 行号范围` 信息
|
|
15
|
+
- **示例格式**:
|
|
16
|
+
```
|
|
17
|
+
# ===== 文件: UserService.java =====
|
|
18
|
+
# 变更块总数: 2
|
|
19
|
+
|
|
20
|
+
# ===== 变更块 0 =====
|
|
21
|
+
# 行号范围: 10 - 15
|
|
22
|
+
# New Start: 10
|
|
23
|
+
# New Count: 6
|
|
24
|
+
[上下文代码]
|
|
25
|
+
+ if (obj == null) return; ← 判空逻辑(块0)
|
|
26
|
+
|
|
27
|
+
# ===== 变更块 1 =====
|
|
28
|
+
# 行号范围: 50 - 53
|
|
29
|
+
# New Start: 50
|
|
30
|
+
# New Count: 4
|
|
31
|
+
[上下文代码]
|
|
32
|
+
+ obj.getXxx(); ← 使用逻辑(块1)
|
|
33
|
+
|
|
34
|
+
# ===== 文件信息总结 =====
|
|
35
|
+
# ⚠️ 行号计算规则:找到问题所在块,从该块第1行代码开始计数
|
|
36
|
+
```
|
|
37
|
+
- **关键优势**:能看到同一文件的所有变更块(块0判空 + 块1使用),**避免误报 NPE**
|
|
38
|
+
|
|
39
|
+
- **类型 B:单块文件**(旧格式,兼容)
|
|
40
|
+
- 文件末尾有 `# File Information` 部分
|
|
41
|
+
- 只包含一个 diff 块的内容
|
|
42
|
+
- 从 `# New Start` 和 `# New Count` 提取行号信息
|
|
43
|
+
|
|
44
|
+
3. **解析行号**(多块文件的关键规则):
|
|
45
|
+
- **⚠️ 多块文件的行号计算方法**:
|
|
46
|
+
1. **识别问题所在的块**:通过 `# ===== 变更块 N =====` 标记
|
|
47
|
+
2. **读取该块的 New Start 值**:从块开头的 `# New Start: X` 提取
|
|
48
|
+
3. **从该块第1行代码开始计数**:行号 = New Start
|
|
49
|
+
4. **每往下一行(+ 或空格开头),行号 +1**
|
|
50
|
+
5. **- 开头的删除行不计数**,行号不增加
|
|
51
|
+
6. **示例**:
|
|
52
|
+
- 问题在"变更块 1",该块 `# New Start: 50`
|
|
53
|
+
- 从块内第1行代码开始计数,第2行是 `obj.getXxx()`
|
|
54
|
+
- 行号 = 50 + 1 = 51
|
|
55
|
+
7. **⚠️ 强制验证**:输出前必须检查行号是否在该块范围内 `[New Start, New Start + New Count - 1]`
|
|
56
|
+
|
|
57
|
+
- **单块文件的行号计算方法**(旧格式兼容):
|
|
58
|
+
1. 从文件末尾 `# File Information` 提取 `New Start` 和 `New Count`
|
|
19
59
|
2. 从第 1 行代码开始,行号 = `New Start`
|
|
20
|
-
3. 每往下一行(`+`
|
|
21
|
-
4. `-`
|
|
22
|
-
5.
|
|
23
|
-
|
|
24
|
-
- **⚠️
|
|
25
|
-
|
|
26
|
-
[代码行 1] <-- 空格开头,行号 = 29(New Start=29)
|
|
27
|
-
+ [代码行 2] <-- + 开头,行号 = 30(29+1)
|
|
28
|
-
+ [代码行 3] <-- + 开头,行号 = 31(30+1)
|
|
29
|
-
[代码行 4] <-- 空格开头,行号 = 32(31+1)
|
|
30
|
-
...(diff 内容结束)
|
|
31
|
-
# File Information
|
|
32
|
-
# New Path: b/src/Test.java
|
|
33
|
-
# New Start: 29 <-- 元数据在末尾,提供行号计算起点
|
|
34
|
-
```
|
|
35
|
-
- **⚠️ 强制验证步骤**(输出前必须执行):
|
|
36
|
-
1. 从元数据提取 `New Start` 和 `New Count`,计算行号范围:`[New Start, New Start + New Count - 1]`
|
|
60
|
+
3. 每往下一行(`+` 或空格开头),行号 +1
|
|
61
|
+
4. `-` 开头的删除行不计数
|
|
62
|
+
5. 验证行号范围 `[New Start, New Start + New Count - 1]`
|
|
63
|
+
|
|
64
|
+
- **⚠️ 强制验证步骤**(所有文件类型必须执行):
|
|
65
|
+
1. 从元数据提取 `New Start` 和 `New Count`,计算行号范围
|
|
37
66
|
2. **逐个检查**每个问题的行号是否在该范围内
|
|
38
67
|
3. **如果任何行号超出范围,必须重新计数**
|
|
39
68
|
4. **禁止**直接使用从完整文件读取的行号
|
|
40
|
-
- **示例**:
|
|
41
|
-
- `New Start: 1, New Count: 56` 的行号范围是 [1, 56],**不能返回 57、60 等超出范围的值**
|
|
42
|
-
- `New Start: 167, New Count: 6` 的行号范围是 [167, 172],**不能返回 <167 或 >172 的值**
|
|
43
69
|
|
|
44
70
|
4. **读取上下文**:基于 New Path 读取变更后文件,涉及方法调用时追踪读取实现代码
|
|
45
71
|
- **⚠️ 重要**:读取完整文件仅用于理解代码逻辑,**不得**直接使用完整文件中的行号
|
|
46
72
|
- 如果从完整文件中定位到问题行号,**必须验证**该行号是否在 diff 块范围内
|
|
47
73
|
- **验证公式**:`new_start <= 行号 <= new_start + new_count - 1`
|
|
48
|
-
- **如果行号超出范围**:必须回到 diff
|
|
74
|
+
- **如果行号超出范围**:必须回到 diff 块内容,重新逐行计数
|
|
49
75
|
|
|
50
76
|
5. **执行审查**:按照 @.claude/rules/code-review-rules.md 中的规则进行审查
|
|
77
|
+
- **⚠️ 多块文件的审查优势**:
|
|
78
|
+
- 能看到同一文件的所有变更块(块0判空 + 块1使用)
|
|
79
|
+
- **避免误报"块1未判空"**(因为能看到块0已有判空)
|
|
80
|
+
- **跨块上下文分析**:合并同一文件多个块的信息,判断逻辑是否安全
|
|
51
81
|
|
|
52
82
|
6. **输出结果**:
|
|
53
83
|
- 审查结果放入 `<REPORT>` 标签
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# GitLab Code Review AI Tool 技术文档
|
|
2
2
|
|
|
3
3
|
**项目名称**: job51-gitlab-cr-node
|
|
4
|
-
**当前版本**: 2.
|
|
4
|
+
**当前版本**: 2.7.4
|
|
5
5
|
**作者**: tao.jing
|
|
6
|
-
**最后更新**: 2026-04-
|
|
6
|
+
**最后更新**: 2026-04-20
|
|
7
7
|
**项目地址**: https://gitdev.51job.com/51jobweb/ai-agent
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -27,6 +27,50 @@
|
|
|
27
27
|
|
|
28
28
|
## 版本历史
|
|
29
29
|
|
|
30
|
+
### v2.7.4 (2026-04-20)
|
|
31
|
+
|
|
32
|
+
**当前版本**: 日志标识增强
|
|
33
|
+
|
|
34
|
+
**优化**:
|
|
35
|
+
- **日志追踪标识**:为每个 diff 块的 AI 调用添加唯一标识,解决 CI 环境中处理多个 diff 块重试时日志追踪困难的问题
|
|
36
|
+
- 问题现象:当某个 diff 块需要重试时,日志中无法清晰识别是哪个块的输出
|
|
37
|
+
- 修复方案:
|
|
38
|
+
- 在 `processBlock` 函数中构造块标识:`文件路径#块索引`(如 `UserService.java#0`)
|
|
39
|
+
- 将标识传递给 `reviewDiffWithClaudeUsingFile` 方法
|
|
40
|
+
- 更新所有关键日志输出,添加 `[块标识]` 前缀
|
|
41
|
+
- 影响范围:
|
|
42
|
+
- 所有 AI 调用的日志(开始审核、重试、完成、决策判断等)
|
|
43
|
+
- LINE_INFO 检查、严重问题检查、标题验证等关键决策点的日志
|
|
44
|
+
- 修复文件:
|
|
45
|
+
- `index.js:110-112`:构造块标识并传递给审查方法
|
|
46
|
+
- `index.js:182-183`:添加 `blockIdentifier` 参数
|
|
47
|
+
- `index.js:202-274`:更新所有日志输出添加标识前缀
|
|
48
|
+
|
|
49
|
+
### v2.7.3 (2026-04-17)
|
|
50
|
+
|
|
51
|
+
**优化**:
|
|
52
|
+
- **技术文档版本同步**:更新文档版本号与 package.json 保持一致
|
|
53
|
+
|
|
54
|
+
### v2.7.2 (2026-04-17)
|
|
55
|
+
|
|
56
|
+
**优化**:
|
|
57
|
+
- **技术文档版本同步**:更新文档版本号与 package.json 保持一致
|
|
58
|
+
|
|
59
|
+
### v2.7.1 (2026-04-17)
|
|
60
|
+
|
|
61
|
+
**优化**:
|
|
62
|
+
- **技能文件优化**:更新 simple-code-review 技能定义,简化行号计算说明
|
|
63
|
+
|
|
64
|
+
### v2.7.0 (2026-04-17)
|
|
65
|
+
|
|
66
|
+
**功能增强**:
|
|
67
|
+
- **审查流程优化**:改进代码审查逻辑和行号计算准确性
|
|
68
|
+
|
|
69
|
+
### v2.6.12 (2026-04-17)
|
|
70
|
+
|
|
71
|
+
**优化**:
|
|
72
|
+
- **版本发布准备**:更新项目版本号和文档
|
|
73
|
+
|
|
30
74
|
### v2.6.11 (2026-04-17)
|
|
31
75
|
|
|
32
76
|
**Bug 修复**:
|
package/index.js
CHANGED
|
@@ -63,79 +63,129 @@ class GitLabCodeReviewer {
|
|
|
63
63
|
// 开始审查统计
|
|
64
64
|
this.metrics.startReview();
|
|
65
65
|
|
|
66
|
-
// 获取diff
|
|
66
|
+
// 获取diff信息(每个 diff 对应一个文件的变更)
|
|
67
67
|
const diffs = await this.getMergeRequestDiffs(projectId, mergeRequestIid);
|
|
68
|
-
debugLog(`获取到 ${diffs.length}
|
|
68
|
+
debugLog(`获取到 ${diffs.length} 个文件变更`);
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
debugLog('
|
|
70
|
+
// 按文件分组所有块(关键改进:同一文件的所有块合并审查)
|
|
71
|
+
debugLog('开始按文件分组所有变更块');
|
|
72
|
+
const fileBlocksMap = new Map(); // 文件路径 → 所有块数组
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
for (const diff of diffs) {
|
|
75
|
+
const diffObjects = this.getDiffBlocks(diff);
|
|
76
|
+
const filePath = diff.new_path || diff.old_path;
|
|
77
|
+
|
|
78
|
+
if (!fileBlocksMap.has(filePath)) {
|
|
79
|
+
fileBlocksMap.set(filePath, []);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 为每个块分配全局块索引(文件内唯一)
|
|
83
|
+
const existingBlocks = fileBlocksMap.get(filePath);
|
|
84
|
+
for (let i = 0; i < diffObjects.length; i++) {
|
|
85
|
+
diffObjects[i].block_index = existingBlocks.length + i;
|
|
86
|
+
existingBlocks.push(diffObjects[i]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
debugLog(`分组完成:共 ${fileBlocksMap.size} 个文件,包含 ${Array.from(fileBlocksMap.values()).reduce((sum, blocks) => sum + blocks.length, 0)} 个变更块`);
|
|
91
|
+
|
|
92
|
+
// 打印每个文件的块数(用于调试)
|
|
93
|
+
for (const [filePath, blocks] of fileBlocksMap.entries()) {
|
|
94
|
+
debugLog(`文件 ${filePath}: ${blocks.length} 个变更块`);
|
|
95
|
+
blocks.forEach((block, i) => {
|
|
96
|
+
debugLog(` - 块 ${block.block_index}: new_start=${block.line_info?.new_start}, new_count=${block.line_info?.new_count}`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 创建处理单个文件的函数(替代原来的 processBlock)
|
|
101
|
+
const processFile = async (filePath, blocks) => {
|
|
102
|
+
// 创建临时文件存储该文件的所有变更块
|
|
103
|
+
const fileName = `temp-diff-file-${Date.now()}-${filePath.replace(/[\/\\]/g, '_')}.diff`;
|
|
77
104
|
const tmpFileName = path.join(process.cwd(), fileName);
|
|
78
105
|
|
|
79
106
|
try {
|
|
80
|
-
|
|
81
|
-
debugLog(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
107
|
+
debugLog(`========== 文件审查开始: ${filePath} ==========`);
|
|
108
|
+
debugLog(`文件包含 ${blocks.length} 个变更块`);
|
|
109
|
+
|
|
110
|
+
// 构建包含所有块的临时文件内容
|
|
111
|
+
const fileDiffContent = this.buildFileDiffContent(filePath, blocks);
|
|
112
|
+
fs.writeFileSync(tmpFileName, fileDiffContent);
|
|
113
|
+
|
|
114
|
+
// 输出临时文件前 20 行用于调试
|
|
115
|
+
const tmpFilePreview = fileDiffContent.split('\n').slice(0, 20).join('\n');
|
|
116
|
+
debugLog(`临时文件前 20 行预览:\n${tmpFilePreview}`);
|
|
117
|
+
|
|
118
|
+
// 审查整个文件的所有变更(能看到完整上下文)
|
|
119
|
+
const fileStartTime = Date.now();
|
|
120
|
+
const fileIdentifier = filePath;
|
|
121
|
+
const review_result = await this.reviewDiffWithClaudeUsingFile(tmpFileName, fileIdentifier);
|
|
122
|
+
const reviewTime = Date.now() - fileStartTime;
|
|
123
|
+
|
|
124
|
+
// 记录审查指标(按文件记录,而不是按块)
|
|
125
|
+
const hasSeriousProblems = review_result && review_result.reportContent && review_result.reportContent.includes('严重问题');
|
|
126
|
+
const totalDiffSize = blocks.reduce((sum, block) => sum + (block.diff?.length || 0), 0);
|
|
127
|
+
this.metrics.recordFileReviewed();
|
|
128
|
+
blocks.forEach(block => {
|
|
129
|
+
const blockProblemsCount = hasSeriousProblems ? 1 : 0;
|
|
130
|
+
this.metrics.recordBlockReviewed(reviewTime / blocks.length, blockProblemsCount, hasSeriousProblems, block.diff?.length || 0, filePath);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 解析所有问题的行号信息
|
|
134
|
+
const allLineInfo = this.parseAllLineInfoFromReviewResult(review_result?.lineInfo);
|
|
135
|
+
debugLog(`解析到 ${allLineInfo.length} 个问题的行号信息`);
|
|
136
|
+
|
|
137
|
+
// 检查审查结果中是否包含严重问题
|
|
138
|
+
if (hasSeriousProblems && allLineInfo.length > 0) {
|
|
139
|
+
debugLog(`文件 ${filePath} 包含 ${allLineInfo.length} 个严重问题,开始发布评论`);
|
|
140
|
+
|
|
141
|
+
// 为每个问题发布评论
|
|
142
|
+
for (let i = 0; i < allLineInfo.length; i++) {
|
|
143
|
+
const problemInfo = allLineInfo[i];
|
|
144
|
+
debugLog(`处理第 ${i + 1}/${allLineInfo.length} 个问题:文件=${problemInfo.new_path}, 行号=${problemInfo.new_line}`);
|
|
145
|
+
|
|
146
|
+
// 提取单个问题的报告内容
|
|
147
|
+
const singleProblemContent = this.extractSingleProblemReport(review_result.reportContent, i + 1);
|
|
148
|
+
|
|
149
|
+
// 找到对应的块信息(根据行号范围匹配)
|
|
150
|
+
const matchedBlock = blocks.find(block => {
|
|
151
|
+
const newStart = block.line_info?.new_start || 1;
|
|
152
|
+
const newCount = block.line_info?.new_count || 1;
|
|
153
|
+
const newEnd = newStart + newCount - 1;
|
|
154
|
+
return problemInfo.new_line >= newStart && problemInfo.new_line <= newEnd;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!matchedBlock) {
|
|
158
|
+
debugLog(`警告:问题行号 ${problemInfo.new_line} 无法匹配到任何块,发布为一般讨论`);
|
|
159
|
+
await this.createGeneralDiscussion(projectId, mergeRequestIid, `${filePath}#L${problemInfo.new_line}`, singleProblemContent);
|
|
160
|
+
this.metrics.recordCommentPublished();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 发布评论到对应块
|
|
165
|
+
await this.postSingleCommentToGitLab(projectId, mergeRequestIid, {
|
|
166
|
+
diff_info: matchedBlock,
|
|
167
|
+
block_index: matchedBlock.block_index,
|
|
168
|
+
review_result: {
|
|
169
|
+
reportContent: singleProblemContent,
|
|
170
|
+
lineInfo: `<LINE_INFO>[${JSON.stringify(problemInfo)}]</LINE_INFO>`
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
this.metrics.recordCommentPublished();
|
|
174
|
+
}
|
|
128
175
|
} else {
|
|
129
|
-
debugLog(
|
|
176
|
+
debugLog(`文件 ${filePath} 不包含严重问题,跳过评论发布`);
|
|
130
177
|
}
|
|
131
178
|
|
|
179
|
+
debugLog(`========== 文件审查完成: ${filePath} ==========`);
|
|
180
|
+
|
|
132
181
|
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
review_result
|
|
182
|
+
file_path: filePath,
|
|
183
|
+
blocks_count: blocks.length,
|
|
184
|
+
review_result,
|
|
136
185
|
temp_file_path: tmpFileName,
|
|
137
186
|
};
|
|
138
187
|
} catch (error) {
|
|
188
|
+
console.error(`文件 ${filePath} 审查失败:`, error.message);
|
|
139
189
|
throw error;
|
|
140
190
|
} finally {
|
|
141
191
|
try {
|
|
@@ -148,23 +198,13 @@ class GitLabCodeReviewer {
|
|
|
148
198
|
}
|
|
149
199
|
};
|
|
150
200
|
|
|
151
|
-
//
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const diffObjects = this.getDiffBlocks(diff);
|
|
155
|
-
for (let i = 0; i < diffObjects.length; i++) {
|
|
156
|
-
// 更新块索引
|
|
157
|
-
diffObjects[i].block_index = i;
|
|
158
|
-
allBlocks.push({ diffObject: diffObjects[i], blockIndex: i });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 使用线程池控制并发数量
|
|
163
|
-
const results = await this.processWithThreadPool(allBlocks, processBlock, maxConcurrency);
|
|
201
|
+
// 使用线程池控制并发数量(按文件并发,而不是按块)
|
|
202
|
+
const fileTasks = Array.from(fileBlocksMap.entries()).map(([filePath, blocks]) => ({ filePath, blocks }));
|
|
203
|
+
const results = await this.processWithThreadPool(fileTasks, (task) => processFile(task.filePath, task.blocks), maxConcurrency);
|
|
164
204
|
|
|
165
|
-
debugLog(`总共处理了 ${results.length}
|
|
205
|
+
debugLog(`总共处理了 ${results.length} 个文件`);
|
|
166
206
|
|
|
167
|
-
debugLog('
|
|
207
|
+
debugLog('所有文件审核并发布评论完成');
|
|
168
208
|
// 结束审查统计
|
|
169
209
|
this.metrics.endReview();
|
|
170
210
|
|
|
@@ -174,10 +214,11 @@ class GitLabCodeReviewer {
|
|
|
174
214
|
/**
|
|
175
215
|
* 使用Claude对单个diff文件进行代码审核
|
|
176
216
|
* @param {string} filePath 临时文件路径
|
|
217
|
+
* @param {string} blockIdentifier 块标识(格式:文件路径#块索引),用于日志追踪
|
|
177
218
|
* @returns {Promise<string>} 审核结果
|
|
178
219
|
*/
|
|
179
|
-
async reviewDiffWithClaudeUsingFile(filePath) {
|
|
180
|
-
debugLog(
|
|
220
|
+
async reviewDiffWithClaudeUsingFile(filePath, blockIdentifier = 'unknown') {
|
|
221
|
+
debugLog(`[${blockIdentifier}] 开始审核文件: ${filePath}`);
|
|
181
222
|
const startTime = Date.now();
|
|
182
223
|
|
|
183
224
|
const prompt = `请调用 simple-code-review 技能审核代码变更。
|
|
@@ -196,7 +237,7 @@ class GitLabCodeReviewer {
|
|
|
196
237
|
4. **必须输出 '<LINE_INFO>' 标签**,包含所有问题的行号信息(无问题时输出空数组 [])
|
|
197
238
|
5. 不要输出任何额外的解释、问候或总结文本`;
|
|
198
239
|
//打印
|
|
199
|
-
debugLog(`Claude命令: ${prompt}`);
|
|
240
|
+
debugLog(`[${blockIdentifier}] Claude命令: ${prompt}`);
|
|
200
241
|
// 最多重试5次,直到结果包含"🤖 AI 代码审查结果"或达到最大重试次数
|
|
201
242
|
let attempts = 0;
|
|
202
243
|
const maxAttempts = 5;
|
|
@@ -205,20 +246,20 @@ class GitLabCodeReviewer {
|
|
|
205
246
|
while (attempts < maxAttempts) {
|
|
206
247
|
attempts++;
|
|
207
248
|
try {
|
|
208
|
-
debugLog(
|
|
249
|
+
debugLog(`[${blockIdentifier}] 调用本地AI命令审核文件 (尝试 ${attempts}/${maxAttempts})`);
|
|
209
250
|
|
|
210
251
|
// 直接将prompt内容(包含文件路径)传递给Claude命令
|
|
211
252
|
claudeResult = await runClaudeCommand(prompt);
|
|
212
253
|
//若结果为空,则记录日志
|
|
213
254
|
if (!claudeResult) {
|
|
214
|
-
debugLog(
|
|
255
|
+
debugLog(`[${blockIdentifier}] 本地AI命令审核结果为空`);
|
|
215
256
|
return { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n</REPORT>', lineInfo: '[]' };
|
|
216
257
|
}
|
|
217
|
-
debugLog(
|
|
258
|
+
debugLog(`[${blockIdentifier}] 本地 AI 命令审核完成,审核结果长度:${claudeResult?.length || 0}`);
|
|
218
259
|
|
|
219
260
|
// 打印报告内容前 500 字符(避免过长)
|
|
220
261
|
const reportPreview = claudeResult?.length > 500 ? claudeResult.substring(0, 500) + '...' : claudeResult;
|
|
221
|
-
debugLog(`AI 审核报告内容预览:${claudeResult}`);
|
|
262
|
+
debugLog(`[${blockIdentifier}] AI 审核报告内容预览:${claudeResult}`);
|
|
222
263
|
|
|
223
264
|
// 使用正则提取 LINE_INFO 内容(支持换行)
|
|
224
265
|
const lineInfoMatch = claudeResult?.match(/<LINE_INFO>\s*\[([^\]]*)\]\s*<\/LINE_INFO>/);
|
|
@@ -228,11 +269,11 @@ class GitLabCodeReviewer {
|
|
|
228
269
|
const lineInfoContent = hasLineInfoTag ? lineInfoMatch[1].replace(/\s/g, '') : '';
|
|
229
270
|
const hasNonEmptyLineInfo = hasLineInfoTag && lineInfoContent !== '';
|
|
230
271
|
|
|
231
|
-
debugLog(`LINE_INFO 检查结果:hasLineInfoTag=${hasLineInfoTag}, hasNonEmptyLineInfo=${hasNonEmptyLineInfo}, lineInfoContent=[${lineInfoContent}]`);
|
|
272
|
+
debugLog(`[${blockIdentifier}] LINE_INFO 检查结果:hasLineInfoTag=${hasLineInfoTag}, hasNonEmptyLineInfo=${hasNonEmptyLineInfo}, lineInfoContent=[${lineInfoContent}]`);
|
|
232
273
|
|
|
233
274
|
// LINE_INFO 为空或不存在 → 说明无问题,直接返回标准空格式
|
|
234
275
|
if (!hasLineInfoTag || !hasNonEmptyLineInfo) {
|
|
235
|
-
debugLog(
|
|
276
|
+
debugLog(`[${blockIdentifier}] 【决策】LINE_INFO 为空或不存在,说明无问题,直接返回标准空格式`);
|
|
236
277
|
return { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n</REPORT>', lineInfo: '[]' };
|
|
237
278
|
}
|
|
238
279
|
|
|
@@ -240,32 +281,32 @@ class GitLabCodeReviewer {
|
|
|
240
281
|
// 先检查是否有严重问题(放宽匹配,只检查"严重问题"关键词)
|
|
241
282
|
const hasSeriousProblem = claudeResult && claudeResult.includes('严重问题');
|
|
242
283
|
|
|
243
|
-
debugLog(
|
|
284
|
+
debugLog(`[${blockIdentifier}] 严重问题检查:hasSeriousProblem=${hasSeriousProblem}`);
|
|
244
285
|
|
|
245
286
|
// 无严重问题 → 直接返回标准空格式
|
|
246
287
|
if (!hasSeriousProblem) {
|
|
247
|
-
debugLog(
|
|
288
|
+
debugLog(`[${blockIdentifier}] 【决策】报告无严重问题,返回标准空格式`);
|
|
248
289
|
return { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n</REPORT>', lineInfo: '[]' };
|
|
249
290
|
}
|
|
250
291
|
|
|
251
292
|
// 有严重问题 → 检查标题是否符合要求
|
|
252
293
|
const hasValidTitle = claudeResult && claudeResult.includes('🤖 AI 代码审查结果');
|
|
253
294
|
|
|
254
|
-
debugLog(
|
|
295
|
+
debugLog(`[${blockIdentifier}] 标题检查:hasValidTitle=${hasValidTitle}`);
|
|
255
296
|
|
|
256
297
|
if (hasValidTitle) {
|
|
257
|
-
debugLog(
|
|
298
|
+
debugLog(`[${blockIdentifier}] 【决策】报告包含严重问题且标题正确,接受结果 (尝试 ${attempts})`);
|
|
258
299
|
return extractReportContent(claudeResult);
|
|
259
300
|
}
|
|
260
301
|
|
|
261
302
|
// 有严重问题但标题不符合 → 重试
|
|
262
|
-
debugLog(
|
|
303
|
+
debugLog(`[${blockIdentifier}] 【决策】报告包含严重问题但标题不符合要求 (尝试 ${attempts}),将重试...`);
|
|
263
304
|
if (attempts >= maxAttempts) {
|
|
264
|
-
debugLog(
|
|
305
|
+
debugLog(`[${blockIdentifier}] 【决策】已达到最大重试次数 ${maxAttempts},返回最后一次结果`);
|
|
265
306
|
return extractReportContent(claudeResult);
|
|
266
307
|
}
|
|
267
308
|
} catch (error) {
|
|
268
|
-
console.error(`AI审核失败 (尝试 ${attempts}/${maxAttempts}):`, error.message);
|
|
309
|
+
console.error(`[${blockIdentifier}] AI审核失败 (尝试 ${attempts}/${maxAttempts}):`, error.message);
|
|
269
310
|
if (attempts >= maxAttempts) {
|
|
270
311
|
return `审核失败: ${error.message}`;
|
|
271
312
|
}
|
|
@@ -275,6 +316,60 @@ class GitLabCodeReviewer {
|
|
|
275
316
|
}
|
|
276
317
|
}
|
|
277
318
|
|
|
319
|
+
/**
|
|
320
|
+
* 构建包含文件所有变更块的临时文件内容
|
|
321
|
+
* @param {string} filePath 文件路径
|
|
322
|
+
* @param {Array} blocks 该文件的所有变更块
|
|
323
|
+
* @returns {string} 临时文件内容
|
|
324
|
+
*/
|
|
325
|
+
buildFileDiffContent(filePath, blocks) {
|
|
326
|
+
let content = '';
|
|
327
|
+
|
|
328
|
+
// 文件头部信息
|
|
329
|
+
content += `# ===== 文件: ${filePath} =====\n`;
|
|
330
|
+
content += `# 变更块总数: ${blocks.length}\n\n`;
|
|
331
|
+
|
|
332
|
+
// 遍历每个块,标注块边界和行号信息
|
|
333
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
334
|
+
const block = blocks[i];
|
|
335
|
+
const newStart = block.line_info?.new_start || 1;
|
|
336
|
+
const newCount = block.line_info?.new_count || 1;
|
|
337
|
+
const newEnd = newStart + newCount - 1;
|
|
338
|
+
|
|
339
|
+
// 块分隔标记
|
|
340
|
+
content += `# ===== 变更块 ${block.block_index} =====\n`;
|
|
341
|
+
content += `# 行号范围: ${newStart} - ${newEnd}\n`;
|
|
342
|
+
content += `# New Start: ${newStart}\n`;
|
|
343
|
+
content += `# New Count: ${newCount}\n`;
|
|
344
|
+
content += `# 行号计算: 此块内第1行代码 = ${newStart}, 每往下一行行号+1\n`;
|
|
345
|
+
content += `# ⚠️ 重要: -开头的删除行不计数,行号不增加\n\n`;
|
|
346
|
+
|
|
347
|
+
// 解析并添加块内容(移除 @@ 行)
|
|
348
|
+
const diffLines = block.diff.split('\n');
|
|
349
|
+
const codeLines = diffLines.filter(line => !line.startsWith('@@'));
|
|
350
|
+
|
|
351
|
+
// 添加代码行(保留 +、-、空格前缀)
|
|
352
|
+
content += codeLines.join('\n');
|
|
353
|
+
content += '\n\n';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 文件尾部元数据
|
|
357
|
+
content += `# ===== 文件信息总结 =====\n`;
|
|
358
|
+
content += `# 文件路径: ${filePath}\n`;
|
|
359
|
+
content += `# 变更块数量: ${blocks.length}\n`;
|
|
360
|
+
blocks.forEach(block => {
|
|
361
|
+
content += `# - 块 ${block.block_index}: 行号 ${block.line_info?.new_start}-${block.line_info?.new_start + (block.line_info?.new_count || 1) - 1}\n`;
|
|
362
|
+
});
|
|
363
|
+
content += `# ⚠️ 行号计算规则:\n`;
|
|
364
|
+
content += `# 1. 找到问题所在块(根据块边界标识)\n`;
|
|
365
|
+
content += `# 2. 从该块第1行代码开始计数,行号 = New Start\n`;
|
|
366
|
+
content += `# 3. 每往下一行(+开头或空格开头),行号+1\n`;
|
|
367
|
+
content += `# 4. -开头的删除行不计数,行号不增加\n`;
|
|
368
|
+
content += `# 5. 输出前验证:行号必须在对应块范围内 [New Start, New Start + New Count - 1]\n`;
|
|
369
|
+
|
|
370
|
+
return content;
|
|
371
|
+
}
|
|
372
|
+
|
|
278
373
|
/**
|
|
279
374
|
* 使用线程池控制并发执行
|
|
280
375
|
* @param {Array} tasks 任务数组
|
|
@@ -291,8 +386,8 @@ class GitLabCodeReviewer {
|
|
|
291
386
|
for (let i = 0; i < tasks.length; i++) {
|
|
292
387
|
const task = tasks[i];
|
|
293
388
|
|
|
294
|
-
//
|
|
295
|
-
const promise = processor(task
|
|
389
|
+
// 创建一个异步任务(直接传递 task 对象,不再拆分参数)
|
|
390
|
+
const promise = processor(task)
|
|
296
391
|
.then(result => {
|
|
297
392
|
results.push(result);
|
|
298
393
|
// 从执行队列中移除已完成的任务
|