@spaceflow/review 0.81.0 → 0.82.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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LlmJsonPut, REVIEW_STATE, addLocaleResources, calculateNewLineNumber, createStreamLoggerState, defineExtension, logStreamEvent, normalizeVerbose, parallel, parseChangedLinesFromPatch, parseDiffText, parseHunksFromPatch, parseRepoUrl, parseVerbose, shouldLog, t, z } from "@spaceflow/core";
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 = new Set();
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(`| 总问题数 | ${stats.total} |`);
1718
- lines.push(`| 🟢 已修复 | ${stats.fixed} |`);
1719
- lines.push(`| 已解决 | ${stats.resolved} |`);
1720
- lines.push(`| 无效 | ${stats.invalid} |`);
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
- const taggedNewIssues = newResult.issues.map((issue)=>({
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
- * 简化策略:如果文件在最新 commit 中有变更,则将该文件的所有历史问题标记为无效。
2602
- */ async invalidateChangedFiles(headSha, verbose) {
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.resolved || issue.valid === "false") {
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
- console.log(` 📊 共标记 ${invalidatedCount} 个历史问题为无效(文件有变更)`);
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
- * 如果传入 preloaded(specs/fileContents),直接使用;否则从 PR 获取
3767
- */ async verifyAndUpdateIssues(context, issues, commits, preloaded, pr) {
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
- let specs;
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
- console.log(` 🔍 有效 commit hashes: ${Array.from(validCommitHashes).join(", ")}`);
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
- // 检查问题行范围内是否有任意一行属于本次 PR 的有效 commits
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
- if (actualHash !== "-------" && validCommitHashes.has(actualHash)) {
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
- // 问题行都不属于本次 PR 的有效 commits
3976
+ // 问题行都不属于本次变更范围
4041
3977
  if (shouldLog(verbose, 2)) {
4042
- console.log(` Issue ${issue.file}:${issue.line} 不在本次 PR 变更行范围内,跳过`);
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(` 过滤非本次 PR commits 问题后: ${beforeCount} -> ${filtered.length} 个问题`);
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.filter((f)=>f.status !== "deleted" && f.filename).map((file)=>{
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.filter((f)=>f.status === "added").length;
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.service.ts
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
- class ReviewService {
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
- llmProcessor;
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.contextBuilder = new ReviewContextBuilder(gitProvider, config, gitSdk);
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
- * 1. 加载审查规范(specs)
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
- * @param context 审查上下文,包含 owner、repo、prNumber 等信息
5349
- * @returns 审查结果,包含发现的问题列表和统计信息
5350
- */ async execute(context) {
5351
- const { specSources, verbose, llmMode, deletionOnly } = context;
5352
- if (shouldLog(verbose, 1)) {
5353
- console.log(`🔍 Review 启动`);
5354
- console.log(` DRY-RUN mode: ${context.dryRun ? "enabled" : "disabled"}`);
5355
- console.log(` CI mode: ${context.ci ? "enabled" : "disabled"}`);
5356
- if (context.localMode) console.log(` Local mode: ${context.localMode}`);
5357
- console.log(` Verbose: ${verbose}`);
5358
- }
5359
- // 早期分流
5360
- if (deletionOnly) return this.executeDeletionOnly(context);
5361
- if (context.eventAction === "closed" || context.flush) return this.executeCollectOnly(context);
5362
- // 1. 解析输入数据(本地/PR/分支模式 + 前置过滤)
5363
- const source = await this.resolveSourceData(context);
5364
- if (source.earlyReturn) return source.earlyReturn;
5365
- const { prModel, commits, changedFiles, headSha, isDirectFileMode } = source;
5366
- const effectiveWhenModifiedCode = isDirectFileMode ? undefined : context.whenModifiedCode;
5367
- if (isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
5368
- console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
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
- // 2. 规则匹配
5371
- const specs = await this.issueFilter.loadSpecs(specSources, verbose);
5372
- const applicableSpecs = this.reviewSpecService.filterApplicableSpecs(specs, changedFiles);
5373
- if (shouldLog(verbose, 2)) {
5374
- console.log(`[execute] loadSpecs: loaded ${specs.length} specs from sources: ${JSON.stringify(specSources)}`);
5375
- console.log(`[execute] filterApplicableSpecs: ${applicableSpecs.length} applicable out of ${specs.length}, changedFiles=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
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.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(` 适用的规则文件: ${applicableSpecs.length}`);
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
- // 3. 获取文件内容 + LLM 审查
5384
- const fileContents = await this.getFileContents(context.owner, context.repo, changedFiles, commits, headSha, context.prNumber, verbose, source.isLocalMode);
5385
- if (!llmMode) throw new Error("必须指定 LLM 类型");
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(`⚙️ 追加 ${reviewPrompt.staticIssues.length} 个静态规则系统问题`);
5320
+ console.log(`ℹ️ 没有${localMode === "staged" ? "暂存区" : "未提交"}的代码变更,回退到分支比较模式`);
5429
5321
  }
5430
- }
5431
- if (shouldLog(verbose, 1)) {
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(`📥 本地模式: 获取${localMode === "staged" ? "暂存区" : "未提交"}的代码变更`);
5325
+ console.log(`📌 自动检测分支: base=${effectiveBaseRef}, head=${effectiveHeadRef}`);
5464
5326
  }
5465
- const localFiles = localMode === "staged" ? this.gitSdk.getStagedFiles() : this.gitSdk.getUncommittedFiles();
5466
- if (localFiles.length === 0) {
5467
- // 本地无变更,回退到分支比较模式
5468
- if (shouldLog(verbose, 1)) {
5469
- console.log(`ℹ️ 没有${localMode === "staged" ? "暂存区" : "未提交"}的代码变更,回退到分支比较模式`);
5470
- }
5471
- isLocalMode = false;
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,533 @@ 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
+ };
5347
+ }
5348
+ return {
5349
+ changedFiles: [],
5350
+ isLocalMode: false,
5351
+ effectiveBaseRef,
5352
+ effectiveHeadRef
5353
+ };
5354
+ }
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
+ };
5510
5402
  }
5511
5403
  }
5512
- // 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
5513
- if (isDirectFileMode) {
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 } = context;
5439
+ let changedFiles = ChangedFileCollection.from(rawChangedFiles);
5440
+ // 0. 过滤掉 merge commit
5441
+ {
5442
+ const before = commits.length;
5443
+ commits = commits.filter((c)=>{
5444
+ const message = c.commit?.message || "";
5445
+ return !message.startsWith("Merge ");
5446
+ });
5447
+ if (before !== commits.length && shouldLog(verbose, 1)) {
5448
+ console.log(` 跳过 Merge Commits: ${before} -> ${commits.length} 个`);
5449
+ }
5450
+ }
5451
+ // 1. 按指定的 files 过滤
5452
+ if (files && files.length > 0) {
5453
+ const before = changedFiles.length;
5454
+ changedFiles = changedFiles.filterByFilenames(files);
5514
5455
  if (shouldLog(verbose, 1)) {
5515
- console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
5456
+ console.log(` Files 过滤文件: ${before} -> ${changedFiles.length} 个文件`);
5516
5457
  }
5517
- changedFiles = files.map((f)=>({
5518
- filename: f,
5519
- status: "modified"
5520
- }));
5521
- isLocalMode = true;
5522
- } else if (prNumber) {
5458
+ }
5459
+ // 2. 按指定的 commits 过滤(同时过滤文件:仅保留属于匹配 commits 的文件)
5460
+ if (filterCommits && filterCommits.length > 0) {
5461
+ const beforeCommits = commits.length;
5462
+ commits = commits.filter((c)=>filterCommits.some((fc)=>fc && c.sha?.startsWith(fc)));
5523
5463
  if (shouldLog(verbose, 1)) {
5524
- console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
5464
+ console.log(` Commits 过滤: ${beforeCommits} -> ${commits.length} 个`);
5525
5465
  }
5526
- prModel = new PullRequestModel(this.gitProvider, owner, repo, prNumber);
5527
- const prInfo = await prModel.getInfo();
5528
- commits = await prModel.getCommits();
5529
- changedFiles = await prModel.getFiles();
5466
+ const beforeFiles = changedFiles.length;
5467
+ const commitFilenames = new Set();
5468
+ for (const commit of commits){
5469
+ if (!commit.sha) continue;
5470
+ const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
5471
+ commitFiles.forEach((f)=>commitFilenames.add(f));
5472
+ }
5473
+ changedFiles = changedFiles.filterByCommitFiles(commitFilenames);
5530
5474
  if (shouldLog(verbose, 1)) {
5531
- console.log(` PR: ${prInfo?.title}`);
5532
- console.log(` Commits: ${commits.length}`);
5533
- console.log(` Changed files: ${changedFiles.length}`);
5534
- }
5535
- // 检查是否有其他同名 review workflow 正在运行中
5536
- if (duplicateWorkflowResolved !== "off" && ci && prInfo?.head?.sha) {
5537
- const duplicateResult = await this.checkDuplicateWorkflow(prModel, prInfo.head.sha, duplicateWorkflowResolved, verbose);
5538
- if (duplicateResult) {
5539
- return {
5540
- prModel,
5541
- commits,
5542
- changedFiles,
5543
- headSha: prInfo.head.sha,
5544
- isLocalMode,
5545
- isDirectFileMode,
5546
- earlyReturn: duplicateResult
5547
- };
5475
+ console.log(` Commits 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
5476
+ }
5477
+ }
5478
+ // 3. 使用 includes 过滤文件和 commits(支持 added|/modified|/deleted| 前缀语法)
5479
+ if (isDirectFileMode && includes && includes.length > 0) {
5480
+ if (shouldLog(verbose, 1)) {
5481
+ console.log(`ℹ️ 直接文件模式下忽略 includes 过滤`);
5482
+ }
5483
+ } else if (includes && includes.length > 0) {
5484
+ const beforeFiles = changedFiles.length;
5485
+ if (shouldLog(verbose, 2)) {
5486
+ console.log(`[resolveSourceData] filterFilesByIncludes: before=${JSON.stringify(changedFiles.map((f)=>({
5487
+ filename: f.filename,
5488
+ status: f.status
5489
+ })))}, includes=${JSON.stringify(includes)}`);
5490
+ }
5491
+ changedFiles = ChangedFileCollection.from(filterFilesByIncludes(changedFiles.toArray(), includes));
5492
+ if (shouldLog(verbose, 1)) {
5493
+ console.log(` Includes 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
5494
+ }
5495
+ if (shouldLog(verbose, 2)) {
5496
+ console.log(`[resolveSourceData] filterFilesByIncludes: after=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
5497
+ }
5498
+ // 按 includes glob 过滤 commits:仅保留涉及匹配文件的 commits
5499
+ const globs = extractGlobsFromIncludes(includes);
5500
+ const beforeCommits = commits.length;
5501
+ const filteredCommits = [];
5502
+ for (const commit of commits){
5503
+ if (!commit.sha) continue;
5504
+ const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
5505
+ if (micromatch_0.some(commitFiles, globs)) {
5506
+ filteredCommits.push(commit);
5548
5507
  }
5549
5508
  }
5550
- } else if (effectiveBaseRef && effectiveHeadRef) {
5551
- if (changedFiles.length === 0) {
5552
- if (shouldLog(verbose, 1)) {
5553
- console.log(`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`);
5509
+ commits = filteredCommits;
5510
+ if (shouldLog(verbose, 1)) {
5511
+ console.log(` Includes 过滤 Commits: ${beforeCommits} -> ${commits.length} 个`);
5512
+ }
5513
+ }
5514
+ return {
5515
+ commits,
5516
+ changedFiles: changedFiles.toArray()
5517
+ };
5518
+ }
5519
+ // ─── 文件内容 ─────────────────────────────────────────
5520
+ /**
5521
+ * 获取文件内容并构建行号到 commit hash 的映射
5522
+ * 返回 Map<filename, Array<[commitHash, lineCode]>>
5523
+ */ async getFileContents(owner, repo, changedFiles, commits, ref, prNumber, isLocalMode, verbose) {
5524
+ const contents = new Map();
5525
+ const latestCommitHash = commits[commits.length - 1]?.sha?.slice(0, 7) || "+local+";
5526
+ if (shouldLog(verbose, 1)) {
5527
+ console.log(`📊 正在构建行号到变更的映射...`);
5528
+ }
5529
+ for (const file of changedFiles){
5530
+ if (file.filename && file.status !== "deleted") {
5531
+ try {
5532
+ let rawContent;
5533
+ if (isLocalMode) {
5534
+ rawContent = this.gitSdk.getWorkingFileContent(file.filename);
5535
+ } else if (prNumber) {
5536
+ rawContent = await this.gitProvider.getFileContent(owner, repo, file.filename, ref);
5537
+ } else {
5538
+ rawContent = await this.gitSdk.getFileContent(ref, file.filename);
5539
+ }
5540
+ const lines = rawContent.split("\n");
5541
+ let changedLines = parseChangedLinesFromPatch(file.patch);
5542
+ const isNewFile = file.status === "added" || file.status === "A" || file.additions && file.additions > 0 && file.deletions === 0 && !file.patch;
5543
+ if (changedLines.size === 0 && isNewFile) {
5544
+ changedLines = new Set(lines.map((_, i)=>i + 1));
5545
+ if (shouldLog(verbose, 2)) {
5546
+ console.log(` ℹ️ ${file.filename}: 新增文件无 patch,将所有 ${lines.length} 行标记为变更`);
5547
+ }
5548
+ }
5549
+ let blameMap;
5550
+ if (!isLocalMode) {
5551
+ try {
5552
+ blameMap = await this.gitSdk.getFileBlame(ref, file.filename);
5553
+ } catch {
5554
+ // blame 失败时回退到 latestCommitHash
5555
+ }
5556
+ }
5557
+ if (shouldLog(verbose, 3)) {
5558
+ console.log(` 📄 ${file.filename}: ${lines.length} 行, ${changedLines.size} 行变更`);
5559
+ console.log(` blame: ${blameMap ? `${blameMap.size} 行` : `不可用,回退到 ${latestCommitHash}`}`);
5560
+ if (changedLines.size > 0 && changedLines.size <= 20) {
5561
+ console.log(` 变更行号: ${Array.from(changedLines).sort((a, b)=>a - b).join(", ")}`);
5562
+ } else if (changedLines.size > 20) {
5563
+ console.log(` 变更行号: (共 ${changedLines.size} 行,省略详情)`);
5564
+ }
5565
+ if (!file.patch) {
5566
+ console.log(` ⚠️ 该文件没有 patch 信息 (status=${file.status}, additions=${file.additions}, deletions=${file.deletions})`);
5567
+ } else {
5568
+ console.log(` patch 前 200 字符: ${file.patch.slice(0, 200).replace(/\n/g, "\\n")}`);
5569
+ }
5570
+ }
5571
+ const contentLines = lines.map((line, index)=>{
5572
+ const lineNum = index + 1;
5573
+ if (!changedLines.has(lineNum)) {
5574
+ return [
5575
+ "-------",
5576
+ line
5577
+ ];
5578
+ }
5579
+ const hash = blameMap?.get(lineNum) ?? latestCommitHash;
5580
+ return [
5581
+ hash,
5582
+ line
5583
+ ];
5584
+ });
5585
+ contents.set(file.filename, contentLines);
5586
+ } catch (error) {
5587
+ console.warn(`警告: 无法获取文件内容: ${file.filename}`, error);
5588
+ }
5589
+ }
5590
+ }
5591
+ if (shouldLog(verbose, 1)) {
5592
+ console.log(`📊 映射构建完成,共 ${contents.size} 个文件`);
5593
+ }
5594
+ return contents;
5595
+ }
5596
+ // ─── 重复 workflow 检查 ──────────────────────────────────
5597
+ /**
5598
+ * 检查是否有其他同名 review workflow 正在运行中。
5599
+ * 根据 duplicateWorkflowResolved 配置决定是跳过还是删除旧评论。
5600
+ */ async checkDuplicateWorkflow(prModel, headSha, mode, verbose) {
5601
+ const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
5602
+ const prMatch = ref.match(/refs\/pull\/(\d+)/);
5603
+ const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prModel.number;
5604
+ try {
5605
+ const runningWorkflows = await prModel.listWorkflowRuns({
5606
+ status: "in_progress"
5607
+ });
5608
+ const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
5609
+ const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
5610
+ const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
5611
+ if (duplicateReviewRuns.length > 0) {
5612
+ if (mode === "delete") {
5613
+ // 删除模式:清理旧的 AI Review 评论和 PR Review
5614
+ if (shouldLog(verbose, 1)) {
5615
+ console.log(`🗑️ 检测到 ${duplicateReviewRuns.length} 个同名 workflow,清理旧的 AI Review 评论...`);
5616
+ }
5617
+ await this.cleanupDuplicateAiReviews(prModel, verbose);
5618
+ // 清理后继续执行当前审查
5619
+ return null;
5554
5620
  }
5555
- changedFiles = await this.getChangedFilesBetweenRefs(owner, repo, effectiveBaseRef, effectiveHeadRef);
5556
- commits = await this.getCommitsBetweenRefs(effectiveBaseRef, effectiveHeadRef);
5621
+ // 跳过模式(默认)
5557
5622
  if (shouldLog(verbose, 1)) {
5558
- console.log(` Changed files: ${changedFiles.length}`);
5559
- console.log(` Commits: ${commits.length}`);
5623
+ console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
5560
5624
  }
5625
+ return {
5626
+ success: true,
5627
+ description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
5628
+ issues: [],
5629
+ summary: [],
5630
+ round: 1
5631
+ };
5561
5632
  }
5562
- } else if (!isLocalMode) {
5633
+ } catch (error) {
5563
5634
  if (shouldLog(verbose, 1)) {
5564
- console.log(`❌ 错误: 缺少 prNumber baseRef/headRef`, {
5565
- prNumber,
5566
- baseRef,
5567
- headRef
5568
- });
5635
+ console.warn(`⚠️ 无法检查重复 workflow(可能缺少 repo owner 权限),跳过此检查:`, error instanceof Error ? error.message : error);
5569
5636
  }
5570
- throw new Error("必须指定 PR 编号或者 base/head 分支");
5571
5637
  }
5572
- // ── 前置过滤 ──────────────────────────────────────────
5573
- // 0. 过滤掉 merge commit
5574
- {
5575
- const before = commits.length;
5576
- commits = commits.filter((c)=>{
5577
- const message = c.commit?.message || "";
5578
- return !message.startsWith("Merge ");
5579
- });
5580
- if (before !== commits.length && shouldLog(verbose, 1)) {
5581
- console.log(` 跳过 Merge Commits: ${before} -> ${commits.length} 个`);
5582
- }
5638
+ return null;
5639
+ }
5640
+ /**
5641
+ * 清理重复的 AI Review 评论(Issue Comments 和 PR Reviews)
5642
+ */ async cleanupDuplicateAiReviews(prModel, verbose) {
5643
+ try {
5644
+ // 删除 Issue Comments(主评论)
5645
+ const comments = await prModel.getComments();
5646
+ const aiComments = comments.filter((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
5647
+ let deletedComments = 0;
5648
+ for (const comment of aiComments){
5649
+ if (comment.id) {
5650
+ try {
5651
+ await prModel.deleteComment(comment.id);
5652
+ deletedComments++;
5653
+ } catch {
5654
+ // 忽略删除失败
5655
+ }
5656
+ }
5657
+ }
5658
+ if (deletedComments > 0 && shouldLog(verbose, 1)) {
5659
+ console.log(` 已删除 ${deletedComments} 个重复的 AI Review 主评论`);
5660
+ }
5661
+ // 删除 PR Reviews(行级评论)
5662
+ const reviews = await prModel.getReviews();
5663
+ const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
5664
+ let deletedReviews = 0;
5665
+ for (const review of aiReviews){
5666
+ if (review.id) {
5667
+ try {
5668
+ await prModel.deleteReview(review.id);
5669
+ deletedReviews++;
5670
+ } catch {
5671
+ // 已提交的 review 无法删除,忽略
5672
+ }
5673
+ }
5674
+ }
5675
+ if (deletedReviews > 0 && shouldLog(verbose, 1)) {
5676
+ console.log(` 已删除 ${deletedReviews} 个重复的 AI Review PR Review`);
5677
+ }
5678
+ } catch (error) {
5679
+ if (shouldLog(verbose, 1)) {
5680
+ console.warn(`⚠️ 清理旧评论失败:`, error instanceof Error ? error.message : error);
5681
+ }
5682
+ }
5683
+ }
5684
+ }
5685
+
5686
+ ;// CONCATENATED MODULE: ./src/review.service.ts
5687
+
5688
+
5689
+
5690
+
5691
+
5692
+
5693
+
5694
+
5695
+
5696
+
5697
+
5698
+ class ReviewService {
5699
+ gitProvider;
5700
+ config;
5701
+ reviewSpecService;
5702
+ llmProxyService;
5703
+ reviewReportService;
5704
+ issueVerifyService;
5705
+ deletionImpactService;
5706
+ gitSdk;
5707
+ contextBuilder;
5708
+ issueFilter;
5709
+ llmProcessor;
5710
+ resultModelDeps;
5711
+ sourceResolver;
5712
+ constructor(gitProvider, config, reviewSpecService, llmProxyService, reviewReportService, issueVerifyService, deletionImpactService, gitSdk){
5713
+ this.gitProvider = gitProvider;
5714
+ this.config = config;
5715
+ this.reviewSpecService = reviewSpecService;
5716
+ this.llmProxyService = llmProxyService;
5717
+ this.reviewReportService = reviewReportService;
5718
+ this.issueVerifyService = issueVerifyService;
5719
+ this.deletionImpactService = deletionImpactService;
5720
+ this.gitSdk = gitSdk;
5721
+ this.contextBuilder = new ReviewContextBuilder(gitProvider, config, gitSdk);
5722
+ this.issueFilter = new ReviewIssueFilter(gitProvider, config, reviewSpecService, issueVerifyService, gitSdk);
5723
+ this.llmProcessor = new ReviewLlmProcessor(llmProxyService, reviewSpecService);
5724
+ this.sourceResolver = new ReviewSourceResolver(gitProvider, gitSdk, this.issueFilter);
5725
+ this.resultModelDeps = {
5726
+ gitProvider,
5727
+ config,
5728
+ reviewSpecService,
5729
+ reviewReportService
5730
+ };
5731
+ }
5732
+ async getContextFromEnv(options) {
5733
+ return this.contextBuilder.getContextFromEnv(options);
5734
+ }
5735
+ /**
5736
+ * 执行代码审查的主方法
5737
+ * 该方法负责协调整个审查流程,包括:
5738
+ * 1. 加载审查规范(specs)
5739
+ * 2. 获取 PR/分支的变更文件和提交记录
5740
+ * 3. 调用 LLM 进行代码审查
5741
+ * 4. 处理历史 issue(更新行号、验证修复状态)
5742
+ * 5. 生成并发布审查报告
5743
+ *
5744
+ * @param context 审查上下文,包含 owner、repo、prNumber 等信息
5745
+ * @returns 审查结果,包含发现的问题列表和统计信息
5746
+ */ async execute(context) {
5747
+ const { specSources, verbose, llmMode, deletionOnly } = context;
5748
+ if (shouldLog(verbose, 1)) {
5749
+ console.log(`🔍 Review 启动`);
5750
+ console.log(` DRY-RUN mode: ${context.dryRun ? "enabled" : "disabled"}`);
5751
+ console.log(` CI mode: ${context.ci ? "enabled" : "disabled"}`);
5752
+ if (context.localMode) console.log(` Local mode: ${context.localMode}`);
5753
+ console.log(` Verbose: ${verbose}`);
5754
+ }
5755
+ // 早期分流
5756
+ if (deletionOnly) return this.executeDeletionOnly(context);
5757
+ if (context.eventAction === "closed" || context.flush) return this.executeCollectOnly(context);
5758
+ // 1. 解析输入数据(本地/PR/分支模式 + 前置过滤)
5759
+ const source = await this.resolveSourceData(context);
5760
+ if (source.earlyReturn) return source.earlyReturn;
5761
+ const effectiveWhenModifiedCode = source.isDirectFileMode ? undefined : context.whenModifiedCode;
5762
+ if (source.isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
5763
+ console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
5583
5764
  }
5584
- // 1. 按指定的 files 过滤
5585
- if (files && files.length > 0) {
5586
- const before = changedFiles.length;
5587
- changedFiles = changedFiles.filter((f)=>files.includes(f.filename || ""));
5588
- if (shouldLog(verbose, 1)) {
5589
- console.log(` Files 过滤文件: ${before} -> ${changedFiles.length} 个文件`);
5590
- }
5765
+ // 2. 规则匹配
5766
+ const allSpecs = await this.issueFilter.loadSpecs(specSources, verbose);
5767
+ const specs = this.reviewSpecService.filterApplicableSpecs(allSpecs, source.changedFiles);
5768
+ if (shouldLog(verbose, 2)) {
5769
+ console.log(`[execute] loadSpecs: loaded ${specs.length} specs from sources: ${JSON.stringify(specSources)}`);
5770
+ console.log(`[execute] filterApplicableSpecs: ${specs.length} applicable out of ${allSpecs.length}, changedFiles=${JSON.stringify(source.changedFiles.filenames())}`);
5591
5771
  }
5592
- // 2. 按指定的 commits 过滤
5593
- if (filterCommits && filterCommits.length > 0) {
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
- }
5772
+ if (shouldLog(verbose, 1)) {
5773
+ console.log(` 适用的规则文件: ${specs.length}`);
5610
5774
  }
5611
- // 3. 使用 includes 过滤文件和 commits(支持 added|/modified|/deleted| 前缀语法)
5612
- if (isDirectFileMode && includes && includes.length > 0) {
5613
- if (shouldLog(verbose, 1)) {
5614
- console.log(`ℹ️ 直接文件模式下忽略 includes 过滤`);
5615
- }
5616
- } else if (includes && includes.length > 0) {
5617
- const beforeFiles = changedFiles.length;
5618
- if (shouldLog(verbose, 2)) {
5619
- console.log(`[resolveSourceData] filterFilesByIncludes: before=${JSON.stringify(changedFiles.map((f)=>({
5620
- filename: f.filename,
5621
- status: f.status
5622
- })))}, includes=${JSON.stringify(includes)}`);
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} 个`);
5775
+ if (specs.length === 0 || source.changedFiles.length === 0) {
5776
+ return this.handleNoApplicableSpecs(context, specs, source.changedFiles, source.commits);
5777
+ }
5778
+ // 3. LLM 审查
5779
+ const { fileContents } = source;
5780
+ if (!llmMode) throw new Error("必须指定 LLM 类型");
5781
+ // 获取上一次的审查结果(用于提示词优化和轮次推进)
5782
+ let existingResultModel = null;
5783
+ if (context.ci && source.prModel) {
5784
+ existingResultModel = await ReviewResultModel.loadFromPr(source.prModel, this.resultModelDeps);
5785
+ if (existingResultModel && shouldLog(verbose, 1)) {
5786
+ console.log(`📋 获取到上一次审查结果,包含 ${existingResultModel.issues.length} 个问题`);
5644
5787
  }
5645
5788
  }
5646
- const headSha = prModel ? await prModel.getHeadSha() : headRef || "HEAD";
5647
- return {
5648
- prModel,
5789
+ if (shouldLog(verbose, 1)) {
5790
+ console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
5791
+ }
5792
+ const reviewPrompt = await this.llmProcessor.buildReviewPrompt(specs, source.changedFiles, fileContents, source.commits, existingResultModel?.result ?? null, effectiveWhenModifiedCode, verbose, context.systemRules);
5793
+ // 4. 运行 LLM 审查 + 过滤新 issues
5794
+ const result = await this.buildReviewResult(context, reviewPrompt, llmMode, {
5795
+ specs,
5796
+ fileContents,
5797
+ changedFiles: source.changedFiles,
5798
+ commits: source.commits,
5799
+ isDirectFileMode: source.isDirectFileMode
5800
+ });
5801
+ // 5. 构建最终的 ReviewResultModel
5802
+ const finalModel = await this.buildFinalModel(context, result, {
5803
+ prModel: source.prModel,
5804
+ commits: source.commits,
5805
+ headSha: source.headSha,
5806
+ specs,
5807
+ fileContents
5808
+ }, existingResultModel);
5809
+ // 6. 保存 + 输出
5810
+ await this.saveAndOutput(context, finalModel, source.commits);
5811
+ return finalModel.result;
5812
+ }
5813
+ /**
5814
+ * 运行 LLM 审查并构建过滤后的 ReviewResult:
5815
+ * - 调用 LLM 生成问题列表
5816
+ * - 填充 PR 标题/描述
5817
+ * - 过滤新 issues(去重、commit 范围等)
5818
+ * - 合并静态规则问题
5819
+ */ async buildReviewResult(context, reviewPrompt, llmMode, source) {
5820
+ const { verbose } = context;
5821
+ const { specs, fileContents, changedFiles, commits, isDirectFileMode } = source;
5822
+ const result = await this.llmProcessor.runLLMReview(llmMode, reviewPrompt, {
5823
+ verbose,
5824
+ concurrency: context.concurrency,
5825
+ timeout: context.timeout,
5826
+ retries: context.retries,
5827
+ retryDelay: context.retryDelay
5828
+ });
5829
+ // 填充 PR 功能描述和标题
5830
+ const prInfo = context.generateDescription ? await this.llmProcessor.generatePrDescription(commits, changedFiles, llmMode, fileContents, verbose) : await this.llmProcessor.buildBasicDescription(commits, changedFiles);
5831
+ result.title = prInfo.title;
5832
+ result.description = prInfo.description;
5833
+ if (shouldLog(verbose, 1)) {
5834
+ console.log(`📝 LLM 审查完成,发现 ${result.issues.length} 个问题`);
5835
+ }
5836
+ result.issues = await this.issueFilter.fillIssueCode(result.issues, fileContents);
5837
+ result.issues = this.filterNewIssues(result.issues, specs, {
5649
5838
  commits,
5839
+ fileContents,
5650
5840
  changedFiles,
5651
- headSha,
5652
- isLocalMode,
5653
- isDirectFileMode
5654
- };
5841
+ isDirectFileMode,
5842
+ context
5843
+ });
5844
+ // 静态规则产生的系统问题直接合并,不经过过滤管道
5845
+ if (reviewPrompt.staticIssues?.length) {
5846
+ result.issues = [
5847
+ ...reviewPrompt.staticIssues,
5848
+ ...result.issues
5849
+ ];
5850
+ if (shouldLog(verbose, 1)) {
5851
+ console.log(`⚙️ 追加 ${reviewPrompt.staticIssues.length} 个静态规则系统问题`);
5852
+ }
5853
+ }
5854
+ if (shouldLog(verbose, 1)) {
5855
+ console.log(`📝 最终发现 ${result.issues.length} 个问题`);
5856
+ }
5857
+ return result;
5858
+ }
5859
+ /**
5860
+ * 解析输入数据:委托给 ReviewSourceResolver。
5861
+ * @see ReviewSourceResolver#resolve
5862
+ */ async resolveSourceData(context) {
5863
+ return this.sourceResolver.resolve(context);
5655
5864
  }
5656
5865
  /**
5657
5866
  * LLM 审查后的 issue 过滤管道:
5658
5867
  * includes → 规则存在性 → overrides → 变更行过滤 → 格式化
5659
- */ filterNewIssues(issues, specs, applicableSpecs, opts) {
5868
+ */ filterNewIssues(issues, specs, opts) {
5660
5869
  const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
5661
5870
  const { verbose } = context;
5662
- let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, applicableSpecs);
5871
+ let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
5663
5872
  if (shouldLog(verbose, 1)) {
5664
5873
  console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
5665
5874
  }
@@ -5667,26 +5876,26 @@ class ReviewService {
5667
5876
  if (shouldLog(verbose, 1)) {
5668
5877
  console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
5669
5878
  }
5670
- filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, applicableSpecs, verbose);
5879
+ filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
5671
5880
  // 变更行过滤
5672
5881
  if (shouldLog(verbose, 3)) {
5673
5882
  console.log(` 🔍 变更行过滤条件检查:`);
5674
5883
  console.log(` showAll=${context.showAll}, isDirectFileMode=${isDirectFileMode}, commits.length=${commits.length}`);
5675
5884
  }
5676
- if (!context.showAll && !isDirectFileMode && commits.length > 0) {
5885
+ if (!context.showAll && !isDirectFileMode) {
5677
5886
  if (shouldLog(verbose, 2)) {
5678
5887
  console.log(` 🔍 开始变更行过滤,当前 ${filtered.length} 个问题`);
5679
5888
  }
5680
- filtered = this.filterIssuesByValidCommits(filtered, commits, fileContents, verbose);
5889
+ filtered = this.issueFilter.filterIssuesByValidCommits(filtered, commits, fileContents, verbose);
5681
5890
  if (shouldLog(verbose, 2)) {
5682
5891
  console.log(` 🔍 变更行过滤完成,剩余 ${filtered.length} 个问题`);
5683
5892
  }
5684
5893
  } else if (shouldLog(verbose, 1)) {
5685
- console.log(` 跳过变更行过滤 (${context.showAll ? "showAll=true" : isDirectFileMode ? "直接审查文件模式" : "commits.length=0"})`);
5894
+ console.log(` 跳过变更行过滤 (${context.showAll ? "showAll=true" : "直接审查文件模式"})`);
5686
5895
  }
5687
5896
  filtered = this.reviewSpecService.formatIssues(filtered, {
5688
5897
  specs,
5689
- changedFiles
5898
+ changedFiles: changedFiles.toArray()
5690
5899
  });
5691
5900
  if (shouldLog(verbose, 1)) {
5692
5901
  console.log(` 应用格式化后: ${filtered.length} 个问题`);
@@ -5698,6 +5907,7 @@ class ReviewService {
5698
5907
  */ async buildFinalModel(context, result, source, existingResultModel) {
5699
5908
  const { prModel, commits, headSha, specs, fileContents } = source;
5700
5909
  const { verbose, ci } = context;
5910
+ result.headSha = headSha;
5701
5911
  if (ci && prModel && existingResultModel && existingResultModel.issues.length > 0) {
5702
5912
  if (shouldLog(verbose, 1)) {
5703
5913
  console.log(`📋 已有评论中存在 ${existingResultModel.issues.length} 个问题`);
@@ -5707,32 +5917,24 @@ class ReviewService {
5707
5917
  // 如果文件有变更,将该文件的历史问题标记为无效
5708
5918
  const reviewConf = this.config.getPluginConfig("review");
5709
5919
  if (reviewConf.invalidateChangedFiles !== "off" && reviewConf.invalidateChangedFiles !== "keep") {
5710
- await existingResultModel.invalidateChangedFiles(headSha, verbose);
5920
+ await existingResultModel.invalidateChangedFiles(headSha, fileContents, verbose);
5711
5921
  }
5712
5922
  // 验证历史问题是否已修复
5713
5923
  if (context.verifyFixes) {
5714
5924
  existingResultModel.issues = await this.issueFilter.verifyAndUpdateIssues(context, existingResultModel.issues, commits, {
5715
5925
  specs,
5716
5926
  fileContents
5717
- }, prModel);
5927
+ });
5718
5928
  } else {
5719
5929
  if (shouldLog(verbose, 1)) {
5720
5930
  console.log(` ⏭️ 跳过历史问题验证 (verifyFixes=false)`);
5721
5931
  }
5722
5932
  }
5723
- // 去重:与所有历史 issues 去重
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 合并
5933
+ // 自动 round 递增 + 去重 + issues 合并
5731
5934
  return existingResultModel.nextRound(result);
5732
5935
  }
5733
5936
  // 首次审查或无历史结果
5734
5937
  result.round = 1;
5735
- result.headSha = headSha;
5736
5938
  result.issues = result.issues.map((issue)=>({
5737
5939
  ...issue,
5738
5940
  round: 1
@@ -5746,7 +5948,7 @@ class ReviewService {
5746
5948
  const prModel = finalModel.pr.number > 0 ? finalModel.pr : undefined;
5747
5949
  // 填充 author 信息
5748
5950
  if (commits.length > 0) {
5749
- finalModel.issues = await this.fillIssueAuthors(finalModel.issues, commits, owner, repo, verbose);
5951
+ finalModel.issues = await this.contextBuilder.fillIssueAuthors(finalModel.issues, commits, owner, repo, verbose);
5750
5952
  }
5751
5953
  // 删除代码影响分析(在 save 之前完成,避免多次 save 产生重复的 Round 评论)
5752
5954
  if (context.analyzeDeletions && llmMode) {
@@ -5813,16 +6015,27 @@ class ReviewService {
5813
6015
  }
5814
6016
  // 2. 获取 commits 并填充 author 信息
5815
6017
  const commits = await prModel.getCommits();
5816
- resultModel.issues = await this.fillIssueAuthors(resultModel.issues, commits, owner, repo, verbose);
6018
+ resultModel.issues = await this.contextBuilder.fillIssueAuthors(resultModel.issues, commits, owner, repo, verbose);
5817
6019
  // 3. 同步已解决的评论状态
5818
6020
  await resultModel.syncResolved();
5819
6021
  // 4. 同步评论 reactions(👍/👎/☹️)
5820
6022
  await resultModel.syncReactions(verbose);
5821
6023
  // 5. LLM 验证历史问题是否已修复
5822
- try {
5823
- resultModel.issues = await this.issueFilter.verifyAndUpdateIssues(context, resultModel.issues, commits, undefined, prModel);
5824
- } catch (error) {
5825
- console.warn("⚠️ LLM 验证修复状态失败,跳过:", error);
6024
+ if (context.verifyFixes && context.specSources?.length) {
6025
+ try {
6026
+ const changedFiles = await prModel.getFiles();
6027
+ const headSha = await prModel.getHeadSha();
6028
+ const verifySpecs = await this.issueFilter.loadSpecs(context.specSources, verbose);
6029
+ const verifyFileContents = await this.sourceResolver.getFileContents(owner, repo, changedFiles, commits, headSha, prNumber, false, verbose);
6030
+ resultModel.issues = await this.issueFilter.verifyAndUpdateIssues(context, resultModel.issues, commits, {
6031
+ specs: verifySpecs,
6032
+ fileContents: verifyFileContents
6033
+ });
6034
+ } catch (error) {
6035
+ console.warn("⚠️ LLM 验证修复状态失败,跳过:", error);
6036
+ }
6037
+ } else if (!context.verifyFixes && shouldLog(verbose, 1)) {
6038
+ console.log(` ⏭️ 跳过历史问题验证 (verifyFixes=false)`);
5826
6039
  }
5827
6040
  // 6. 统计问题状态并设置到 result
5828
6041
  const stats = resultModel.updateStats();
@@ -5865,20 +6078,20 @@ class ReviewService {
5865
6078
  // 获取 commits 和 changedFiles 用于生成描述
5866
6079
  let prModel;
5867
6080
  let commits = [];
5868
- let changedFiles = [];
6081
+ let changedFiles = ChangedFileCollection.empty();
5869
6082
  if (prNumber) {
5870
6083
  prModel = new PullRequestModel(this.gitProvider, owner, repo, prNumber);
5871
6084
  commits = await prModel.getCommits();
5872
- changedFiles = await prModel.getFiles();
6085
+ changedFiles = ChangedFileCollection.from(await prModel.getFiles());
5873
6086
  } else if (baseRef && headRef) {
5874
- changedFiles = await this.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef);
5875
- commits = await this.getCommitsBetweenRefs(baseRef, headRef);
6087
+ changedFiles = ChangedFileCollection.from(await this.issueFilter.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef));
6088
+ commits = await this.issueFilter.getCommitsBetweenRefs(baseRef, headRef);
5876
6089
  }
5877
6090
  // 使用 includes 过滤文件(支持 added|/modified|/deleted| 前缀语法)
5878
6091
  if (context.includes && context.includes.length > 0) {
5879
- changedFiles = filterFilesByIncludes(changedFiles, context.includes);
6092
+ changedFiles = ChangedFileCollection.from(filterFilesByIncludes(changedFiles.toArray(), context.includes));
5880
6093
  }
5881
- const prDesc = context.generateDescription ? await this.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.buildBasicDescription(commits, changedFiles);
6094
+ const prDesc = context.generateDescription ? await this.llmProcessor.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.llmProcessor.buildBasicDescription(commits, changedFiles);
5882
6095
  const result = {
5883
6096
  success: true,
5884
6097
  title: prDesc.title,
@@ -5926,13 +6139,13 @@ class ReviewService {
5926
6139
  }
5927
6140
  const currentRound = (existingResultModel?.round ?? 0) + 1;
5928
6141
  // 即使没有适用的规则,也为每个变更文件生成摘要
5929
- const summary = changedFiles.filter((f)=>f.filename && f.status !== "deleted").map((f)=>({
6142
+ const summary = changedFiles.nonDeletedFiles().map((f)=>({
5930
6143
  file: f.filename,
5931
6144
  resolved: 0,
5932
6145
  unresolved: 0,
5933
6146
  summary: applicableSpecs.length === 0 ? "无适用的审查规则" : "已跳过"
5934
6147
  }));
5935
- const prDesc = context.generateDescription && llmMode ? await this.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.buildBasicDescription(commits, changedFiles);
6148
+ const prDesc = context.generateDescription && llmMode ? await this.llmProcessor.generatePrDescription(commits, changedFiles, llmMode, undefined, verbose) : await this.llmProcessor.buildBasicDescription(commits, changedFiles);
5936
6149
  const result = {
5937
6150
  success: true,
5938
6151
  title: prDesc.title,
@@ -5958,139 +6171,6 @@ class ReviewService {
5958
6171
  return result;
5959
6172
  }
5960
6173
  /**
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
6174
  * 确保 Claude CLI 已安装
6095
6175
  */ async ensureClaudeCli(ci) {
6096
6176
  try {
@@ -6971,6 +7051,7 @@ class DeletionImpactService {
6971
7051
 
6972
7052
 
6973
7053
 
7054
+
6974
7055
  /** MCP 工具输入 schema */ const listRulesInputSchema = z.object({});
6975
7056
  const getRulesForFileInputSchema = z.object({
6976
7057
  filePath: z.string().describe(t("review:mcp.dto.filePath")),
@@ -7067,11 +7148,11 @@ const tools = [
7067
7148
  const workDir = ctx.cwd;
7068
7149
  const allSpecs = await loadAllSpecs(workDir, ctx);
7069
7150
  const specService = new ReviewSpecService();
7070
- const applicableSpecs = specService.filterApplicableSpecs(allSpecs, [
7151
+ const applicableSpecs = specService.filterApplicableSpecs(allSpecs, ChangedFileCollection.from([
7071
7152
  {
7072
7153
  filename: filePath
7073
7154
  }
7074
- ]);
7155
+ ]));
7075
7156
  const micromatchModule = await __webpack_require__.e(/* import() */ "551").then(__webpack_require__.bind(__webpack_require__, 946));
7076
7157
  const micromatch = micromatchModule.default || micromatchModule;
7077
7158
  const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{