job51-gitlab-cr-node-jt-1 2.2.9 → 2.3.0

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.
@@ -3,14 +3,36 @@
3
3
  **核心原则:高准确率、低误报、深度上下文分析、风格一致性**
4
4
 
5
5
  **⚠️ 最高优先级强制规则(违反将导致严重误报)**:
6
- > - **报告运行时异常风险前,必须先用 Read 工具追踪读取相关方法/函数的实现代码**
7
- > - 适用场景:空指针/空引用异常、类型转换异常、集合越界等所有运行时异常
8
- > - 示例:看到 `obj = service.getData(id); obj.getProperty()` 时,**必须先读取** `service.getData` 方法的源码,确认其返回值是否可能为 null
9
- > - 如果方法/函数内部已有防护措施(如返回默认对象、空对象模式、边界检查等),则**禁止报告**该问题
10
- > - **禁止仅凭代码表面形式判断运行时异常风险**
11
- > - **⚠️ 特别强调**:当前 CR 流程处于版本开发的最后合并阶段,代码逻辑已经过开发自测和 QA 测试验证。审查时应**基于项目原有的代码风格**,不得强制要求添加项目惯例中不存在的内容。
12
- > - **⚠️ 风格一致性原则**:参数校验、异常处理、注解使用等应与项目原有风格匹配,**不得报告"建议添加 XX 注解/XX 校验/XX 异常处理"** 这类问题。
13
- > - **⚠️ 问题定位原则**:**只在真正会执行出错的代码行上报告问题**,其他地方(如类定义、导入语句、方法签名等)禁止报告
6
+
7
+ 1. **审查范围限制**(关键规则):
8
+ > - **只审查当前 diff 块内的新增代码**(+ 开头的行)
9
+ > - **只报告当前 diff 块内能直接发现的问题**
10
+ > - **不要追踪方法调用链去其他文件中报告问题**(如"XX 方法返回 null,导致其他地方空指针")
11
+ > - 可以分析方法返回值是否可能为 null,但**问题只能报告在当前调用处**
12
+
13
+ 2. **同一问题只报告一次**:
14
+ > - 同一个空指针问题可能在多处出现(import 行、依赖注入行、方法调用行)
15
+ > - **只在真正会执行出错的代码行上报告一次**
16
+ > - 示例:`@Autowired UserTaskSDK userTaskSDK;` 和 `userTaskSDK.xxx()` 都有空指针风险
17
+ > - **正确做法**:只在 `userTaskSDK.xxx()` 调用行报告,**禁止**在依赖注入行报告
18
+
19
+ 3. **禁止报告问题的位置**:
20
+ > - import 导入语句行
21
+ > - 类定义、package 声明行
22
+ > - 方法签名行(除非签名本身有安全问题)
23
+ > - 依赖注入/字段声明行(如 `@Autowired XXX xxx;`)
24
+ > - 仅定义变量但未使用的行
25
+ > - 其他不会直接导致运行时错误的位置
26
+
27
+ 4. **深度分析但精准报告**:
28
+ > - **报告运行时异常风险前,必须先用 Read 工具追踪读取相关方法/函数的实现代码**
29
+ > - 适用场景:空指针/空引用异常、类型转换异常、集合越界等所有运行时异常
30
+ > - 示例:看到 `obj = service.getData(id); obj.getProperty()` 时,**必须先读取** `service.getData` 方法的源码,确认其返回值是否可能为 null
31
+ > - 如果方法/函数内部已有防护措施(如返回默认对象、空对象模式、边界检查等),则**禁止报告**该问题
32
+ > - **禁止仅凭代码表面形式判断运行时异常风险**
33
+
34
+ 5. **⚠️ 特别强调**:当前 CR 流程处于版本开发的最后合并阶段,代码逻辑已经过开发自测和 QA 测试验证。审查时应**基于项目原有的代码风格**,不得强制要求添加项目惯例中不存在的内容。
35
+ > - **⚠️ 风格一致性原则**:参数校验、异常处理、注解使用等应与项目原有风格匹配,**不得报告"建议添加 XX 注解/XX 校验/XX 异常处理"** 这类问题。
14
36
 
15
37
  0. **Diff 数据结构与上下文读取规则**:
16
38
  > - **临时文件格式说明**:
@@ -88,6 +110,10 @@
88
110
  > - 方法签名行(除非签名本身有安全问题)
89
111
  > - 仅定义变量但未使用的行
90
112
  > - 其他不会直接导致运行时错误的位置
