@spaceflow/review 0.69.0 → 0.70.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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.69.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.68.0...@spaceflow/review@0.69.0) (2026-03-04)
4
+
5
+ ### 代码重构
6
+
7
+ * **review:** 优化问题统计逻辑,区分 fixed 和 resolved 状态 ([c4dda30](https://github.com/Lydanne/spaceflow/commit/c4dda30fed17ce020fcce9af8874dfa89ccca20b))
8
+
9
+ ### 其他修改
10
+
11
+ * **review-summary:** released version 0.36.0 [no ci] ([95c3d5c](https://github.com/Lydanne/spaceflow/commit/95c3d5cbac67c1ffa4c821faaffb476c502fe2c7))
12
+
3
13
  ## [0.68.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.67.0...@spaceflow/review@0.68.0) (2026-03-04)
4
14
 
5
15
  ### 代码重构
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
@@ -2116,7 +2118,7 @@ class ReviewService {
2116
2118
  * @param context 审查上下文,包含 owner、repo、prNumber 等信息
2117
2119
  * @returns 审查结果,包含发现的问题列表和统计信息
2118
2120
  */ async execute(context) {
2119
- const { owner, repo, prNumber, baseRef, headRef, specSources, dryRun, ci, verbose, includes, llmMode, files, commits: filterCommits, deletionOnly, localMode } = context;
2121
+ const { owner, repo, prNumber, baseRef, headRef, specSources, dryRun, ci, verbose, includes, llmMode, files, commits: filterCommits, deletionOnly, localMode, skipDuplicateWorkflow, autoApprove } = context;
2120
2122
  // 直接审查文件模式:指定了 -f 文件且 base=head
2121
2123
  const isDirectFileMode = files && files.length > 0 && baseRef === headRef;
2122
2124
  // 本地模式:审查未提交的代码(可能回退到分支比较)
@@ -2203,6 +2205,37 @@ class ReviewService {
2203
2205
  console.log(` Commits: ${commits.length}`);
2204
2206
  console.log(` Changed files: ${changedFiles.length}`);
2205
2207
  }
2208
+ // 检查是否有其他同名 review workflow 正在运行中(防止同一 PR 重复审查)
2209
+ // 需要显式启用 skipDuplicateWorkflow 配置
2210
+ if (skipDuplicateWorkflow && ci && pr?.head?.sha) {
2211
+ const headSha = pr.head.sha;
2212
+ // 获取当前 PR 编号(从 CI 环境变量)
2213
+ // GitHub: GITHUB_REF = refs/pull/123/merge
2214
+ // Gitea: GITEA_REF = refs/pull/123/head
2215
+ const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
2216
+ const prMatch = ref.match(/refs\/pull\/(\d+)/);
2217
+ const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prNumber;
2218
+ const runningWorkflows = await this.gitProvider.listWorkflowRuns(owner, repo, {
2219
+ status: "in_progress"
2220
+ });
2221
+ // 获取当前 workflow 名称和 run ID
2222
+ const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
2223
+ const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
2224
+ // 只检查同 PR 同名的其他 workflow run(排除当前 run)
2225
+ const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
2226
+ if (duplicateReviewRuns.length > 0) {
2227
+ if (shouldLog(verbose, 1)) {
2228
+ console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
2229
+ }
2230
+ return {
2231
+ success: true,
2232
+ description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
2233
+ issues: [],
2234
+ summary: [],
2235
+ round: 1
2236
+ };
2237
+ }
2238
+ }
2206
2239
  } else if (effectiveBaseRef && effectiveHeadRef) {
2207
2240
  // 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
2208
2241
  if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
@@ -2449,7 +2482,7 @@ class ReviewService {
2449
2482
  await this.postOrUpdateReviewComment(owner, repo, prNumber, {
2450
2483
  ...result,
2451
2484
  issues: allIssues
2452
- }, verbose);
2485
+ }, verbose, autoApprove);
2453
2486
  if (shouldLog(verbose, 1)) {
2454
2487
  console.log(`✅ 评论已提交`);
2455
2488
  }
@@ -2496,7 +2529,7 @@ class ReviewService {
2496
2529
  * 仅收集 review 状态模式(用于 PR 关闭或 --flush 指令)
2497
2530
  * 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
2498
2531
  */ async executeCollectOnly(context) {
2499
- const { owner, repo, prNumber, verbose, ci, dryRun } = context;
2532
+ const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
2500
2533
  if (shouldLog(verbose, 1)) {
2501
2534
  console.log(`📊 仅收集 review 状态模式`);
2502
2535
  }
@@ -2541,7 +2574,7 @@ class ReviewService {
2541
2574
  if (shouldLog(verbose, 1)) {
2542
2575
  console.log(`💬 更新 PR 评论...`);
2543
2576
  }
2544
- await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose);
2577
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose, autoApprove);
2545
2578
  if (shouldLog(verbose, 1)) {
2546
2579
  console.log(`✅ 评论已更新`);
2547
2580
  }
@@ -2621,11 +2654,12 @@ class ReviewService {
2621
2654
  const fixed = validIssue.filter((i)=>i.fixed).length;
2622
2655
  const resolved = validIssue.filter((i)=>i.resolved).length;
2623
2656
  const invalid = total - validTotal;
2624
- const pending = validTotal - fixed;
2657
+ const pending = validTotal - fixed - resolved;
2625
2658
  const fixRate = validTotal > 0 ? Math.round(fixed / validTotal * 100 * 10) / 10 : 0;
2626
2659
  const resolveRate = validTotal > 0 ? Math.round(resolved / validTotal * 100 * 10) / 10 : 0;
2627
2660
  return {
2628
2661
  total,
2662
+ validTotal,
2629
2663
  fixed,
2630
2664
  resolved,
2631
2665
  invalid,
@@ -2637,7 +2671,7 @@ class ReviewService {
2637
2671
  /**
2638
2672
  * 仅执行删除代码分析模式
2639
2673
  */ async executeDeletionOnly(context) {
2640
- const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } = context;
2674
+ const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } = context;
2641
2675
  if (shouldLog(verbose, 1)) {
2642
2676
  console.log(`🗑️ 仅执行删除代码分析模式`);
2643
2677
  }
@@ -2688,7 +2722,7 @@ class ReviewService {
2688
2722
  if (shouldLog(verbose, 1)) {
2689
2723
  console.log(`💬 提交 PR 评论...`);
2690
2724
  }
2691
- await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
2725
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
2692
2726
  if (shouldLog(verbose, 1)) {
2693
2727
  console.log(`✅ 评论已提交`);
2694
2728
  }
@@ -3425,7 +3459,7 @@ ${fileChanges || "无"}`;
3425
3459
  }
3426
3460
  return this.reviewReportService.format(result, format);
3427
3461
  }
3428
- async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose) {
3462
+ async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove) {
3429
3463
  // 获取配置
3430
3464
  const reviewConf = this.config.getPluginConfig("review");
3431
3465
  // 如果配置启用且有 AI 生成的标题,只在第一轮审查时更新 PR 标题
@@ -3490,29 +3524,42 @@ ${fileChanges || "无"}`;
3490
3524
  console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
3491
3525
  }
3492
3526
  // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
3527
+ // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
3493
3528
  let lineIssues = [];
3494
3529
  let comments = [];
3495
3530
  if (reviewConf.lineComments) {
3496
3531
  lineIssues = result.issues.filter((issue)=>issue.round === result.round && !issue.fixed && !issue.resolved && issue.valid !== "false");
3497
3532
  comments = lineIssues.map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
3498
3533
  }
3534
+ // 计算是否需要自动批准
3535
+ // 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
3536
+ const stats = this.calculateIssueStats(result.issues);
3537
+ const shouldAutoApprove = autoApprove && stats.pending === 0;
3499
3538
  if (reviewConf.lineComments) {
3500
- const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
3539
+ const lineReviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
3540
+ // 如果需要自动批准,追加批准信息到 body
3541
+ const finalReviewBody = shouldAutoApprove ? lineReviewBody + `\n\n---\n\n✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。` : lineReviewBody;
3542
+ const reviewEvent = shouldAutoApprove ? REVIEW_STATE.APPROVE : REVIEW_STATE.COMMENT;
3501
3543
  if (comments.length > 0) {
3502
3544
  try {
3503
3545
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3504
- event: REVIEW_STATE.COMMENT,
3505
- body: reviewBody,
3546
+ event: reviewEvent,
3547
+ body: finalReviewBody,
3506
3548
  comments,
3507
3549
  commit_id: commitId
3508
3550
  });
3509
- console.log(`✅ 已发布 ${comments.length} 条行级评论`);
3551
+ if (shouldAutoApprove) {
3552
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3553
+ } else {
3554
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
3555
+ }
3510
3556
  } catch {
3511
3557
  // 批量失败时逐条发布,跳过无法定位的评论
3512
3558
  console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
3513
3559
  let successCount = 0;
3514
3560
  for (const comment of comments){
3515
3561
  try {
3562
+ // 逐条发布时只用 COMMENT event,避免重复 APPROVE
3516
3563
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3517
3564
  event: REVIEW_STATE.COMMENT,
3518
3565
  body: successCount === 0 ? reviewBody : undefined,
@@ -3528,6 +3575,19 @@ ${fileChanges || "无"}`;
3528
3575
  }
3529
3576
  if (successCount > 0) {
3530
3577
  console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
3578
+ // 如果需要自动批准,单独发一个 APPROVE review
3579
+ if (shouldAutoApprove) {
3580
+ try {
3581
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
3582
+ event: REVIEW_STATE.APPROVE,
3583
+ body: `✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。`,
3584
+ commit_id: commitId
3585
+ });
3586
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3587
+ } catch (error) {
3588
+ console.warn("⚠️ 自动批准失败:", error);
3589
+ }
3590
+ }
3531
3591
  } else {
3532
3592
  console.warn("⚠️ 所有行级评论均无法定位,已跳过");
3533
3593
  }
@@ -3536,16 +3596,32 @@ ${fileChanges || "无"}`;
3536
3596
  // 本轮无新问题,仍发布 Round 状态(含上轮回顾)
3537
3597
  try {
3538
3598
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3539
- event: REVIEW_STATE.COMMENT,
3540
- body: reviewBody,
3599
+ event: reviewEvent,
3600
+ body: finalReviewBody,
3541
3601
  comments: [],
3542
3602
  commit_id: commitId
3543
3603
  });
3544
- console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
3604
+ if (shouldAutoApprove) {
3605
+ console.log(`✅ 已自动批准 PR #${prNumber}(Round ${result.round},所有问题已解决)`);
3606
+ } else {
3607
+ console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
3608
+ }
3545
3609
  } catch (error) {
3546
3610
  console.warn("⚠️ 发布审查状态失败:", error);
3547
3611
  }
3548
3612
  }
3613
+ } else if (shouldAutoApprove) {
3614
+ // 未启用 lineComments 但需要自动批准
3615
+ try {
3616
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
3617
+ event: REVIEW_STATE.APPROVE,
3618
+ body: `✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。`,
3619
+ commit_id: commitId
3620
+ });
3621
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3622
+ } catch (error) {
3623
+ console.warn("⚠️ 自动批准失败:", error);
3624
+ }
3549
3625
  }
3550
3626
  }
3551
3627
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.69.0",
3
+ "version": "0.70.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 推导) */
@@ -433,6 +433,8 @@ export class ReviewService {
433
433
  commits: filterCommits,
434
434
  deletionOnly,
435
435
  localMode,
436
+ skipDuplicateWorkflow,
437
+ autoApprove,
436
438
  } = context;
437
439
 
438
440
  // 直接审查文件模式:指定了 -f 文件且 base=head
@@ -532,6 +534,46 @@ export class ReviewService {
532
534
  console.log(` Commits: ${commits.length}`);
533
535
  console.log(` Changed files: ${changedFiles.length}`);
534
536
  }
537
+
538
+ // 检查是否有其他同名 review workflow 正在运行中(防止同一 PR 重复审查)
539
+ // 需要显式启用 skipDuplicateWorkflow 配置
540
+ if (skipDuplicateWorkflow && ci && pr?.head?.sha) {
541
+ const headSha = pr.head.sha;
542
+ // 获取当前 PR 编号(从 CI 环境变量)
543
+ // GitHub: GITHUB_REF = refs/pull/123/merge
544
+ // Gitea: GITEA_REF = refs/pull/123/head
545
+ const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
546
+ const prMatch = ref.match(/refs\/pull\/(\d+)/);
547
+ const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prNumber;
548
+
549
+ const runningWorkflows = await this.gitProvider.listWorkflowRuns(owner, repo, {
550
+ status: "in_progress",
551
+ });
552
+ // 获取当前 workflow 名称和 run ID
553
+ const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
554
+ const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
555
+ // 只检查同 PR 同名的其他 workflow run(排除当前 run)
556
+ const duplicateReviewRuns = runningWorkflows.filter(
557
+ (w) =>
558
+ w.sha === headSha &&
559
+ w.name === currentWorkflowName &&
560
+ (!currentRunId || String(w.id) !== currentRunId),
561
+ );
562
+ if (duplicateReviewRuns.length > 0) {
563
+ if (shouldLog(verbose, 1)) {
564
+ console.log(
565
+ `⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`,
566
+ );
567
+ }
568
+ return {
569
+ success: true,
570
+ description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
571
+ issues: [],
572
+ summary: [],
573
+ round: 1,
574
+ };
575
+ }
576
+ }
535
577
  } else if (effectiveBaseRef && effectiveHeadRef) {
536
578
  // 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
537
579
  if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
@@ -856,6 +898,7 @@ export class ReviewService {
856
898
  issues: allIssues,
857
899
  },
858
900
  verbose,
901
+ autoApprove,
859
902
  );
860
903
  if (shouldLog(verbose, 1)) {
861
904
  console.log(`✅ 评论已提交`);
@@ -916,7 +959,7 @@ export class ReviewService {
916
959
  * 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
917
960
  */
918
961
  protected async executeCollectOnly(context: ReviewContext): Promise<ReviewResult> {
919
- const { owner, repo, prNumber, verbose, ci, dryRun } = context;
962
+ const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
920
963
 
921
964
  if (shouldLog(verbose, 1)) {
922
965
  console.log(`📊 仅收集 review 状态模式`);
@@ -982,7 +1025,14 @@ export class ReviewService {
982
1025
  if (shouldLog(verbose, 1)) {
983
1026
  console.log(`💬 更新 PR 评论...`);
984
1027
  }
985
- await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose);
1028
+ await this.postOrUpdateReviewComment(
1029
+ owner,
1030
+ repo,
1031
+ prNumber,
1032
+ existingResult,
1033
+ verbose,
1034
+ autoApprove,
1035
+ );
986
1036
  if (shouldLog(verbose, 1)) {
987
1037
  console.log(`✅ 评论已更新`);
988
1038
  }
@@ -1099,17 +1149,18 @@ export class ReviewService {
1099
1149
  const fixed = validIssue.filter((i) => i.fixed).length;
1100
1150
  const resolved = validIssue.filter((i) => i.resolved).length;
1101
1151
  const invalid = total - validTotal;
1102
- const pending = validTotal - fixed;
1152
+ const pending = validTotal - fixed - resolved;
1103
1153
  const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
1104
1154
  const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
1105
- return { total, fixed, resolved, invalid, pending, fixRate, resolveRate };
1155
+ return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };
1106
1156
  }
1107
1157
 
1108
1158
  /**
1109
1159
  * 仅执行删除代码分析模式
1110
1160
  */
1111
1161
  protected async executeDeletionOnly(context: ReviewContext): Promise<ReviewResult> {
1112
- const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } = context;
1162
+ const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } =
1163
+ context;
1113
1164
 
1114
1165
  if (shouldLog(verbose, 1)) {
1115
1166
  console.log(`🗑️ 仅执行删除代码分析模式`);
@@ -1174,7 +1225,7 @@ export class ReviewService {
1174
1225
  if (shouldLog(verbose, 1)) {
1175
1226
  console.log(`💬 提交 PR 评论...`);
1176
1227
  }
1177
- await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
1228
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
1178
1229
  if (shouldLog(verbose, 1)) {
1179
1230
  console.log(`✅ 评论已提交`);
1180
1231
  }
@@ -2063,6 +2114,7 @@ ${fileChanges || "无"}`;
2063
2114
  prNumber: number,
2064
2115
  result: ReviewResult,
2065
2116
  verbose?: VerboseLevel,
2117
+ autoApprove?: boolean,
2066
2118
  ): Promise<void> {
2067
2119
  // 获取配置
2068
2120
  const reviewConf = this.config.getPluginConfig<ReviewConfig>("review");
@@ -2137,6 +2189,7 @@ ${fileChanges || "无"}`;
2137
2189
  }
2138
2190
 
2139
2191
  // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
2192
+ // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
2140
2193
  let lineIssues: ReviewIssue[] = [];
2141
2194
  let comments: CreatePullReviewComment[] = [];
2142
2195
  if (reviewConf.lineComments) {
@@ -2151,23 +2204,47 @@ ${fileChanges || "无"}`;
2151
2204
  .map((issue) => this.issueToReviewComment(issue))
2152
2205
  .filter((comment): comment is CreatePullReviewComment => comment !== null);
2153
2206
  }
2207
+
2208
+ // 计算是否需要自动批准
2209
+ // 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
2210
+ const stats = this.calculateIssueStats(result.issues);
2211
+ const shouldAutoApprove = autoApprove && stats.pending === 0;
2212
+
2154
2213
  if (reviewConf.lineComments) {
2155
- const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
2214
+ const lineReviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
2215
+
2216
+ // 如果需要自动批准,追加批准信息到 body
2217
+ const finalReviewBody = shouldAutoApprove
2218
+ ? lineReviewBody +
2219
+ `\n\n---\n\n✅ **自动批准合并**\n\n${
2220
+ stats.validTotal > 0
2221
+ ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
2222
+ : "代码审查通过,未发现问题,"
2223
+ }自动批准此 PR。`
2224
+ : lineReviewBody;
2225
+
2226
+ const reviewEvent = shouldAutoApprove ? REVIEW_STATE.APPROVE : REVIEW_STATE.COMMENT;
2227
+
2156
2228
  if (comments.length > 0) {
2157
2229
  try {
2158
2230
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2159
- event: REVIEW_STATE.COMMENT,
2160
- body: reviewBody,
2231
+ event: reviewEvent,
2232
+ body: finalReviewBody,
2161
2233
  comments,
2162
2234
  commit_id: commitId,
2163
2235
  });
2164
- console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2236
+ if (shouldAutoApprove) {
2237
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2238
+ } else {
2239
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2240
+ }
2165
2241
  } catch {
2166
2242
  // 批量失败时逐条发布,跳过无法定位的评论
2167
2243
  console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
2168
2244
  let successCount = 0;
2169
2245
  for (const comment of comments) {
2170
2246
  try {
2247
+ // 逐条发布时只用 COMMENT event,避免重复 APPROVE
2171
2248
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2172
2249
  event: REVIEW_STATE.COMMENT,
2173
2250
  body: successCount === 0 ? reviewBody : undefined,
@@ -2181,6 +2258,23 @@ ${fileChanges || "无"}`;
2181
2258
  }
2182
2259
  if (successCount > 0) {
2183
2260
  console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
2261
+ // 如果需要自动批准,单独发一个 APPROVE review
2262
+ if (shouldAutoApprove) {
2263
+ try {
2264
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
2265
+ event: REVIEW_STATE.APPROVE,
2266
+ body: `✅ **自动批准合并**\n\n${
2267
+ stats.validTotal > 0
2268
+ ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
2269
+ : "代码审查通过,未发现问题,"
2270
+ }自动批准此 PR。`,
2271
+ commit_id: commitId,
2272
+ });
2273
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2274
+ } catch (error) {
2275
+ console.warn("⚠️ 自动批准失败:", error);
2276
+ }
2277
+ }
2184
2278
  } else {
2185
2279
  console.warn("⚠️ 所有行级评论均无法定位,已跳过");
2186
2280
  }
@@ -2189,16 +2283,36 @@ ${fileChanges || "无"}`;
2189
2283
  // 本轮无新问题,仍发布 Round 状态(含上轮回顾)
2190
2284
  try {
2191
2285
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2192
- event: REVIEW_STATE.COMMENT,
2193
- body: reviewBody,
2286
+ event: reviewEvent,
2287
+ body: finalReviewBody,
2194
2288
  comments: [],
2195
2289
  commit_id: commitId,
2196
2290
  });
2197
- console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
2291
+ if (shouldAutoApprove) {
2292
+ console.log(`✅ 已自动批准 PR #${prNumber}(Round ${result.round},所有问题已解决)`);
2293
+ } else {
2294
+ console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
2295
+ }
2198
2296
  } catch (error) {
2199
2297
  console.warn("⚠️ 发布审查状态失败:", error);
2200
2298
  }
2201
2299
  }
2300
+ } else if (shouldAutoApprove) {
2301
+ // 未启用 lineComments 但需要自动批准
2302
+ try {
2303
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
2304
+ event: REVIEW_STATE.APPROVE,
2305
+ body: `✅ **自动批准合并**\n\n${
2306
+ stats.validTotal > 0
2307
+ ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
2308
+ : "代码审查通过,未发现问题,"
2309
+ }自动批准此 PR。`,
2310
+ commit_id: commitId,
2311
+ });
2312
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2313
+ } catch (error) {
2314
+ console.warn("⚠️ 自动批准失败:", error);
2315
+ }
2202
2316
  }
2203
2317
  }
2204
2318