job51-gitlab-cr-node-jt-1 2.7.4 → 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.7.3
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,9 +27,29 @@
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
+
30
49
  ### v2.7.3 (2026-04-17)
31
50
 
32
- **当前版本**: 技术文档版本更新
51
+ **优化**:
52
+ - **技术文档版本同步**:更新文档版本号与 package.json 保持一致
33
53
 
34
54
  ### v2.7.2 (2026-04-17)
35
55
 
package/index.js CHANGED
@@ -63,81 +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
- // 构造块标识:文件路径#块索引,用于日志追踪
111
- const blockIdentifier = `${diffObject.new_path || diffObject.old_path}#${blockIndex}`;
112
- const review_result = await this.reviewDiffWithClaudeUsingFile(tmpFileName, blockIdentifier);
113
- const blockObj = { ...diffObject, review_result, temp_file_path: tmpFileName };
114
- // 记录审查指标
115
- const reviewTime = Date.now() - blockStartTime;
116
- const hasSeriousProblems = blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('严重问题');
117
- const problemsCount = hasSeriousProblems ? 1 : 0; // 简化计算,实际可以更复杂
118
- const diffSize = diffObject.diff?.length || 0;
119
- const filePath = diffObject.new_path || diffObject.old_path || '';
120
- this.metrics.recordBlockReviewed(reviewTime, problemsCount, hasSeriousProblems, diffSize, filePath);
121
-
122
- // 检查审查结果中是否包含严重问题,只有包含严重问题才发布评论
123
- if (blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('严重问题')) {
124
- // 立即发布评论
125
- await this.postSingleCommentToGitLab(projectId, mergeRequestIid, {
126
- diff_info: blockObj,
127
- block_index: blockObj.block_index,
128
- review_result: blockObj.review_result,
129
- });
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
+ }
130
175
  } else {
131
- debugLog(`该块不包含严重问题,跳过评论发布: ${blockObj.new_path || blockObj.old_path}#${blockObj.block_index}`);
176
+ debugLog(`文件 ${filePath} 不包含严重问题,跳过评论发布`);
132
177
  }
133
178
 
179
+ debugLog(`========== 文件审查完成: ${filePath} ==========`);
180
+
134
181
  return {
135
- diff_info: blockObj,
136
- block_index: blockObj.block_index,
137
- review_result: blockObj.review_result,
182
+ file_path: filePath,
183
+ blocks_count: blocks.length,
184
+ review_result,
138
185
  temp_file_path: tmpFileName,
139
186
  };
140
187
  } catch (error) {
188
+ console.error(`文件 ${filePath} 审查失败:`, error.message);
141
189
  throw error;
142
190
  } finally {
143
191
  try {
@@ -150,23 +198,13 @@ class GitLabCodeReviewer {
150
198
  }
151
199
  };
152
200
 
153
- // 收集所有需要处理的块
154
- const allBlocks = [];
155
- for (const diff of diffs) {
156
- const diffObjects = this.getDiffBlocks(diff);
157
- for (let i = 0; i < diffObjects.length; i++) {
158
- // 更新块索引
159
- diffObjects[i].block_index = i;
160
- allBlocks.push({ diffObject: diffObjects[i], blockIndex: i });
161
- }
162
- }
163
-
164
- // 使用线程池控制并发数量
165
- 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);
166
204
 
167
- debugLog(`总共处理了 ${results.length} 个diff block块`);
205
+ debugLog(`总共处理了 ${results.length} 个文件`);
168
206
 
169
- debugLog('所有diff块审核并发布评论完成');
207
+ debugLog('所有文件审核并发布评论完成');
170
208
  // 结束审查统计
171
209
  this.metrics.endReview();
172
210
 
@@ -278,6 +316,60 @@ class GitLabCodeReviewer {
278
316
  }
279
317
  }
280
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
+
281
373
  /**
282
374
  * 使用线程池控制并发执行
283
375
  * @param {Array} tasks 任务数组
@@ -294,8 +386,8 @@ class GitLabCodeReviewer {
294
386
  for (let i = 0; i < tasks.length; i++) {
295
387
  const task = tasks[i];
296
388
 
297
- // 创建一个异步任务
298
- const promise = processor(task.diffObject, task.blockIndex)
389
+ // 创建一个异步任务(直接传递 task 对象,不再拆分参数)
390
+ const promise = processor(task)
299
391
  .then(result => {
300
392
  results.push(result);
301
393
  // 从执行队列中移除已完成的任务
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job51-gitlab-cr-node-jt-1",
3
- "version": "2.7.4",
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": {