113
+ > - **Feign Client 远程调用返回值校验**:
114
+ > - Feign Client 接口调用(如 `xxClient.getXxx()`)的返回值**不纳入空指针审查范围**
115
+ > - Feign 框架会在调用失败时抛出异常,而不是返回 null
116
+ > - 示例:`Result<UserTaskPop> userTaskPop = userTaskClient.getUserTaskPop(accountId);` 后续调用 `userTaskPop.requestSucceeded()` **不应报告 NPE 问题**
91
117
  5. 使用当前系统时间填充 `[当前时间]`,并根据实际变更内容计算 `[文件数量]`;
92
118
  6. 所有占位符必须被真实内容替换;
93
119
  > - **修改建议中的代码示例**:
@@ -119,51 +145,38 @@
119
145
  > - 示例:`UserTaskPop pop = sdk.method(); log.debug(pop.getId())` 问题应报告在 `pop.getId()` 行,不是 `sdk.method()` 或变量定义行
120
146
  > - **每个 diff 块只报告本块内实际会出错的代码**,不要在无关的 diff 块(如 package 行、import 行、类定义行)报告问题
121
147
 
122
- 10. **警告问题(⚠️)**:
123
- > - 代码可读性差(魔法数字、命名不规范、缺少必要注释)
124
- > - 性能潜在问题(低效算法、不必要的数据库查询、内存使用不当)
125
- > - 边界条件处理不完善(但不导致运行时异常)
126
- > - 异常处理不完善(捕获异常未记录或吞掉异常)
127
- > - 资源使用不规范(未关闭资源但无泄漏风险)
128
- > - 代码重复或违反 DRY 原则
129
- > - 类型安全警告(原始类型使用、unchecked 转换)
130
- > - **强制排除**:警告中**不得包含**空指针/空引用、类型转换/拆箱、集合越界等运行时异常描述
131
- > - **行号去重**:若某行号已在严重问题中报告,警告中**禁止**再出现
132
- > - **风格一致性排除**:**不得报告"建议添加 XX 注解/XX 校验/XX 异常处理"** 这类问题,除非项目已有明确惯例而此处遗漏
133
-
134
- 11. **问题描述准确性**:
135
- > - 若代码变更可能导致运行时异常,必须优先描述异常风险,不得降级为性能或健壮性描述
136
-
137
- 12. **问题分级规则**:
148
+ 11. **问题分级规则**:
138
149
  > - 可能导致运行时异常/错误、数据丢失/损坏、安全漏洞的,**必须**归类为严重问题
139
150
  > - 同一代码块的多个问题,全部归入同一风险级别并依次展示
140
151
 
141
- 13. 最终输出必须以 `<REPORT>` 开始,以 `</REPORT>` 结束;
142
-
143
- 14. **行号计算规范**:
144
- > - **Diff 头信息格式**:`@@ -84,9 +84,11 @@` 表示变更后代码从原始文件的第 84 行开始,共有 11 行
145
- > - **行号计算**:
146
- > - 空格开头(上下文):行号 = 起始行号 + 已处理的上下文行数
147
- > - `-` 开头(删除):不计入变更后行号
148
- > - `+` 开头(新增):行号 = 起始行号 + 已处理的保留/新增行数
149
- > - **问题行号范围**:
150
- > - 单行问题:`UserService.java:87`
151
- > - 多行问题:`UserService.java:90-91`
152
- > - **关键**:问题行号必须准确指向变更后代码的实际行号,不得引用已删除的行
153
-
154
- 15. `<REPORT>` 标签示例中的问题块数量仅用于展示格式,实际数量由 review 结果决定;
155
-
156
- 16. **输出前验证流程**:
157
- > - **去重检查**:检查是否存在行号重叠,若发现重叠仅保留最高优先级(严重问题 > 警告)
158
- > - **跨块重复问题去重**:**同一文件同一问题在不同 diff 块中出现时,只报告一次**
159
- > - 示例:同一方法 `userTaskSDK.userTaskModuleControl()` 在块 1、块 2、块 3 中被调用,未判空问题**只报告一次**
160
- > - 方法:**按「文件 + 问题类型 + 根本原因」进行去重**,相同组合的问题只保留第一个
161
- > - **警告验证**:检查警告中是否包含运行时异常关键词,若包含则升级为严重问题
152
+ 12. 最终输出必须以 `<REPORT>` 开始,以 `</REPORT>` 结束;
153
+
154
+ 13. **行号计算规范**:
155
+ > - **Diff 头信息格式**:`@@ -84,9 +84,11 @@`
156
+ > - `-84,9`:旧文件从第 84 行开始,共 9 行
157
+ > - `+84,11`:新文件从第 84 行开始,共 11
158
+ > - **行号计算**(绝对行号,非相对偏移):
159
+ > - 初始值 = `new_start`(从 diff 头解析)
160
+ > - 空格开头(上下文):当前行号 = 计数器值,计数器 +1
161
+ > - `-` 开头(删除):跳过,计数器不变
162
+ > - `+` 开头(新增):当前行号 = 计数器值,计数器 +1
163
+ > - **示例**:`@@ -0,0 +1,49 @@`(新增文件)
164
+ > - 第 1 行 `+`:行号 = 1
165
+ > - 2 `+`:行号 = 2
166
+ > - **关键**:问题行号必须准确指向变更后代码的实际行号(绝对行号)
167
+
168
+ 14. `<REPORT>` 标签示例中的问题块数量仅用于展示格式,实际数量由 review 结果决定;
169
+
170
+ 15. **输出前验证流程**:
171
+ > - **块内去重检查**:**同一 diff 块内由同一根本原因导致的问题,只报告一次**
172
+ > - 示例:同一个变量 `userTaskSDK` 在块内多处被调用,若都存在未判空问题,**合并为一条报告**
173
+ > - 方法:**按「变量名 + 问题类型 + 根本原因」进行去重**,相同组合的问题只保留第一个
174
+ > - **注意**:每个 diff 块是独立审查的,无法跨块去重,只需保证当前块内不重复
162
175
  > - **代码示例验证**:确保错误代码示例不包含 `-` 开头的已删除代码
