@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 +10 -0
- package/dist/index.js +92 -16
- package/package.json +2 -2
- package/src/review-spec/types.ts +2 -0
- package/src/review.config.ts +14 -0
- package/src/review.service.ts +127 -13
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
|
|
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:
|
|
3505
|
-
body:
|
|
3546
|
+
event: reviewEvent,
|
|
3547
|
+
body: finalReviewBody,
|
|
3506
3548
|
comments,
|
|
3507
3549
|
commit_id: commitId
|
|
3508
3550
|
});
|
|
3509
|
-
|
|
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:
|
|
3540
|
-
body:
|
|
3599
|
+
event: reviewEvent,
|
|
3600
|
+
body: finalReviewBody,
|
|
3541
3601
|
comments: [],
|
|
3542
3602
|
commit_id: commitId
|
|
3543
3603
|
});
|
|
3544
|
-
|
|
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.
|
|
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.
|
|
31
|
+
"@spaceflow/core": "0.27.0"
|
|
32
32
|
},
|
|
33
33
|
"spaceflow": {
|
|
34
34
|
"type": "flow",
|
package/src/review-spec/types.ts
CHANGED
package/src/review.config.ts
CHANGED
|
@@ -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 推导) */
|
package/src/review.service.ts
CHANGED
|
@@ -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(
|
|
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 } =
|
|
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
|
|
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:
|
|
2160
|
-
body:
|
|
2231
|
+
event: reviewEvent,
|
|
2232
|
+
body: finalReviewBody,
|
|
2161
2233
|
comments,
|
|
2162
2234
|
commit_id: commitId,
|
|
2163
2235
|
});
|
|
2164
|
-
|
|
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:
|
|
2193
|
-
body:
|
|
2286
|
+
event: reviewEvent,
|
|
2287
|
+
body: finalReviewBody,
|
|
2194
2288
|
comments: [],
|
|
2195
2289
|
commit_id: commitId,
|
|
2196
2290
|
});
|
|
2197
|
-
|
|
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
|
|