@spaceflow/review 0.69.0 → 0.71.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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.70.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.69.0...@spaceflow/review@0.70.0) (2026-04-02)
4
+
5
+ ### 新特性
6
+
7
+ * **review:** 新增自动批准功能,当所有问题解决时自动提交 APPROVE review ([4155342](https://github.com/Lydanne/spaceflow/commit/4155342511f76d6cd9001a14a144fce55a7a8946))
8
+ * **review:** 添加 workflow 重复运行检测,防止同一 PR 并发审查 ([a3777af](https://github.com/Lydanne/spaceflow/commit/a3777affb10b6fb4f88a83660ce2a81f9f2d6498))
9
+
10
+ ### 其他修改
11
+
12
+ * **core:** released version 0.27.0 [no ci] ([053d624](https://github.com/Lydanne/spaceflow/commit/053d624997b04e47537dac4ca0c7a99ed4a4cd36))
13
+ * **publish:** released version 0.51.0 [no ci] ([1fbe22a](https://github.com/Lydanne/spaceflow/commit/1fbe22a538cdcb6264e3937c5706d75f67fa4f2a))
14
+ * **review-summary:** released version 0.37.0 [no ci] ([455c581](https://github.com/Lydanne/spaceflow/commit/455c58140def453d95c8fc85b1ea8fde39037abc))
15
+
16
+ ## [0.69.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.68.0...@spaceflow/review@0.69.0) (2026-03-04)
17
+
18
+ ### 代码重构
19
+
20
+ * **review:** 优化问题统计逻辑,区分 fixed 和 resolved 状态 ([c4dda30](https://github.com/Lydanne/spaceflow/commit/c4dda30fed17ce020fcce9af8874dfa89ccca20b))
21
+
22
+ ### 其他修改
23
+
24
+ * **review-summary:** released version 0.36.0 [no ci] ([95c3d5c](https://github.com/Lydanne/spaceflow/commit/95c3d5cbac67c1ffa4c821faaffb476c502fe2c7))
25
+
3
26
  ## [0.68.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.67.0...@spaceflow/review@0.68.0) (2026-03-04)
4
27
 
5
28
  ### 代码重构
package/dist/index.js CHANGED
@@ -1630,7 +1630,9 @@ class ReviewReportService {
1630
1630
  timeout: z.number().optional(),
1631
1631
  retries: z.number().default(0).optional(),
1632
1632
  retryDelay: z.number().default(1000).optional(),
1633
- invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional()
1633
+ invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
1634
+ skipDuplicateWorkflow: z.boolean().default(false).optional(),
1635
+ autoApprove: z.boolean().default(false).optional()
1634
1636
  });
1635
1637
 
1636
1638
  ;// CONCATENATED MODULE: ./src/parse-title-options.ts
@@ -2022,7 +2024,9 @@ class ReviewService {
2022
2024
  showAll: options.showAll ?? false,
2023
2025
  flush: options.flush ?? false,
2024
2026
  eventAction: options.eventAction,
2025
- localMode
2027
+ localMode,
2028
+ skipDuplicateWorkflow: options.skipDuplicateWorkflow ?? reviewConf.skipDuplicateWorkflow ?? false,
2029
+ autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false
2026
2030
  };
2027
2031
  }
2028
2032
  /**
@@ -2116,7 +2120,7 @@ class ReviewService {
2116
2120
  * @param context 审查上下文,包含 owner、repo、prNumber 等信息
2117
2121
  * @returns 审查结果,包含发现的问题列表和统计信息
2118
2122
  */ async execute(context) {
2119
- const { owner, repo, prNumber, baseRef, headRef, specSources, dryRun, ci, verbose, includes, llmMode, files, commits: filterCommits, deletionOnly, localMode } = context;
2123
+ const { owner, repo, prNumber, baseRef, headRef, specSources, dryRun, ci, verbose, includes, llmMode, files, commits: filterCommits, deletionOnly, localMode, skipDuplicateWorkflow, autoApprove } = context;
2120
2124
  // 直接审查文件模式:指定了 -f 文件且 base=head
2121
2125
  const isDirectFileMode = files && files.length > 0 && baseRef === headRef;
2122
2126
  // 本地模式:审查未提交的代码(可能回退到分支比较)
@@ -2203,6 +2207,37 @@ class ReviewService {
2203
2207
  console.log(` Commits: ${commits.length}`);
2204
2208
  console.log(` Changed files: ${changedFiles.length}`);
2205
2209
  }
2210
+ // 检查是否有其他同名 review workflow 正在运行中(防止同一 PR 重复审查)
2211
+ // 需要显式启用 skipDuplicateWorkflow 配置
2212
+ if (skipDuplicateWorkflow && ci && pr?.head?.sha) {
2213
+ const headSha = pr.head.sha;
2214
+ // 获取当前 PR 编号(从 CI 环境变量)
2215
+ // GitHub: GITHUB_REF = refs/pull/123/merge
2216
+ // Gitea: GITEA_REF = refs/pull/123/head
2217
+ const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
2218
+ const prMatch = ref.match(/refs\/pull\/(\d+)/);
2219
+ const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prNumber;
2220
+ const runningWorkflows = await this.gitProvider.listWorkflowRuns(owner, repo, {
2221
+ status: "in_progress"
2222
+ });
2223
+ // 获取当前 workflow 名称和 run ID
2224
+ const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
2225
+ const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
2226
+ // 只检查同 PR 同名的其他 workflow run(排除当前 run)
2227
+ const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
2228
+ if (duplicateReviewRuns.length > 0) {
2229
+ if (shouldLog(verbose, 1)) {
2230
+ console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
2231
+ }
2232
+ return {
2233
+ success: true,
2234
+ description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
2235
+ issues: [],
2236
+ summary: [],
2237
+ round: 1
2238
+ };
2239
+ }
2240
+ }
2206
2241
  } else if (effectiveBaseRef && effectiveHeadRef) {
2207
2242
  // 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
2208
2243
  if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
@@ -2449,7 +2484,7 @@ class ReviewService {
2449
2484
  await this.postOrUpdateReviewComment(owner, repo, prNumber, {
2450
2485
  ...result,
2451
2486
  issues: allIssues
2452
- }, verbose);
2487
+ }, verbose, autoApprove);
2453
2488
  if (shouldLog(verbose, 1)) {
2454
2489
  console.log(`✅ 评论已提交`);
2455
2490
  }
@@ -2496,7 +2531,7 @@ class ReviewService {
2496
2531
  * 仅收集 review 状态模式(用于 PR 关闭或 --flush 指令)
2497
2532
  * 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
2498
2533
  */ async executeCollectOnly(context) {
2499
- const { owner, repo, prNumber, verbose, ci, dryRun } = context;
2534
+ const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
2500
2535
  if (shouldLog(verbose, 1)) {
2501
2536
  console.log(`📊 仅收集 review 状态模式`);
2502
2537
  }
@@ -2541,7 +2576,7 @@ class ReviewService {
2541
2576
  if (shouldLog(verbose, 1)) {
2542
2577
  console.log(`💬 更新 PR 评论...`);
2543
2578
  }
2544
- await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose);
2579
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose, autoApprove);
2545
2580
  if (shouldLog(verbose, 1)) {
2546
2581
  console.log(`✅ 评论已更新`);
2547
2582
  }
@@ -2621,11 +2656,12 @@ class ReviewService {
2621
2656
  const fixed = validIssue.filter((i)=>i.fixed).length;
2622
2657
  const resolved = validIssue.filter((i)=>i.resolved).length;
2623
2658
  const invalid = total - validTotal;
2624
- const pending = validTotal - fixed;
2659
+ const pending = validTotal - fixed - resolved;
2625
2660
  const fixRate = validTotal > 0 ? Math.round(fixed / validTotal * 100 * 10) / 10 : 0;
2626
2661
  const resolveRate = validTotal > 0 ? Math.round(resolved / validTotal * 100 * 10) / 10 : 0;
2627
2662
  return {
2628
2663
  total,
2664
+ validTotal,
2629
2665
  fixed,
2630
2666
  resolved,
2631
2667
  invalid,
@@ -2637,7 +2673,7 @@ class ReviewService {
2637
2673
  /**
2638
2674
  * 仅执行删除代码分析模式
2639
2675
  */ async executeDeletionOnly(context) {
2640
- const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } = context;
2676
+ const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } = context;
2641
2677
  if (shouldLog(verbose, 1)) {
2642
2678
  console.log(`🗑️ 仅执行删除代码分析模式`);
2643
2679
  }
@@ -2688,7 +2724,7 @@ class ReviewService {
2688
2724
  if (shouldLog(verbose, 1)) {
2689
2725
  console.log(`💬 提交 PR 评论...`);
2690
2726
  }
2691
- await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
2727
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
2692
2728
  if (shouldLog(verbose, 1)) {
2693
2729
  console.log(`✅ 评论已提交`);
2694
2730
  }
@@ -3425,7 +3461,7 @@ ${fileChanges || "无"}`;
3425
3461
  }
3426
3462
  return this.reviewReportService.format(result, format);
3427
3463
  }
3428
- async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose) {
3464
+ async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove) {
3429
3465
  // 获取配置
3430
3466
  const reviewConf = this.config.getPluginConfig("review");
3431
3467
  // 如果配置启用且有 AI 生成的标题,只在第一轮审查时更新 PR 标题
@@ -3490,29 +3526,42 @@ ${fileChanges || "无"}`;
3490
3526
  console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
3491
3527
  }
3492
3528
  // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
3529
+ // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
3493
3530
  let lineIssues = [];
3494
3531
  let comments = [];
3495
3532
  if (reviewConf.lineComments) {
3496
3533
  lineIssues = result.issues.filter((issue)=>issue.round === result.round && !issue.fixed && !issue.resolved && issue.valid !== "false");
3497
3534
  comments = lineIssues.map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
3498
3535
  }
3536
+ // 计算是否需要自动批准
3537
+ // 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
3538
+ const stats = this.calculateIssueStats(result.issues);
3539
+ const shouldAutoApprove = autoApprove && stats.pending === 0;
3499
3540
  if (reviewConf.lineComments) {
3500
- const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
3541
+ const lineReviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
3542
+ // 如果需要自动批准,追加批准信息到 body
3543
+ const finalReviewBody = shouldAutoApprove ? lineReviewBody + `\n\n---\n\n✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。` : lineReviewBody;
3544
+ const reviewEvent = shouldAutoApprove ? REVIEW_STATE.APPROVE : REVIEW_STATE.COMMENT;
3501
3545
  if (comments.length > 0) {
3502
3546
  try {
3503
3547
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3504
- event: REVIEW_STATE.COMMENT,
3505
- body: reviewBody,
3548
+ event: reviewEvent,
3549
+ body: finalReviewBody,
3506
3550
  comments,
3507
3551
  commit_id: commitId
3508
3552
  });
3509
- console.log(`✅ 已发布 ${comments.length} 条行级评论`);
3553
+ if (shouldAutoApprove) {
3554
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3555
+ } else {
3556
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
3557
+ }
3510
3558
  } catch {
3511
3559
  // 批量失败时逐条发布,跳过无法定位的评论
3512
3560
  console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
3513
3561
  let successCount = 0;
3514
3562
  for (const comment of comments){
3515
3563
  try {
3564
+ // 逐条发布时只用 COMMENT event,避免重复 APPROVE
3516
3565
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3517
3566
  event: REVIEW_STATE.COMMENT,
3518
3567
  body: successCount === 0 ? reviewBody : undefined,
@@ -3528,6 +3577,19 @@ ${fileChanges || "无"}`;
3528
3577
  }
3529
3578
  if (successCount > 0) {
3530
3579
  console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
3580
+ // 如果需要自动批准,单独发一个 APPROVE review
3581
+ if (shouldAutoApprove) {
3582
+ try {
3583
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
3584
+ event: REVIEW_STATE.APPROVE,
3585
+ body: `✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。`,
3586
+ commit_id: commitId
3587
+ });
3588
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3589
+ } catch (error) {
3590
+ console.warn("⚠️ 自动批准失败:", error);
3591
+ }
3592
+ }
3531
3593
  } else {
3532
3594
  console.warn("⚠️ 所有行级评论均无法定位,已跳过");
3533
3595
  }
@@ -3536,16 +3598,32 @@ ${fileChanges || "无"}`;
3536
3598
  // 本轮无新问题,仍发布 Round 状态(含上轮回顾)
3537
3599
  try {
3538
3600
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3539
- event: REVIEW_STATE.COMMENT,
3540
- body: reviewBody,
3601
+ event: reviewEvent,
3602
+ body: finalReviewBody,
3541
3603
  comments: [],
3542
3604
  commit_id: commitId
3543
3605
  });
3544
- console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
3606
+ if (shouldAutoApprove) {
3607
+ console.log(`✅ 已自动批准 PR #${prNumber}(Round ${result.round},所有问题已解决)`);
3608
+ } else {
3609
+ console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
3610
+ }
3545
3611
  } catch (error) {
3546
3612
  console.warn("⚠️ 发布审查状态失败:", error);
3547
3613
  }
3548
3614
  }
3615
+ } else if (shouldAutoApprove) {
3616
+ // 未启用 lineComments 但需要自动批准
3617
+ try {
3618
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
3619
+ event: REVIEW_STATE.APPROVE,
3620
+ body: `✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。`,
3621
+ commit_id: commitId
3622
+ });
3623
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3624
+ } catch (error) {
3625
+ console.warn("⚠️ 自动批准失败:", error);
3626
+ }
3549
3627
  }
3550
3628
  }
3551
3629
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -28,7 +28,7 @@
28
28
  "@spaceflow/cli": "0.40.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@spaceflow/core": "0.26.0"
31
+ "@spaceflow/core": "0.27.0"
32
32
  },
33
33
  "spaceflow": {
34
34
  "type": "flow",
@@ -117,6 +117,8 @@ export interface DeletionImpactResult {
117
117
  export interface ReviewStats {
118
118
  /** 总问题数 */
119
119
  total: number;
120
+ /** 有效问题数(排除无效) */
121
+ validTotal: number;
120
122
  /** AI 验证已修复数 */
121
123
  fixed: number;
122
124
  /** 用户手动 resolve 数 */
@@ -79,6 +79,18 @@ export interface ReviewOptions {
79
79
  * 在非 CI 和非 PR 模式下默认为 'uncommitted'
80
80
  */
81
81
  local?: LocalReviewMode;
82
+ /**
83
+ * 跳过重复的 review workflow 检查
84
+ * - true: 启用检查,当检测到同名 workflow 正在运行时跳过审查
85
+ * - false: 禁用检查(默认)
86
+ */
87
+ skipDuplicateWorkflow?: boolean;
88
+ /**
89
+ * 自动批准合并
90
+ * - true: 当所有问题都已解决时,自动提交 APPROVE review
91
+ * - false: 不自动批准(默认)
92
+ */
93
+ autoApprove?: boolean;
82
94
  }
83
95
 
84
96
  /** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
@@ -100,6 +112,8 @@ export const reviewSchema = () =>
100
112
  retries: z.number().default(0).optional(),
101
113
  retryDelay: z.number().default(1000).optional(),
102
114
  invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
115
+ skipDuplicateWorkflow: z.boolean().default(false).optional(),
116
+ autoApprove: z.boolean().default(false).optional(),
103
117
  });
104
118
 
105
119
  /** review 配置类型(从 schema 推导) */
@@ -306,6 +306,9 @@ export class ReviewService {
306
306
  flush: options.flush ?? false,
307
307
  eventAction: options.eventAction,
308
308
  localMode,
309
+ skipDuplicateWorkflow:
310
+ options.skipDuplicateWorkflow ?? reviewConf.skipDuplicateWorkflow ?? false,
311
+ autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false,
309
312
  };
310
313
  }
311
314
 
@@ -433,6 +436,8 @@ export class ReviewService {
433
436
  commits: filterCommits,
434
437
  deletionOnly,
435
438
  localMode,
439
+ skipDuplicateWorkflow,
440
+ autoApprove,
436
441
  } = context;
437
442
 
438
443
  // 直接审查文件模式:指定了 -f 文件且 base=head
@@ -532,6 +537,46 @@ export class ReviewService {
532
537
  console.log(` Commits: ${commits.length}`);
533
538
  console.log(` Changed files: ${changedFiles.length}`);
534
539
  }
540
+
541
+ // 检查是否有其他同名 review workflow 正在运行中(防止同一 PR 重复审查)
542
+ // 需要显式启用 skipDuplicateWorkflow 配置
543
+ if (skipDuplicateWorkflow && ci && pr?.head?.sha) {
544
+ const headSha = pr.head.sha;
545
+ // 获取当前 PR 编号(从 CI 环境变量)
546
+ // GitHub: GITHUB_REF = refs/pull/123/merge
547
+ // Gitea: GITEA_REF = refs/pull/123/head
548
+ const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
549
+ const prMatch = ref.match(/refs\/pull\/(\d+)/);
550
+ const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prNumber;
551
+
552
+ const runningWorkflows = await this.gitProvider.listWorkflowRuns(owner, repo, {
553
+ status: "in_progress",
554
+ });
555
+ // 获取当前 workflow 名称和 run ID
556
+ const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
557
+ const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
558
+ // 只检查同 PR 同名的其他 workflow run(排除当前 run)
559
+ const duplicateReviewRuns = runningWorkflows.filter(
560
+ (w) =>
561
+ w.sha === headSha &&
562
+ w.name === currentWorkflowName &&
563
+ (!currentRunId || String(w.id) !== currentRunId),
564
+ );
565
+ if (duplicateReviewRuns.length > 0) {
566
+ if (shouldLog(verbose, 1)) {
567
+ console.log(
568
+ `⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`,
569
+ );
570
+ }
571
+ return {
572
+ success: true,
573
+ description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
574
+ issues: [],
575
+ summary: [],
576
+ round: 1,
577
+ };
578
+ }
579
+ }
535
580
  } else if (effectiveBaseRef && effectiveHeadRef) {
536
581
  // 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
537
582
  if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
@@ -856,6 +901,7 @@ export class ReviewService {
856
901
  issues: allIssues,
857
902
  },
858
903
  verbose,
904
+ autoApprove,
859
905
  );
860
906
  if (shouldLog(verbose, 1)) {
861
907
  console.log(`✅ 评论已提交`);
@@ -916,7 +962,7 @@ export class ReviewService {
916
962
  * 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
917
963
  */
918
964
  protected async executeCollectOnly(context: ReviewContext): Promise<ReviewResult> {
919
- const { owner, repo, prNumber, verbose, ci, dryRun } = context;
965
+ const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
920
966
 
921
967
  if (shouldLog(verbose, 1)) {
922
968
  console.log(`📊 仅收集 review 状态模式`);
@@ -982,7 +1028,14 @@ export class ReviewService {
982
1028
  if (shouldLog(verbose, 1)) {
983
1029
  console.log(`💬 更新 PR 评论...`);
984
1030
  }
985
- await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose);
1031
+ await this.postOrUpdateReviewComment(
1032
+ owner,
1033
+ repo,
1034
+ prNumber,
1035
+ existingResult,
1036
+ verbose,
1037
+ autoApprove,
1038
+ );
986
1039
  if (shouldLog(verbose, 1)) {
987
1040
  console.log(`✅ 评论已更新`);
988
1041
  }
@@ -1099,17 +1152,18 @@ export class ReviewService {
1099
1152
  const fixed = validIssue.filter((i) => i.fixed).length;
1100
1153
  const resolved = validIssue.filter((i) => i.resolved).length;
1101
1154
  const invalid = total - validTotal;
1102
- const pending = validTotal - fixed;
1155
+ const pending = validTotal - fixed - resolved;
1103
1156
  const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
1104
1157
  const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
1105
- return { total, fixed, resolved, invalid, pending, fixRate, resolveRate };
1158
+ return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };
1106
1159
  }
1107
1160
 
1108
1161
  /**
1109
1162
  * 仅执行删除代码分析模式
1110
1163
  */
1111
1164
  protected async executeDeletionOnly(context: ReviewContext): Promise<ReviewResult> {
1112
- const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } = context;
1165
+ const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } =
1166
+ context;
1113
1167
 
1114
1168
  if (shouldLog(verbose, 1)) {
1115
1169
  console.log(`🗑️ 仅执行删除代码分析模式`);
@@ -1174,7 +1228,7 @@ export class ReviewService {
1174
1228
  if (shouldLog(verbose, 1)) {
1175
1229
  console.log(`💬 提交 PR 评论...`);
1176
1230
  }
1177
- await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
1231
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
1178
1232
  if (shouldLog(verbose, 1)) {
1179
1233
  console.log(`✅ 评论已提交`);
1180
1234
  }
@@ -2063,6 +2117,7 @@ ${fileChanges || "无"}`;
2063
2117
  prNumber: number,
2064
2118
  result: ReviewResult,
2065
2119
  verbose?: VerboseLevel,
2120
+ autoApprove?: boolean,
2066
2121
  ): Promise<void> {
2067
2122
  // 获取配置
2068
2123
  const reviewConf = this.config.getPluginConfig<ReviewConfig>("review");
@@ -2137,6 +2192,7 @@ ${fileChanges || "无"}`;
2137
2192
  }
2138
2193
 
2139
2194
  // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
2195
+ // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
2140
2196
  let lineIssues: ReviewIssue[] = [];
2141
2197
  let comments: CreatePullReviewComment[] = [];
2142
2198
  if (reviewConf.lineComments) {
@@ -2151,23 +2207,47 @@ ${fileChanges || "无"}`;
2151
2207
  .map((issue) => this.issueToReviewComment(issue))
2152
2208
  .filter((comment): comment is CreatePullReviewComment => comment !== null);
2153
2209
  }
2210
+
2211
+ // 计算是否需要自动批准
2212
+ // 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
2213
+ const stats = this.calculateIssueStats(result.issues);
2214
+ const shouldAutoApprove = autoApprove && stats.pending === 0;
2215
+
2154
2216
  if (reviewConf.lineComments) {
2155
- const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
2217
+ const lineReviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
2218
+
2219
+ // 如果需要自动批准,追加批准信息到 body
2220
+ const finalReviewBody = shouldAutoApprove
2221
+ ? lineReviewBody +
2222
+ `\n\n---\n\n✅ **自动批准合并**\n\n${
2223
+ stats.validTotal > 0
2224
+ ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
2225
+ : "代码审查通过,未发现问题,"
2226
+ }自动批准此 PR。`
2227
+ : lineReviewBody;
2228
+
2229
+ const reviewEvent = shouldAutoApprove ? REVIEW_STATE.APPROVE : REVIEW_STATE.COMMENT;
2230
+
2156
2231
  if (comments.length > 0) {
2157
2232
  try {
2158
2233
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2159
- event: REVIEW_STATE.COMMENT,
2160
- body: reviewBody,
2234
+ event: reviewEvent,
2235
+ body: finalReviewBody,
2161
2236
  comments,
2162
2237
  commit_id: commitId,
2163
2238
  });
2164
- console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2239
+ if (shouldAutoApprove) {
2240
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2241
+ } else {
2242
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2243
+ }
2165
2244
  } catch {
2166
2245
  // 批量失败时逐条发布,跳过无法定位的评论
2167
2246
  console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
2168
2247
  let successCount = 0;
2169
2248
  for (const comment of comments) {
2170
2249
  try {
2250
+ // 逐条发布时只用 COMMENT event,避免重复 APPROVE
2171
2251
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2172
2252
  event: REVIEW_STATE.COMMENT,
2173
2253
  body: successCount === 0 ? reviewBody : undefined,
@@ -2181,6 +2261,23 @@ ${fileChanges || "无"}`;
2181
2261
  }
2182
2262
  if (successCount > 0) {
2183
2263
  console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
2264
+ // 如果需要自动批准,单独发一个 APPROVE review
2265
+ if (shouldAutoApprove) {
2266
+ try {
2267
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
2268
+ event: REVIEW_STATE.APPROVE,
2269
+ body: `✅ **自动批准合并**\n\n${
2270
+ stats.validTotal > 0
2271
+ ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
2272
+ : "代码审查通过,未发现问题,"
2273
+ }自动批准此 PR。`,
2274
+ commit_id: commitId,
2275
+ });
2276
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2277
+ } catch (error) {
2278
+ console.warn("⚠️ 自动批准失败:", error);
2279
+ }
2280
+ }
2184
2281
  } else {
2185
2282
  console.warn("⚠️ 所有行级评论均无法定位,已跳过");
2186
2283
  }
@@ -2189,16 +2286,36 @@ ${fileChanges || "无"}`;
2189
2286
  // 本轮无新问题,仍发布 Round 状态(含上轮回顾)
2190
2287
  try {
2191
2288
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2192
- event: REVIEW_STATE.COMMENT,
2193
- body: reviewBody,
2289
+ event: reviewEvent,
2290
+ body: finalReviewBody,
2194
2291
  comments: [],
2195
2292
  commit_id: commitId,
2196
2293
  });
2197
- console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
2294
+ if (shouldAutoApprove) {
2295
+ console.log(`✅ 已自动批准 PR #${prNumber}(Round ${result.round},所有问题已解决)`);
2296
+ } else {
2297
+ console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
2298
+ }
2198
2299
  } catch (error) {
2199
2300
  console.warn("⚠️ 发布审查状态失败:", error);
2200
2301
  }
2201
2302
  }
2303
+ } else if (shouldAutoApprove) {
2304
+ // 未启用 lineComments 但需要自动批准
2305
+ try {
2306
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
2307
+ event: REVIEW_STATE.APPROVE,
2308
+ body: `✅ **自动批准合并**\n\n${
2309
+ stats.validTotal > 0
2310
+ ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
2311
+ : "代码审查通过,未发现问题,"
2312
+ }自动批准此 PR。`,
2313
+ commit_id: commitId,
2314
+ });
2315
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2316
+ } catch (error) {
2317
+ console.warn("⚠️ 自动批准失败:", error);
2318
+ }
2202
2319
  }
2203
2320
  }
2204
2321