job51-gitlab-cr-node-jt-1 2.9.7 → 2.9.9
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/.claude/skills/simple-code-review/SKILL.md +32 -19
- package/index.js +165 -5
- package/package.json +1 -1
|
@@ -11,26 +11,22 @@ description: 代码审查技能,审查变更代码并输出 REPORT 和 LINE_IN
|
|
|
11
11
|
- `New Start`: 当前 diff 块在变更后文件中的起始行号
|
|
12
12
|
- `New Count`: 当前 diff 块的行数
|
|
13
13
|
- `New Path`: 文件路径(去掉 `a/` 或 `b/` 前缀)
|
|
14
|
+
- `Is Large File`: 如果为 true,表示这是完整文件审查(GitLab diff 截断)
|
|
14
15
|
- **注意**:元数据在文件末尾,`@@` 行已被移除,不影响行号计数
|
|
15
16
|
|
|
16
|
-
3.
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
3. **如果任何行号超出范围,必须重新计数**
|
|
28
|
-
4. **禁止**直接使用从完整文件读取的行号
|
|
17
|
+
3. **解析行号**:
|
|
18
|
+
- **大文件模式(Is Large File: true)**:
|
|
19
|
+
- 这是完整文件内容审查,所有代码都是新增
|
|
20
|
+
- 行号从 1 开始,逐行计数
|
|
21
|
+
- 发现问题时,直接使用实际行号
|
|
22
|
+
- **正常 diff 模式**:
|
|
23
|
+
- 从元数据中读取 `New Start` 值
|
|
24
|
+
- 从第 1 行代码开始,行号 = `New Start`
|
|
25
|
+
- 每往下一行(`+` 开头或空格开头),行号 +1
|
|
26
|
+
- `-` 开头的删除行**不计数**
|
|
27
|
+
- **忽略元数据行**:文件末尾的 `# File Information` 等以 `# ` 开头的行都**不编号**
|
|
29
28
|
|
|
30
29
|
4. **读取上下文**:基于 New Path 读取变更后文件,涉及方法调用时追踪读取实现代码
|
|
31
|
-
- **⚠️ 重要**:读取完整文件用于理解代码逻辑,但问题行号必须基于 diff 块计算
|
|
32
|
-
- 如果从完整文件中定位到问题行号,**必须验证**该行号是否在 diff 块范围内
|
|
33
|
-
- **验证公式**:`new_start <= 行号 <= new_start + new_count - 1`
|
|
34
30
|
|
|
35
31
|
5. **执行审查**:按照 @.claude/rules/code-review-rules.md 中的规则进行审查
|
|
36
32
|
|
|
@@ -77,11 +73,21 @@ description: 代码审查技能,审查变更代码并输出 REPORT 和 LINE_IN
|
|
|
77
73
|
|
|
78
74
|
7. **输出结果**:
|
|
79
75
|
- 审查结果放入 `<REPORT>` 标签
|
|
80
|
-
-
|
|
81
|
-
- LINE_INFO
|
|
76
|
+
- **⚠️ 必须输出 `<LINE_INFO>` 标签**(这是强制要求,没有例外)
|
|
77
|
+
- LINE_INFO 格式:`[{"new_path":"文件路径","new_line":行号}]`
|
|
82
78
|
- **问题和行号对应**:`<LINE_INFO>` 中的第 N 个元素对应第 N 个问题的行号
|
|
79
|
+
- **无问题时**:LINE_INFO 必须输出空数组 `<LINE_INFO>[]</LINE_INFO>`
|
|
83
80
|
- **⚠️ 关键格式要求**:`<LINE_INFO>` 标签必须放置在 `</REPORT>` 标签之后
|
|
84
81
|
|
|
82
|
+
## ⚠️ 大文件审查特殊要求
|
|
83
|
+
|
|
84
|
+
当 `Is Large File: true` 时:
|
|
85
|
+
1. 这是完整文件内容审查,所有代码行都是新增(`+` 开头)
|
|
86
|
+
2. **必须审查整个文件**,不要只看部分代码
|
|
87
|
+
3. **发现任何问题时,必须输出 `<LINE_INFO>` 标签**,包含准确的行号
|
|
88
|
+
4. 行号基于完整文件内容计算,从第 1 行开始
|
|
89
|
+
5. **即使只有一个问题,也必须输出 LINE_INFO**
|
|
90
|
+
|
|
85
91
|
## 输出模板
|
|
86
92
|
|
|
87
93
|
**⚠️ 强制格式要求(违反会导致解析失败)**:
|
|
@@ -101,6 +107,8 @@ description: 代码审查技能,审查变更代码并输出 REPORT 和 LINE_IN
|
|
|
101
107
|
6. **禁止输出错误代码示例**:已经有行号定位了,不需要重复输出错误代码
|
|
102
108
|
|
|
103
109
|
7. **必须输出 `<LINE_INFO>` 标签**:即使无问题也要输出空数组 `<LINE_INFO>[]</LINE_INFO>`
|
|
110
|
+
- **⚠️ 再次强调**:如果发现严重问题,必须同时输出 LINE_INFO 标签包含行号信息
|
|
111
|
+
- **如果只输出 REPORT 而不输出 LINE_INFO,审查结果会被系统丢弃**
|
|
104
112
|
|
|
105
113
|
<REPORT>
|
|
106
114
|
## 🤖 AI 代码审查结果
|
|
@@ -111,10 +119,14 @@ description: 代码审查技能,审查变更代码并输出 REPORT 和 LINE_IN
|
|
|
111
119
|
**文件及行号**:[文件路径:行号]<br/>
|
|
112
120
|
**修改建议**:[正确示例代码或说明]
|
|
113
121
|
|
|
122
|
+
**问题 2**:[问题描述]<br/>
|
|
123
|
+
**文件及行号**:[文件路径:行号]<br/>
|
|
124
|
+
**修改建议**:[正确示例代码或说明]
|
|
125
|
+
|
|
114
126
|
</REPORT>
|
|
115
127
|
|
|
116
128
|
<LINE_INFO>
|
|
117
|
-
[{"new_path":"文件路径","new_line":行号}]
|
|
129
|
+
[{"new_path":"文件路径","new_line":行号},{"new_path":"文件路径","new_line":行号}]
|
|
118
130
|
</LINE_INFO>
|
|
119
131
|
|
|
120
132
|
**说明**:
|
|
@@ -123,3 +135,4 @@ description: 代码审查技能,审查变更代码并输出 REPORT 和 LINE_IN
|
|
|
123
135
|
- **`<LINE_INFO>` 数组中的元素顺序必须与问题顺序一致**
|
|
124
136
|
- 行号:返回变更后文件中的绝对行号(从 1 开始计数)
|
|
125
137
|
- **无严重问题时,LINE_INFO 必须输出空数组 `[]`**
|
|
138
|
+
- **⚠️ 有严重问题时,LINE_INFO 必须包含对应的行号信息,否则审查结果会被丢弃**
|
package/index.js
CHANGED
|
@@ -81,9 +81,100 @@ class GitLabCodeReviewer {
|
|
|
81
81
|
debugLog(`========== Diff Block ${blockIndex} 开始 ==========`);
|
|
82
82
|
debugLog(`文件路径:${diffObject.new_path || diffObject.old_path}`);
|
|
83
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(`
|
|
84
|
+
debugLog(`is_large_file: ${diffObject.is_large_file || false}`);
|
|
85
|
+
debugLog(`diff 内容长度:${diffObject.diff?.length || 0}`);
|
|
85
86
|
debugLog(`========== Diff Block ${blockIndex} 结束 ==========`);
|
|
86
87
|
|
|
88
|
+
// 处理大文件情况:diff 内容为空,需要读取完整文件
|
|
89
|
+
if (diffObject.is_large_file || !diffObject.diff || diffObject.diff.trim() === '') {
|
|
90
|
+
debugLog(`检测到大文件或空 diff,将读取完整文件内容进行审查`);
|
|
91
|
+
|
|
92
|
+
// 读取完整文件内容
|
|
93
|
+
const fullPath = path.join(process.cwd(), diffObject.new_path || diffObject.old_path);
|
|
94
|
+
let fileContent = '';
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(fullPath)) {
|
|
98
|
+
fileContent = fs.readFileSync(fullPath, 'utf-8');
|
|
99
|
+
const lineCount = fileContent.split('\n').length;
|
|
100
|
+
debugLog(`成功读取完整文件,行数:${lineCount}`);
|
|
101
|
+
} else {
|
|
102
|
+
debugLog(`文件不存在:${fullPath},跳过审查`);
|
|
103
|
+
return {
|
|
104
|
+
diff_info: diffObject,
|
|
105
|
+
block_index: blockIndex,
|
|
106
|
+
review_result: { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n文件无法读取,跳过审查。\n</REPORT>', lineInfo: '[]' },
|
|
107
|
+
temp_file_path: null,
|
|
108
|
+
hallucination_detected: false,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
} catch (readError) {
|
|
112
|
+
debugLog(`读取文件失败:${readError.message}`);
|
|
113
|
+
return {
|
|
114
|
+
diff_info: diffObject,
|
|
115
|
+
block_index: blockIndex,
|
|
116
|
+
review_result: { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n文件读取失败,跳过审查。\n</REPORT>', lineInfo: '[]' },
|
|
117
|
+
temp_file_path: null,
|
|
118
|
+
hallucination_detected: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 对于新增文件,全部代码都是新增
|
|
123
|
+
// 对于修改文件,需要标记为"全文件审查"
|
|
124
|
+
const prefix = diffObject.new_file ? '+' : ' ';
|
|
125
|
+
const prefixedContent = fileContent.split('\n').map(line => prefix + line).join('\n');
|
|
126
|
+
|
|
127
|
+
// 构造临时文件内容
|
|
128
|
+
const diffContentWithMetadata = `${prefixedContent}
|
|
129
|
+
|
|
130
|
+
# File Information
|
|
131
|
+
# New Path: ${diffObject.new_path || 'N/A'}
|
|
132
|
+
# Old Path: ${diffObject.old_path || 'N/A'}
|
|
133
|
+
# New Start: 1
|
|
134
|
+
# New Count: ${fileContent.split('\n').length}
|
|
135
|
+
# Is Large File: true
|
|
136
|
+
# 注意:这是完整文件内容审查,因为 GitLab diff 被截断。所有代码行都应审查。
|
|
137
|
+
# 行号计算:从第 1 行代码开始计数,第 1 行行号=1,每往下一行行号 +1`;
|
|
138
|
+
|
|
139
|
+
// 将内容写入临时文件
|
|
140
|
+
fs.writeFileSync(tmpFileName, diffContentWithMetadata);
|
|
141
|
+
debugLog(`大文件临时文件已创建,大小:${diffContentWithMetadata.length}`);
|
|
142
|
+
|
|
143
|
+
// 审核完整文件
|
|
144
|
+
const blockStartTime = Date.now();
|
|
145
|
+
const blockIdentifier = `${diffObject.new_path || diffObject.old_path}#large-file`;
|
|
146
|
+
const review_result = await this.reviewDiffWithClaudeUsingFile(tmpFileName, blockIdentifier, true);
|
|
147
|
+
const blockObj = { ...diffObject, review_result, temp_file_path: tmpFileName, is_large_file: true };
|
|
148
|
+
|
|
149
|
+
// 记录审查指标
|
|
150
|
+
const reviewTime = Date.now() - blockStartTime;
|
|
151
|
+
const hasSeriousProblems = blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('严重问题');
|
|
152
|
+
const filePath = diffObject.new_path || diffObject.old_path || '';
|
|
153
|
+
this.metrics.recordBlockReviewed(reviewTime, hasSeriousProblems ? 1 : 0, hasSeriousProblems, fileContent.length, filePath);
|
|
154
|
+
|
|
155
|
+
let hallucinationDetected = false;
|
|
156
|
+
|
|
157
|
+
// 检查审查结果中是否包含严重问题
|
|
158
|
+
if (blockObj.review_result && blockObj.review_result.reportContent && blockObj.review_result.reportContent.includes('严重问题')) {
|
|
159
|
+
// 对于大文件,发布为一般讨论(无法精确定位行号)
|
|
160
|
+
const file_path_with_line = `${diffObject.new_path || diffObject.old_path}#L1`;
|
|
161
|
+
await this.createGeneralDiscussion(projectId, mergeRequestIid, file_path_with_line, blockObj.review_result.reportContent);
|
|
162
|
+
debugLog(`大文件审查结果已发布为一般讨论: ${file_path_with_line}`);
|
|
163
|
+
this.metrics.recordCommentPublished();
|
|
164
|
+
} else {
|
|
165
|
+
debugLog(`大文件审查未发现严重问题: ${diffObject.new_path || diffObject.old_path}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
diff_info: blockObj,
|
|
170
|
+
block_index: blockIndex,
|
|
171
|
+
review_result: blockObj.review_result,
|
|
172
|
+
temp_file_path: tmpFileName,
|
|
173
|
+
hallucination_detected: hallucinationDetected,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 正常 diff 处理逻辑
|
|
87
178
|
// 解析 diff 内容,移除 @@ 行,将其信息放入元数据
|
|
88
179
|
// 这样 AI 不会把 @@ 行误计数
|
|
89
180
|
const diffLines = diffObject.diff.split('\n');
|
|
@@ -449,15 +540,28 @@ ${allReportsText}
|
|
|
449
540
|
* 使用Claude对单个diff文件进行代码审核
|
|
450
541
|
* @param {string} filePath 临时文件路径
|
|
451
542
|
* @param {string} blockIdentifier 块标识(格式:文件路径#块索引),用于日志追踪
|
|
543
|
+
* @param {boolean} isLargeFile 是否为大文件审查模式
|
|
452
544
|
* @returns {Promise<string>} 审核结果
|
|
453
545
|
*/
|
|
454
|
-
async reviewDiffWithClaudeUsingFile(filePath, blockIdentifier = 'unknown') {
|
|
455
|
-
debugLog(`[${blockIdentifier}] 开始审核文件: ${filePath}`);
|
|
546
|
+
async reviewDiffWithClaudeUsingFile(filePath, blockIdentifier = 'unknown', isLargeFile = false) {
|
|
547
|
+
debugLog(`[${blockIdentifier}] 开始审核文件: ${filePath}, isLargeFile: ${isLargeFile}`);
|
|
456
548
|
const startTime = Date.now();
|
|
457
549
|
|
|
550
|
+
// 大文件模式下的特殊提示
|
|
551
|
+
const largeFilePrompt = isLargeFile ? `
|
|
552
|
+
**⚠️ 大文件审查模式**:
|
|
553
|
+
这是一个完整文件内容的审查(因为 GitLab diff 被截断)。
|
|
554
|
+
1. 所有代码行都是新增或修改的内容
|
|
555
|
+
2. 必须审查整个文件的业务逻辑,包括:
|
|
556
|
+
- 所有方法中的循环逻辑、状态管理
|
|
557
|
+
- 所有数据库操作和资源管理
|
|
558
|
+
- 所有框架特性使用(如 @Async、MyBatis-Plus)
|
|
559
|
+
3. 发现问题时,准确报告行号(基于文件内容的实际行号)
|
|
560
|
+
` : '';
|
|
561
|
+
|
|
458
562
|
const prompt = `请调用 simple-code-review 技能审核代码变更。
|
|
459
563
|
文件路径:${filePath}
|
|
460
|
-
|
|
564
|
+
${largeFilePrompt}
|
|
461
565
|
**重要审查规则**:
|
|
462
566
|
1. **严格按照 .claude/rules/code-review-rules.md 中的规则执行审查**,该文件包含详细的审查规则
|
|
463
567
|
2. **重点检测以下问题类型**(详见规则文件):
|
|
@@ -613,6 +717,61 @@ ${allReportsText}
|
|
|
613
717
|
*/
|
|
614
718
|
getDiffBlocks(diffObj) {
|
|
615
719
|
const regex = /(?=@@\s-\d+(?:,\d+)?\s\+\d+(?:,\d+)?\s@@)/g;
|
|
720
|
+
|
|
721
|
+
// 检查 diff 内容是否有效
|
|
722
|
+
if (!diffObj.diff || diffObj.diff.trim() === '') {
|
|
723
|
+
debugLog(`文件 ${diffObj.new_path || diffObj.old_path} diff 内容为空,可能文件过大被 GitLab 截断`);
|
|
724
|
+
// 对于新增文件,返回一个标记对象,后续会用完整文件内容审查
|
|
725
|
+
if (diffObj.new_file) {
|
|
726
|
+
debugLog(`文件 ${diffObj.new_path} 是新增文件,将使用完整文件内容审查`);
|
|
727
|
+
return [{
|
|
728
|
+
diff: '', // 空内容,后续会读取完整文件
|
|
729
|
+
new_path: diffObj.new_path,
|
|
730
|
+
old_path: diffObj.old_path,
|
|
731
|
+
a_mode: diffObj.a_mode,
|
|
732
|
+
b_mode: diffObj.b_mode,
|
|
733
|
+
new_file: diffObj.new_file,
|
|
734
|
+
renamed_file: diffObj.renamed_file,
|
|
735
|
+
deleted_file: diffObj.deleted_file,
|
|
736
|
+
generated_file: diffObj.generated_file,
|
|
737
|
+
block_index: 0,
|
|
738
|
+
line_info: {
|
|
739
|
+
old_start: 0,
|
|
740
|
+
old_count: 0,
|
|
741
|
+
new_start: 1,
|
|
742
|
+
new_count: 0, // 未知,后续会从完整文件计算
|
|
743
|
+
firstLineFirstChar: '+'
|
|
744
|
+
},
|
|
745
|
+
is_large_file: true // 标记为大文件,需要特殊处理
|
|
746
|
+
}];
|
|
747
|
+
}
|
|
748
|
+
// 对于修改文件但 diff 为空,同样返回标记对象
|
|
749
|
+
if (diffObj.new_path && !diffObj.deleted_file) {
|
|
750
|
+
debugLog(`文件 ${diffObj.new_path} 是修改文件但 diff 截断,将使用完整文件内容审查`);
|
|
751
|
+
return [{
|
|
752
|
+
diff: '',
|
|
753
|
+
new_path: diffObj.new_path,
|
|
754
|
+
old_path: diffObj.old_path,
|
|
755
|
+
a_mode: diffObj.a_mode,
|
|
756
|
+
b_mode: diffObj.b_mode,
|
|
757
|
+
new_file: false,
|
|
758
|
+
renamed_file: diffObj.renamed_file,
|
|
759
|
+
deleted_file: diffObj.deleted_file,
|
|
760
|
+
generated_file: diffObj.generated_file,
|
|
761
|
+
block_index: 0,
|
|
762
|
+
line_info: {
|
|
763
|
+
old_start: 0,
|
|
764
|
+
old_count: 0,
|
|
765
|
+
new_start: 1,
|
|
766
|
+
new_count: 0,
|
|
767
|
+
firstLineFirstChar: ' '
|
|
768
|
+
},
|
|
769
|
+
is_large_file: true
|
|
770
|
+
}];
|
|
771
|
+
}
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
|
|
616
775
|
const diffBlocks = diffObj.diff.split(regex);
|
|
617
776
|
// 过滤掉空块并提取行号信息
|
|
618
777
|
const result = diffBlocks
|
|
@@ -623,7 +782,8 @@ ${allReportsText}
|
|
|
623
782
|
const headerMatch = block.match(headerRegex);
|
|
624
783
|
|
|
625
784
|
// 取block第一行是否是加号或者减号开头还是没有开头,block需要用.split(/\r?\n/)
|
|
626
|
-
const
|
|
785
|
+
const lines = block.split(/\r?\n/);
|
|
786
|
+
const firstLine = lines[1] || '';
|
|
627
787
|
// 取出第一行的第一个字符
|
|
628
788
|
const firstLineFirstChar = firstLine.charAt(0);
|
|
629
789
|
|