163
176
  > - **删除代码验证**:**逐条检查问题涉及的代码行,确认不是 `-` 开头的已删除代码**
164
177
  > - **安全问题验证**:确保不针对已删除代码报告安全问题
165
178
 
166
- 17. **代码变更方向识别**:
179
+ 16. **代码变更方向识别**:
167
180
  > - `-` 开头:**已删除的代码**,**禁止**作为审查对象,**禁止**报告任何问题
168
181
  > - `+` 开头:**新增的代码**,是审查的唯一重点
169
182
  > - 空格开头:未变更的上下文代码,仅作参考
@@ -172,9 +185,12 @@
172
185
  > - **⚠️ 问题报告位置规则**:
173
186
  > - **空指针/未判空问题**:只报告在**实际调用方法访问返回值的行**(如 `obj.getXxx()`),而不是方法调用或变量定义的行
174
187
  > - **示例**:`UserTaskPop pop = sdk.method(); pop.getId()` 问题应报告在 `pop.getId()` 这一行,而不是 `sdk.method()` 或变量定义行
188
+ > - **依赖注入/字段声明**:如 `@Autowired UserTaskSDK userTaskSDK;`,**禁止**在此行报告空指针问题
189
+ > - **Import 语句**:**禁止**在 import 行报告任何问题
190
+ > - **同一变量多次使用**:只在第一次可能出错的地方报告一次
175
191
  > - **禁止**在类定义、package、import、方法签名等位置报告与这些行无关的问题
176
192
 
177
- 18. **变更内容解析步骤**(分析前必须按顺序执行):
193
+ 17. **变更内容解析步骤**(分析前必须按顺序执行):
178
194
  > - **步骤 1:识别变更类型**
179
195
  > - 遍历代码块,标记每行类型:`-`(删除)、`+`(新增)、空格(保留)
180
196
  > - **步骤 2:丢弃已删除代码**(关键步骤)
@@ -194,9 +210,22 @@
194
210
  > - 空指针问题:报告在实际调用 `obj.getXxx()` 的行,不是变量定义行
195
211
  > - 禁止在 package、import、类定义、方法签名等位置报告无关问题
196
212
 
197
- 19. **输出格式**:严格参照 SKILL.md 模板,无某类问题时省略对应部分;
198
-
199
- 20. **误报防控检查清单**(报告前必须确认):
213
+ 18. **输出格式**:严格参照 SKILL.md 模板,无某类问题时省略对应部分;
214
+ > - **必须输出 `<LINE_INFO>` 标签**:包含所有问题的行号信息
215
+ > - **LINE_INFO 格式**(与 targetLine 结构一致):
216
+ ```json
217
+ [{"new_path":"文件路径","new_line":行号,"old_path":"文件路径","old_line":行号}]
218
+ ```
219
+ > - **字段说明**:
220
+ - `new_path`: 变更后文件路径(必填)
221
+ - `new_line`: 变更后代码的**绝对行号**(必填,指向 `+` 开头的新增代码在文件中的实际行号)
222
+ - `old_path`: 变更前文件路径(可选,仅删除代码相关问题使用)
223
+ - `old_line`: 变更前代码的**绝对行号**(可选,仅删除代码相关问题使用)
224
+ > - **行号计算**:参考第 13 条 行号计算规范
225
+ > - **行号定位**:参考第 16 条 问题报告位置规则
226
+ > - 无问题时输出空数组:`<LINE_INFO>[]</LINE_INFO>`
227
+
228
+ 19. **误报防控检查清单**(报告前必须确认):
200
229
  > - [ ] 已追踪读取相关方法实现,确认可能返回危险值
