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. **解析文件信息**:从文件末尾的 `# File Information` 部分提取
11
- - `New Start`: 当前 diff 块在变更后文件中的起始行号
12
- - `New Count`: 当前 diff 块的行数
13
- - `New Path`: 文件路径(去掉 `a/` `b/` 前缀)
14
- - **注意**:元数据在文件末尾,`@@` 行已被移除,不影响行号计数
15
-
16
- 3. **解析行号**:**基于 diff 块内容逐行计数,计算绝对行号**
17
- - **行号计算方法**:
18
- 1. 从元数据中读取 `New Start` 值
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. 每往下一行(`+` 开头或空格开头),行号 +1
21
- 4. `-` 开头的删除行**不计数**,行号不增加
22
- 5. 示例:`New Start: 1` 时,第 1 行代码行号=1,第 2 行行号=2,以此类推
23
- - **忽略元数据行**:文件末尾的 `# File Information` 等以 `# ` 开头的行都**不编号**,直接跳过
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.6.11
4
+ **当前版本**: 2.7.4
5
5
  **作者**: tao.jing
6
- **最后更新**: 2026-04-17
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} 个diff块`);
68
+ debugLog(`获取到 ${diffs.length} 个文件变更`);
69
69
 
70
- // 对每个diff进一步按变更块拆分并审核
71
- debugLog('开始处理所有diff块的变更块拆分');
70
+ // 按文件分组所有块(关键改进:同一文件的所有块合并审查)
71
+ debugLog('开始按文件分组所有变更块');
72
+ const fileBlocksMap = new Map(); // 文件路径 → 所有块数组
72
73
 
73
- // 创建处理单个块的函数
74
- const processBlock = async (diffObject, blockIndex) => {
75
- // 创建临时文件存储diff内容,文件地址选择当前文件夹下,避免权限问题
76
- const fileName = `temp-diff-block-${Date.now()}-${blockIndex}.diff`;
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
- // 打印 diff 块完整信息(用于调试行号问题)
81
- debugLog(`========== Diff Block ${blockIndex} 开始 ==========`);
82
- debugLog(`文件路径:${diffObject.new_path || diffObject.old_path}`);
83
- debugLog(`line_info: old_start=${diffObject.line_info?.old_start}, old_count=${diffObject.line_info?.old_count}, new_start=${diffObject.line_info?.new_start}, new_count=${diffObject.line_info?.new_count}`);
84
- debugLog(`diff 内容:${diffObject.diff}`);
85
- debugLog(`========== Diff Block ${blockIndex} 结束 ==========`);
86
-
87
- // 解析 diff 内容,移除 @@ 行,将其信息放入元数据
88
- // 这样 AI 不会把 @@ 行误计数
89
- const diffLines = diffObject.diff.split('\n');
90
- const codeLines = diffLines.filter(line => !line.startsWith('@@')).join('\n');
91
-
92
- // 构造临时文件内容:纯代码在前,元数据在后
93
- const diffContentWithMetadata = `${codeLines}
94
-
95
- # File Information
96
- # New Path: ${diffObject.new_path || 'N/A'}
97
- # New Start: ${diffObject.line_info?.new_start || 1}
98
- # New Count: ${diffObject.line_info?.new_count || 1}
99
- # 行号计算:从第 1 行代码开始计数,第 1 行行号=NewStart,每往下一行行号 +1,#开头的元数据行不计数`;
100
-
101
- // 将diff内容写入临时文件
102
- fs.writeFileSync(tmpFileName, diffContentWithMetadata);
103
-
104
- // 输出临时文件前 10 行用于调试(确认元数据格式)
105
- const tmpFileLines = diffContentWithMetadata.split('\n').slice(0, 10).join('\n');
106
- debugLog(`临时文件前 10 行预览:\n${tmpFileLines}`);
107
-
108
- // 审核当前块(传入临时的文件而不是直接的diff内容)
109
- const blockStartTime = Date.now();
110
- const review_result = await this.reviewDiffWithClaudeUsingFile(tmpFileName);
111
- const blockObj = { ...diffObject, review_result, temp_file_path: tmpFileName };
112
- // 记录审查指标
113
- const reviewTime = Date.now() - blockStartTime;
114
- const hasSeriousProblems = blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('严重问题');
115
- const problemsCount = hasSeriousProblems ? 1 : 0; // 简化计算,实际可以更复杂
116
- const diffSize = diffObject.diff?.length || 0;
117
- const filePath = diffObject.new_path || diffObject.old_path || '';
118
- this.metrics.recordBlockReviewed(reviewTime, problemsCount, hasSeriousProblems, diffSize, filePath);
119
-
120
- // 检查审查结果中是否包含严重问题,只有包含严重问题才发布评论
121
- if (blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('严重问题')) {
122
- // 立即发布评论
123
- await this.postSingleCommentToGitLab(projectId, mergeRequestIid, {
124
- diff_info: blockObj,
125
- block_index: blockObj.block_index,
126
- review_result: blockObj.review_result,
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(`该块不包含严重问题,跳过评论发布: ${blockObj.new_path || blockObj.old_path}#${blockObj.block_index}`);
176
+ debugLog(`文件 ${filePath} 不包含严重问题,跳过评论发布`);
130
177
  }
131
178
 
179
+ debugLog(`========== 文件审查完成: ${filePath} ==========`);
180
+
132
181
  return {
133
- diff_info: blockObj,
134
- block_index: blockObj.block_index,
135
- review_result: blockObj.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 allBlocks = [];
153
- for (const diff of diffs) {
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} 个diff block块`);
205
+ debugLog(`总共处理了 ${results.length} 个文件`);
166
206
 
167
- debugLog('所有diff块审核并发布评论完成');
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(`开始审核文件: ${filePath}`);
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(`调用本地AI命令审核文件 (尝试 ${attempts}/${maxAttempts})`);
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(`本地AI命令审核结果为空`);
255
+ debugLog(`[${blockIdentifier}] 本地AI命令审核结果为空`);
215
256
  return { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n</REPORT>', lineInfo: '[]' };
216
257
  }
217
- debugLog(`本地 AI 命令审核完成,审核结果长度:${claudeResult?.length || 0}`);
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(`【决策】LINE_INFO 为空或不存在,说明无问题,直接返回标准空格式`);
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(`严重问题检查:hasSeriousProblem=${hasSeriousProblem}`);
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(`标题检查:hasValidTitle=${hasValidTitle}`);
295
+ debugLog(`[${blockIdentifier}] 标题检查:hasValidTitle=${hasValidTitle}`);
255
296
 
256
297
  if (hasValidTitle) {
257
- debugLog(`【决策】报告包含严重问题且标题正确,接受结果 (尝试 ${attempts})`);
298
+ debugLog(`[${blockIdentifier}] 【决策】报告包含严重问题且标题正确,接受结果 (尝试 ${attempts})`);
258
299
  return extractReportContent(claudeResult);
259
300
  }
260
301
 
261
302
  // 有严重问题但标题不符合 → 重试
262
- debugLog(`【决策】报告包含严重问题但标题不符合要求 (尝试 ${attempts}),将重试...`);
303
+ debugLog(`[${blockIdentifier}] 【决策】报告包含严重问题但标题不符合要求 (尝试 ${attempts}),将重试...`);
263
304
  if (attempts >= maxAttempts) {
264
- debugLog(`【决策】已达到最大重试次数 ${maxAttempts},返回最后一次结果`);
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.diffObject, task.blockIndex)
389
+ // 创建一个异步任务(直接传递 task 对象,不再拆分参数)
390
+ const promise = processor(task)
296
391
  .then(result => {
297
392
  results.push(result);
298
393
  // 从执行队列中移除已完成的任务
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job51-gitlab-cr-node-jt-1",
3
- "version": "2.7.3",
3
+ "version": "2.7.5",
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": {