@raolin2025/claude-code-node 2.0.0 → 2.2.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.
- package/README.md +117 -0
- package/package.json +1 -1
- package/src/__tests__/git-tool.integration.test.js +89 -0
- package/src/__tests__/git-tool.test.js +144 -0
- package/src/channel/notify-daemon.js +16 -13
- package/src/core/cli.js +45 -5
- package/src/core/query-engine.js +57 -54
- package/src/core/session.js +3 -1
- package/src/git/github-api.js +360 -0
- package/src/git/index.js +37 -0
- package/src/git/llm-assistant.js +83 -0
- package/src/git/pr-merge-policy.js +367 -0
- package/src/git/pr-reviewer.js +533 -0
- package/src/git/utils/diff-parser.js +330 -0
- package/src/mcp/client.js +13 -3
- package/src/security/path-guard.js +11 -1
- package/src/tools/git-tool.js +308 -0
- package/src/tools/index.js +2 -0
- package/src/tools/web-fetch.js +3 -1
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 自动审查引擎
|
|
3
|
+
* 结合规则检查 + LLM 智能评估
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseDiff, splitDiffByFile, countFileChanges } from './utils/diff-parser.js'
|
|
7
|
+
import { GitHubAPI } from './github-api.js'
|
|
8
|
+
import { callLLM, buildPRReviewPrompt } from './llm-assistant.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PR 审查器
|
|
12
|
+
*/
|
|
13
|
+
export class PRReviewer {
|
|
14
|
+
/**
|
|
15
|
+
* @param {GitHubAPI} github - GitHub API 实例
|
|
16
|
+
* @param {Object} rules - 审查规则配置
|
|
17
|
+
*/
|
|
18
|
+
constructor(github, rules = {}, options = {}) {
|
|
19
|
+
this.github = github
|
|
20
|
+
this.rules = {
|
|
21
|
+
checks: {
|
|
22
|
+
codeQuality: true,
|
|
23
|
+
security: true,
|
|
24
|
+
tests: true,
|
|
25
|
+
docs: true,
|
|
26
|
+
complexity: true,
|
|
27
|
+
duplication: true
|
|
28
|
+
},
|
|
29
|
+
...rules
|
|
30
|
+
}
|
|
31
|
+
this.apiConfig = options.apiConfig || {}
|
|
32
|
+
this.enableLLM = options.enableLLM !== false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 审查 PR
|
|
37
|
+
* @param {number} prNumber
|
|
38
|
+
* @param {Object} options - 审查选项
|
|
39
|
+
* @returns {Promise<ReviewResult>}
|
|
40
|
+
*/
|
|
41
|
+
async reviewPR(prNumber, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
analyzeLLM = true, // 是否使用 LLM 智能分析
|
|
44
|
+
commentThreshold = 'WARNING' // 'INFO' | 'WARNING' | 'ERROR'
|
|
45
|
+
} = options
|
|
46
|
+
|
|
47
|
+
// 1. 获取 PR 数据
|
|
48
|
+
const pr = await this.github.getPR(prNumber)
|
|
49
|
+
const diff = await this.github.getPRDiff(prNumber)
|
|
50
|
+
const files = await this.github.getPRFiles(prNumber)
|
|
51
|
+
|
|
52
|
+
// 2. 解析 diff
|
|
53
|
+
const fileMap = splitDiffByFile(diff)
|
|
54
|
+
const allHunks = parseDiff(diff)
|
|
55
|
+
|
|
56
|
+
// 3. 运行规则检查
|
|
57
|
+
const findings = await this.runAllChecks(pr, files, diff, fileMap)
|
|
58
|
+
|
|
59
|
+
// 4. LLM 智能分析(可选)
|
|
60
|
+
let llmSummary = null
|
|
61
|
+
if (analyzeLLM && this.enableLLM && findings.length > 0) {
|
|
62
|
+
llmSummary = await this.llmAnalysis(pr, findings, diff)
|
|
63
|
+
} else if (findings.length === 0) {
|
|
64
|
+
llmSummary = { summary: '✅ No issues found', riskLevel: 'LOW', recommendations: ['PR looks good to merge'] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 5. 生成审查报告
|
|
68
|
+
const report = this.generateReviewReport(pr, findings, llmSummary)
|
|
69
|
+
|
|
70
|
+
// 6. 准备评论列表(根据 threshold 过滤)
|
|
71
|
+
const comments = findings
|
|
72
|
+
.filter(f => this.shouldComment(f, commentThreshold))
|
|
73
|
+
.map(f => this.buildReviewComment(f))
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
prNumber,
|
|
77
|
+
prTitle: pr.title,
|
|
78
|
+
prBody: pr.body,
|
|
79
|
+
findings,
|
|
80
|
+
summary: report,
|
|
81
|
+
llmSummary,
|
|
82
|
+
comments,
|
|
83
|
+
meta: {
|
|
84
|
+
totalFiles: files.length,
|
|
85
|
+
totalLinesChanged: files.reduce((sum, f) => sum + f.additions + f.deletions, 0),
|
|
86
|
+
reviewedAt: new Date().toISOString()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 执行所有检查
|
|
93
|
+
*/
|
|
94
|
+
async runAllChecks(pr, files, diff, fileMap) {
|
|
95
|
+
const findings = []
|
|
96
|
+
|
|
97
|
+
// 并行运行所有检查器
|
|
98
|
+
const checks = []
|
|
99
|
+
|
|
100
|
+
if (this.rules.checks.codeQuality) {
|
|
101
|
+
checks.push(this.checkCodeQuality(files, fileMap))
|
|
102
|
+
}
|
|
103
|
+
if (this.rules.checks.security) {
|
|
104
|
+
checks.push(this.checkSecurity(files, fileMap))
|
|
105
|
+
}
|
|
106
|
+
if (this.rules.checks.tests) {
|
|
107
|
+
checks.push(this.checkTests(files, fileMap))
|
|
108
|
+
}
|
|
109
|
+
if (this.rules.checks.docs) {
|
|
110
|
+
checks.push(this.checkDocs(pr, files, fileMap))
|
|
111
|
+
}
|
|
112
|
+
if (this.rules.checks.complexity) {
|
|
113
|
+
checks.push(this.checkComplexity(files, fileMap))
|
|
114
|
+
}
|
|
115
|
+
if (this.rules.checks.duplication) {
|
|
116
|
+
checks.push(this.checkDuplication(files, fileMap))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const results = await Promise.all(checks)
|
|
120
|
+
findings.push(...results.flat())
|
|
121
|
+
|
|
122
|
+
return findings
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============ 检查器实现 ============
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 代码质量检查
|
|
129
|
+
*/
|
|
130
|
+
async checkCodeQuality(files, fileMap) {
|
|
131
|
+
const findings = []
|
|
132
|
+
const patterns = {
|
|
133
|
+
'console.log': { severity: 'WARNING', message: 'Remove console.log before merging' },
|
|
134
|
+
'debugger': { severity: 'ERROR', message: 'Remove debugger statements' },
|
|
135
|
+
'FIXME': { severity: 'INFO', message: 'FIXME comment found' },
|
|
136
|
+
'TODO': { severity: 'INFO', message: 'TODO comment found' },
|
|
137
|
+
'XXX': { severity: 'WARNING', message: 'XXX comment indicates incomplete code' },
|
|
138
|
+
'var ': { severity: 'WARNING', message: 'Consider using const/let instead of var' },
|
|
139
|
+
'===': { severity: 'WARNING', message: 'Strict equality === is fine, but consider type-safe comparison' } // false positive demo
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const [filePath, hunks] of fileMap.entries()) {
|
|
143
|
+
const isCodeFile = this.isCodeFile(filePath)
|
|
144
|
+
if (!isCodeFile) continue
|
|
145
|
+
|
|
146
|
+
const changes = countFileChanges(hunks)
|
|
147
|
+
if (changes.changes === 0) continue
|
|
148
|
+
|
|
149
|
+
for (const hunk of hunks) {
|
|
150
|
+
for (const line of hunk.lines) {
|
|
151
|
+
if (line.type !== '+') continue // 只检查新增的代码
|
|
152
|
+
|
|
153
|
+
const content = line.content
|
|
154
|
+
for (const [pattern, info] of Object.entries(patterns)) {
|
|
155
|
+
if (content.includes(pattern)) {
|
|
156
|
+
findings.push({
|
|
157
|
+
type: 'code-quality',
|
|
158
|
+
severity: info.severity,
|
|
159
|
+
file: filePath,
|
|
160
|
+
line: line.newLineNum,
|
|
161
|
+
column: 0,
|
|
162
|
+
message: `${info.message} (line: ${content.trim()})`,
|
|
163
|
+
snippet: content.trim(),
|
|
164
|
+
rule: pattern
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return findings
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 安全检查(关键漏洞检测)
|
|
177
|
+
*/
|
|
178
|
+
async checkSecurity(files, fileMap) {
|
|
179
|
+
const findings = []
|
|
180
|
+
const securityPatterns = {
|
|
181
|
+
// 硬编码密钥
|
|
182
|
+
'password': { severity: 'ERROR', message: 'Potential hardcoded password' },
|
|
183
|
+
'secret': { severity: 'ERROR', message: 'Potential hardcoded secret' },
|
|
184
|
+
'api_key': { severity: 'ERROR', message: 'Potential API key' },
|
|
185
|
+
'token': { severity: 'WARNING', message: 'Potential token hardcoded' },
|
|
186
|
+
// SQL 注入风险
|
|
187
|
+
'SELECT .* FROM': { severity: 'ERROR', message: 'Possible SQL injection if string concatenation' },
|
|
188
|
+
'eval(': { severity: 'CRITICAL', message: 'eval() is dangerous' },
|
|
189
|
+
// 路径遍历
|
|
190
|
+
'../': { severity: 'WARNING', message: 'Path traversal risk' },
|
|
191
|
+
'__dirname': { severity: 'WARNING', message: 'Check path construction safety' },
|
|
192
|
+
// XSS
|
|
193
|
+
'innerHTML': { severity: 'WARNING', message: 'Potential XSS when using innerHTML' },
|
|
194
|
+
'document.write': { severity: 'ERROR', message: 'Avoid document.write' }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const [filePath, hunks] of fileMap.entries()) {
|
|
198
|
+
const isCodeFile = this.isCodeFile(filePath)
|
|
199
|
+
if (!isCodeFile) continue
|
|
200
|
+
|
|
201
|
+
for (const hunk of hunks) {
|
|
202
|
+
for (const line of hunk.lines) {
|
|
203
|
+
if (line.type !== '+') continue
|
|
204
|
+
|
|
205
|
+
const content = line.content.toLowerCase()
|
|
206
|
+
for (const [pattern, info] of Object.entries(securityPatterns)) {
|
|
207
|
+
const regex = new RegExp(pattern, 'i')
|
|
208
|
+
if (regex.test(content)) {
|
|
209
|
+
findings.push({
|
|
210
|
+
type: 'security',
|
|
211
|
+
severity: info.severity,
|
|
212
|
+
file: filePath,
|
|
213
|
+
line: line.newLineNum,
|
|
214
|
+
message: info.message,
|
|
215
|
+
snippet: line.content.trim(),
|
|
216
|
+
rule: pattern
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return findings
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 测试覆盖率检查
|
|
229
|
+
*/
|
|
230
|
+
async checkTests(files, fileMap) {
|
|
231
|
+
const findings = []
|
|
232
|
+
|
|
233
|
+
// 检查是否有新增代码但缺少对应测试
|
|
234
|
+
const modifiedFiles = new Set()
|
|
235
|
+
const testFiles = new Set()
|
|
236
|
+
|
|
237
|
+
for (const [filePath] of fileMap.entries()) {
|
|
238
|
+
modifiedFiles.add(filePath)
|
|
239
|
+
if (this.isTestFile(filePath)) {
|
|
240
|
+
testFiles.add(filePath)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 统计未覆盖的修改文件
|
|
245
|
+
for (const file of modifiedFiles) {
|
|
246
|
+
if (this.isTestFile(file)) continue
|
|
247
|
+
|
|
248
|
+
const hasCorrespondingTest = this.findTestFile(file, Array.from(testFiles))
|
|
249
|
+
if (!hasCorrespondingTest) {
|
|
250
|
+
const changes = countFileChanges(fileMap.get(file) || [])
|
|
251
|
+
if (changes.additions > 5) { // 只提示较明显的修改
|
|
252
|
+
findings.push({
|
|
253
|
+
type: 'tests',
|
|
254
|
+
severity: 'WARNING',
|
|
255
|
+
file,
|
|
256
|
+
line: null,
|
|
257
|
+
message: `New/modified code but missing test file for ${this.getFileName(file)}`,
|
|
258
|
+
rule: 'missing-test'
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return findings
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 文档检查
|
|
269
|
+
*/
|
|
270
|
+
async checkDocs(pr, files, fileMap) {
|
|
271
|
+
const findings = []
|
|
272
|
+
|
|
273
|
+
// 检查 PR 描述是否足够详细
|
|
274
|
+
if (!pr.body || pr.body.trim().length < 50) {
|
|
275
|
+
findings.push({
|
|
276
|
+
type: 'docs',
|
|
277
|
+
severity: 'INFO',
|
|
278
|
+
file: null,
|
|
279
|
+
line: null,
|
|
280
|
+
message: 'PR description is brief; consider adding more context (purpose, testing steps, breaking changes)',
|
|
281
|
+
rule: 'pr-description'
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 检查 API 变更是否更新文档
|
|
286
|
+
const hasAPIFiles = Array.from(fileMap.keys()).some(p => p.includes('src/') || p.includes('lib/'))
|
|
287
|
+
const hasDocsFiles = Array.from(fileMap.keys()).some(p => p.includes('docs/') || p.includes('README'))
|
|
288
|
+
|
|
289
|
+
if (hasAPIFiles && !hasDocsFiles) {
|
|
290
|
+
findings.push({
|
|
291
|
+
type: 'docs',
|
|
292
|
+
severity: 'WARNING',
|
|
293
|
+
file: null,
|
|
294
|
+
line: null,
|
|
295
|
+
message: 'Code changes detected but no documentation updates found',
|
|
296
|
+
rule: 'missing-docs'
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return findings
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 复杂度检查(简易版:检查大函数)
|
|
305
|
+
*/
|
|
306
|
+
async checkComplexity(files, fileMap) {
|
|
307
|
+
const findings = []
|
|
308
|
+
const complexityThreshold = 50 // 行数阈值
|
|
309
|
+
|
|
310
|
+
for (const [filePath, hunks] of fileMap.entries()) {
|
|
311
|
+
const isCodeFile = this.isCodeFile(filePath)
|
|
312
|
+
if (!isCodeFile) continue
|
|
313
|
+
|
|
314
|
+
// 简单统计:统计新增的连续行
|
|
315
|
+
for (const hunk of hunks) {
|
|
316
|
+
let addedBlockStart = null
|
|
317
|
+
let addedBlockLines = 0
|
|
318
|
+
|
|
319
|
+
for (const line of hunk.lines) {
|
|
320
|
+
if (line.type === '+') {
|
|
321
|
+
if (addedBlockStart === null) {
|
|
322
|
+
addedBlockStart = line.newLineNum
|
|
323
|
+
}
|
|
324
|
+
addedBlockLines++
|
|
325
|
+
} else {
|
|
326
|
+
if (addedBlockLines > 0 && addedBlockLines >= complexityThreshold) {
|
|
327
|
+
findings.push({
|
|
328
|
+
type: 'complexity',
|
|
329
|
+
severity: 'WARNING',
|
|
330
|
+
file: filePath,
|
|
331
|
+
line: addedBlockStart,
|
|
332
|
+
message: `Large addition (${addedBlockLines} lines) may indicate complex logic`,
|
|
333
|
+
rule: 'large-block'
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
addedBlockStart = null
|
|
337
|
+
addedBlockLines = 0
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return findings
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 重复代码检查(简易版:检测重复行)
|
|
348
|
+
*/
|
|
349
|
+
async checkDuplication(files, fileMap) {
|
|
350
|
+
const findings = []
|
|
351
|
+
const addedLines = []
|
|
352
|
+
|
|
353
|
+
// 收集所有新增行
|
|
354
|
+
for (const [filePath, hunks] of fileMap.entries()) {
|
|
355
|
+
const isCodeFile = this.isCodeFile(filePath)
|
|
356
|
+
if (!isCodeFile) continue
|
|
357
|
+
|
|
358
|
+
for (const hunk of hunks) {
|
|
359
|
+
for (const line of hunk.lines) {
|
|
360
|
+
if (line.type === '+') {
|
|
361
|
+
addedLines.push({
|
|
362
|
+
file: filePath,
|
|
363
|
+
line: line.newLineNum,
|
|
364
|
+
content: line.content.trim()
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 检查重复行(简单 O(n^2) 检查,适合增量数量)
|
|
372
|
+
const duplicates = new Set()
|
|
373
|
+
for (let i = 0; i < addedLines.length; i++) {
|
|
374
|
+
for (let j = i + 1; j < addedLines.length; j++) {
|
|
375
|
+
if (addedLines[i].content === addedLines[j].content &&
|
|
376
|
+
addedLines[i].content.length > 20 && // 忽略短行
|
|
377
|
+
!duplicates.has(i) && !duplicates.has(j)) {
|
|
378
|
+
findings.push({
|
|
379
|
+
type: 'duplication',
|
|
380
|
+
severity: 'WARNING',
|
|
381
|
+
file: addedLines[i].file,
|
|
382
|
+
line: addedLines[i].line,
|
|
383
|
+
message: 'Potential duplicate code found in another location',
|
|
384
|
+
rule: 'duplicate-lines',
|
|
385
|
+
duplicateLocation: `${addedLines[j].file}:${addedLines[j].line}`
|
|
386
|
+
})
|
|
387
|
+
duplicates.add(i)
|
|
388
|
+
duplicates.add(j)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return findings
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ============ LLM 智能分析 ============
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 使用 LLM 进行整体评估
|
|
400
|
+
*/
|
|
401
|
+
async llmAnalysis(pr, findings, diff) {
|
|
402
|
+
try {
|
|
403
|
+
const prompt = buildPRReviewPrompt(pr, diff, findings)
|
|
404
|
+
const response = await callLLM(
|
|
405
|
+
'You are an expert code reviewer. Provide concise, actionable feedback.',
|
|
406
|
+
prompt,
|
|
407
|
+
this.apiConfig
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
// 解析 LLM 输出(简化:直接返回文本)
|
|
411
|
+
return {
|
|
412
|
+
summary: response,
|
|
413
|
+
riskLevel: this._inferRiskLevel(response),
|
|
414
|
+
recommendations: this._extractRecommendations(response)
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error('LLM analysis failed:', error)
|
|
418
|
+
// 降级到规则统计
|
|
419
|
+
return this._fallbackAnalysis(findings)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
_inferRiskLevel(text) {
|
|
424
|
+
const lower = text.toLowerCase()
|
|
425
|
+
if (lower.includes('reject') || lower.includes('critical')) return 'HIGH'
|
|
426
|
+
if (lower.includes('needs work') || lower.includes('issues')) return 'MEDIUM'
|
|
427
|
+
return 'LOW'
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
_extractRecommendations(text) {
|
|
431
|
+
const lines = text.split('\n')
|
|
432
|
+
const recs = []
|
|
433
|
+
for (const line of lines) {
|
|
434
|
+
if (line.match(/^\s*[-*•] /i) || line.includes('Recommendation') || line.includes('建议')) {
|
|
435
|
+
recs.push(line.trim())
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return recs.length > 0 ? recs : ['See LLM analysis for details']
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_fallbackAnalysis(findings) {
|
|
442
|
+
const severityCount = { CRITICAL: 0, ERROR: 0, WARNING: 0, INFO: 0 }
|
|
443
|
+
for (const f of findings) {
|
|
444
|
+
severityCount[f.severity] = (severityCount[f.severity] || 0) + 1
|
|
445
|
+
}
|
|
446
|
+
const total = findings.length
|
|
447
|
+
const riskLevel = total > 20 ? 'HIGH' : total > 10 ? 'MEDIUM' : 'LOW'
|
|
448
|
+
return {
|
|
449
|
+
summary: `PR 审查完成:发现 ${total} 个问题(CRITICAL: ${severityCount.CRITICAL}, ERROR: ${severityCount.ERROR}, WARNING: ${severityCount.WARNING}, INFO: ${severityCount.INFO})`,
|
|
450
|
+
riskLevel,
|
|
451
|
+
recommendations: total > 0
|
|
452
|
+
? ['建议修复 CRITICAL 和 ERROR 级别问题', '注意安全问题', '补充测试用例']
|
|
453
|
+
: ['PR 质量良好,可以合并']
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============ 辅助方法 ============
|
|
458
|
+
|
|
459
|
+
shouldComment(finding, threshold) {
|
|
460
|
+
const severityOrder = { CRITICAL: 4, ERROR: 3, WARNING: 2, INFO: 1 }
|
|
461
|
+
const thresholdOrder = { ERROR: 3, WARNING: 2, INFO: 1 }
|
|
462
|
+
return severityOrder[finding.severity] >= thresholdOrder[threshold]
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
buildReviewComment(finding) {
|
|
466
|
+
const emoji = this.getSeverityEmoji(finding.severity)
|
|
467
|
+
const body = `${emoji} **${finding.severity}** - ${finding.message}`
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
body,
|
|
471
|
+
path: finding.file,
|
|
472
|
+
line: finding.line, // 保留原始行号,外层调用方决定是否转 position
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
getSeverityEmoji(severity) {
|
|
477
|
+
switch (severity) {
|
|
478
|
+
case 'CRITICAL': return '🔴'
|
|
479
|
+
case 'ERROR': return '🚫'
|
|
480
|
+
case 'WARNING': return '⚠️'
|
|
481
|
+
case 'INFO': return 'ℹ️'
|
|
482
|
+
default: return '📝'
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
generateReviewReport(pr, findings, llmSummary) {
|
|
487
|
+
const severityCount = { CRITICAL: 0, ERROR: 0, WARNING: 0, INFO: 0 }
|
|
488
|
+
for (const f of findings) {
|
|
489
|
+
severityCount[f.severity] = (severityCount[f.severity] || 0) + 1
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const total = findings.length
|
|
493
|
+
const blockers = severityCount.CRITICAL + severityCount.ERROR
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
overall: llmSummary?.riskLevel || (blockers > 0 ? 'NEEDS_WORK' : 'CLEAN'),
|
|
497
|
+
totalFindings: total,
|
|
498
|
+
severityCount,
|
|
499
|
+
blockers,
|
|
500
|
+
summary: llmSummary?.summary || `Review completed: ${total} issues found (${blockers} blocking)`,
|
|
501
|
+
recommendations: total > 0
|
|
502
|
+
? ['Fix CRITICAL/ERROR issues before merging', 'Address WARNING items', 'Consider INFO suggestions']
|
|
503
|
+
: ['PR looks good to merge']
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
isCodeFile(filePath) {
|
|
508
|
+
const codeExt = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h', '.hpp']
|
|
509
|
+
return codeExt.some(ext => filePath.endsWith(ext))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
isTestFile(filePath) {
|
|
513
|
+
const testPatterns = ['test', 'spec', '__tests__', '.test.', '.spec.']
|
|
514
|
+
return testPatterns.some(p => filePath.includes(p))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
getFileName(filePath) {
|
|
518
|
+
return filePath.split('/').pop()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
findTestFile(sourceFile, testFiles) {
|
|
522
|
+
const name = this.getFileName(sourceFile)
|
|
523
|
+
// 简单的命名约定:xxx.js → xxx.test.js / xxx.spec.js / test/xxx.test.js
|
|
524
|
+
const patterns = [
|
|
525
|
+
`test/${name}.test.js`,
|
|
526
|
+
`test/${name}.spec.js`,
|
|
527
|
+
`${name}.test.js`,
|
|
528
|
+
`${name}.spec.js`,
|
|
529
|
+
`__tests__/${name}.js`
|
|
530
|
+
]
|
|
531
|
+
return patterns.some(p => testFiles.includes(p))
|
|
532
|
+
}
|
|
533
|
+
}
|