201
230
  > - [ ] 已跨 diff 块分析,确认不是已被新逻辑替代的删除代码
202
231
  > - [ ] 已分析方法内联上下文,确认没有前置判空保护
@@ -1,55 +1,52 @@
1
- ---
2
- name: simple-code-review
3
- description: 你是一个专业的代码审查助手。请严格根据代码规范、安全规则(如 SQL 注入防护、XSS 防护)、性能要求(如时间复杂度优化、内存占用控制)、可读性要求(如注释完整性、命名规范性)及功能正确性标准处理指定文件中的代码变更内容
4
- ---
5
-
6
- ## 处理步骤
7
- 1. 首先使用 Read 工具读取 diff 文件:$ARGUMENTS
8
- 2. **解析文件信息**:从文件内容中提取以下信息
9
- - New Path: 变更后的文件路径(可能包含 `b/` 前缀)
10
- - Old Path: 变更前的文件路径(可能包含 `a/` 前缀)
11
- - Diff Content: 实际的代码变更内容
12
- 3. **处理文件路径**:去掉路径中的 `a/` `b/` 前缀,得到实际的项目相对路径
13
- 4. **读取项目上下文文件**:基于解析出的文件路径,主动读取相关的项目文件(详见规则文档)
14
- 5. **深度分析代码上下文(必须执行)**:
15
- - 对于diff中涉及的方法/函数调用,**必须**使用 Read 工具追踪读取该方法的实现代码
16
- - 示例:若看到 `accountInfo = userSDK.getAccountInfo(id, false); accountInfo.getXxx()`,必须读取 `userSDK.getAccountInfo` 方法的实现,确认其返回值是否可能为 null
17
- - **禁止**仅凭代码表面形式判断存在空指针/空引用等运行时异常
18
- - **跨 diff 块分析**:同一文件可能有多个 diff 块,必须合并分析。如块A删除了 `obj.setValue(xxx)`,但块B新增了 `if(condition) obj.setValue(yyy)`,不应报告"删除了set值会影响数据"
19
- - 只有在确认方法/函数确实可能返回危险值时,才能报告严重问题
20
- 6. **结合项目上下文**,按照 @.claude/rules/code-review-rules.md 中的规则进行审查
21
- 7. 输出格式必须以下方模板为准
22
-
23
- ## 输出模板
24
-
25
- ``**说明**:
26
- - 必须以 `<REPORT>` 开始,以 `</REPORT>` 结束
27
- - 若某类问题不存在,则**完全省略该模块**(不输出对应标题)
28
- - 所有占位符必须被真实内容替换
29
- - 带圈数字序号:① ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩``
30
-
31
- <REPORT>
32
- ## 🤖 AI 代码审查结果
33
-
34
- **生成时间**: [当前时间]
35
- **审查范围**: [文件数量] 个文件
36
-
37
- ---
38
-
39
- ### 审查总览
40
-
41
- [简要总结]
42
-
43
- ### 🔴 严重问题
44
-
45
- [带圈数字序号][问题描述]<br/>
46
- **文件及行号**:[完整文件路径:起始行 - 结束行,如:src/main/java/com/example/UserService.java:87-91]<br/>
47
- **修改建议**:[修复建议 + 错误代码 + 正确示例]
48
-
49
- ### 🟡 警告
50
-
51
- [带圈数字序号][问题描述]<br/>
52
- **文件及行号**:[完整文件路径:起始行 - 结束行]<br/>
53
- **修改建议**:[改进建议 + 代码示例]
54
-
55
- </REPORT>
1
+ ---
2
+ name: simple-code-review
3
+ description: 代码审查技能,审查变更代码并输出 REPORT LINE_INFO
4
+ ---
5
+
6
+ ## 处理步骤
7
+
8
+ 1. **读取 diff 文件**:使用 Read 工具读取文件:$ARGUMENTS
9
+
10
+ 2. **解析文件信息**:从 `=== File Information ===` 部分提取
11
+ - `Block Index`: 当前块索引号
12
+ - `Line Info`: 行号信息 JSON 对象
13
+ - `New Path` / `Old Path`: 文件路径(去掉 `a/` 或 `b/` 前缀)
14
+
15
+ 3. **解析行号**:从 `Line Info` 中获取 `new_start`,遍历 diff 内容计算每行行号
16
+ - 行号计算规则:
17
+ - 从 diff 头 `@@ -old_start,old_count +new_start,new_count @@` 中提取 `new_start`
18
+ - 遍历 diff 内容,只计数 `+` 开头(新增)和空格开头(上下文)的行
19
+ - 第 1 行 `+` 或空格:行号 = `new_start`
20
+ - 2 行 `+` 或空格:行号 = `new_start + 1`
21
+ - 以此类推,`-` 开头的删除行不计入行号
22
+ - **重要**:行号是变更后文件中的绝对行号,必须准确对应实际代码位置
23
+
24
+ 4. **读取上下文**:基于 New Path 读取变更后文件,涉及方法调用时追踪读取实现代码
25
+
26
+ 5. **执行审查**:按照 @.claude/rules/code-review-rules.md 中的规则进行审查
27
+
28
+ 6. **输出结果**:
29
+ - 审查结果放入 `<REPORT>` 标签
30
+ - 行号信息放入 `<LINE_INFO>` 标签(必须输出)
31
+ - LINE_INFO 格式参考 @.claude/rules/code-review-rules.md 第 19 条
32
+
33
+ ## 输出模板
34
+
35
+ <REPORT>
36
+ ## 🤖 AI 代码审查结果
37
+
38
+ ### 🔴 严重问题
39
+
40
+ [带圈数字序号][问题描述]<br/>
41
+ **文件及行号**:[文件路径:行号,如:51job-dev-boot-bbs/src/main/java/com/job51/dev/bbs/service/impl/ForumCategoryForumServiceImpl.java:40]<br/>
42
+ **修改建议**:[正确示例代码]
43
+
44
+ </REPORT>
45
+
46
+ <LINE_INFO>
47
+ [{"new_path":"文件路径","new_line":行号,"old_path":"文件路径","old_line":行号}]
48
+ </LINE_INFO>
49
+
50
+ **说明**:
51
+ - 无问题时省略对应标题,但 `<LINE_INFO>` 必须输出(空数组:`[]`)
52
+ - 行号格式参考 @.claude/rules/code-review-rules.md 第 19 条
package/index.js CHANGED
@@ -78,6 +78,7 @@ class GitLabCodeReviewer {
78
78
 
79
79
 
80
80
  // 提取REPORT标签内容并返回
81
+ // 提取 REPORT 标签和 LINE_INFO 标签内容并返回对象
81
82
  return extractReportContent(claudeResult);
82
83
  } else {
83
84
  debugLog(`AI审核结果不包含"🤖 AI 代码审查结果" (尝试 ${attempts}),将重试...`);
@@ -85,7 +86,8 @@ class GitLabCodeReviewer {
85
86
  debugLog(`已达到最大重试次数 ${maxAttempts},返回最后一次结果,${claudeResult}`);
86
87
 
87
88
  // 提取REPORT标签内容并返回
88
- return extractReportContent(claudeResult);
89
+ // 提取 REPORT 标签和 LINE_INFO 标签内容并返回对象
90
+ return extractReportContent(claudeResult);
89
91
  }
90
92
  }
