@spaceflow/review 0.81.0 → 0.83.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/CHANGELOG.md +50 -0
- package/dist/index.js +813 -718
- package/package.json +2 -2
- package/src/README.md +0 -1
- package/src/changed-file-collection.ts +87 -0
- package/src/mcp/index.ts +5 -1
- package/src/prompt/issue-verify.ts +8 -3
- package/src/review-context.spec.ts +214 -0
- package/src/review-issue-filter.spec.ts +794 -0
- package/src/review-issue-filter.ts +20 -279
- package/src/review-llm.spec.ts +287 -0
- package/src/review-llm.ts +19 -23
- package/src/review-report/formatters/markdown.formatter.ts +6 -7
- package/src/review-result-model.spec.ts +35 -4
- package/src/review-result-model.ts +58 -10
- package/src/review-source-resolver.ts +654 -0
- package/src/review-spec/review-spec.service.spec.ts +5 -4
- package/src/review-spec/review-spec.service.ts +5 -15
- package/src/review.service.spec.ts +186 -1154
- package/src/review.service.ts +185 -535
- package/src/types/changed-file-collection.ts +5 -0
- package/src/types/review-source-resolver.ts +55 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LlmJsonPut, REVIEW_STATE, addLocaleResources,
|
|
1
|
+
import { LlmJsonPut, REVIEW_STATE, addLocaleResources, createStreamLoggerState, defineExtension, logStreamEvent, normalizeVerbose, parallel, parseChangedLinesFromPatch, parseDiffText, parseRepoUrl, parseVerbose, shouldLog, t, z } from "@spaceflow/core";
|
|
2
2
|
import { access, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
@@ -378,15 +378,7 @@ class ReviewSpecService {
|
|
|
378
378
|
* 根据变更文件的扩展名过滤适用的规则文件
|
|
379
379
|
* 只按扩展名过滤,includes 和 override 在 LLM 审查后处理
|
|
380
380
|
*/ filterApplicableSpecs(specs, changedFiles) {
|
|
381
|
-
const changedExtensions =
|
|
382
|
-
for (const file of changedFiles){
|
|
383
|
-
if (file.filename) {
|
|
384
|
-
const ext = extname(file.filename).slice(1).toLowerCase();
|
|
385
|
-
if (ext) {
|
|
386
|
-
changedExtensions.add(ext);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
381
|
+
const changedExtensions = changedFiles.extensions();
|
|
390
382
|
console.log(`[filterApplicableSpecs] changedExtensions=${JSON.stringify([
|
|
391
383
|
...changedExtensions
|
|
392
384
|
])}, specs count=${specs.length}`);
|
|
@@ -1714,13 +1706,10 @@ class MarkdownFormatter {
|
|
|
1714
1706
|
`| 指标 | 数量 |`,
|
|
1715
1707
|
`|------|------|`
|
|
1716
1708
|
];
|
|
1717
|
-
lines.push(`|
|
|
1718
|
-
lines.push(`|
|
|
1719
|
-
lines.push(`|
|
|
1720
|
-
lines.push(`|
|
|
1721
|
-
lines.push(`| ⚠️ 待处理 | ${stats.pending} |`);
|
|
1722
|
-
lines.push(`| 修复率 | ${stats.fixRate}% |`);
|
|
1723
|
-
lines.push(`| 解决率 | ${stats.resolveRate}% |`);
|
|
1709
|
+
lines.push(`| 有效问题 | ${stats.validTotal} (🟢已验收 ${stats.fixed}, ⚪已解决 ${stats.resolved}, ⚠️待处理 ${stats.pending}) |`);
|
|
1710
|
+
lines.push(`| ❌ 无效问题 | ${stats.invalid} |`);
|
|
1711
|
+
lines.push(`| 验收率 | ${stats.fixRate}% (${stats.fixed}/${stats.validTotal}) |`);
|
|
1712
|
+
lines.push(`| 解决率 | ${stats.resolveRate}% (${stats.resolved}/${stats.validTotal}) |`);
|
|
1724
1713
|
return lines.join("\n");
|
|
1725
1714
|
}
|
|
1726
1715
|
}
|
|
@@ -2370,10 +2359,13 @@ function generateIssueKey(issue) {
|
|
|
2370
2359
|
* - 合并历史 issues(this.issues)+ newIssues
|
|
2371
2360
|
* - 复制 newResult 的元信息(title/description/deletionImpact 等)
|
|
2372
2361
|
*
|
|
2373
|
-
* 调用方应在调用前完成对历史 issues 的预处理(syncResolved、invalidateChangedFiles、verifyFixes
|
|
2362
|
+
* 调用方应在调用前完成对历史 issues 的预处理(syncResolved、invalidateChangedFiles、verifyFixes 等)。
|
|
2374
2363
|
*/ nextRound(newResult) {
|
|
2375
2364
|
const nextRoundNum = this._result.round + 1;
|
|
2376
|
-
|
|
2365
|
+
// 去重:过滤掉已存在于历史 issues 中的新问题(含 valid:false 的都参与去重)
|
|
2366
|
+
const existingKeys = new Set(this._result.issues.map((i)=>generateIssueKey(i)));
|
|
2367
|
+
const dedupedNewIssues = newResult.issues.filter((i)=>!existingKeys.has(generateIssueKey(i)));
|
|
2368
|
+
const taggedNewIssues = dedupedNewIssues.map((issue)=>({
|
|
2377
2369
|
...issue,
|
|
2378
2370
|
round: nextRoundNum
|
|
2379
2371
|
}));
|
|
@@ -2598,8 +2590,9 @@ function generateIssueKey(issue) {
|
|
|
2598
2590
|
}
|
|
2599
2591
|
/**
|
|
2600
2592
|
* 将有变更文件的历史 issue 标记为无效。
|
|
2601
|
-
*
|
|
2602
|
-
|
|
2593
|
+
* 策略:如果文件在最新 commit 中有变更,则将该文件的历史问题标记为无效,但以下情况保留:
|
|
2594
|
+
* - issue 已被用户手动 resolved 且当前代码行内容与 issue.code 不同(说明用户 resolve 后代码已变,应保留其 resolve 状态)
|
|
2595
|
+
*/ async invalidateChangedFiles(headSha, fileContents, verbose) {
|
|
2603
2596
|
if (!headSha) {
|
|
2604
2597
|
if (shouldLog(verbose, 1)) {
|
|
2605
2598
|
console.log(` ⚠️ 无法获取 PR head SHA,跳过变更文件检查`);
|
|
@@ -2628,13 +2621,32 @@ function generateIssueKey(issue) {
|
|
|
2628
2621
|
}
|
|
2629
2622
|
// 将变更文件的历史 issue 标记为无效
|
|
2630
2623
|
let invalidatedCount = 0;
|
|
2624
|
+
let preservedCount = 0;
|
|
2631
2625
|
this._result.issues = this._result.issues.map((issue)=>{
|
|
2632
|
-
// 如果 issue
|
|
2633
|
-
if (issue.fixed || issue.
|
|
2626
|
+
// 如果 issue 已修复或已无效,不需要处理
|
|
2627
|
+
if (issue.fixed || issue.valid === "false") {
|
|
2634
2628
|
return issue;
|
|
2635
2629
|
}
|
|
2636
|
-
// 如果 issue
|
|
2630
|
+
// 如果 issue 所在文件有变更
|
|
2637
2631
|
if (changedFileSet.has(issue.file)) {
|
|
2632
|
+
// 已 resolved 的 issue:检查当前代码行是否与 issue.code 不同
|
|
2633
|
+
// 不同说明用户 resolve 后代码确实变了,保留其 resolve 状态
|
|
2634
|
+
if (issue.resolved && issue.code && fileContents) {
|
|
2635
|
+
const contentLines = fileContents.get(issue.file);
|
|
2636
|
+
if (contentLines) {
|
|
2637
|
+
const lineNums = issue.line.split("-").map(Number).filter((n)=>!isNaN(n));
|
|
2638
|
+
const startLine = lineNums[0];
|
|
2639
|
+
const endLine = lineNums[lineNums.length - 1];
|
|
2640
|
+
const currentCode = contentLines.slice(startLine - 1, endLine).map(([, line])=>line).join("\n").trim();
|
|
2641
|
+
if (currentCode !== issue.code) {
|
|
2642
|
+
preservedCount++;
|
|
2643
|
+
if (shouldLog(verbose, 1)) {
|
|
2644
|
+
console.log(` ✅ Issue ${issue.file}:${issue.line} 已 resolved 且代码已变更,保留`);
|
|
2645
|
+
}
|
|
2646
|
+
return issue;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2638
2650
|
invalidatedCount++;
|
|
2639
2651
|
if (shouldLog(verbose, 1)) {
|
|
2640
2652
|
console.log(` 🗑️ Issue ${issue.file}:${issue.line} 所在文件有变更,标记为无效`);
|
|
@@ -2647,8 +2659,11 @@ function generateIssueKey(issue) {
|
|
|
2647
2659
|
}
|
|
2648
2660
|
return issue;
|
|
2649
2661
|
});
|
|
2650
|
-
if (invalidatedCount > 0 && shouldLog(verbose, 1)) {
|
|
2651
|
-
|
|
2662
|
+
if ((invalidatedCount > 0 || preservedCount > 0) && shouldLog(verbose, 1)) {
|
|
2663
|
+
const parts = [];
|
|
2664
|
+
if (invalidatedCount > 0) parts.push(`标记 ${invalidatedCount} 个无效`);
|
|
2665
|
+
if (preservedCount > 0) parts.push(`保留 ${preservedCount} 个已 resolved`);
|
|
2666
|
+
console.log(` 📊 Issue 处理: ${parts.join(",")}`);
|
|
2652
2667
|
}
|
|
2653
2668
|
} catch (error) {
|
|
2654
2669
|
if (shouldLog(verbose, 1)) {
|
|
@@ -3200,6 +3215,80 @@ function generateIssueKey(issue) {
|
|
|
3200
3215
|
}).optional()
|
|
3201
3216
|
});
|
|
3202
3217
|
|
|
3218
|
+
;// CONCATENATED MODULE: ./src/changed-file-collection.ts
|
|
3219
|
+
|
|
3220
|
+
/**
|
|
3221
|
+
* 变更文件集合,封装 ChangedFile[] 并提供常用访问器。
|
|
3222
|
+
*/ class ChangedFileCollection {
|
|
3223
|
+
_files;
|
|
3224
|
+
constructor(files){
|
|
3225
|
+
this._files = files;
|
|
3226
|
+
}
|
|
3227
|
+
static from(files) {
|
|
3228
|
+
return new ChangedFileCollection(files);
|
|
3229
|
+
}
|
|
3230
|
+
static empty() {
|
|
3231
|
+
return new ChangedFileCollection([]);
|
|
3232
|
+
}
|
|
3233
|
+
get length() {
|
|
3234
|
+
return this._files.length;
|
|
3235
|
+
}
|
|
3236
|
+
toArray() {
|
|
3237
|
+
return [
|
|
3238
|
+
...this._files
|
|
3239
|
+
];
|
|
3240
|
+
}
|
|
3241
|
+
[Symbol.iterator]() {
|
|
3242
|
+
return this._files[Symbol.iterator]();
|
|
3243
|
+
}
|
|
3244
|
+
filenames() {
|
|
3245
|
+
return this._files.map((f)=>f.filename ?? "").filter(Boolean);
|
|
3246
|
+
}
|
|
3247
|
+
extensions() {
|
|
3248
|
+
const exts = new Set();
|
|
3249
|
+
for (const f of this._files){
|
|
3250
|
+
if (f.filename) {
|
|
3251
|
+
const ext = extname(f.filename).replace(/^\./, "").toLowerCase();
|
|
3252
|
+
if (ext) exts.add(ext);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
return exts;
|
|
3256
|
+
}
|
|
3257
|
+
has(filename) {
|
|
3258
|
+
return this._files.some((f)=>f.filename === filename);
|
|
3259
|
+
}
|
|
3260
|
+
filter(predicate) {
|
|
3261
|
+
return new ChangedFileCollection(this._files.filter(predicate));
|
|
3262
|
+
}
|
|
3263
|
+
map(fn) {
|
|
3264
|
+
return this._files.map(fn);
|
|
3265
|
+
}
|
|
3266
|
+
countByStatus() {
|
|
3267
|
+
let added = 0, modified = 0, deleted = 0;
|
|
3268
|
+
for (const f of this._files){
|
|
3269
|
+
if (f.status === "added") added++;
|
|
3270
|
+
else if (f.status === "modified") modified++;
|
|
3271
|
+
else if (f.status === "deleted") deleted++;
|
|
3272
|
+
}
|
|
3273
|
+
return {
|
|
3274
|
+
added,
|
|
3275
|
+
modified,
|
|
3276
|
+
deleted
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
nonDeletedFiles() {
|
|
3280
|
+
return this.filter((f)=>f.status !== "deleted" && !!f.filename);
|
|
3281
|
+
}
|
|
3282
|
+
filterByFilenames(names) {
|
|
3283
|
+
const nameSet = new Set(names);
|
|
3284
|
+
return this.filter((f)=>!!f.filename && nameSet.has(f.filename));
|
|
3285
|
+
}
|
|
3286
|
+
filterByCommitFiles(commitFilenames) {
|
|
3287
|
+
const nameSet = new Set(commitFilenames);
|
|
3288
|
+
return this.filter((f)=>!!f.filename && nameSet.has(f.filename));
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3203
3292
|
;// CONCATENATED MODULE: ./src/parse-title-options.ts
|
|
3204
3293
|
|
|
3205
3294
|
/**
|
|
@@ -3763,9 +3852,8 @@ class ReviewIssueFilter {
|
|
|
3763
3852
|
}
|
|
3764
3853
|
/**
|
|
3765
3854
|
* LLM 验证历史问题是否已修复
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
const { llmMode, specSources, verbose } = context;
|
|
3855
|
+
*/ async verifyAndUpdateIssues(context, issues, commits, preloaded) {
|
|
3856
|
+
const { llmMode, verbose } = context;
|
|
3769
3857
|
const unfixedIssues = issues.filter((i)=>i.valid !== "false" && !i.fixed);
|
|
3770
3858
|
if (unfixedIssues.length === 0) {
|
|
3771
3859
|
return issues;
|
|
@@ -3776,26 +3864,10 @@ class ReviewIssueFilter {
|
|
|
3776
3864
|
}
|
|
3777
3865
|
return issues;
|
|
3778
3866
|
}
|
|
3779
|
-
if (!preloaded && (!specSources?.length || !pr)) {
|
|
3780
|
-
if (shouldLog(verbose, 1)) {
|
|
3781
|
-
console.log(` ⏭️ 跳过 LLM 验证(缺少 specSources 或 pr)`);
|
|
3782
|
-
}
|
|
3783
|
-
return issues;
|
|
3784
|
-
}
|
|
3785
3867
|
if (shouldLog(verbose, 1)) {
|
|
3786
3868
|
console.log(`\n🔍 开始 LLM 验证 ${unfixedIssues.length} 个未修复问题...`);
|
|
3787
3869
|
}
|
|
3788
|
-
|
|
3789
|
-
let fileContents;
|
|
3790
|
-
if (preloaded) {
|
|
3791
|
-
specs = preloaded.specs;
|
|
3792
|
-
fileContents = preloaded.fileContents;
|
|
3793
|
-
} else {
|
|
3794
|
-
const changedFiles = await pr.getFiles();
|
|
3795
|
-
const headSha = await pr.getHeadSha();
|
|
3796
|
-
specs = await this.loadSpecs(specSources, verbose);
|
|
3797
|
-
fileContents = await this.getFileContents(pr.owner, pr.repo, changedFiles, commits, headSha, pr.number, verbose);
|
|
3798
|
-
}
|
|
3870
|
+
const { specs, fileContents } = preloaded;
|
|
3799
3871
|
return await this.issueVerifyService.verifyIssueFixes(issues, fileContents, specs, llmMode, verbose, context.verifyConcurrency);
|
|
3800
3872
|
}
|
|
3801
3873
|
async getChangedFilesBetweenRefs(_owner, _repo, baseRef, headRef) {
|
|
@@ -3832,77 +3904,6 @@ class ReviewIssueFilter {
|
|
|
3832
3904
|
return this.gitSdk.getFilesForCommit(sha);
|
|
3833
3905
|
}
|
|
3834
3906
|
}
|
|
3835
|
-
/**
|
|
3836
|
-
* 获取文件内容并构建行号到 commit hash 的映射
|
|
3837
|
-
* 返回 Map<filename, Array<[commitHash, lineCode]>>
|
|
3838
|
-
*/ async getFileContents(owner, repo, changedFiles, commits, ref, prNumber, verbose, isLocalMode) {
|
|
3839
|
-
const contents = new Map();
|
|
3840
|
-
const latestCommitHash = commits[commits.length - 1]?.sha?.slice(0, 7) || "+local+";
|
|
3841
|
-
// 优先使用 changedFiles 中的 patch 字段(来自 PR 的整体 diff base...head)
|
|
3842
|
-
// 这样行号是相对于最终文件的,而不是每个 commit 的父 commit
|
|
3843
|
-
// buildLineCommitMap 遍历每个 commit 的 diff,行号可能与最终文件不一致
|
|
3844
|
-
if (shouldLog(verbose, 1)) {
|
|
3845
|
-
console.log(`📊 正在构建行号到变更的映射...`);
|
|
3846
|
-
}
|
|
3847
|
-
for (const file of changedFiles){
|
|
3848
|
-
if (file.filename && file.status !== "deleted") {
|
|
3849
|
-
try {
|
|
3850
|
-
let rawContent;
|
|
3851
|
-
if (isLocalMode) {
|
|
3852
|
-
// 本地模式:读取工作区文件的当前内容
|
|
3853
|
-
rawContent = this.gitSdk.getWorkingFileContent(file.filename);
|
|
3854
|
-
} else if (prNumber) {
|
|
3855
|
-
rawContent = await this.gitProvider.getFileContent(owner, repo, file.filename, ref);
|
|
3856
|
-
} else {
|
|
3857
|
-
rawContent = await this.gitSdk.getFileContent(ref, file.filename);
|
|
3858
|
-
}
|
|
3859
|
-
const lines = rawContent.split("\n");
|
|
3860
|
-
// 优先使用 file.patch(PR 整体 diff),这是相对于最终文件的行号
|
|
3861
|
-
let changedLines = parseChangedLinesFromPatch(file.patch);
|
|
3862
|
-
// 如果 changedLines 为空,需要判断是否应该将所有行标记为变更
|
|
3863
|
-
// 情况1: 文件是新增的(status 为 added/A)
|
|
3864
|
-
// 情况2: patch 为空但文件有 additions(部分 Git Provider API 可能不返回完整 patch)
|
|
3865
|
-
const isNewFile = file.status === "added" || file.status === "A" || file.additions && file.additions > 0 && file.deletions === 0 && !file.patch;
|
|
3866
|
-
if (changedLines.size === 0 && isNewFile) {
|
|
3867
|
-
changedLines = new Set(lines.map((_, i)=>i + 1));
|
|
3868
|
-
if (shouldLog(verbose, 2)) {
|
|
3869
|
-
console.log(` ℹ️ ${file.filename}: 新增文件无 patch,将所有 ${lines.length} 行标记为变更`);
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
if (shouldLog(verbose, 3)) {
|
|
3873
|
-
console.log(` 📄 ${file.filename}: ${lines.length} 行, ${changedLines.size} 行变更`);
|
|
3874
|
-
console.log(` latestCommitHash: ${latestCommitHash}`);
|
|
3875
|
-
if (changedLines.size > 0 && changedLines.size <= 20) {
|
|
3876
|
-
console.log(` 变更行号: ${Array.from(changedLines).sort((a, b)=>a - b).join(", ")}`);
|
|
3877
|
-
} else if (changedLines.size > 20) {
|
|
3878
|
-
console.log(` 变更行号: (共 ${changedLines.size} 行,省略详情)`);
|
|
3879
|
-
}
|
|
3880
|
-
if (!file.patch) {
|
|
3881
|
-
console.log(` ⚠️ 该文件没有 patch 信息 (status=${file.status}, additions=${file.additions}, deletions=${file.deletions})`);
|
|
3882
|
-
} else {
|
|
3883
|
-
console.log(` patch 前 200 字符: ${file.patch.slice(0, 200).replace(/\n/g, "\\n")}`);
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
const contentLines = lines.map((line, index)=>{
|
|
3887
|
-
const lineNum = index + 1;
|
|
3888
|
-
// 如果该行在 PR 的整体 diff 中被标记为变更,则使用最新 commit hash
|
|
3889
|
-
const hash = changedLines.has(lineNum) ? latestCommitHash : "-------";
|
|
3890
|
-
return [
|
|
3891
|
-
hash,
|
|
3892
|
-
line
|
|
3893
|
-
];
|
|
3894
|
-
});
|
|
3895
|
-
contents.set(file.filename, contentLines);
|
|
3896
|
-
} catch (error) {
|
|
3897
|
-
console.warn(`警告: 无法获取文件内容: ${file.filename}`, error);
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
}
|
|
3901
|
-
if (shouldLog(verbose, 1)) {
|
|
3902
|
-
console.log(`📊 映射构建完成,共 ${contents.size} 个文件`);
|
|
3903
|
-
}
|
|
3904
|
-
return contents;
|
|
3905
|
-
}
|
|
3906
3907
|
async fillIssueCode(issues, fileContents) {
|
|
3907
3908
|
return issues.map((issue)=>{
|
|
3908
3909
|
const contentLines = fileContents.get(issue.file);
|
|
@@ -3927,85 +3928,18 @@ class ReviewIssueFilter {
|
|
|
3927
3928
|
});
|
|
3928
3929
|
}
|
|
3929
3930
|
/**
|
|
3930
|
-
* 根据代码变更更新历史 issue 的行号
|
|
3931
|
-
* 当代码发生变化时,之前发现的 issue 行号可能已经不准确
|
|
3932
|
-
* 此方法通过分析 diff 来计算新的行号
|
|
3933
|
-
*/ updateIssueLineNumbers(issues, filePatchMap, verbose) {
|
|
3934
|
-
let updatedCount = 0;
|
|
3935
|
-
let invalidatedCount = 0;
|
|
3936
|
-
const updatedIssues = issues.map((issue)=>{
|
|
3937
|
-
// 如果 issue 已修复、已解决或无效,不需要更新行号
|
|
3938
|
-
if (issue.fixed || issue.resolved || issue.valid === "false") {
|
|
3939
|
-
return issue;
|
|
3940
|
-
}
|
|
3941
|
-
const patch = filePatchMap.get(issue.file);
|
|
3942
|
-
if (!patch) {
|
|
3943
|
-
// 文件没有变更,行号不变
|
|
3944
|
-
return issue;
|
|
3945
|
-
}
|
|
3946
|
-
const lines = this.reviewSpecService.parseLineRange(issue.line);
|
|
3947
|
-
if (lines.length === 0) {
|
|
3948
|
-
return issue;
|
|
3949
|
-
}
|
|
3950
|
-
const startLine = lines[0];
|
|
3951
|
-
const endLine = lines[lines.length - 1];
|
|
3952
|
-
const hunks = parseHunksFromPatch(patch);
|
|
3953
|
-
// 计算新的起始行号
|
|
3954
|
-
const newStartLine = calculateNewLineNumber(startLine, hunks);
|
|
3955
|
-
if (newStartLine === null) {
|
|
3956
|
-
// 起始行被删除,直接标记为无效问题
|
|
3957
|
-
invalidatedCount++;
|
|
3958
|
-
if (shouldLog(verbose, 1)) {
|
|
3959
|
-
console.log(`📍 Issue ${issue.file}:${issue.line} 对应的代码已被删除,标记为无效`);
|
|
3960
|
-
}
|
|
3961
|
-
return {
|
|
3962
|
-
...issue,
|
|
3963
|
-
valid: "false",
|
|
3964
|
-
originalLine: issue.originalLine ?? issue.line
|
|
3965
|
-
};
|
|
3966
|
-
}
|
|
3967
|
-
// 如果是范围行号,计算新的结束行号
|
|
3968
|
-
let newLine;
|
|
3969
|
-
if (startLine === endLine) {
|
|
3970
|
-
newLine = String(newStartLine);
|
|
3971
|
-
} else {
|
|
3972
|
-
const newEndLine = calculateNewLineNumber(endLine, hunks);
|
|
3973
|
-
if (newEndLine === null || newEndLine === newStartLine) {
|
|
3974
|
-
// 结束行被删除或范围缩小为单行,使用起始行
|
|
3975
|
-
newLine = String(newStartLine);
|
|
3976
|
-
} else {
|
|
3977
|
-
newLine = `${newStartLine}-${newEndLine}`;
|
|
3978
|
-
}
|
|
3979
|
-
}
|
|
3980
|
-
// 如果行号发生变化,更新 issue
|
|
3981
|
-
if (newLine !== issue.line) {
|
|
3982
|
-
updatedCount++;
|
|
3983
|
-
if (shouldLog(verbose, 1)) {
|
|
3984
|
-
console.log(`📍 Issue 行号更新: ${issue.file}:${issue.line} -> ${issue.file}:${newLine}`);
|
|
3985
|
-
}
|
|
3986
|
-
return {
|
|
3987
|
-
...issue,
|
|
3988
|
-
line: newLine,
|
|
3989
|
-
originalLine: issue.originalLine ?? issue.line
|
|
3990
|
-
};
|
|
3991
|
-
}
|
|
3992
|
-
return issue;
|
|
3993
|
-
});
|
|
3994
|
-
if ((updatedCount > 0 || invalidatedCount > 0) && shouldLog(verbose, 1)) {
|
|
3995
|
-
const parts = [];
|
|
3996
|
-
if (updatedCount > 0) parts.push(`更新 ${updatedCount} 个行号`);
|
|
3997
|
-
if (invalidatedCount > 0) parts.push(`标记 ${invalidatedCount} 个无效`);
|
|
3998
|
-
console.log(`📊 Issue 行号处理: ${parts.join(",")}`);
|
|
3999
|
-
}
|
|
4000
|
-
return updatedIssues;
|
|
4001
|
-
}
|
|
4002
|
-
/**
|
|
4003
3931
|
* 过滤掉不属于本次 PR commits 的问题(排除 merge commit 引入的代码)
|
|
4004
3932
|
* 根据 fileContents 中问题行的实际 commit hash 进行验证,而不是依赖 LLM 填写的 commit
|
|
4005
3933
|
*/ filterIssuesByValidCommits(issues, commits, fileContents, verbose) {
|
|
4006
3934
|
const validCommitHashes = new Set(commits.map((c)=>c.sha?.slice(0, 7)).filter(Boolean));
|
|
3935
|
+
// commits 为空时(如分支比较模式本地无 commit 信息),退化为"行是否在 diff 变更范围内"模式
|
|
3936
|
+
const useChangedLinesMode = validCommitHashes.size === 0;
|
|
4007
3937
|
if (shouldLog(verbose, 3)) {
|
|
4008
|
-
|
|
3938
|
+
if (useChangedLinesMode) {
|
|
3939
|
+
console.log(` 🔍 commits 为空,使用变更行模式过滤`);
|
|
3940
|
+
} else {
|
|
3941
|
+
console.log(` 🔍 有效 commit hashes: ${Array.from(validCommitHashes).join(", ")}`);
|
|
3942
|
+
}
|
|
4009
3943
|
}
|
|
4010
3944
|
const beforeCount = issues.length;
|
|
4011
3945
|
const filtered = issues.filter((issue)=>{
|
|
@@ -4024,12 +3958,14 @@ class ReviewIssueFilter {
|
|
|
4024
3958
|
}
|
|
4025
3959
|
return true;
|
|
4026
3960
|
}
|
|
4027
|
-
//
|
|
3961
|
+
// 检查问题行范围内是否有任意一行属于本次变更(diff 范围)
|
|
4028
3962
|
for (const lineNum of lineNums){
|
|
4029
3963
|
const lineData = contentLines[lineNum - 1];
|
|
4030
3964
|
if (lineData) {
|
|
4031
3965
|
const [actualHash] = lineData;
|
|
4032
|
-
|
|
3966
|
+
const isChangedLine = actualHash !== "-------";
|
|
3967
|
+
const isValid = useChangedLinesMode ? isChangedLine : isChangedLine && validCommitHashes.has(actualHash);
|
|
3968
|
+
if (isValid) {
|
|
4033
3969
|
if (shouldLog(verbose, 3)) {
|
|
4034
3970
|
console.log(` ✅ Issue ${issue.file}:${issue.line} - 行 ${lineNum} hash=${actualHash} 匹配,保留`);
|
|
4035
3971
|
}
|
|
@@ -4037,9 +3973,9 @@ class ReviewIssueFilter {
|
|
|
4037
3973
|
}
|
|
4038
3974
|
}
|
|
4039
3975
|
}
|
|
4040
|
-
//
|
|
3976
|
+
// 问题行都不属于本次变更范围
|
|
4041
3977
|
if (shouldLog(verbose, 2)) {
|
|
4042
|
-
console.log(` Issue ${issue.file}:${issue.line}
|
|
3978
|
+
console.log(` Issue ${issue.file}:${issue.line} 不在本次变更行范围内,跳过`);
|
|
4043
3979
|
}
|
|
4044
3980
|
if (shouldLog(verbose, 3)) {
|
|
4045
3981
|
const hashes = lineNums.map((ln)=>{
|
|
@@ -4051,7 +3987,7 @@ class ReviewIssueFilter {
|
|
|
4051
3987
|
return false;
|
|
4052
3988
|
});
|
|
4053
3989
|
if (beforeCount !== filtered.length && shouldLog(verbose, 1)) {
|
|
4054
|
-
console.log(`
|
|
3990
|
+
console.log(` 变更行过滤后: ${beforeCount} -> ${filtered.length} 个问题`);
|
|
4055
3991
|
}
|
|
4056
3992
|
return filtered;
|
|
4057
3993
|
}
|
|
@@ -4071,43 +4007,6 @@ class ReviewIssueFilter {
|
|
|
4071
4007
|
generateIssueKey(issue) {
|
|
4072
4008
|
return generateIssueKey(issue);
|
|
4073
4009
|
}
|
|
4074
|
-
/**
|
|
4075
|
-
* 构建文件行号到 commit hash 的映射
|
|
4076
|
-
* 遍历每个 commit,获取其修改的文件和行号
|
|
4077
|
-
* 优先使用 API,失败时回退到 git 命令
|
|
4078
|
-
*/ async buildLineCommitMap(owner, repo, commits, verbose) {
|
|
4079
|
-
// Map<filename, Map<lineNumber, commitHash>>
|
|
4080
|
-
const fileLineMap = new Map();
|
|
4081
|
-
// 按时间顺序遍历 commits(早的在前),后面的 commit 会覆盖前面的
|
|
4082
|
-
for (const commit of commits){
|
|
4083
|
-
if (!commit.sha) continue;
|
|
4084
|
-
const shortHash = commit.sha.slice(0, 7);
|
|
4085
|
-
let files = [];
|
|
4086
|
-
// 优先使用 getCommitDiff API 获取 diff 文本
|
|
4087
|
-
try {
|
|
4088
|
-
const diffText = await this.gitProvider.getCommitDiff(owner, repo, commit.sha);
|
|
4089
|
-
files = parseDiffText(diffText);
|
|
4090
|
-
} catch {
|
|
4091
|
-
// API 失败,回退到 git 命令
|
|
4092
|
-
files = this.gitSdk.getCommitDiff(commit.sha);
|
|
4093
|
-
}
|
|
4094
|
-
if (shouldLog(verbose, 2)) console.log(` commit ${shortHash}: ${files.length} 个文件变更`);
|
|
4095
|
-
for (const file of files){
|
|
4096
|
-
// 解析这个 commit 修改的行号
|
|
4097
|
-
const changedLines = parseChangedLinesFromPatch(file.patch);
|
|
4098
|
-
// 获取或创建文件的行号映射
|
|
4099
|
-
if (!fileLineMap.has(file.filename)) {
|
|
4100
|
-
fileLineMap.set(file.filename, new Map());
|
|
4101
|
-
}
|
|
4102
|
-
const lineMap = fileLineMap.get(file.filename);
|
|
4103
|
-
// 记录每行对应的 commit hash
|
|
4104
|
-
for (const lineNum of changedLines){
|
|
4105
|
-
lineMap.set(lineNum, shortHash);
|
|
4106
|
-
}
|
|
4107
|
-
}
|
|
4108
|
-
}
|
|
4109
|
-
return fileLineMap;
|
|
4110
|
-
}
|
|
4111
4010
|
}
|
|
4112
4011
|
|
|
4113
4012
|
;// CONCATENATED MODULE: ./src/utils/review-llm.ts
|
|
@@ -4738,6 +4637,7 @@ const buildDeletionImpactAgentPrompt = (ctx)=>{
|
|
|
4738
4637
|
const { spec, rule } = ctx.ruleInfo;
|
|
4739
4638
|
ruleSection = `### ${spec.filename} (${spec.type})\n\n${spec.content.slice(0, 200)}...\n\n#### 规则\n- ${rule.id}: ${rule.title}\n ${rule.description}`;
|
|
4740
4639
|
}
|
|
4640
|
+
const originalCodeSection = ctx.issue.code ? `\n## 问题发现时的原始代码(用于对比)\n\n\`\`\`\n${ctx.issue.code}\n\`\`\`\n` : "";
|
|
4741
4641
|
const userPrompt = `## 规则定义
|
|
4742
4642
|
|
|
4743
4643
|
${ruleSection}
|
|
@@ -4745,18 +4645,19 @@ ${ruleSection}
|
|
|
4745
4645
|
## 之前发现的问题
|
|
4746
4646
|
|
|
4747
4647
|
- **文件**: ${ctx.issue.file}
|
|
4748
|
-
- **行号**: ${ctx.issue.line}
|
|
4648
|
+
- **行号**: ${ctx.issue.line}(问题发现时的行号,可能因代码变更而偏移)
|
|
4749
4649
|
- **规则**: ${ctx.issue.ruleId} (来自 ${ctx.issue.specFile})
|
|
4750
4650
|
- **问题描述**: ${ctx.issue.reason}
|
|
4751
4651
|
${ctx.issue.suggestion ? `- **原建议**: ${ctx.issue.suggestion}` : ""}
|
|
4752
|
-
|
|
4652
|
+
${originalCodeSection}
|
|
4753
4653
|
## 当前文件内容
|
|
4754
4654
|
|
|
4755
4655
|
\`\`\`
|
|
4756
4656
|
${linesWithNumbers}
|
|
4757
4657
|
\`\`\`
|
|
4758
4658
|
|
|
4759
|
-
|
|
4659
|
+
请判断这个问题是否有效,以及是否已经被修复。
|
|
4660
|
+
**注意**:如果提供了"问题发现时的原始代码",请优先通过搜索该代码片段来定位问题位置,而不是仅依赖行号(行号可能因代码变更已经偏移)。`;
|
|
4760
4661
|
return {
|
|
4761
4662
|
systemPrompt,
|
|
4762
4663
|
userPrompt
|
|
@@ -4908,8 +4809,8 @@ class ReviewLlmProcessor {
|
|
|
4908
4809
|
}
|
|
4909
4810
|
async buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResult, whenModifiedCode, verbose, systemRules) {
|
|
4910
4811
|
const round = (existingResult?.round ?? 0) + 1;
|
|
4911
|
-
const { staticIssues, skippedFiles } = applyStaticRules(changedFiles, fileContents, systemRules, round, verbose);
|
|
4912
|
-
const fileDataList = changedFiles.
|
|
4812
|
+
const { staticIssues, skippedFiles } = applyStaticRules(changedFiles.toArray(), fileContents, systemRules, round, verbose);
|
|
4813
|
+
const fileDataList = changedFiles.nonDeletedFiles().map((file)=>{
|
|
4913
4814
|
const filename = file.filename;
|
|
4914
4815
|
if (skippedFiles.has(filename)) return null;
|
|
4915
4816
|
const contentLines = fileContents.get(filename);
|
|
@@ -5190,7 +5091,7 @@ class ReviewLlmProcessor {
|
|
|
5190
5091
|
*/ async generatePrDescription(commits, changedFiles, llmMode, fileContents, verbose) {
|
|
5191
5092
|
const { userPrompt } = buildPrDescriptionPrompt({
|
|
5192
5093
|
commits,
|
|
5193
|
-
changedFiles,
|
|
5094
|
+
changedFiles: changedFiles.toArray(),
|
|
5194
5095
|
fileContents
|
|
5195
5096
|
});
|
|
5196
5097
|
try {
|
|
@@ -5230,7 +5131,7 @@ class ReviewLlmProcessor {
|
|
|
5230
5131
|
*/ async generatePrTitle(commits, changedFiles) {
|
|
5231
5132
|
const { userPrompt } = buildPrTitlePrompt({
|
|
5232
5133
|
commits,
|
|
5233
|
-
changedFiles
|
|
5134
|
+
changedFiles: changedFiles.toArray()
|
|
5234
5135
|
});
|
|
5235
5136
|
try {
|
|
5236
5137
|
const stream = this.llmProxyService.chatStream([
|
|
@@ -5273,9 +5174,7 @@ class ReviewLlmProcessor {
|
|
|
5273
5174
|
}
|
|
5274
5175
|
}
|
|
5275
5176
|
if (changedFiles.length > 0) {
|
|
5276
|
-
const added = changedFiles.
|
|
5277
|
-
const modified = changedFiles.filter((f)=>f.status === "modified").length;
|
|
5278
|
-
const deleted = changedFiles.filter((f)=>f.status === "deleted").length;
|
|
5177
|
+
const { added, modified, deleted } = changedFiles.countByStatus();
|
|
5279
5178
|
const stats = [];
|
|
5280
5179
|
if (added > 0) stats.push(`新增 ${added}`);
|
|
5281
5180
|
if (modified > 0) stats.push(`修改 ${modified}`);
|
|
@@ -5289,200 +5188,153 @@ class ReviewLlmProcessor {
|
|
|
5289
5188
|
}
|
|
5290
5189
|
}
|
|
5291
5190
|
|
|
5292
|
-
;// CONCATENATED MODULE: ./src/review.
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5191
|
+
;// CONCATENATED MODULE: ./src/review-source-resolver.ts
|
|
5296
5192
|
|
|
5297
5193
|
|
|
5298
5194
|
|
|
5299
5195
|
|
|
5300
5196
|
|
|
5301
5197
|
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5198
|
+
/**
|
|
5199
|
+
* 审查源数据解析器:根据审查模式(本地/PR/分支比较)获取 commits、changedFiles 等输入数据,
|
|
5200
|
+
* 并应用前置过滤管道(merge commit、files、commits、includes)。
|
|
5201
|
+
*
|
|
5202
|
+
* 从 ReviewService 中提取,职责单一化:只负责"获取和过滤源数据",不涉及 LLM 审查、报告生成等。
|
|
5203
|
+
*/ class ReviewSourceResolver {
|
|
5305
5204
|
gitProvider;
|
|
5306
|
-
config;
|
|
5307
|
-
reviewSpecService;
|
|
5308
|
-
llmProxyService;
|
|
5309
|
-
reviewReportService;
|
|
5310
|
-
issueVerifyService;
|
|
5311
|
-
deletionImpactService;
|
|
5312
5205
|
gitSdk;
|
|
5313
|
-
contextBuilder;
|
|
5314
5206
|
issueFilter;
|
|
5315
|
-
|
|
5316
|
-
resultModelDeps;
|
|
5317
|
-
constructor(gitProvider, config, reviewSpecService, llmProxyService, reviewReportService, issueVerifyService, deletionImpactService, gitSdk){
|
|
5207
|
+
constructor(gitProvider, gitSdk, issueFilter){
|
|
5318
5208
|
this.gitProvider = gitProvider;
|
|
5319
|
-
this.config = config;
|
|
5320
|
-
this.reviewSpecService = reviewSpecService;
|
|
5321
|
-
this.llmProxyService = llmProxyService;
|
|
5322
|
-
this.reviewReportService = reviewReportService;
|
|
5323
|
-
this.issueVerifyService = issueVerifyService;
|
|
5324
|
-
this.deletionImpactService = deletionImpactService;
|
|
5325
5209
|
this.gitSdk = gitSdk;
|
|
5326
|
-
this.
|
|
5327
|
-
this.issueFilter = new ReviewIssueFilter(gitProvider, config, reviewSpecService, issueVerifyService, gitSdk);
|
|
5328
|
-
this.llmProcessor = new ReviewLlmProcessor(llmProxyService, reviewSpecService);
|
|
5329
|
-
this.resultModelDeps = {
|
|
5330
|
-
gitProvider,
|
|
5331
|
-
config,
|
|
5332
|
-
reviewSpecService,
|
|
5333
|
-
reviewReportService
|
|
5334
|
-
};
|
|
5335
|
-
}
|
|
5336
|
-
async getContextFromEnv(options) {
|
|
5337
|
-
return this.contextBuilder.getContextFromEnv(options);
|
|
5210
|
+
this.issueFilter = issueFilter;
|
|
5338
5211
|
}
|
|
5339
5212
|
/**
|
|
5340
|
-
*
|
|
5341
|
-
*
|
|
5342
|
-
*
|
|
5343
|
-
* 2. 获取 PR/分支的变更文件和提交记录
|
|
5344
|
-
* 3. 调用 LLM 进行代码审查
|
|
5345
|
-
* 4. 处理历史 issue(更新行号、验证修复状态)
|
|
5346
|
-
* 5. 生成并发布审查报告
|
|
5213
|
+
* 解析输入数据:根据模式(本地/PR/分支比较)获取 commits、changedFiles 等。
|
|
5214
|
+
* 包含前置过滤(merge commit、files、commits、includes)。
|
|
5215
|
+
* 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
|
|
5347
5216
|
*
|
|
5348
|
-
*
|
|
5349
|
-
*
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5217
|
+
* 数据获取流程:
|
|
5218
|
+
* 1. 本地模式 → resolveLocalFiles(暂存区/未提交变更,无变更时回退分支比较)
|
|
5219
|
+
* 2. 直接文件模式(-f)→ 构造 changedFiles
|
|
5220
|
+
* 3. PR 模式 → resolvePrData(含重复 workflow 检查)
|
|
5221
|
+
* 4. 分支比较模式 → resolveBranchCompareData
|
|
5222
|
+
*
|
|
5223
|
+
* 前置过滤管道(applyPreFilters):
|
|
5224
|
+
* 0. merge commit 过滤
|
|
5225
|
+
* 1. --files 过滤
|
|
5226
|
+
* 2. --commits 过滤
|
|
5227
|
+
* 3. --includes 过滤(支持 status| 前缀语法)
|
|
5228
|
+
*/ async resolve(context) {
|
|
5229
|
+
const { prNumber, verbose, files, localMode } = context;
|
|
5230
|
+
const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
|
|
5231
|
+
let isLocalMode = !!localMode;
|
|
5232
|
+
let effectiveBaseRef = context.baseRef;
|
|
5233
|
+
let effectiveHeadRef = context.headRef;
|
|
5234
|
+
let prModel;
|
|
5235
|
+
let commits = [];
|
|
5236
|
+
let changedFiles = [];
|
|
5237
|
+
// ── 阶段 1:按模式获取 commits + changedFiles ──────────
|
|
5238
|
+
if (isLocalMode) {
|
|
5239
|
+
const local = this.resolveLocalFiles(localMode, verbose);
|
|
5240
|
+
if (local.earlyReturn) return {
|
|
5241
|
+
...local.earlyReturn,
|
|
5242
|
+
changedFiles: ChangedFileCollection.from(local.earlyReturn.changedFiles),
|
|
5243
|
+
isDirectFileMode: false,
|
|
5244
|
+
fileContents: new Map()
|
|
5245
|
+
};
|
|
5246
|
+
isLocalMode = local.isLocalMode;
|
|
5247
|
+
changedFiles = local.changedFiles;
|
|
5248
|
+
effectiveBaseRef = local.effectiveBaseRef ?? effectiveBaseRef;
|
|
5249
|
+
effectiveHeadRef = local.effectiveHeadRef ?? effectiveHeadRef;
|
|
5369
5250
|
}
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5251
|
+
if (isDirectFileMode) {
|
|
5252
|
+
// 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
|
|
5253
|
+
if (shouldLog(verbose, 1)) {
|
|
5254
|
+
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
|
|
5255
|
+
}
|
|
5256
|
+
changedFiles = files.map((f)=>({
|
|
5257
|
+
filename: f,
|
|
5258
|
+
status: "modified"
|
|
5259
|
+
}));
|
|
5260
|
+
isLocalMode = true;
|
|
5261
|
+
} else if (prNumber) {
|
|
5262
|
+
const prData = await this.resolvePrData(context);
|
|
5263
|
+
if (prData.earlyReturn) {
|
|
5264
|
+
return {
|
|
5265
|
+
...prData,
|
|
5266
|
+
changedFiles: ChangedFileCollection.from(prData.changedFiles),
|
|
5267
|
+
headSha: prData.headSha,
|
|
5268
|
+
isLocalMode,
|
|
5269
|
+
isDirectFileMode,
|
|
5270
|
+
fileContents: new Map()
|
|
5271
|
+
};
|
|
5272
|
+
}
|
|
5273
|
+
prModel = prData.prModel;
|
|
5274
|
+
commits = prData.commits;
|
|
5275
|
+
changedFiles = prData.changedFiles;
|
|
5276
|
+
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
5277
|
+
if (changedFiles.length === 0) {
|
|
5278
|
+
const branchData = await this.resolveBranchCompareData(context, effectiveBaseRef, effectiveHeadRef);
|
|
5279
|
+
commits = branchData.commits;
|
|
5280
|
+
changedFiles = branchData.changedFiles;
|
|
5281
|
+
}
|
|
5282
|
+
} else if (!isLocalMode) {
|
|
5283
|
+
if (shouldLog(verbose, 1)) {
|
|
5284
|
+
console.log(`❌ 错误: 缺少 prNumber 或 baseRef/headRef`, {
|
|
5285
|
+
prNumber,
|
|
5286
|
+
baseRef: context.baseRef,
|
|
5287
|
+
headRef: context.headRef
|
|
5288
|
+
});
|
|
5289
|
+
}
|
|
5290
|
+
throw new Error("必须指定 PR 编号或者 base/head 分支");
|
|
5376
5291
|
}
|
|
5292
|
+
// ── 阶段 2:前置过滤管道 ─────────────────────────────
|
|
5293
|
+
({ commits, changedFiles } = await this.applyPreFilters(context, commits, changedFiles, isDirectFileMode));
|
|
5294
|
+
const headSha = prModel ? await prModel.getHeadSha() : context.headRef || "HEAD";
|
|
5295
|
+
const collectedFiles = ChangedFileCollection.from(changedFiles);
|
|
5296
|
+
const fileContents = await this.getFileContents(context.owner, context.repo, collectedFiles.toArray(), commits, headSha, context.prNumber, isLocalMode, context.showAll, context.verbose);
|
|
5297
|
+
return {
|
|
5298
|
+
prModel,
|
|
5299
|
+
commits,
|
|
5300
|
+
changedFiles: collectedFiles,
|
|
5301
|
+
headSha,
|
|
5302
|
+
isLocalMode,
|
|
5303
|
+
isDirectFileMode,
|
|
5304
|
+
fileContents
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
// ─── 数据获取子方法 ──────────────────────────────────────
|
|
5308
|
+
/**
|
|
5309
|
+
* 本地模式:获取暂存区或未提交的变更文件。
|
|
5310
|
+
* 如果本地无变更,自动回退到分支比较模式并检测 base/head 分支。
|
|
5311
|
+
* 同分支时通过 earlyReturn 提前终止。
|
|
5312
|
+
*/ resolveLocalFiles(localMode, verbose) {
|
|
5377
5313
|
if (shouldLog(verbose, 1)) {
|
|
5378
|
-
console.log(
|
|
5379
|
-
}
|
|
5380
|
-
if (applicableSpecs.length === 0 || changedFiles.length === 0) {
|
|
5381
|
-
return this.handleNoApplicableSpecs(context, applicableSpecs, changedFiles, commits);
|
|
5314
|
+
console.log(`📥 本地模式: 获取${localMode === "staged" ? "暂存区" : "未提交"}的代码变更`);
|
|
5382
5315
|
}
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
// 获取上一次的审查结果(用于提示词优化和轮次推进)
|
|
5387
|
-
let existingResultModel = null;
|
|
5388
|
-
if (context.ci && prModel) {
|
|
5389
|
-
existingResultModel = await ReviewResultModel.loadFromPr(prModel, this.resultModelDeps);
|
|
5390
|
-
if (existingResultModel && shouldLog(verbose, 1)) {
|
|
5391
|
-
console.log(`📋 获取到上一次审查结果,包含 ${existingResultModel.issues.length} 个问题`);
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
if (shouldLog(verbose, 1)) {
|
|
5395
|
-
console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
|
|
5396
|
-
}
|
|
5397
|
-
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null, effectiveWhenModifiedCode, verbose, context.systemRules);
|
|
5398
|
-
const result = await this.runLLMReview(llmMode, reviewPrompt, {
|
|
5399
|
-
verbose,
|
|
5400
|
-
concurrency: context.concurrency,
|
|
5401
|
-
timeout: context.timeout,
|
|
5402
|
-
retries: context.retries,
|
|
5403
|
-
retryDelay: context.retryDelay
|
|
5404
|
-
});
|
|
5405
|
-
// 填充 PR 功能描述和标题
|
|
5406
|
-
const prInfo = context.generateDescription ? await this.generatePrDescription(commits, changedFiles, llmMode, fileContents, verbose) : await this.buildBasicDescription(commits, changedFiles);
|
|
5407
|
-
result.title = prInfo.title;
|
|
5408
|
-
result.description = prInfo.description;
|
|
5409
|
-
if (shouldLog(verbose, 1)) {
|
|
5410
|
-
console.log(`📝 LLM 审查完成,发现 ${result.issues.length} 个问题`);
|
|
5411
|
-
}
|
|
5412
|
-
// 4. 过滤新 issues
|
|
5413
|
-
result.issues = await this.fillIssueCode(result.issues, fileContents);
|
|
5414
|
-
result.issues = this.filterNewIssues(result.issues, specs, applicableSpecs, {
|
|
5415
|
-
commits,
|
|
5416
|
-
fileContents,
|
|
5417
|
-
changedFiles,
|
|
5418
|
-
isDirectFileMode,
|
|
5419
|
-
context
|
|
5420
|
-
});
|
|
5421
|
-
// 静态规则产生的系统问题直接合并,不经过过滤管道
|
|
5422
|
-
if (reviewPrompt.staticIssues?.length) {
|
|
5423
|
-
result.issues = [
|
|
5424
|
-
...reviewPrompt.staticIssues,
|
|
5425
|
-
...result.issues
|
|
5426
|
-
];
|
|
5316
|
+
const localFiles = localMode === "staged" ? this.gitSdk.getStagedFiles() : this.gitSdk.getUncommittedFiles();
|
|
5317
|
+
if (localFiles.length === 0) {
|
|
5318
|
+
// 本地无变更,回退到分支比较模式
|
|
5427
5319
|
if (shouldLog(verbose, 1)) {
|
|
5428
|
-
console.log(
|
|
5320
|
+
console.log(`ℹ️ 没有${localMode === "staged" ? "暂存区" : "未提交"}的代码变更,回退到分支比较模式`);
|
|
5429
5321
|
}
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
console.log(`📝 最终发现 ${result.issues.length} 个问题`);
|
|
5433
|
-
}
|
|
5434
|
-
// 5. 构建最终的 ReviewResultModel
|
|
5435
|
-
const finalModel = await this.buildFinalModel(context, result, {
|
|
5436
|
-
prModel,
|
|
5437
|
-
commits,
|
|
5438
|
-
headSha,
|
|
5439
|
-
specs,
|
|
5440
|
-
fileContents
|
|
5441
|
-
}, existingResultModel);
|
|
5442
|
-
// 6. 保存 + 输出
|
|
5443
|
-
await this.saveAndOutput(context, finalModel, commits);
|
|
5444
|
-
return finalModel.result;
|
|
5445
|
-
}
|
|
5446
|
-
// ─── 提取的子方法 ──────────────────────────────────────
|
|
5447
|
-
/**
|
|
5448
|
-
* 解析输入数据:根据模式(本地/PR/分支比较)获取 commits、changedFiles 等。
|
|
5449
|
-
* 包含前置过滤(merge commit、files、commits、includes)。
|
|
5450
|
-
* 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
|
|
5451
|
-
*/ async resolveSourceData(context) {
|
|
5452
|
-
const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode, duplicateWorkflowResolved } = context;
|
|
5453
|
-
const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
|
|
5454
|
-
let isLocalMode = !!localMode;
|
|
5455
|
-
let effectiveBaseRef = baseRef;
|
|
5456
|
-
let effectiveHeadRef = headRef;
|
|
5457
|
-
let prModel;
|
|
5458
|
-
let commits = [];
|
|
5459
|
-
let changedFiles = [];
|
|
5460
|
-
if (isLocalMode) {
|
|
5461
|
-
// 本地模式:从 git 获取未提交/暂存区的变更
|
|
5322
|
+
const effectiveHeadRef = this.gitSdk.getCurrentBranch() ?? "HEAD";
|
|
5323
|
+
const effectiveBaseRef = this.gitSdk.getDefaultBranch();
|
|
5462
5324
|
if (shouldLog(verbose, 1)) {
|
|
5463
|
-
console.log(
|
|
5325
|
+
console.log(`📌 自动检测分支: base=${effectiveBaseRef}, head=${effectiveHeadRef}`);
|
|
5464
5326
|
}
|
|
5465
|
-
|
|
5466
|
-
if (
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
effectiveHeadRef = this.gitSdk.getCurrentBranch() ?? "HEAD";
|
|
5473
|
-
effectiveBaseRef = this.gitSdk.getDefaultBranch();
|
|
5474
|
-
if (shouldLog(verbose, 1)) {
|
|
5475
|
-
console.log(`📌 自动检测分支: base=${effectiveBaseRef}, head=${effectiveHeadRef}`);
|
|
5476
|
-
}
|
|
5477
|
-
// 同分支无法比较,提前返回
|
|
5478
|
-
if (effectiveBaseRef === effectiveHeadRef) {
|
|
5479
|
-
console.log(`ℹ️ 当前分支 ${effectiveHeadRef} 与默认分支相同,没有可审查的代码变更`);
|
|
5480
|
-
return {
|
|
5327
|
+
// 同分支无法比较,提前返回
|
|
5328
|
+
if (effectiveBaseRef === effectiveHeadRef) {
|
|
5329
|
+
console.log(`ℹ️ 当前分支 ${effectiveHeadRef} 与默认分支相同,没有可审查的代码变更`);
|
|
5330
|
+
return {
|
|
5331
|
+
changedFiles: [],
|
|
5332
|
+
isLocalMode: false,
|
|
5333
|
+
earlyReturn: {
|
|
5481
5334
|
commits: [],
|
|
5482
5335
|
changedFiles: [],
|
|
5483
5336
|
headSha: "HEAD",
|
|
5484
5337
|
isLocalMode: false,
|
|
5485
|
-
isDirectFileMode: false,
|
|
5486
5338
|
earlyReturn: {
|
|
5487
5339
|
success: true,
|
|
5488
5340
|
description: "",
|
|
@@ -5490,176 +5342,543 @@ class ReviewService {
|
|
|
5490
5342
|
summary: [],
|
|
5491
5343
|
round: 1
|
|
5492
5344
|
}
|
|
5493
|
-
}
|
|
5494
|
-
}
|
|
5495
|
-
} else {
|
|
5496
|
-
// 一次性获取所有 diff,避免每个文件调用一次 git 命令
|
|
5497
|
-
const localDiffs = localMode === "staged" ? this.gitSdk.getStagedDiff() : this.gitSdk.getUncommittedDiff();
|
|
5498
|
-
const diffMap = new Map(localDiffs.map((d)=>[
|
|
5499
|
-
d.filename,
|
|
5500
|
-
d.patch
|
|
5501
|
-
]));
|
|
5502
|
-
changedFiles = localFiles.map((f)=>({
|
|
5503
|
-
filename: f.filename,
|
|
5504
|
-
status: f.status,
|
|
5505
|
-
patch: diffMap.get(f.filename)
|
|
5506
|
-
}));
|
|
5507
|
-
if (shouldLog(verbose, 1)) {
|
|
5508
|
-
console.log(` Changed files: ${changedFiles.length}`);
|
|
5509
|
-
}
|
|
5345
|
+
}
|
|
5346
|
+
};
|
|
5510
5347
|
}
|
|
5348
|
+
return {
|
|
5349
|
+
changedFiles: [],
|
|
5350
|
+
isLocalMode: false,
|
|
5351
|
+
effectiveBaseRef,
|
|
5352
|
+
effectiveHeadRef
|
|
5353
|
+
};
|
|
5511
5354
|
}
|
|
5512
|
-
//
|
|
5513
|
-
|
|
5355
|
+
// 一次性获取所有 diff,避免每个文件调用一次 git 命令
|
|
5356
|
+
const localDiffs = localMode === "staged" ? this.gitSdk.getStagedDiff() : this.gitSdk.getUncommittedDiff();
|
|
5357
|
+
const diffMap = new Map(localDiffs.map((d)=>[
|
|
5358
|
+
d.filename,
|
|
5359
|
+
d.patch
|
|
5360
|
+
]));
|
|
5361
|
+
const changedFiles = localFiles.map((f)=>({
|
|
5362
|
+
filename: f.filename,
|
|
5363
|
+
status: f.status,
|
|
5364
|
+
patch: diffMap.get(f.filename)
|
|
5365
|
+
}));
|
|
5366
|
+
if (shouldLog(verbose, 1)) {
|
|
5367
|
+
console.log(` Changed files: ${changedFiles.length}`);
|
|
5368
|
+
}
|
|
5369
|
+
return {
|
|
5370
|
+
changedFiles,
|
|
5371
|
+
isLocalMode: true
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5374
|
+
/**
|
|
5375
|
+
* PR 模式:获取 PR 信息、commits、changedFiles。
|
|
5376
|
+
* 同时检查是否有同名 review workflow 正在运行(防止重复审查)。
|
|
5377
|
+
*/ async resolvePrData(context) {
|
|
5378
|
+
const { owner, repo, prNumber, verbose, ci, duplicateWorkflowResolved } = context;
|
|
5379
|
+
if (shouldLog(verbose, 1)) {
|
|
5380
|
+
console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
|
|
5381
|
+
}
|
|
5382
|
+
const prModel = new PullRequestModel(this.gitProvider, owner, repo, prNumber);
|
|
5383
|
+
const prInfo = await prModel.getInfo();
|
|
5384
|
+
const commits = await prModel.getCommits();
|
|
5385
|
+
const changedFiles = await prModel.getFiles();
|
|
5386
|
+
if (shouldLog(verbose, 1)) {
|
|
5387
|
+
console.log(` PR: ${prInfo?.title}`);
|
|
5388
|
+
console.log(` Commits: ${commits.length}`);
|
|
5389
|
+
console.log(` Changed files: ${changedFiles.length}`);
|
|
5390
|
+
}
|
|
5391
|
+
// 检查是否有其他同名 review workflow 正在运行中
|
|
5392
|
+
if (duplicateWorkflowResolved !== "off" && ci && prInfo?.head?.sha) {
|
|
5393
|
+
const duplicateResult = await this.checkDuplicateWorkflow(prModel, prInfo.head.sha, duplicateWorkflowResolved, verbose);
|
|
5394
|
+
if (duplicateResult) {
|
|
5395
|
+
return {
|
|
5396
|
+
prModel,
|
|
5397
|
+
commits,
|
|
5398
|
+
changedFiles,
|
|
5399
|
+
headSha: prInfo.head.sha,
|
|
5400
|
+
earlyReturn: duplicateResult
|
|
5401
|
+
};
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
return {
|
|
5405
|
+
prModel,
|
|
5406
|
+
commits,
|
|
5407
|
+
changedFiles
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
/**
|
|
5411
|
+
* 分支比较模式:获取 base...head 之间的 changedFiles 和 commits。
|
|
5412
|
+
*/ async resolveBranchCompareData(context, baseRef, headRef) {
|
|
5413
|
+
const { owner, repo, verbose } = context;
|
|
5414
|
+
if (shouldLog(verbose, 1)) {
|
|
5415
|
+
console.log(`📥 获取 ${baseRef}...${headRef} 的差异 (owner: ${owner}, repo: ${repo})`);
|
|
5416
|
+
}
|
|
5417
|
+
const changedFiles = await this.issueFilter.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef);
|
|
5418
|
+
const commits = await this.issueFilter.getCommitsBetweenRefs(baseRef, headRef);
|
|
5419
|
+
if (shouldLog(verbose, 1)) {
|
|
5420
|
+
console.log(` Changed files: ${changedFiles.length}`);
|
|
5421
|
+
console.log(` Commits: ${commits.length}`);
|
|
5422
|
+
}
|
|
5423
|
+
return {
|
|
5424
|
+
commits,
|
|
5425
|
+
changedFiles
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5428
|
+
// ─── 前置过滤 ──────────────────────────────────────────
|
|
5429
|
+
/**
|
|
5430
|
+
* 前置过滤管道:对 commits 和 changedFiles 依次执行过滤。
|
|
5431
|
+
*
|
|
5432
|
+
* 过滤顺序:
|
|
5433
|
+
* 0. merge commit — 排除以 "Merge " 开头的 commit
|
|
5434
|
+
* 1. --files — 仅保留用户指定的文件
|
|
5435
|
+
* 2. --commits — 仅保留用户指定的 commit 及其涉及的文件
|
|
5436
|
+
* 3. --includes — glob 模式过滤文件和 commits(支持 status| 前缀语法)
|
|
5437
|
+
*/ async applyPreFilters(context, commits, rawChangedFiles, isDirectFileMode) {
|
|
5438
|
+
const { owner, repo, prNumber, verbose, includes, files, commits: filterCommits, showAll } = context;
|
|
5439
|
+
let changedFiles = ChangedFileCollection.from(rawChangedFiles);
|
|
5440
|
+
// 0. 过滤掉 merge commit(showAll=false 时启用)
|
|
5441
|
+
if (!showAll) {
|
|
5442
|
+
const before = commits.length;
|
|
5443
|
+
commits = commits.filter((c)=>{
|
|
5444
|
+
const message = c.commit?.message || "";
|
|
5445
|
+
return !/^merge\b/i.test(message);
|
|
5446
|
+
});
|
|
5447
|
+
if (before !== commits.length && shouldLog(verbose, 1)) {
|
|
5448
|
+
console.log(` 跳过 Merge Commits: ${before} -> ${commits.length} 个`);
|
|
5449
|
+
}
|
|
5450
|
+
} else if (shouldLog(verbose, 2)) {
|
|
5451
|
+
console.log(` showAll=true,跳过 Merge Commit 过滤`);
|
|
5452
|
+
}
|
|
5453
|
+
// 1. 按指定的 files 过滤
|
|
5454
|
+
if (files && files.length > 0) {
|
|
5455
|
+
const before = changedFiles.length;
|
|
5456
|
+
changedFiles = changedFiles.filterByFilenames(files);
|
|
5514
5457
|
if (shouldLog(verbose, 1)) {
|
|
5515
|
-
console.log(
|
|
5458
|
+
console.log(` Files 过滤文件: ${before} -> ${changedFiles.length} 个文件`);
|
|
5516
5459
|
}
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
} else if (prNumber) {
|
|
5460
|
+
}
|
|
5461
|
+
// 2. 按指定的 commits 过滤(同时过滤文件:仅保留属于匹配 commits 的文件)
|
|
5462
|
+
if (filterCommits && filterCommits.length > 0) {
|
|
5463
|
+
const beforeCommits = commits.length;
|
|
5464
|
+
commits = commits.filter((c)=>filterCommits.some((fc)=>fc && c.sha?.startsWith(fc)));
|
|
5523
5465
|
if (shouldLog(verbose, 1)) {
|
|
5524
|
-
console.log(
|
|
5466
|
+
console.log(` Commits 过滤: ${beforeCommits} -> ${commits.length} 个`);
|
|
5525
5467
|
}
|
|
5526
|
-
|
|
5527
|
-
const
|
|
5528
|
-
|
|
5529
|
-
|
|
5468
|
+
const beforeFiles = changedFiles.length;
|
|
5469
|
+
const commitFilenames = new Set();
|
|
5470
|
+
for (const commit of commits){
|
|
5471
|
+
if (!commit.sha) continue;
|
|
5472
|
+
const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5473
|
+
commitFiles.forEach((f)=>commitFilenames.add(f));
|
|
5474
|
+
}
|
|
5475
|
+
changedFiles = changedFiles.filterByCommitFiles(commitFilenames);
|
|
5530
5476
|
if (shouldLog(verbose, 1)) {
|
|
5531
|
-
console.log(`
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
if (
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5477
|
+
console.log(` 按 Commits 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
// 3. 使用 includes 过滤文件和 commits(支持 added|/modified|/deleted| 前缀语法)
|
|
5481
|
+
if (isDirectFileMode && includes && includes.length > 0) {
|
|
5482
|
+
if (shouldLog(verbose, 1)) {
|
|
5483
|
+
console.log(`ℹ️ 直接文件模式下忽略 includes 过滤`);
|
|
5484
|
+
}
|
|
5485
|
+
} else if (includes && includes.length > 0) {
|
|
5486
|
+
const beforeFiles = changedFiles.length;
|
|
5487
|
+
if (shouldLog(verbose, 2)) {
|
|
5488
|
+
console.log(`[resolveSourceData] filterFilesByIncludes: before=${JSON.stringify(changedFiles.map((f)=>({
|
|
5489
|
+
filename: f.filename,
|
|
5490
|
+
status: f.status
|
|
5491
|
+
})))}, includes=${JSON.stringify(includes)}`);
|
|
5492
|
+
}
|
|
5493
|
+
changedFiles = ChangedFileCollection.from(filterFilesByIncludes(changedFiles.toArray(), includes));
|
|
5494
|
+
if (shouldLog(verbose, 1)) {
|
|
5495
|
+
console.log(` Includes 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
5496
|
+
}
|
|
5497
|
+
if (shouldLog(verbose, 2)) {
|
|
5498
|
+
console.log(`[resolveSourceData] filterFilesByIncludes: after=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
|
|
5499
|
+
}
|
|
5500
|
+
// 按 includes glob 过滤 commits:仅保留涉及匹配文件的 commits
|
|
5501
|
+
const globs = extractGlobsFromIncludes(includes);
|
|
5502
|
+
const beforeCommits = commits.length;
|
|
5503
|
+
const filteredCommits = [];
|
|
5504
|
+
for (const commit of commits){
|
|
5505
|
+
if (!commit.sha) continue;
|
|
5506
|
+
const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5507
|
+
if (micromatch_0.some(commitFiles, globs)) {
|
|
5508
|
+
filteredCommits.push(commit);
|
|
5548
5509
|
}
|
|
5549
5510
|
}
|
|
5550
|
-
|
|
5551
|
-
if (
|
|
5552
|
-
|
|
5553
|
-
|
|
5511
|
+
commits = filteredCommits;
|
|
5512
|
+
if (shouldLog(verbose, 1)) {
|
|
5513
|
+
console.log(` Includes 过滤 Commits: ${beforeCommits} -> ${commits.length} 个`);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
return {
|
|
5517
|
+
commits,
|
|
5518
|
+
changedFiles: changedFiles.toArray()
|
|
5519
|
+
};
|
|
5520
|
+
}
|
|
5521
|
+
// ─── 文件内容 ─────────────────────────────────────────
|
|
5522
|
+
/**
|
|
5523
|
+
* 获取文件内容并构建行号到 commit hash 的映射
|
|
5524
|
+
* 返回 Map<filename, Array<[commitHash, lineCode]>>
|
|
5525
|
+
*/ async getFileContents(owner, repo, changedFiles, commits, ref, prNumber, isLocalMode, showAll, verbose) {
|
|
5526
|
+
const contents = new Map();
|
|
5527
|
+
const latestCommitHash = commits[commits.length - 1]?.sha?.slice(0, 7) || "+local+";
|
|
5528
|
+
const validCommitHashes = new Set(commits.map((c)=>c.sha?.slice(0, 7)).filter(Boolean));
|
|
5529
|
+
const shouldMaskUnknownChangedLines = !showAll && validCommitHashes.size > 0;
|
|
5530
|
+
if (shouldLog(verbose, 1)) {
|
|
5531
|
+
console.log(`📊 正在构建行号到变更的映射...`);
|
|
5532
|
+
}
|
|
5533
|
+
for (const file of changedFiles){
|
|
5534
|
+
if (file.filename && file.status !== "deleted") {
|
|
5535
|
+
try {
|
|
5536
|
+
let rawContent;
|
|
5537
|
+
if (isLocalMode) {
|
|
5538
|
+
rawContent = this.gitSdk.getWorkingFileContent(file.filename);
|
|
5539
|
+
} else if (prNumber) {
|
|
5540
|
+
rawContent = await this.gitProvider.getFileContent(owner, repo, file.filename, ref);
|
|
5541
|
+
} else {
|
|
5542
|
+
rawContent = await this.gitSdk.getFileContent(ref, file.filename);
|
|
5543
|
+
}
|
|
5544
|
+
const lines = rawContent.split("\n");
|
|
5545
|
+
let changedLines = parseChangedLinesFromPatch(file.patch);
|
|
5546
|
+
const isNewFile = file.status === "added" || file.status === "A" || file.additions && file.additions > 0 && file.deletions === 0 && !file.patch;
|
|
5547
|
+
if (changedLines.size === 0 && isNewFile) {
|
|
5548
|
+
changedLines = new Set(lines.map((_, i)=>i + 1));
|
|
5549
|
+
if (shouldLog(verbose, 2)) {
|
|
5550
|
+
console.log(` ℹ️ ${file.filename}: 新增文件无 patch,将所有 ${lines.length} 行标记为变更`);
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
let blameMap;
|
|
5554
|
+
if (!isLocalMode) {
|
|
5555
|
+
try {
|
|
5556
|
+
blameMap = await this.gitSdk.getFileBlame(ref, file.filename);
|
|
5557
|
+
} catch {
|
|
5558
|
+
// blame 失败时回退到 latestCommitHash
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
if (shouldLog(verbose, 3)) {
|
|
5562
|
+
console.log(` 📄 ${file.filename}: ${lines.length} 行, ${changedLines.size} 行变更`);
|
|
5563
|
+
console.log(` blame: ${blameMap ? `${blameMap.size} 行` : `不可用,回退到 ${latestCommitHash}`}`);
|
|
5564
|
+
if (changedLines.size > 0 && changedLines.size <= 20) {
|
|
5565
|
+
console.log(` 变更行号: ${Array.from(changedLines).sort((a, b)=>a - b).join(", ")}`);
|
|
5566
|
+
} else if (changedLines.size > 20) {
|
|
5567
|
+
console.log(` 变更行号: (共 ${changedLines.size} 行,省略详情)`);
|
|
5568
|
+
}
|
|
5569
|
+
if (!file.patch) {
|
|
5570
|
+
console.log(` ⚠️ 该文件没有 patch 信息 (status=${file.status}, additions=${file.additions}, deletions=${file.deletions})`);
|
|
5571
|
+
} else {
|
|
5572
|
+
console.log(` patch 前 200 字符: ${file.patch.slice(0, 200).replace(/\n/g, "\\n")}`);
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
const contentLines = lines.map((line, index)=>{
|
|
5576
|
+
const lineNum = index + 1;
|
|
5577
|
+
if (!changedLines.has(lineNum)) {
|
|
5578
|
+
return [
|
|
5579
|
+
"-------",
|
|
5580
|
+
line
|
|
5581
|
+
];
|
|
5582
|
+
}
|
|
5583
|
+
const hash = blameMap?.get(lineNum) ?? latestCommitHash;
|
|
5584
|
+
if (shouldMaskUnknownChangedLines && !validCommitHashes.has(hash)) {
|
|
5585
|
+
return [
|
|
5586
|
+
"-------",
|
|
5587
|
+
line
|
|
5588
|
+
];
|
|
5589
|
+
}
|
|
5590
|
+
return [
|
|
5591
|
+
hash,
|
|
5592
|
+
line
|
|
5593
|
+
];
|
|
5594
|
+
});
|
|
5595
|
+
contents.set(file.filename, contentLines);
|
|
5596
|
+
} catch (error) {
|
|
5597
|
+
console.warn(`警告: 无法获取文件内容: ${file.filename}`, error);
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
if (shouldLog(verbose, 1)) {
|
|
5602
|
+
console.log(`📊 映射构建完成,共 ${contents.size} 个文件`);
|
|
5603
|
+
}
|
|
5604
|
+
return contents;
|
|
5605
|
+
}
|
|
5606
|
+
// ─── 重复 workflow 检查 ──────────────────────────────────
|
|
5607
|
+
/**
|
|
5608
|
+
* 检查是否有其他同名 review workflow 正在运行中。
|
|
5609
|
+
* 根据 duplicateWorkflowResolved 配置决定是跳过还是删除旧评论。
|
|
5610
|
+
*/ async checkDuplicateWorkflow(prModel, headSha, mode, verbose) {
|
|
5611
|
+
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
5612
|
+
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
5613
|
+
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prModel.number;
|
|
5614
|
+
try {
|
|
5615
|
+
const runningWorkflows = await prModel.listWorkflowRuns({
|
|
5616
|
+
status: "in_progress"
|
|
5617
|
+
});
|
|
5618
|
+
const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
|
|
5619
|
+
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
5620
|
+
const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
|
|
5621
|
+
if (duplicateReviewRuns.length > 0) {
|
|
5622
|
+
if (mode === "delete") {
|
|
5623
|
+
// 删除模式:清理旧的 AI Review 评论和 PR Review
|
|
5624
|
+
if (shouldLog(verbose, 1)) {
|
|
5625
|
+
console.log(`🗑️ 检测到 ${duplicateReviewRuns.length} 个同名 workflow,清理旧的 AI Review 评论...`);
|
|
5626
|
+
}
|
|
5627
|
+
await this.cleanupDuplicateAiReviews(prModel, verbose);
|
|
5628
|
+
// 清理后继续执行当前审查
|
|
5629
|
+
return null;
|
|
5554
5630
|
}
|
|
5555
|
-
|
|
5556
|
-
commits = await this.getCommitsBetweenRefs(effectiveBaseRef, effectiveHeadRef);
|
|
5631
|
+
// 跳过模式(默认)
|
|
5557
5632
|
if (shouldLog(verbose, 1)) {
|
|
5558
|
-
console.log(
|
|
5559
|
-
console.log(` Commits: ${commits.length}`);
|
|
5633
|
+
console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
|
|
5560
5634
|
}
|
|
5635
|
+
return {
|
|
5636
|
+
success: true,
|
|
5637
|
+
description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
|
|
5638
|
+
issues: [],
|
|
5639
|
+
summary: [],
|
|
5640
|
+
round: 1
|
|
5641
|
+
};
|
|
5561
5642
|
}
|
|
5562
|
-
}
|
|
5643
|
+
} catch (error) {
|
|
5563
5644
|
if (shouldLog(verbose, 1)) {
|
|
5564
|
-
console.
|
|
5565
|
-
prNumber,
|
|
5566
|
-
baseRef,
|
|
5567
|
-
headRef
|
|
5568
|
-
});
|
|
5645
|
+
console.warn(`⚠️ 无法检查重复 workflow(可能缺少 repo owner 权限),跳过此检查:`, error instanceof Error ? error.message : error);
|
|
5569
5646
|
}
|
|
5570
|
-
throw new Error("必须指定 PR 编号或者 base/head 分支");
|
|
5571
5647
|
}
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5648
|
+
return null;
|
|
5649
|
+
}
|
|
5650
|
+
/**
|
|
5651
|
+
* 清理重复的 AI Review 评论(Issue Comments 和 PR Reviews)
|
|
5652
|
+
*/ async cleanupDuplicateAiReviews(prModel, verbose) {
|
|
5653
|
+
try {
|
|
5654
|
+
// 删除 Issue Comments(主评论)
|
|
5655
|
+
const comments = await prModel.getComments();
|
|
5656
|
+
const aiComments = comments.filter((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
5657
|
+
let deletedComments = 0;
|
|
5658
|
+
for (const comment of aiComments){
|
|
5659
|
+
if (comment.id) {
|
|
5660
|
+
try {
|
|
5661
|
+
await prModel.deleteComment(comment.id);
|
|
5662
|
+
deletedComments++;
|
|
5663
|
+
} catch {
|
|
5664
|
+
// 忽略删除失败
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
if (deletedComments > 0 && shouldLog(verbose, 1)) {
|
|
5669
|
+
console.log(` 已删除 ${deletedComments} 个重复的 AI Review 主评论`);
|
|
5670
|
+
}
|
|
5671
|
+
// 删除 PR Reviews(行级评论)
|
|
5672
|
+
const reviews = await prModel.getReviews();
|
|
5673
|
+
const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
5674
|
+
let deletedReviews = 0;
|
|
5675
|
+
for (const review of aiReviews){
|
|
5676
|
+
if (review.id) {
|
|
5677
|
+
try {
|
|
5678
|
+
await prModel.deleteReview(review.id);
|
|
5679
|
+
deletedReviews++;
|
|
5680
|
+
} catch {
|
|
5681
|
+
// 已提交的 review 无法删除,忽略
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
if (deletedReviews > 0 && shouldLog(verbose, 1)) {
|
|
5686
|
+
console.log(` 已删除 ${deletedReviews} 个重复的 AI Review PR Review`);
|
|
5687
|
+
}
|
|
5688
|
+
} catch (error) {
|
|
5689
|
+
if (shouldLog(verbose, 1)) {
|
|
5690
|
+
console.warn(`⚠️ 清理旧评论失败:`, error instanceof Error ? error.message : error);
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
|
|
5696
|
+
;// CONCATENATED MODULE: ./src/review.service.ts
|
|
5697
|
+
|
|
5698
|
+
|
|
5699
|
+
|
|
5700
|
+
|
|
5701
|
+
|
|
5702
|
+
|
|
5703
|
+
|
|
5704
|
+
|
|
5705
|
+
|
|
5706
|
+
|
|
5707
|
+
|
|
5708
|
+
class ReviewService {
|
|
5709
|
+
gitProvider;
|
|
5710
|
+
config;
|
|
5711
|
+
reviewSpecService;
|
|
5712
|
+
llmProxyService;
|
|
5713
|
+
reviewReportService;
|
|
5714
|
+
issueVerifyService;
|
|
5715
|
+
deletionImpactService;
|
|
5716
|
+
gitSdk;
|
|
5717
|
+
contextBuilder;
|
|
5718
|
+
issueFilter;
|
|
5719
|
+
llmProcessor;
|
|
5720
|
+
resultModelDeps;
|
|
5721
|
+
sourceResolver;
|
|
5722
|
+
constructor(gitProvider, config, reviewSpecService, llmProxyService, reviewReportService, issueVerifyService, deletionImpactService, gitSdk){
|
|
5723
|
+
this.gitProvider = gitProvider;
|
|
5724
|
+
this.config = config;
|
|
5725
|
+
this.reviewSpecService = reviewSpecService;
|
|
5726
|
+
this.llmProxyService = llmProxyService;
|
|
5727
|
+
this.reviewReportService = reviewReportService;
|
|
5728
|
+
this.issueVerifyService = issueVerifyService;
|
|
5729
|
+
this.deletionImpactService = deletionImpactService;
|
|
5730
|
+
this.gitSdk = gitSdk;
|
|
5731
|
+
this.contextBuilder = new ReviewContextBuilder(gitProvider, config, gitSdk);
|
|
5732
|
+
this.issueFilter = new ReviewIssueFilter(gitProvider, config, reviewSpecService, issueVerifyService, gitSdk);
|
|
5733
|
+
this.llmProcessor = new ReviewLlmProcessor(llmProxyService, reviewSpecService);
|
|
5734
|
+
this.sourceResolver = new ReviewSourceResolver(gitProvider, gitSdk, this.issueFilter);
|
|
5735
|
+
this.resultModelDeps = {
|
|
5736
|
+
gitProvider,
|
|
5737
|
+
config,
|
|
5738
|
+
reviewSpecService,
|
|
5739
|
+
reviewReportService
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
async getContextFromEnv(options) {
|
|
5743
|
+
return this.contextBuilder.getContextFromEnv(options);
|
|
5744
|
+
}
|
|
5745
|
+
/**
|
|
5746
|
+
* 执行代码审查的主方法
|
|
5747
|
+
* 该方法负责协调整个审查流程,包括:
|
|
5748
|
+
* 1. 加载审查规范(specs)
|
|
5749
|
+
* 2. 获取 PR/分支的变更文件和提交记录
|
|
5750
|
+
* 3. 调用 LLM 进行代码审查
|
|
5751
|
+
* 4. 处理历史 issue(更新行号、验证修复状态)
|
|
5752
|
+
* 5. 生成并发布审查报告
|
|
5753
|
+
*
|
|
5754
|
+
* @param context 审查上下文,包含 owner、repo、prNumber 等信息
|
|
5755
|
+
* @returns 审查结果,包含发现的问题列表和统计信息
|
|
5756
|
+
*/ async execute(context) {
|
|
5757
|
+
const { specSources, verbose, llmMode, deletionOnly } = context;
|
|
5758
|
+
if (shouldLog(verbose, 1)) {
|
|
5759
|
+
console.log(`🔍 Review 启动`);
|
|
5760
|
+
console.log(` DRY-RUN mode: ${context.dryRun ? "enabled" : "disabled"}`);
|
|
5761
|
+
console.log(` CI mode: ${context.ci ? "enabled" : "disabled"}`);
|
|
5762
|
+
if (context.localMode) console.log(` Local mode: ${context.localMode}`);
|
|
5763
|
+
console.log(` Verbose: ${verbose}`);
|
|
5764
|
+
}
|
|
5765
|
+
// 早期分流
|
|
5766
|
+
if (deletionOnly) return this.executeDeletionOnly(context);
|
|
5767
|
+
if (context.eventAction === "closed" || context.flush) return this.executeCollectOnly(context);
|
|
5768
|
+
// 1. 解析输入数据(本地/PR/分支模式 + 前置过滤)
|
|
5769
|
+
const source = await this.resolveSourceData(context);
|
|
5770
|
+
if (source.earlyReturn) return source.earlyReturn;
|
|
5771
|
+
const effectiveWhenModifiedCode = source.isDirectFileMode ? undefined : context.whenModifiedCode;
|
|
5772
|
+
if (source.isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
|
|
5773
|
+
console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
|
|
5583
5774
|
}
|
|
5584
|
-
//
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
}
|
|
5775
|
+
// 2. 规则匹配
|
|
5776
|
+
const allSpecs = await this.issueFilter.loadSpecs(specSources, verbose);
|
|
5777
|
+
const specs = this.reviewSpecService.filterApplicableSpecs(allSpecs, source.changedFiles);
|
|
5778
|
+
if (shouldLog(verbose, 2)) {
|
|
5779
|
+
console.log(`[execute] loadSpecs: loaded ${specs.length} specs from sources: ${JSON.stringify(specSources)}`);
|
|
5780
|
+
console.log(`[execute] filterApplicableSpecs: ${specs.length} applicable out of ${allSpecs.length}, changedFiles=${JSON.stringify(source.changedFiles.filenames())}`);
|
|
5591
5781
|
}
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
const beforeCommits = commits.length;
|
|
5595
|
-
commits = commits.filter((c)=>filterCommits.some((fc)=>fc && c.sha?.startsWith(fc)));
|
|
5596
|
-
if (shouldLog(verbose, 1)) {
|
|
5597
|
-
console.log(` Commits 过滤: ${beforeCommits} -> ${commits.length} 个`);
|
|
5598
|
-
}
|
|
5599
|
-
const beforeFiles = changedFiles.length;
|
|
5600
|
-
const commitFilenames = new Set();
|
|
5601
|
-
for (const commit of commits){
|
|
5602
|
-
if (!commit.sha) continue;
|
|
5603
|
-
const commitFiles = await this.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5604
|
-
commitFiles.forEach((f)=>commitFilenames.add(f));
|
|
5605
|
-
}
|
|
5606
|
-
changedFiles = changedFiles.filter((f)=>commitFilenames.has(f.filename || ""));
|
|
5607
|
-
if (shouldLog(verbose, 1)) {
|
|
5608
|
-
console.log(` 按 Commits 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
5609
|
-
}
|
|
5782
|
+
if (shouldLog(verbose, 1)) {
|
|
5783
|
+
console.log(` 适用的规则文件: ${specs.length}`);
|
|
5610
5784
|
}
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
}
|
|
5624
|
-
changedFiles = filterFilesByIncludes(changedFiles, includes);
|
|
5625
|
-
if (shouldLog(verbose, 1)) {
|
|
5626
|
-
console.log(` Includes 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
5627
|
-
}
|
|
5628
|
-
if (shouldLog(verbose, 2)) {
|
|
5629
|
-
console.log(`[resolveSourceData] filterFilesByIncludes: after=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
|
|
5630
|
-
}
|
|
5631
|
-
const globs = extractGlobsFromIncludes(includes);
|
|
5632
|
-
const beforeCommits = commits.length;
|
|
5633
|
-
const filteredCommits = [];
|
|
5634
|
-
for (const commit of commits){
|
|
5635
|
-
if (!commit.sha) continue;
|
|
5636
|
-
const commitFiles = await this.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5637
|
-
if (micromatch_0.some(commitFiles, globs)) {
|
|
5638
|
-
filteredCommits.push(commit);
|
|
5639
|
-
}
|
|
5640
|
-
}
|
|
5641
|
-
commits = filteredCommits;
|
|
5642
|
-
if (shouldLog(verbose, 1)) {
|
|
5643
|
-
console.log(` Includes 过滤 Commits: ${beforeCommits} -> ${commits.length} 个`);
|
|
5785
|
+
if (specs.length === 0 || source.changedFiles.length === 0) {
|
|
5786
|
+
return this.handleNoApplicableSpecs(context, specs, source.changedFiles, source.commits);
|
|
5787
|
+
}
|
|
5788
|
+
// 3. LLM 审查
|
|
5789
|
+
const { fileContents } = source;
|
|
5790
|
+
if (!llmMode) throw new Error("必须指定 LLM 类型");
|
|
5791
|
+
// 获取上一次的审查结果(用于提示词优化和轮次推进)
|
|
5792
|
+
let existingResultModel = null;
|
|
5793
|
+
if (context.ci && source.prModel) {
|
|
5794
|
+
existingResultModel = await ReviewResultModel.loadFromPr(source.prModel, this.resultModelDeps);
|
|
5795
|
+
if (existingResultModel && shouldLog(verbose, 1)) {
|
|
5796
|
+
console.log(`📋 获取到上一次审查结果,包含 ${existingResultModel.issues.length} 个问题`);
|
|
5644
5797
|
}
|
|
5645
5798
|
}
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5799
|
+
if (shouldLog(verbose, 1)) {
|
|
5800
|
+
console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
|
|
5801
|
+
}
|
|
5802
|
+
const reviewPrompt = await this.llmProcessor.buildReviewPrompt(specs, source.changedFiles, fileContents, source.commits, existingResultModel?.result ?? null, effectiveWhenModifiedCode, verbose, context.systemRules);
|
|
5803
|
+
// 4. 运行 LLM 审查 + 过滤新 issues
|
|
5804
|
+
const result = await this.buildReviewResult(context, reviewPrompt, llmMode, {
|
|
5805
|
+
specs,
|
|
5806
|
+
fileContents,
|
|
5807
|
+
changedFiles: source.changedFiles,
|
|
5808
|
+
commits: source.commits,
|
|
5809
|
+
isDirectFileMode: source.isDirectFileMode
|
|
5810
|
+
});
|
|
5811
|
+
// 5. 构建最终的 ReviewResultModel
|
|
5812
|
+
const finalModel = await this.buildFinalModel(context, result, {
|
|
5813
|
+
prModel: source.prModel,
|
|
5814
|
+
commits: source.commits,
|
|
5815
|
+
headSha: source.headSha,
|
|
5816
|
+
specs,
|
|
5817
|
+
fileContents
|
|
5818
|
+
}, existingResultModel);
|
|
5819
|
+
// 6. 保存 + 输出
|
|
5820
|
+
await this.saveAndOutput(context, finalModel, source.commits);
|
|
5821
|
+
return finalModel.result;
|
|
5822
|
+
}
|
|
5823
|
+
/**
|
|
5824
|
+
* 运行 LLM 审查并构建过滤后的 ReviewResult:
|
|
5825
|
+
* - 调用 LLM 生成问题列表
|
|
5826
|
+
* - 填充 PR 标题/描述
|
|
5827
|
+
* - 过滤新 issues(去重、commit 范围等)
|
|
5828
|
+
* - 合并静态规则问题
|
|
5829
|
+
*/ async buildReviewResult(context, reviewPrompt, llmMode, source) {
|
|
5830
|
+
const { verbose } = context;
|
|
5831
|
+
const { specs, fileContents, changedFiles, commits, isDirectFileMode } = source;
|
|
5832
|
+
const result = await this.llmProcessor.runLLMReview(llmMode, reviewPrompt, {
|
|
5833
|
+
verbose,
|
|
5834
|
+
concurrency: context.concurrency,
|
|
5835
|
+
timeout: context.timeout,
|
|
5836
|
+
retries: context.retries,
|
|
5837
|
+
retryDelay: context.retryDelay
|
|
5838
|
+
});
|
|
5839
|
+
// 填充 PR 功能描述和标题
|
|
5840
|
+
const prInfo = context.generateDescription ? await this.llmProcessor.generatePrDescription(commits, changedFiles, llmMode, fileContents, verbose) : await this.llmProcessor.buildBasicDescription(commits, changedFiles);
|
|
5841
|
+
result.title = prInfo.title;
|
|
5842
|
+
result.description = prInfo.description;
|
|
5843
|
+
if (shouldLog(verbose, 1)) {
|
|
5844
|
+
console.log(`📝 LLM 审查完成,发现 ${result.issues.length} 个问题`);
|
|
5845
|
+
}
|
|
5846
|
+
result.issues = await this.issueFilter.fillIssueCode(result.issues, fileContents);
|
|
5847
|
+
result.issues = this.filterNewIssues(result.issues, specs, {
|
|
5649
5848
|
commits,
|
|
5849
|
+
fileContents,
|
|
5650
5850
|
changedFiles,
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5851
|
+
isDirectFileMode,
|
|
5852
|
+
context
|
|
5853
|
+
});
|
|
5854
|
+
// 静态规则产生的系统问题直接合并,不经过过滤管道
|
|
5855
|
+
if (reviewPrompt.staticIssues?.length) {
|
|
5856
|
+
result.issues = [
|
|
5857
|
+
...reviewPrompt.staticIssues,
|
|
5858
|
+
...result.issues
|
|
5859
|
+
];
|
|
5860
|
+
if (shouldLog(verbose, 1)) {
|
|
5861
|
+
console.log(`⚙️ 追加 ${reviewPrompt.staticIssues.length} 个静态规则系统问题`);
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
if (shouldLog(verbose, 1)) {
|
|
5865
|
+
console.log(`📝 最终发现 ${result.issues.length} 个问题`);
|
|
5866
|
+
}
|
|
5867
|
+
return result;
|
|
5868
|
+
}
|
|
5869
|
+
/**
|
|
5870
|
+
* 解析输入数据:委托给 ReviewSourceResolver。
|
|
5871
|
+
* @see ReviewSourceResolver#resolve
|
|
5872
|
+
*/ async resolveSourceData(context) {
|
|
5873
|
+
return this.sourceResolver.resolve(context);
|
|
5655
5874
|
}
|
|
5656
5875
|
/**
|
|
5657
5876
|
* LLM 审查后的 issue 过滤管道:
|
|
5658
5877
|
* includes → 规则存在性 → overrides → 变更行过滤 → 格式化
|
|
5659
|
-
*/ filterNewIssues(issues, specs,
|
|
5878
|
+
*/ filterNewIssues(issues, specs, opts) {
|
|
5660
5879
|
const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
|
|
5661
5880
|
const { verbose } = context;
|
|
5662
|
-
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues,
|
|
5881
|
+
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
|
|
5663
5882
|
if (shouldLog(verbose, 1)) {
|
|
5664
5883
|
console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
|
|
5665
5884
|
}
|
|
@@ -5667,26 +5886,26 @@ class ReviewService {
|
|
|
5667
5886
|
if (shouldLog(verbose, 1)) {
|
|
5668
5887
|
console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
|
|
5669
5888
|
}
|
|
5670
|
-
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered,
|
|
5889
|
+
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
|
|
5671
5890
|
// 变更行过滤
|
|
5672
5891
|
if (shouldLog(verbose, 3)) {
|
|
5673
5892
|
console.log(` 🔍 变更行过滤条件检查:`);
|
|
5674
5893
|
console.log(` showAll=${context.showAll}, isDirectFileMode=${isDirectFileMode}, commits.length=${commits.length}`);
|
|
5675
5894
|
}
|
|
5676
|
-
if (!context.showAll && !isDirectFileMode
|
|
5895
|
+
if (!context.showAll && !isDirectFileMode) {
|
|
5677
5896
|
if (shouldLog(verbose, 2)) {
|
|
5678
5897
|
console.log(` 🔍 开始变更行过滤,当前 ${filtered.length} 个问题`);
|
|
5679
5898
|
}
|
|
5680
|
-
filtered = this.filterIssuesByValidCommits(filtered, commits, fileContents, verbose);
|
|
5899
|
+
filtered = this.issueFilter.filterIssuesByValidCommits(filtered, commits, fileContents, verbose);
|
|
5681
5900
|
if (shouldLog(verbose, 2)) {
|
|
5682
5901
|
console.log(` 🔍 变更行过滤完成,剩余 ${filtered.length} 个问题`);
|
|
5683
5902
|
}
|
|
5684
5903
|
} else if (shouldLog(verbose, 1)) {
|
|
5685
|
-
console.log(` 跳过变更行过滤 (${context.showAll ? "showAll=true" :
|
|
5904
|
+
console.log(` 跳过变更行过滤 (${context.showAll ? "showAll=true" : "直接审查文件模式"})`);
|
|
5686
5905
|
}
|
|
5687
5906
|
filtered = this.reviewSpecService.formatIssues(filtered, {
|
|
5688
5907
|
specs,
|
|
5689
|
-
changedFiles
|
|
5908
|
+
changedFiles: changedFiles.toArray()
|
|
5690
5909
|
});
|
|
5691
5910
|
if (shouldLog(verbose, 1)) {
|
|
5692
5911
|
console.log(` 应用格式化后: ${filtered.length} 个问题`);
|
|
@@ -5698,6 +5917,7 @@ class ReviewService {
|
|
|
5698
5917
|
*/ async buildFinalModel(context, result, source, existingResultModel) {
|
|
5699
5918
|
const { prModel, commits, headSha, specs, fileContents } = source;
|
|
5700
5919
|
const { verbose, ci } = context;
|
|
5920
|
+
result.headSha = headSha;
|
|
5701
5921
|
if (ci && prModel && existingResultModel && existingResultModel.issues.length > 0) {
|
|
5702
5922
|
if (shouldLog(verbose, 1)) {
|
|
5703
5923
|
console.log(`📋 已有评论中存在 ${existingResultModel.issues.length} 个问题`);
|
|
@@ -5707,32 +5927,24 @@ class ReviewService {
|
|
|
5707
5927
|
// 如果文件有变更,将该文件的历史问题标记为无效
|
|
5708
5928
|
const reviewConf = this.config.getPluginConfig("review");
|
|
5709
5929
|
if (reviewConf.invalidateChangedFiles !== "off" && reviewConf.invalidateChangedFiles !== "keep") {
|
|
5710
|
-
await existingResultModel.invalidateChangedFiles(headSha, verbose);
|
|
5930
|
+
await existingResultModel.invalidateChangedFiles(headSha, fileContents, verbose);
|
|
5711
5931
|
}
|
|
5712
5932
|
// 验证历史问题是否已修复
|
|
5713
5933
|
if (context.verifyFixes) {
|
|
5714
5934
|
existingResultModel.issues = await this.issueFilter.verifyAndUpdateIssues(context, existingResultModel.issues, commits, {
|
|
5715
5935
|
specs,
|
|
5716
5936
|
fileContents
|
|
5717
|
-
}
|
|
5937
|
+
});
|
|
5718
5938
|
} else {
|
|
5719
5939
|
if (shouldLog(verbose, 1)) {
|
|
5720
5940
|
console.log(` ⏭️ 跳过历史问题验证 (verifyFixes=false)`);
|
|
5721
5941
|
}
|
|
5722
5942
|
}
|
|
5723
|
-
//
|
|
5724
|
-
const { filteredIssues: newIssues, skippedCount } = this.filterDuplicateIssues(result.issues, existingResultModel.issues);
|
|
5725
|
-
if (skippedCount > 0 && shouldLog(verbose, 1)) {
|
|
5726
|
-
console.log(` 跳过 ${skippedCount} 个重复问题,新增 ${newIssues.length} 个问题`);
|
|
5727
|
-
}
|
|
5728
|
-
result.issues = newIssues;
|
|
5729
|
-
result.headSha = headSha;
|
|
5730
|
-
// 自动 round 递增 + issues 合并
|
|
5943
|
+
// 自动 round 递增 + 去重 + issues 合并
|
|
5731
5944
|
return existingResultModel.nextRound(result);
|
|
5732
5945
|
}
|
|
5733
5946
|
// 首次审查或无历史结果
|
|
5734
5947
|
result.round = 1;
|
|
5735
|
-
result.headSha = headSha;
|
|
5736
5948
|
result.issues = result.issues.map((issue)=>({
|
|
5737
5949
|
...issue,
|
|
5738
5950
|
round: 1
|
|
@@ -5746,7 +5958,7 @@ class ReviewService {
|
|
|
5746
5958
|
const prModel = finalModel.pr.number > 0 ? finalModel.pr : undefined;
|
|
5747
5959
|
// 填充 author 信息
|
|
5748
5960
|
if (commits.length > 0) {
|
|
5749
|
-
finalModel.issues = await this.fillIssueAuthors(finalModel.issues, commits, owner, repo, verbose);
|
|
5961
|
+
finalModel.issues = await this.contextBuilder.fillIssueAuthors(finalModel.issues, commits, owner, repo, verbose);
|
|
5750
5962
|
}
|
|
5751
5963
|
// 删除代码影响分析(在 save 之前完成,避免多次 save 产生重复的 Round 评论)
|
|
5752
5964
|
if (context.analyzeDeletions && llmMode) {
|
|
@@ -5812,17 +6024,32 @@ class ReviewService {
|
|
|
5812
6024
|
console.log(`📋 找到 ${resultModel.issues.length} 个历史问题`);
|
|
5813
6025
|
}
|
|
5814
6026
|
// 2. 获取 commits 并填充 author 信息
|
|
5815
|
-
const
|
|
5816
|
-
|
|
6027
|
+
const allCommits = await prModel.getCommits();
|
|
6028
|
+
const commits = context.showAll ? allCommits : allCommits.filter((c)=>!/^merge\b/i.test(c.commit?.message || ""));
|
|
6029
|
+
if (allCommits.length !== commits.length && shouldLog(verbose, 1)) {
|
|
6030
|
+
console.log(` 跳过 Merge Commits: ${allCommits.length} -> ${commits.length} 个`);
|
|
6031
|
+
}
|
|
6032
|
+
resultModel.issues = await this.contextBuilder.fillIssueAuthors(resultModel.issues, commits, owner, repo, verbose);
|
|
5817
6033
|
// 3. 同步已解决的评论状态
|
|
5818
6034
|
await resultModel.syncResolved();
|
|
5819
6035
|
// 4. 同步评论 reactions(👍/👎/☹️)
|
|
5820
6036
|
await resultModel.syncReactions(verbose);
|
|
5821
6037
|
// 5. LLM 验证历史问题是否已修复
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
6038
|
+
if (context.verifyFixes && context.specSources?.length) {
|
|
6039
|
+
try {
|
|
6040
|
+
const changedFiles = await prModel.getFiles();
|
|
6041
|
+
const headSha = await prModel.getHeadSha();
|
|
6042
|
+
const verifySpecs = await this.issueFilter.loadSpecs(context.specSources, verbose);
|
|
6043
|
+
const verifyFileContents = await this.sourceResolver.getFileContents(owner, repo, changedFiles, commits, headSha, prNumber, false, context.showAll, verbose);
|
|
6044
|
+
resultModel.issues = await this.issueFilter.verifyAndUpdateIssues(context, resultModel.issues, commits, {
|
|
6045
|
+
specs: verifySpecs,
|
|
6046
|
+
fileContents: verifyFileContents
|
|
6047
|
+
});
|
|
6048
|
+
} catch (error) {
|
|
6049
|
+
console.warn("⚠️ LLM 验证修复状态失败,跳过:", error);
|
|
6050
|
+
}
|
|
6051
|
+
} else if (!context.verifyFixes && shouldLog(verbose, 1)) {
|
|
6052
|
+
console.log(` ⏭️ 跳过历史问题验证 (verifyFixes=false)`);
|
|
5826
6053
|
}
|
|
5827
6054
|
// 6. 统计问题状态并设置到 result
|
|
5828
6055
|
const stats = resultModel.updateStats();
|
|
@@ -5865,20 +6092,20 @@ class ReviewService {
|
|
|
5865
6092
|
// 获取 commits 和 changedFiles 用于生成描述
|
|
5866
6093
|
let prModel;
|
|
5867
6094
|
let commits = [];
|
|
5868
|
-
let changedFiles =
|
|
6095
|
+
let changedFiles = ChangedFileCollection.empty();
|
|
5869
6096
|
if (prNumber) {
|
|
5870
6097
|
prModel = new PullRequestModel(this.gitProvider, owner, repo, prNumber);
|
|
5871
6098
|
commits = await prModel.getCommits();
|
|
5872
|
-
changedFiles = await prModel.getFiles();
|
|
6099
|
+
changedFiles = ChangedFileCollection.from(await prModel.getFiles());
|
|
5873
6100
|
} else if (baseRef && headRef) {
|
|
5874
|
-
changedFiles = await this.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef);
|
|
5875
|
-
commits = await this.getCommitsBetweenRefs(baseRef, headRef);
|
|
6101
|
+
changedFiles = ChangedFileCollection.from(await this.issueFilter.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef));
|
|
6102
|
+
commits = await this.issueFilter.getCommitsBetweenRefs(baseRef, headRef);
|
|
5876
6103
|
}
|
|
5877
6104
|
// 使用 includes 过滤文件(支持 added|/modified|/deleted| 前缀语法)
|
|
5878
6105
|
if (context.includes && context.includes.length > 0) {
|
|
5879
|
-
changedFiles = filterFilesByIncludes(changedFiles, context.includes);
|
|
6106
|
+
changedFiles = ChangedFileCollection.from(filterFilesByIncludes(changedFiles.toArray(), context.includes));
|
|
5880
6107
|
}
|
|
5881
|
-
const prDesc = context.generateDescription ? await this.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.buildBasicDescription(commits, changedFiles);
|
|
6108
|
+
const prDesc = context.generateDescription ? await this.llmProcessor.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.llmProcessor.buildBasicDescription(commits, changedFiles);
|
|
5882
6109
|
const result = {
|
|
5883
6110
|
success: true,
|
|
5884
6111
|
title: prDesc.title,
|
|
@@ -5926,13 +6153,13 @@ class ReviewService {
|
|
|
5926
6153
|
}
|
|
5927
6154
|
const currentRound = (existingResultModel?.round ?? 0) + 1;
|
|
5928
6155
|
// 即使没有适用的规则,也为每个变更文件生成摘要
|
|
5929
|
-
const summary = changedFiles.
|
|
6156
|
+
const summary = changedFiles.nonDeletedFiles().map((f)=>({
|
|
5930
6157
|
file: f.filename,
|
|
5931
6158
|
resolved: 0,
|
|
5932
6159
|
unresolved: 0,
|
|
5933
6160
|
summary: applicableSpecs.length === 0 ? "无适用的审查规则" : "已跳过"
|
|
5934
6161
|
}));
|
|
5935
|
-
const prDesc = context.generateDescription && llmMode ? await this.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.buildBasicDescription(commits, changedFiles);
|
|
6162
|
+
const prDesc = context.generateDescription && llmMode ? await this.llmProcessor.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.llmProcessor.buildBasicDescription(commits, changedFiles);
|
|
5936
6163
|
const result = {
|
|
5937
6164
|
success: true,
|
|
5938
6165
|
title: prDesc.title,
|
|
@@ -5958,139 +6185,6 @@ class ReviewService {
|
|
|
5958
6185
|
return result;
|
|
5959
6186
|
}
|
|
5960
6187
|
/**
|
|
5961
|
-
* 检查是否有其他同名 review workflow 正在运行中
|
|
5962
|
-
* 根据 duplicateWorkflowResolved 配置决定是跳过还是删除旧评论
|
|
5963
|
-
*/ async checkDuplicateWorkflow(prModel, headSha, mode, verbose) {
|
|
5964
|
-
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
5965
|
-
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
5966
|
-
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prModel.number;
|
|
5967
|
-
try {
|
|
5968
|
-
const runningWorkflows = await prModel.listWorkflowRuns({
|
|
5969
|
-
status: "in_progress"
|
|
5970
|
-
});
|
|
5971
|
-
const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
|
|
5972
|
-
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
5973
|
-
const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
|
|
5974
|
-
if (duplicateReviewRuns.length > 0) {
|
|
5975
|
-
if (mode === "delete") {
|
|
5976
|
-
// 删除模式:清理旧的 AI Review 评论和 PR Review
|
|
5977
|
-
if (shouldLog(verbose, 1)) {
|
|
5978
|
-
console.log(`🗑️ 检测到 ${duplicateReviewRuns.length} 个同名 workflow,清理旧的 AI Review 评论...`);
|
|
5979
|
-
}
|
|
5980
|
-
await this.cleanupDuplicateAiReviews(prModel, verbose);
|
|
5981
|
-
// 清理后继续执行当前审查
|
|
5982
|
-
return null;
|
|
5983
|
-
}
|
|
5984
|
-
// 跳过模式(默认)
|
|
5985
|
-
if (shouldLog(verbose, 1)) {
|
|
5986
|
-
console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
|
|
5987
|
-
}
|
|
5988
|
-
return {
|
|
5989
|
-
success: true,
|
|
5990
|
-
description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
|
|
5991
|
-
issues: [],
|
|
5992
|
-
summary: [],
|
|
5993
|
-
round: 1
|
|
5994
|
-
};
|
|
5995
|
-
}
|
|
5996
|
-
} catch (error) {
|
|
5997
|
-
if (shouldLog(verbose, 1)) {
|
|
5998
|
-
console.warn(`⚠️ 无法检查重复 workflow(可能缺少 repo owner 权限),跳过此检查:`, error instanceof Error ? error.message : error);
|
|
5999
|
-
}
|
|
6000
|
-
}
|
|
6001
|
-
return null;
|
|
6002
|
-
}
|
|
6003
|
-
/**
|
|
6004
|
-
* 清理重复的 AI Review 评论(Issue Comments 和 PR Reviews)
|
|
6005
|
-
*/ async cleanupDuplicateAiReviews(prModel, verbose) {
|
|
6006
|
-
try {
|
|
6007
|
-
// 删除 Issue Comments(主评论)
|
|
6008
|
-
const comments = await prModel.getComments();
|
|
6009
|
-
const aiComments = comments.filter((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
6010
|
-
let deletedComments = 0;
|
|
6011
|
-
for (const comment of aiComments){
|
|
6012
|
-
if (comment.id) {
|
|
6013
|
-
try {
|
|
6014
|
-
await prModel.deleteComment(comment.id);
|
|
6015
|
-
deletedComments++;
|
|
6016
|
-
} catch {
|
|
6017
|
-
// 忽略删除失败
|
|
6018
|
-
}
|
|
6019
|
-
}
|
|
6020
|
-
}
|
|
6021
|
-
if (deletedComments > 0 && shouldLog(verbose, 1)) {
|
|
6022
|
-
console.log(` 已删除 ${deletedComments} 个重复的 AI Review 主评论`);
|
|
6023
|
-
}
|
|
6024
|
-
// 删除 PR Reviews(行级评论)
|
|
6025
|
-
const reviews = await prModel.getReviews();
|
|
6026
|
-
const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
6027
|
-
let deletedReviews = 0;
|
|
6028
|
-
for (const review of aiReviews){
|
|
6029
|
-
if (review.id) {
|
|
6030
|
-
try {
|
|
6031
|
-
await prModel.deleteReview(review.id);
|
|
6032
|
-
deletedReviews++;
|
|
6033
|
-
} catch {
|
|
6034
|
-
// 已提交的 review 无法删除,忽略
|
|
6035
|
-
}
|
|
6036
|
-
}
|
|
6037
|
-
}
|
|
6038
|
-
if (deletedReviews > 0 && shouldLog(verbose, 1)) {
|
|
6039
|
-
console.log(` 已删除 ${deletedReviews} 个重复的 AI Review PR Review`);
|
|
6040
|
-
}
|
|
6041
|
-
} catch (error) {
|
|
6042
|
-
if (shouldLog(verbose, 1)) {
|
|
6043
|
-
console.warn(`⚠️ 清理旧评论失败:`, error instanceof Error ? error.message : error);
|
|
6044
|
-
}
|
|
6045
|
-
}
|
|
6046
|
-
}
|
|
6047
|
-
// --- Delegation methods for backward compatibility with tests ---
|
|
6048
|
-
async fillIssueAuthors(...args) {
|
|
6049
|
-
return this.contextBuilder.fillIssueAuthors(...args);
|
|
6050
|
-
}
|
|
6051
|
-
async getFileContents(...args) {
|
|
6052
|
-
return this.issueFilter.getFileContents(...args);
|
|
6053
|
-
}
|
|
6054
|
-
async getFilesForCommit(...args) {
|
|
6055
|
-
return this.issueFilter.getFilesForCommit(...args);
|
|
6056
|
-
}
|
|
6057
|
-
async getChangedFilesBetweenRefs(...args) {
|
|
6058
|
-
return this.issueFilter.getChangedFilesBetweenRefs(...args);
|
|
6059
|
-
}
|
|
6060
|
-
async getCommitsBetweenRefs(...args) {
|
|
6061
|
-
return this.issueFilter.getCommitsBetweenRefs(...args);
|
|
6062
|
-
}
|
|
6063
|
-
filterIssuesByValidCommits(...args) {
|
|
6064
|
-
return this.issueFilter.filterIssuesByValidCommits(...args);
|
|
6065
|
-
}
|
|
6066
|
-
filterDuplicateIssues(...args) {
|
|
6067
|
-
return this.issueFilter.filterDuplicateIssues(...args);
|
|
6068
|
-
}
|
|
6069
|
-
async fillIssueCode(...args) {
|
|
6070
|
-
return this.issueFilter.fillIssueCode(...args);
|
|
6071
|
-
}
|
|
6072
|
-
async runLLMReview(...args) {
|
|
6073
|
-
return this.llmProcessor.runLLMReview(...args);
|
|
6074
|
-
}
|
|
6075
|
-
async buildReviewPrompt(...args) {
|
|
6076
|
-
return this.llmProcessor.buildReviewPrompt(...args);
|
|
6077
|
-
}
|
|
6078
|
-
async generatePrDescription(...args) {
|
|
6079
|
-
return this.llmProcessor.generatePrDescription(...args);
|
|
6080
|
-
}
|
|
6081
|
-
async buildBasicDescription(...args) {
|
|
6082
|
-
return this.llmProcessor.buildBasicDescription(...args);
|
|
6083
|
-
}
|
|
6084
|
-
normalizeFilePaths(...args) {
|
|
6085
|
-
return this.contextBuilder.normalizeFilePaths(...args);
|
|
6086
|
-
}
|
|
6087
|
-
resolveAnalyzeDeletions(...args) {
|
|
6088
|
-
return this.contextBuilder.resolveAnalyzeDeletions(...args);
|
|
6089
|
-
}
|
|
6090
|
-
async getPrNumberFromEvent(...args) {
|
|
6091
|
-
return this.contextBuilder.getPrNumberFromEvent(...args);
|
|
6092
|
-
}
|
|
6093
|
-
/**
|
|
6094
6188
|
* 确保 Claude CLI 已安装
|
|
6095
6189
|
*/ async ensureClaudeCli(ci) {
|
|
6096
6190
|
try {
|
|
@@ -6971,6 +7065,7 @@ class DeletionImpactService {
|
|
|
6971
7065
|
|
|
6972
7066
|
|
|
6973
7067
|
|
|
7068
|
+
|
|
6974
7069
|
/** MCP 工具输入 schema */ const listRulesInputSchema = z.object({});
|
|
6975
7070
|
const getRulesForFileInputSchema = z.object({
|
|
6976
7071
|
filePath: z.string().describe(t("review:mcp.dto.filePath")),
|
|
@@ -7067,11 +7162,11 @@ const tools = [
|
|
|
7067
7162
|
const workDir = ctx.cwd;
|
|
7068
7163
|
const allSpecs = await loadAllSpecs(workDir, ctx);
|
|
7069
7164
|
const specService = new ReviewSpecService();
|
|
7070
|
-
const applicableSpecs = specService.filterApplicableSpecs(allSpecs, [
|
|
7165
|
+
const applicableSpecs = specService.filterApplicableSpecs(allSpecs, ChangedFileCollection.from([
|
|
7071
7166
|
{
|
|
7072
7167
|
filename: filePath
|
|
7073
7168
|
}
|
|
7074
|
-
]);
|
|
7169
|
+
]));
|
|
7075
7170
|
const micromatchModule = await __webpack_require__.e(/* import() */ "551").then(__webpack_require__.bind(__webpack_require__, 946));
|
|
7076
7171
|
const micromatch = micromatchModule.default || micromatchModule;
|
|
7077
7172
|
const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{
|