@spaceflow/review 0.68.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,27 @@
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
+
13
+ ## [0.68.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.67.0...@spaceflow/review@0.68.0) (2026-03-04)
14
+
15
+ ### 代码重构
16
+
17
+ * **review:** 区分 ☹️ 和 👎 reaction 的语义,☹️ 标记无效,👎 标记未解决 ([f1419fe](https://github.com/Lydanne/spaceflow/commit/f1419fe47448a80f373ffac082ac3a2e9320d200))
18
+
19
+ ### 其他修改
20
+
21
+ * **review-summary:** released version 0.35.0 [no ci] ([4f2607d](https://github.com/Lydanne/spaceflow/commit/4f2607def2725946f32eccc4aa4e687a3cdd9bab))
22
+ * **scripts:** released version 0.28.0 [no ci] ([55db5cf](https://github.com/Lydanne/spaceflow/commit/55db5cfa1dc0a1e318085caa0cfd9f91b06dcb21))
23
+ * **shell:** released version 0.28.0 [no ci] ([01f180f](https://github.com/Lydanne/spaceflow/commit/01f180f2508e75524a33e66fea580a738adc689f))
24
+
3
25
  ## [0.67.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.66.0...@spaceflow/review@0.67.0) (2026-03-03)
4
26
 
5
27
  ### 新特性
package/dist/index.js CHANGED
@@ -1169,24 +1169,28 @@ class MarkdownFormatter {
1169
1169
  if (summaries.length === 0) {
1170
1170
  return "没有需要审查的文件";
1171
1171
  }
1172
- // 🟢 已修复 | 🔴 待处理error | 🟡 待处理warn | ⚪ 已解决(非代码修复)
1172
+ // 🟢 已修复 | 🔴 error数量 | 🟡 warn数量 | ⚪ 已解决(非代码修复)
1173
1173
  const issuesByFile = new Map();
1174
1174
  for (const issue of issues){
1175
1175
  if (issue.valid === "false") continue;
1176
1176
  const stats = issuesByFile.get(issue.file) || {
1177
+ total: 0,
1177
1178
  fixed: 0,
1178
- pendingErrors: 0,
1179
- pendingWarns: 0,
1179
+ errorCount: 0,
1180
+ warnCount: 0,
1180
1181
  resolved: 0
1181
1182
  };
1183
+ stats.total++;
1182
1184
  if (issue.fixed) {
1183
1185
  stats.fixed++;
1184
- } else if (issue.resolved) {
1186
+ }
1187
+ if (issue.resolved) {
1185
1188
  stats.resolved++;
1186
- } else if (issue.severity === "error") {
1187
- stats.pendingErrors++;
1189
+ }
1190
+ if (issue.severity === "error") {
1191
+ stats.errorCount++;
1188
1192
  } else {
1189
- stats.pendingWarns++;
1193
+ stats.warnCount++;
1190
1194
  }
1191
1195
  issuesByFile.set(issue.file, stats);
1192
1196
  }
@@ -1202,18 +1206,18 @@ class MarkdownFormatter {
1202
1206
  const fileSummaryLines = [];
1203
1207
  for (const fileSummary of summaries){
1204
1208
  const stats = issuesByFile.get(fileSummary.file) || {
1209
+ total: 0,
1205
1210
  fixed: 0,
1206
- pendingErrors: 0,
1207
- pendingWarns: 0,
1211
+ errorCount: 0,
1212
+ warnCount: 0,
1208
1213
  resolved: 0
1209
1214
  };
1210
- const fileTotal = stats.fixed + stats.pendingErrors + stats.pendingWarns + stats.resolved;
1211
- totalAll += fileTotal;
1215
+ totalAll += stats.total;
1212
1216
  totalFixed += stats.fixed;
1213
- totalPendingErrors += stats.pendingErrors;
1214
- totalPendingWarns += stats.pendingWarns;
1217
+ totalPendingErrors += stats.errorCount;
1218
+ totalPendingWarns += stats.warnCount;
1215
1219
  totalResolved += stats.resolved;
1216
- lines.push(`| \`${fileSummary.file}\` | ${fileTotal} | ${stats.fixed} | ${stats.pendingErrors} | ${stats.pendingWarns} | ${stats.resolved} |`);
1220
+ lines.push(`| \`${fileSummary.file}\` | ${stats.total} | ${stats.fixed} | ${stats.errorCount} | ${stats.warnCount} | ${stats.resolved} |`);
1217
1221
  // 收集问题总结用于折叠块展示
1218
1222
  if (fileSummary.summary.trim()) {
1219
1223
  fileSummaryLines.push(`### 💡 \`${fileSummary.file}\``);
@@ -1386,19 +1390,23 @@ class TerminalFormatter {
1386
1390
  for (const issue of issues){
1387
1391
  if (issue.valid === "false") continue;
1388
1392
  const stats = issuesByFile.get(issue.file) || {
1393
+ total: 0,
1389
1394
  fixed: 0,
1390
- pendingErrors: 0,
1391
- pendingWarns: 0,
1395
+ errorCount: 0,
1396
+ warnCount: 0,
1392
1397
  resolved: 0
1393
1398
  };
1399
+ stats.total++;
1394
1400
  if (issue.fixed) {
1395
1401
  stats.fixed++;
1396
- } else if (issue.resolved) {
1402
+ }
1403
+ if (issue.resolved) {
1397
1404
  stats.resolved++;
1398
- } else if (issue.severity === "error") {
1399
- stats.pendingErrors++;
1405
+ }
1406
+ if (issue.severity === "error") {
1407
+ stats.errorCount++;
1400
1408
  } else {
1401
- stats.pendingWarns++;
1409
+ stats.warnCount++;
1402
1410
  }
1403
1411
  issuesByFile.set(issue.file, stats);
1404
1412
  }
@@ -1411,21 +1419,21 @@ class TerminalFormatter {
1411
1419
  const lines = [];
1412
1420
  for (const fileSummary of summaries){
1413
1421
  const stats = issuesByFile.get(fileSummary.file) || {
1422
+ total: 0,
1414
1423
  fixed: 0,
1415
- pendingErrors: 0,
1416
- pendingWarns: 0,
1424
+ errorCount: 0,
1425
+ warnCount: 0,
1417
1426
  resolved: 0
1418
1427
  };
1419
- const fileTotal = stats.fixed + stats.pendingErrors + stats.pendingWarns + stats.resolved;
1420
- totalAll += fileTotal;
1428
+ totalAll += stats.total;
1421
1429
  totalFixed += stats.fixed;
1422
- totalPendingErrors += stats.pendingErrors;
1423
- totalPendingWarns += stats.pendingWarns;
1430
+ totalPendingErrors += stats.errorCount;
1431
+ totalPendingWarns += stats.warnCount;
1424
1432
  totalResolved += stats.resolved;
1425
- const totalText = fileTotal > 0 ? `${BOLD}${fileTotal} 问题${RESET}` : "";
1433
+ const totalText = stats.total > 0 ? `${BOLD}${stats.total} 问题${RESET}` : "";
1426
1434
  const fixedText = stats.fixed > 0 ? `${GREEN}🟢 ${stats.fixed} 已修复${RESET}` : "";
1427
- const errorText = stats.pendingErrors > 0 ? `${RED}🔴 ${stats.pendingErrors} error${RESET}` : "";
1428
- const warnText = stats.pendingWarns > 0 ? `${YELLOW}🟡 ${stats.pendingWarns} warn${RESET}` : "";
1435
+ const errorText = stats.errorCount > 0 ? `${RED}🔴 ${stats.errorCount} error${RESET}` : "";
1436
+ const warnText = stats.warnCount > 0 ? `${YELLOW}🟡 ${stats.warnCount} warn${RESET}` : "";
1429
1437
  const resolvedText = stats.resolved > 0 ? `⚪ ${stats.resolved} 已解决` : "";
1430
1438
  const statsText = [
1431
1439
  totalText,
@@ -1622,7 +1630,9 @@ class ReviewReportService {
1622
1630
  timeout: z.number().optional(),
1623
1631
  retries: z.number().default(0).optional(),
1624
1632
  retryDelay: z.number().default(1000).optional(),
1625
- 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()
1626
1636
  });
1627
1637
 
1628
1638
  ;// CONCATENATED MODULE: ./src/parse-title-options.ts
@@ -2108,7 +2118,7 @@ class ReviewService {
2108
2118
  * @param context 审查上下文,包含 owner、repo、prNumber 等信息
2109
2119
  * @returns 审查结果,包含发现的问题列表和统计信息
2110
2120
  */ async execute(context) {
2111
- 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;
2112
2122
  // 直接审查文件模式:指定了 -f 文件且 base=head
2113
2123
  const isDirectFileMode = files && files.length > 0 && baseRef === headRef;
2114
2124
  // 本地模式:审查未提交的代码(可能回退到分支比较)
@@ -2195,6 +2205,37 @@ class ReviewService {
2195
2205
  console.log(` Commits: ${commits.length}`);
2196
2206
  console.log(` Changed files: ${changedFiles.length}`);
2197
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
+ }
2198
2239
  } else if (effectiveBaseRef && effectiveHeadRef) {
2199
2240
  // 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
2200
2241
  if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
@@ -2441,7 +2482,7 @@ class ReviewService {
2441
2482
  await this.postOrUpdateReviewComment(owner, repo, prNumber, {
2442
2483
  ...result,
2443
2484
  issues: allIssues
2444
- }, verbose);
2485
+ }, verbose, autoApprove);
2445
2486
  if (shouldLog(verbose, 1)) {
2446
2487
  console.log(`✅ 评论已提交`);
2447
2488
  }
@@ -2488,7 +2529,7 @@ class ReviewService {
2488
2529
  * 仅收集 review 状态模式(用于 PR 关闭或 --flush 指令)
2489
2530
  * 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
2490
2531
  */ async executeCollectOnly(context) {
2491
- const { owner, repo, prNumber, verbose, ci, dryRun } = context;
2532
+ const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
2492
2533
  if (shouldLog(verbose, 1)) {
2493
2534
  console.log(`📊 仅收集 review 状态模式`);
2494
2535
  }
@@ -2533,7 +2574,7 @@ class ReviewService {
2533
2574
  if (shouldLog(verbose, 1)) {
2534
2575
  console.log(`💬 更新 PR 评论...`);
2535
2576
  }
2536
- await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose);
2577
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose, autoApprove);
2537
2578
  if (shouldLog(verbose, 1)) {
2538
2579
  console.log(`✅ 评论已更新`);
2539
2580
  }
@@ -2602,20 +2643,23 @@ class ReviewService {
2602
2643
  specs = await this.loadSpecs(specSources, verbose);
2603
2644
  fileContents = await this.getFileContents(owner, repo, changedFiles, commits, headSha, prNumber, verbose);
2604
2645
  }
2605
- return this.issueVerifyService.verifyIssueFixes(issues, fileContents, specs, llmMode, verbose, context.verifyConcurrency);
2646
+ return await this.issueVerifyService.verifyIssueFixes(issues, fileContents, specs, llmMode, verbose, context.verifyConcurrency);
2606
2647
  }
2607
2648
  /**
2608
2649
  * 计算问题状态统计
2609
2650
  */ calculateIssueStats(issues) {
2610
2651
  const total = issues.length;
2611
- const fixed = issues.filter((i)=>i.fixed).length;
2612
- const resolved = issues.filter((i)=>i.resolved && !i.fixed).length;
2613
- const invalid = issues.filter((i)=>i.valid === "false" && !i.fixed && !i.resolved).length;
2614
- const pending = total - fixed - resolved - invalid;
2615
- const fixRate = total > 0 ? Math.round(fixed / total * 100 * 10) / 10 : 0;
2616
- const resolveRate = total > 0 ? Math.round((fixed + resolved) / total * 100 * 10) / 10 : 0;
2652
+ const validIssue = issues.filter((i)=>i.valid !== "false");
2653
+ const validTotal = validIssue.length;
2654
+ const fixed = validIssue.filter((i)=>i.fixed).length;
2655
+ const resolved = validIssue.filter((i)=>i.resolved).length;
2656
+ const invalid = total - validTotal;
2657
+ const pending = validTotal - fixed - resolved;
2658
+ const fixRate = validTotal > 0 ? Math.round(fixed / validTotal * 100 * 10) / 10 : 0;
2659
+ const resolveRate = validTotal > 0 ? Math.round(resolved / validTotal * 100 * 10) / 10 : 0;
2617
2660
  return {
2618
2661
  total,
2662
+ validTotal,
2619
2663
  fixed,
2620
2664
  resolved,
2621
2665
  invalid,
@@ -2627,7 +2671,7 @@ class ReviewService {
2627
2671
  /**
2628
2672
  * 仅执行删除代码分析模式
2629
2673
  */ async executeDeletionOnly(context) {
2630
- const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } = context;
2674
+ const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } = context;
2631
2675
  if (shouldLog(verbose, 1)) {
2632
2676
  console.log(`🗑️ 仅执行删除代码分析模式`);
2633
2677
  }
@@ -2678,7 +2722,7 @@ class ReviewService {
2678
2722
  if (shouldLog(verbose, 1)) {
2679
2723
  console.log(`💬 提交 PR 评论...`);
2680
2724
  }
2681
- await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
2725
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
2682
2726
  if (shouldLog(verbose, 1)) {
2683
2727
  console.log(`✅ 评论已提交`);
2684
2728
  }
@@ -3415,7 +3459,7 @@ ${fileChanges || "无"}`;
3415
3459
  }
3416
3460
  return this.reviewReportService.format(result, format);
3417
3461
  }
3418
- async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose) {
3462
+ async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove) {
3419
3463
  // 获取配置
3420
3464
  const reviewConf = this.config.getPluginConfig("review");
3421
3465
  // 如果配置启用且有 AI 生成的标题,只在第一轮审查时更新 PR 标题
@@ -3480,29 +3524,42 @@ ${fileChanges || "无"}`;
3480
3524
  console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
3481
3525
  }
3482
3526
  // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
3527
+ // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
3483
3528
  let lineIssues = [];
3484
3529
  let comments = [];
3485
3530
  if (reviewConf.lineComments) {
3486
3531
  lineIssues = result.issues.filter((issue)=>issue.round === result.round && !issue.fixed && !issue.resolved && issue.valid !== "false");
3487
3532
  comments = lineIssues.map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
3488
3533
  }
3534
+ // 计算是否需要自动批准
3535
+ // 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
3536
+ const stats = this.calculateIssueStats(result.issues);
3537
+ const shouldAutoApprove = autoApprove && stats.pending === 0;
3489
3538
  if (reviewConf.lineComments) {
3490
- 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;
3491
3543
  if (comments.length > 0) {
3492
3544
  try {
3493
3545
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3494
- event: REVIEW_STATE.COMMENT,
3495
- body: reviewBody,
3546
+ event: reviewEvent,
3547
+ body: finalReviewBody,
3496
3548
  comments,
3497
3549
  commit_id: commitId
3498
3550
  });
3499
- console.log(`✅ 已发布 ${comments.length} 条行级评论`);
3551
+ if (shouldAutoApprove) {
3552
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
3553
+ } else {
3554
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
3555
+ }
3500
3556
  } catch {
3501
3557
  // 批量失败时逐条发布,跳过无法定位的评论
3502
3558
  console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
3503
3559
  let successCount = 0;
3504
3560
  for (const comment of comments){
3505
3561
  try {
3562
+ // 逐条发布时只用 COMMENT event,避免重复 APPROVE
3506
3563
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3507
3564
  event: REVIEW_STATE.COMMENT,
3508
3565
  body: successCount === 0 ? reviewBody : undefined,
@@ -3518,6 +3575,19 @@ ${fileChanges || "无"}`;
3518
3575
  }
3519
3576
  if (successCount > 0) {
3520
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
+ }
3521
3591
  } else {
3522
3592
  console.warn("⚠️ 所有行级评论均无法定位,已跳过");
3523
3593
  }
@@ -3526,16 +3596,32 @@ ${fileChanges || "无"}`;
3526
3596
  // 本轮无新问题,仍发布 Round 状态(含上轮回顾)
3527
3597
  try {
3528
3598
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
3529
- event: REVIEW_STATE.COMMENT,
3530
- body: reviewBody,
3599
+ event: reviewEvent,
3600
+ body: finalReviewBody,
3531
3601
  comments: [],
3532
3602
  commit_id: commitId
3533
3603
  });
3534
- 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
+ }
3535
3609
  } catch (error) {
3536
3610
  console.warn("⚠️ 发布审查状态失败:", error);
3537
3611
  }
3538
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
+ }
3539
3625
  }
3540
3626
  }
3541
3627
  /**
@@ -4314,6 +4400,7 @@ class IssueVerifyService {
4314
4400
  }
4315
4401
  verifiedIssues.push({
4316
4402
  ...issue,
4403
+ resolved: new Date().toISOString(),
4317
4404
  fixed: new Date().toISOString(),
4318
4405
  valid: FALSE,
4319
4406
  reason: "文件已删除"
@@ -4397,6 +4484,7 @@ class IssueVerifyService {
4397
4484
  console.log(` ✅ 已修复: ${result.reason}`);
4398
4485
  }
4399
4486
  updatedIssue.fixed = new Date().toISOString();
4487
+ updatedIssue.resolved = new Date().toISOString();
4400
4488
  } else if (!result.valid) {
4401
4489
  if (shouldLog(verbose, 1)) {
4402
4490
  console.log(` ❌ 无效问题: ${result.reason}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.68.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",
@@ -107,6 +107,7 @@ export class IssueVerifyService {
107
107
  }
108
108
  verifiedIssues.push({
109
109
  ...issue,
110
+ resolved: new Date().toISOString(),
110
111
  fixed: new Date().toISOString(),
111
112
  valid: FALSE,
112
113
  reason: "文件已删除",
@@ -209,6 +210,7 @@ export class IssueVerifyService {
209
210
  console.log(` ✅ 已修复: ${result.reason}`);
210
211
  }
211
212
  updatedIssue.fixed = new Date().toISOString();
213
+ updatedIssue.resolved = new Date().toISOString();
212
214
  } else if (!result.valid) {
213
215
  if (shouldLog(verbose, 1)) {
214
216
  console.log(` ❌ 无效问题: ${result.reason}`);
@@ -130,27 +130,37 @@ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportPar
130
130
  return "没有需要审查的文件";
131
131
  }
132
132
 
133
- // 🟢 已修复 | 🔴 待处理error | 🟡 待处理warn | ⚪ 已解决(非代码修复)
133
+ // 🟢 已修复 | 🔴 error数量 | 🟡 warn数量 | ⚪ 已解决(非代码修复)
134
134
  const issuesByFile = new Map<
135
135
  string,
136
- { fixed: number; pendingErrors: number; pendingWarns: number; resolved: number }
136
+ {
137
+ fixed: number;
138
+ errorCount: number;
139
+ warnCount: number;
140
+ resolved: number;
141
+ total: number;
142
+ }
137
143
  >();
138
144
  for (const issue of issues) {
139
145
  if (issue.valid === "false") continue;
140
146
  const stats = issuesByFile.get(issue.file) || {
147
+ total: 0,
141
148
  fixed: 0,
142
- pendingErrors: 0,
143
- pendingWarns: 0,
149
+ errorCount: 0,
150
+ warnCount: 0,
144
151
  resolved: 0,
145
152
  };
153
+ stats.total++;
146
154
  if (issue.fixed) {
147
155
  stats.fixed++;
148
- } else if (issue.resolved) {
156
+ }
157
+ if (issue.resolved) {
149
158
  stats.resolved++;
150
- } else if (issue.severity === "error") {
151
- stats.pendingErrors++;
159
+ }
160
+ if (issue.severity === "error") {
161
+ stats.errorCount++;
152
162
  } else {
153
- stats.pendingWarns++;
163
+ stats.warnCount++;
154
164
  }
155
165
  issuesByFile.set(issue.file, stats);
156
166
  }
@@ -169,20 +179,20 @@ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportPar
169
179
  const fileSummaryLines: string[] = [];
170
180
  for (const fileSummary of summaries) {
171
181
  const stats = issuesByFile.get(fileSummary.file) || {
182
+ total: 0,
172
183
  fixed: 0,
173
- pendingErrors: 0,
174
- pendingWarns: 0,
184
+ errorCount: 0,
185
+ warnCount: 0,
175
186
  resolved: 0,
176
187
  };
177
- const fileTotal = stats.fixed + stats.pendingErrors + stats.pendingWarns + stats.resolved;
178
- totalAll += fileTotal;
188
+ totalAll += stats.total;
179
189
  totalFixed += stats.fixed;
180
- totalPendingErrors += stats.pendingErrors;
181
- totalPendingWarns += stats.pendingWarns;
190
+ totalPendingErrors += stats.errorCount;
191
+ totalPendingWarns += stats.warnCount;
182
192
  totalResolved += stats.resolved;
183
193
 
184
194
  lines.push(
185
- `| \`${fileSummary.file}\` | ${fileTotal} | ${stats.fixed} | ${stats.pendingErrors} | ${stats.pendingWarns} | ${stats.resolved} |`,
195
+ `| \`${fileSummary.file}\` | ${stats.total} | ${stats.fixed} | ${stats.errorCount} | ${stats.warnCount} | ${stats.resolved} |`,
186
196
  );
187
197
 
188
198
  // 收集问题总结用于折叠块展示
@@ -31,24 +31,34 @@ export class TerminalFormatter implements ReviewReportFormatter {
31
31
  // 🟢 已修复 | 🔴 待处理error | 🟡 待处理warn | ⚪ 已解决(非代码修复)
32
32
  const issuesByFile = new Map<
33
33
  string,
34
- { fixed: number; pendingErrors: number; pendingWarns: number; resolved: number }
34
+ {
35
+ fixed: number;
36
+ errorCount: number;
37
+ warnCount: number;
38
+ resolved: number;
39
+ total: number;
40
+ }
35
41
  >();
36
42
  for (const issue of issues) {
37
43
  if (issue.valid === "false") continue;
38
44
  const stats = issuesByFile.get(issue.file) || {
45
+ total: 0,
39
46
  fixed: 0,
40
- pendingErrors: 0,
41
- pendingWarns: 0,
47
+ errorCount: 0,
48
+ warnCount: 0,
42
49
  resolved: 0,
43
50
  };
51
+ stats.total++;
44
52
  if (issue.fixed) {
45
53
  stats.fixed++;
46
- } else if (issue.resolved) {
54
+ }
55
+ if (issue.resolved) {
47
56
  stats.resolved++;
48
- } else if (issue.severity === "error") {
49
- stats.pendingErrors++;
57
+ }
58
+ if (issue.severity === "error") {
59
+ stats.errorCount++;
50
60
  } else {
51
- stats.pendingWarns++;
61
+ stats.warnCount++;
52
62
  }
53
63
  issuesByFile.set(issue.file, stats);
54
64
  }
@@ -63,24 +73,22 @@ export class TerminalFormatter implements ReviewReportFormatter {
63
73
  const lines: string[] = [];
64
74
  for (const fileSummary of summaries) {
65
75
  const stats = issuesByFile.get(fileSummary.file) || {
76
+ total: 0,
66
77
  fixed: 0,
67
- pendingErrors: 0,
68
- pendingWarns: 0,
78
+ errorCount: 0,
79
+ warnCount: 0,
69
80
  resolved: 0,
70
81
  };
71
- const fileTotal = stats.fixed + stats.pendingErrors + stats.pendingWarns + stats.resolved;
72
- totalAll += fileTotal;
82
+ totalAll += stats.total;
73
83
  totalFixed += stats.fixed;
74
- totalPendingErrors += stats.pendingErrors;
75
- totalPendingWarns += stats.pendingWarns;
84
+ totalPendingErrors += stats.errorCount;
85
+ totalPendingWarns += stats.warnCount;
76
86
  totalResolved += stats.resolved;
77
87
 
78
- const totalText = fileTotal > 0 ? `${BOLD}${fileTotal} 问题${RESET}` : "";
88
+ const totalText = stats.total > 0 ? `${BOLD}${stats.total} 问题${RESET}` : "";
79
89
  const fixedText = stats.fixed > 0 ? `${GREEN}🟢 ${stats.fixed} 已修复${RESET}` : "";
80
- const errorText =
81
- stats.pendingErrors > 0 ? `${RED}🔴 ${stats.pendingErrors} error${RESET}` : "";
82
- const warnText =
83
- stats.pendingWarns > 0 ? `${YELLOW}🟡 ${stats.pendingWarns} warn${RESET}` : "";
90
+ const errorText = stats.errorCount > 0 ? `${RED}🔴 ${stats.errorCount} error${RESET}` : "";
91
+ const warnText = stats.warnCount > 0 ? `${YELLOW}🟡 ${stats.warnCount} warn${RESET}` : "";
84
92
  const resolvedText = stats.resolved > 0 ? `⚪ ${stats.resolved} 已解决` : "";
85
93
  const statsText = [totalText, fixedText, errorText, warnText, resolvedText]
86
94
  .filter(Boolean)
@@ -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 数 */
@@ -125,9 +127,9 @@ export interface ReviewStats {
125
127
  invalid: number;
126
128
  /** 待处理数 */
127
129
  pending: number;
128
- /** 修复率 (0-100),仅计算代码修复:fixed / total */
130
+ /** 修复率 (0-100),仅计算代码修复:fixed / validTotal */
129
131
  fixRate: number;
130
- /** 解决率 (0-100),计算修复+解决:(fixed + resolved) / total */
132
+ /** 解决率 (0-100),计算已解决:resolved / validTotal */
131
133
  resolveRate: number;
132
134
  }
133
135
 
@@ -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
  }
@@ -1079,7 +1129,7 @@ export class ReviewService {
1079
1129
  );
1080
1130
  }
1081
1131
 
1082
- return this.issueVerifyService.verifyIssueFixes(
1132
+ return await this.issueVerifyService.verifyIssueFixes(
1083
1133
  issues,
1084
1134
  fileContents,
1085
1135
  specs,
@@ -1094,20 +1144,23 @@ export class ReviewService {
1094
1144
  */
1095
1145
  protected calculateIssueStats(issues: ReviewIssue[]): ReviewStats {
1096
1146
  const total = issues.length;
1097
- const fixed = issues.filter((i) => i.fixed).length;
1098
- const resolved = issues.filter((i) => i.resolved && !i.fixed).length;
1099
- const invalid = issues.filter((i) => i.valid === "false" && !i.fixed && !i.resolved).length;
1100
- const pending = total - fixed - resolved - invalid;
1101
- const fixRate = total > 0 ? Math.round((fixed / total) * 100 * 10) / 10 : 0;
1102
- const resolveRate = total > 0 ? Math.round(((fixed + resolved) / total) * 100 * 10) / 10 : 0;
1103
- return { total, fixed, resolved, invalid, pending, fixRate, resolveRate };
1147
+ const validIssue = issues.filter((i) => i.valid !== "false");
1148
+ const validTotal = validIssue.length;
1149
+ const fixed = validIssue.filter((i) => i.fixed).length;
1150
+ const resolved = validIssue.filter((i) => i.resolved).length;
1151
+ const invalid = total - validTotal;
1152
+ const pending = validTotal - fixed - resolved;
1153
+ const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
1154
+ const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
1155
+ return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };
1104
1156
  }
1105
1157
 
1106
1158
  /**
1107
1159
  * 仅执行删除代码分析模式
1108
1160
  */
1109
1161
  protected async executeDeletionOnly(context: ReviewContext): Promise<ReviewResult> {
1110
- 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;
1111
1164
 
1112
1165
  if (shouldLog(verbose, 1)) {
1113
1166
  console.log(`🗑️ 仅执行删除代码分析模式`);
@@ -1172,7 +1225,7 @@ export class ReviewService {
1172
1225
  if (shouldLog(verbose, 1)) {
1173
1226
  console.log(`💬 提交 PR 评论...`);
1174
1227
  }
1175
- await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
1228
+ await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
1176
1229
  if (shouldLog(verbose, 1)) {
1177
1230
  console.log(`✅ 评论已提交`);
1178
1231
  }
@@ -2061,6 +2114,7 @@ ${fileChanges || "无"}`;
2061
2114
  prNumber: number,
2062
2115
  result: ReviewResult,
2063
2116
  verbose?: VerboseLevel,
2117
+ autoApprove?: boolean,
2064
2118
  ): Promise<void> {
2065
2119
  // 获取配置
2066
2120
  const reviewConf = this.config.getPluginConfig<ReviewConfig>("review");
@@ -2135,6 +2189,7 @@ ${fileChanges || "无"}`;
2135
2189
  }
2136
2190
 
2137
2191
  // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
2192
+ // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
2138
2193
  let lineIssues: ReviewIssue[] = [];
2139
2194
  let comments: CreatePullReviewComment[] = [];
2140
2195
  if (reviewConf.lineComments) {
@@ -2149,23 +2204,47 @@ ${fileChanges || "无"}`;
2149
2204
  .map((issue) => this.issueToReviewComment(issue))
2150
2205
  .filter((comment): comment is CreatePullReviewComment => comment !== null);
2151
2206
  }
2207
+
2208
+ // 计算是否需要自动批准
2209
+ // 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
2210
+ const stats = this.calculateIssueStats(result.issues);
2211
+ const shouldAutoApprove = autoApprove && stats.pending === 0;
2212
+
2152
2213
  if (reviewConf.lineComments) {
2153
- 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
+
2154
2228
  if (comments.length > 0) {
2155
2229
  try {
2156
2230
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2157
- event: REVIEW_STATE.COMMENT,
2158
- body: reviewBody,
2231
+ event: reviewEvent,
2232
+ body: finalReviewBody,
2159
2233
  comments,
2160
2234
  commit_id: commitId,
2161
2235
  });
2162
- console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2236
+ if (shouldAutoApprove) {
2237
+ console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
2238
+ } else {
2239
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2240
+ }
2163
2241
  } catch {
2164
2242
  // 批量失败时逐条发布,跳过无法定位的评论
2165
2243
  console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
2166
2244
  let successCount = 0;
2167
2245
  for (const comment of comments) {
2168
2246
  try {
2247
+ // 逐条发布时只用 COMMENT event,避免重复 APPROVE
2169
2248
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2170
2249
  event: REVIEW_STATE.COMMENT,
2171
2250
  body: successCount === 0 ? reviewBody : undefined,
@@ -2179,6 +2258,23 @@ ${fileChanges || "无"}`;
2179
2258
  }
2180
2259
  if (successCount > 0) {
2181
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
+ }
2182
2278
  } else {
2183
2279
  console.warn("⚠️ 所有行级评论均无法定位,已跳过");
2184
2280
  }
@@ -2187,16 +2283,36 @@ ${fileChanges || "无"}`;
2187
2283
  // 本轮无新问题,仍发布 Round 状态(含上轮回顾)
2188
2284
  try {
2189
2285
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2190
- event: REVIEW_STATE.COMMENT,
2191
- body: reviewBody,
2286
+ event: reviewEvent,
2287
+ body: finalReviewBody,
2192
2288
  comments: [],
2193
2289
  commit_id: commitId,
2194
2290
  });
2195
- 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
+ }
2196
2296
  } catch (error) {
2197
2297
  console.warn("⚠️ 发布审查状态失败:", error);
2198
2298
  }
2199
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
+ }
2200
2316
  }
2201
2317
  }
2202
2318