91
93
  } catch (error) {
@@ -138,9 +140,8 @@ ${diffObject.diff}`;
138
140
  const blockObj = { ...diffObject, review_result, temp_file_path: tmpFileName };
139
141
 
140
142
  // 检查审查结果中是否包含严重问题,只有包含严重问题才发布评论
141
- if (blockObj.review_result && blockObj.review_result.includes('🔴 严重问题')) {
143
+ if (blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('🔴 严重问题')) {
142
144
  // 立即发布评论
143
- console.log(`🤖 AI 代码审查结果: ${blockObj.review_result}`);
144
145
  await this.postSingleCommentToGitLab(projectId, mergeRequestIid, {
145
146
  diff_info: blockObj,
146
147
  block_index: blockObj.block_index,
@@ -201,11 +202,18 @@ ${diffObject.diff}`;
201
202
  const prompt = `请调用 simple-code-review 技能审核代码变更。
202
203
  文件路径:${filePath}
203
204
 
204
- 输出要求:
205
+ **重要审查规则**:
206
+ 1. **只审查当前 diff 块内的新增代码**(+ 开头的行)
207
+ 2. **只报告当前 diff 块内能直接发现的问题**,不要追踪方法调用链去其他地方报告问题
208
+ 3. **同一问题只报告一次**:如 import 行、声明行、调用行都有问题,只在真正会出错的调用行报告一次
209
+ 4. **禁止报告的位置**:import 语句、类定义、方法签名、依赖注入声明行
210
+
211
+ **输出要求**:
205
212
  1. 严格按照 .claude/skills/simple-code-review/SKILL.md 中定义的模板格式输出
206
213
  2. **每个模块不是必须的**:没有对应问题时,完全省略该模块(不输出标题)
207
214
  3. 必须以 <REPORT> 开始,以 </REPORT> 结束
208
- 4. 不要输出任何额外的解释、问候或总结文本`;
215
+ 4. **必须输出 '<LINE_INFO>' 标签**,包含所有问题的行号信息(无问题时输出空数组 [])
216
+ 5. 不要输出任何额外的解释、问候或总结文本`;
209
217
  //打印
210
218
  debugLog(`Claude命令: ${prompt}`);
211
219
  // 最多重试5次,直到结果包含"🤖 AI 代码审查结果"或达到最大重试次数
@@ -229,6 +237,7 @@ ${diffObject.diff}`;
229
237
  debugLog(`AI审核成功,包含"🤖 AI 代码审查结果" (尝试 ${attempts})`);
230
238
 
231
239
  // 提取REPORT标签内容并返回
240
+ // 提取 REPORT 标签和 LINE_INFO 标签内容并返回对象
232
241
  return extractReportContent(claudeResult);
233
242
  } else {
234
243
  debugLog(`AI审核结果不包含"🤖 AI 代码审查结果" (尝试 ${attempts}),将重试...`);
@@ -236,7 +245,8 @@ ${diffObject.diff}`;
236
245
  debugLog(`已达到最大重试次数 ${maxAttempts},返回最后一次结果,${claudeResult}`);
237
246
 
238
247
  // 提取REPORT标签内容并返回
239
- return extractReportContent(claudeResult);
248
+ // 提取 REPORT 标签和 LINE_INFO 标签内容并返回对象
249
+ return extractReportContent(claudeResult);
240
250
  }
241
251
  }
242
252
  } catch (error) {
@@ -352,6 +362,48 @@ ${diffObject.diff}`;
352
362
  });
353
363
  }
354
364
 
365
+ /**
366
+ * 从 LINE_INFO 标签字符串中解析行号信息
367
+ * @param {string} lineInfoTag LINE_INFO 标签字符串,如 "<LINE_INFO>[...]</LINE_INFO>"
368
+ * @returns {Object|null} 行号信息对象 {new_path, new_line, old_path, old_line} 或 null
369
+ */
370
+ parseLineInfoFromReviewResult(lineInfoTag) {
371
+ if (!lineInfoTag) return null;
372
+
373
+ try {
374
+ // 从标签中提取 JSON 内容:<LINE_INFO>[{...}]</LINE_INFO>
375
+ const jsonContent = lineInfoTag.replace(/<LINE_INFO>\s*/g, '').replace(/\s*<\/LINE_INFO>/g, '').trim();
376
+
377
+ debugLog(`解析 LINE_INFO 原始内容:${lineInfoTag}`);
378
+ debugLog(`解析 LINE_INFO JSON 内容:${jsonContent}`);
379
+
380
+ // 如果是空数组,返回 null 使用后备方案
381
+ if (jsonContent === '[]') {
382
+ debugLog('LINE_INFO 为空数组,使用 diff 块起始行号作为后备方案');
383
+ return null;
384
+ }
385
+
386
+ const lineInfoArray = JSON.parse(jsonContent);
387
+ debugLog(`解析 LINE_INFO JSON 成功:${JSON.stringify(lineInfoArray)}`);
388
+
389
+ if (Array.isArray(lineInfoArray) && lineInfoArray.length > 0) {
390
+ debugLog(`从 LINE_INFO 中解析出行号信息:${JSON.stringify(lineInfoArray)}`);
391
+ // 返回第一个问题的行号信息
392
+ const lineInfo = lineInfoArray[0];
393
+ // GitLab API 只需要 new_path 和 new_line
394
+ return {
395
+ new_path: lineInfo.new_path,
396
+ new_line: lineInfo.new_line
397
+ };
398
+ }
399
+ } catch (error) {
400
+ debugLog(`解析 LINE_INFO JSON 失败:${error.message}`);
401
+ }
402
+
403
+ debugLog('无法从 LINE_INFO 中解析行号,将使用 diff 块起始行号作为后备方案');
404
+ return null;
405
+ }
406
+
355
407
  /**
356
408
  * 获取合并请求的最新版本信息
357
409
  * @param {number} projectId GitLab项目ID
@@ -380,6 +432,9 @@ ${diffObject.diff}`;
380
432
  async postSingleCommentToGitLab(projectId, mergeRequestIid, result) {
381
433
  try {
382
434
  const { diff_info, review_result, block_index } = result;
435
+ // review_result 现在是对象:{ reportContent, lineInfo }
436
+ const reviewContent = review_result?.reportContent || '';
437
+ const lineInfoTag = review_result?.lineInfo || null;
383
438
  const file_path = diff_info.new_path || diff_info.old_path;
384
439
  const file_path_with_line = `${file_path}#L${block_index}`;
385
440
 
@@ -399,19 +454,37 @@ ${diffObject.diff}`;
399
454
  // 直接使用diff_info中的line_info
400
455
  const lineInfo = diff_info.line_info;
401
456
 
402
- if (!lineInfo) {
457
+ if (!lineInfo && !lineInfoTag) {
403
458
  // 如果没有行号信息,创建一般讨论
404
- await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, review_result);
459
+ await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, reviewContent);
405
460
  debugLog(`评论已发布到文件 ${file_path_with_line} (无法解析行号)`);
406
461
  return;
407
462
  }
408
463
 
409
- // 如果lineInfo里的firstLineFirstChar为+,则targetLine为new_path的new_start行;为-,则targetLine为old_path的old_start行;否则new和old都要赋值
410
- const targetLine = lineInfo.firstLineFirstChar === '+'
464
+ // 如果 lineInfo 里的 firstLineFirstChar 为 +,则 targetLine new_path new_start 行;为 -,则 targetLine old_path old_start
465
+ // GitLab API 要求:diff 评论只能传递 new_line 或 old_line 中的一个,不能同时传递
466
+ let targetLine = lineInfo.firstLineFirstChar === '+'
411
467
  ? { new_line: lineInfo.new_start, new_path: diff_info.new_path }
412
468
  : lineInfo.firstLineFirstChar === '-'
413
469
  ? { old_line: lineInfo.old_start, old_path: diff_info.old_path }
414
- : { new_line: lineInfo.new_start, new_path: diff_info.new_path, old_line: lineInfo.old_start, old_path: diff_info.old_path };
470
+ : { new_line: lineInfo.new_start, new_path: diff_info.new_path };
471
+
472
+ // 尝试从审查结果中解析更精确的行号信息(从 LINE_INFO 标签)
473
+ const parsedLineInfo = lineInfoTag ? this.parseLineInfoFromReviewResult(lineInfoTag) : null;
474
+ if (parsedLineInfo) {
475
+ // 验证解析的行号是否在当前 diff 块范围内
476
+ const newStart = lineInfo.new_start;
477
+ const newCount = lineInfo.new_count || 0;
478
+ const newEnd = newStart + newCount - 1;
479
+ debugLog(`diff 块信息:new_start=${newStart}, new_count=${newCount}, 范围=[${newStart}, ${newEnd}]`);
480
+ debugLog(`解析的行号:${parsedLineInfo.new_line}`);
481
+ if (parsedLineInfo.new_line >= newStart && parsedLineInfo.new_line <= newEnd) {
482
+ debugLog(`从 LINE_INFO 中解析出行号成功,使用解析后的行号覆盖 diff 块起始行号`);
483
+ targetLine = parsedLineInfo;
484
+ } else {
485
+ debugLog(`解析的行号 ${parsedLineInfo.new_line} 不在 diff 块范围 [${newStart}, ${newEnd}] 内,使用 diff 块起始行号`);
486
+ }
487
+ }
415
488
 
416
489
  //打印targetLine
417
490
  debugLog(`targetLine: ${JSON.stringify(targetLine)}`);
@@ -419,7 +492,7 @@ ${diffObject.diff}`;
419
492
  if (targetLine) {
420
493
  // 创建diff评论
421
494
  const payload = {
422
- body: review_result,
495
+ body: reviewContent,
423
496
  position: {
424
497
  position_type: 'text',
425
498
  base_sha: baseSha,
@@ -433,13 +506,15 @@ ${diffObject.diff}`;
433
506
  await this.createDiffDiscussion(projectId, mergeRequestIid, payload);
434
507
  debugLog(`评论已发布到文件 ${file_path_with_line} 的相关变更区域`);
435
508
  } catch (error) {
509
+ // 打印详细的错误响应信息
510
+ debugLog(`GitLab API 错误详情:${JSON.stringify(error.response?.data || error.message)}`);
436
511
  console.error(`发布评论到文件 ${file_path_with_line} 的变更区域失败,改用一般讨论:`, error.message);
437
- await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, review_result);
512
+ await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, reviewContent);
438
513
  debugLog(`评论已发布到文件 ${file_path_with_line} (作为一般讨论)`);
439
514
  }
440
515
  } else {
441
516
  // 如果没有找到任何行号,创建一般讨论
442
- await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, review_result);
517
+ await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, reviewContent);
443
518
  debugLog(`评论已发布到文件 ${file_path_with_line} (无特定行号)`);
444
519
  }
445
520
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job51-gitlab-cr-node-jt-1",
3
- "version": "2.2.9",
3
+ "version": "2.3.0",
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": {
package/utils.js CHANGED
@@ -1,67 +1,74 @@
1
- /**
2
- * 工具函数集合
3
- */
4
-
5
- const axios = require('axios');
6
-
7
- // 调试日志函数
8
- function debugLog(message) {
9
- console.log('[DEBUG]', new Date().toISOString(), message);
10
- }
11
-
12
- /**
13
- * 提取REPORT标签之间的内容
14
- * @param {string} text 包含REPORT标签的文本
15
- * @returns {string} 提取后的内容
16
- */
17
- function extractReportContent(text) {
18
- const reportRegex = /<REPORT>([\s\S]*?)<\/REPORT>/;
19
- const match = text.match(reportRegex);
20
- return match ? match[1].trim() : text;
21
- }
22
-
23
- // 创建一个可配置的GitLab API客户端
24
- class GitLabAPIClient {
25
- constructor(gitlabToken, gitlabUrl = null) {
26
- this.gitlabToken = gitlabToken;
27
- this.gitlabUrl = gitlabUrl || 'https://gitdev.51job.com';
28
- this.axiosInstance = axios.create({
29
- headers: {
30
- 'PRIVATE-TOKEN': this.gitlabToken,
31
- },
32
- });
33
- }
34
-
35
- /**
36
- * 通用的GitLab API调用函数
37
- * @param {string} endpoint API端点
38
- * @param {Object} options 额外选项(method, data等)
39
- * @returns {Promise} API调用结果
40
- */
41
- async callGitLabAPI(endpoint, options = {}) {
42
- const { method = 'GET', data = null, headers = {} } = options;
43
-
44
- try {
45
- const response = await this.axiosInstance({
46
- method,
47
- url: `${this.gitlabUrl}${endpoint}`,
48
- headers: {
49
- 'Content-Type': 'application/json',
50
- ...headers
51
- },
52
- data
53
- });
54
-
55
- return response.data;
56
- } catch (error) {
57
- console.error(`GitLab API调用失败: ${method} ${endpoint}`, error.message);
58
- throw error;
59
- }
60
- }
61
- }
62
-
63
- module.exports = {
64
- GitLabAPIClient,
65
- debugLog,
66
- extractReportContent
67
- };
1
+ /**
2
+ * 工具函数集合
3
+ */
4
+
5
+ const axios = require('axios');
6
+
7
+ // 调试日志函数
8
+ function debugLog(message) {
9
+ console.log('[DEBUG]', new Date().toISOString(), message);
10
+ }
11
+
12
+ /**
13
+ * 提取 REPORT 标签和 LINE_INFO 标签的内容
14
+ * @param {string} text 包含 REPORT 和 LINE_INFO 标签的文本
15
+ * @returns {{reportContent: string, lineInfo: string|null}} 包含 REPORT 内容和 LINE_INFO 标签的对象
16
+ */
17
+ function extractReportContent(text) {
18
+ const reportRegex = /<REPORT>([\s\S]*?)<\/REPORT>/;
19
+ const lineInfoRegex = /<LINE_INFO>[\s\S]*?<\/LINE_INFO>/;
20
+
21
+ const reportMatch = text.match(reportRegex);
22
+ const lineInfoMatch = text.match(lineInfoRegex);
23
+
24
+ const reportContent = reportMatch ? reportMatch[1].trim() : text;
25
+ const lineInfo = lineInfoMatch ? lineInfoMatch[0] : null;
26
+
27
+ return { reportContent, lineInfo };
28
+ }
29
+
30
+ // 创建一个可配置的 GitLab API 客户端
31
+ class GitLabAPIClient {
32
+ constructor(gitlabToken, gitlabUrl = null) {
33
+ this.gitlabToken = gitlabToken;
34
+ this.gitlabUrl = gitlabUrl || 'https://gitdev.51job.com';
35
+ this.axiosInstance = axios.create({
36
+ headers: {
37
+ 'PRIVATE-TOKEN': this.gitlabToken,
38
+ },
39
+ });
40
+ }
41
+
42
+ /**
43
+ * 通用的 GitLab API 调用函数
44
+ * @param {string} endpoint API 端点
45
+ * @param {Object} options 额外选项(method, data 等)
46
+ * @returns {Promise} API 调用结果
47
+ */
48
+ async callGitLabAPI(endpoint, options = {}) {
49
+ const { method = 'GET', data = null, headers = {} } = options;
50
+
51
+ try {
52
+ const response = await this.axiosInstance({
53
+ method,
54
+ url: `${this.gitlabUrl}${endpoint}`,
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ ...headers
58
+ },
59
+ data
60
+ });
61
+
62
+ return response.data;
63
+ } catch (error) {
64
+ console.error(`GitLab API 调用失败:${method} ${endpoint}`, error.message);
65
+ throw error;
66
+ }
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ GitLabAPIClient,
72
+ debugLog,
73
+ extractReportContent
74
+ };