@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 +22 -0
- package/dist/index.js +139 -51
- package/package.json +2 -2
- package/src/issue-verify.service.ts +2 -0
- package/src/review-report/formatters/markdown.formatter.ts +25 -15
- package/src/review-report/formatters/terminal.formatter.ts +26 -18
- package/src/review-spec/types.ts +4 -2
- package/src/review.config.ts +14 -0
- package/src/review.service.ts +135 -19
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
|
-
// 🟢 已修复 | 🔴
|
|
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
|
-
|
|
1179
|
-
|
|
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
|
-
}
|
|
1186
|
+
}
|
|
1187
|
+
if (issue.resolved) {
|
|
1185
1188
|
stats.resolved++;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1189
|
+
}
|
|
1190
|
+
if (issue.severity === "error") {
|
|
1191
|
+
stats.errorCount++;
|
|
1188
1192
|
} else {
|
|
1189
|
-
stats.
|
|
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
|
-
|
|
1207
|
-
|
|
1211
|
+
errorCount: 0,
|
|
1212
|
+
warnCount: 0,
|
|
1208
1213
|
resolved: 0
|
|
1209
1214
|
};
|
|
1210
|
-
|
|
1211
|
-
totalAll += fileTotal;
|
|
1215
|
+
totalAll += stats.total;
|
|
1212
1216
|
totalFixed += stats.fixed;
|
|
1213
|
-
totalPendingErrors += stats.
|
|
1214
|
-
totalPendingWarns += stats.
|
|
1217
|
+
totalPendingErrors += stats.errorCount;
|
|
1218
|
+
totalPendingWarns += stats.warnCount;
|
|
1215
1219
|
totalResolved += stats.resolved;
|
|
1216
|
-
lines.push(`| \`${fileSummary.file}\` | ${
|
|
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
|
-
|
|
1391
|
-
|
|
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
|
-
}
|
|
1402
|
+
}
|
|
1403
|
+
if (issue.resolved) {
|
|
1397
1404
|
stats.resolved++;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1405
|
+
}
|
|
1406
|
+
if (issue.severity === "error") {
|
|
1407
|
+
stats.errorCount++;
|
|
1400
1408
|
} else {
|
|
1401
|
-
stats.
|
|
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
|
-
|
|
1416
|
-
|
|
1424
|
+
errorCount: 0,
|
|
1425
|
+
warnCount: 0,
|
|
1417
1426
|
resolved: 0
|
|
1418
1427
|
};
|
|
1419
|
-
|
|
1420
|
-
totalAll += fileTotal;
|
|
1428
|
+
totalAll += stats.total;
|
|
1421
1429
|
totalFixed += stats.fixed;
|
|
1422
|
-
totalPendingErrors += stats.
|
|
1423
|
-
totalPendingWarns += stats.
|
|
1430
|
+
totalPendingErrors += stats.errorCount;
|
|
1431
|
+
totalPendingWarns += stats.warnCount;
|
|
1424
1432
|
totalResolved += stats.resolved;
|
|
1425
|
-
const totalText =
|
|
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.
|
|
1428
|
-
const warnText = stats.
|
|
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
|
|
2612
|
-
const
|
|
2613
|
-
const
|
|
2614
|
-
const
|
|
2615
|
-
const
|
|
2616
|
-
const
|
|
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
|
|
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:
|
|
3495
|
-
body:
|
|
3546
|
+
event: reviewEvent,
|
|
3547
|
+
body: finalReviewBody,
|
|
3496
3548
|
comments,
|
|
3497
3549
|
commit_id: commitId
|
|
3498
3550
|
});
|
|
3499
|
-
|
|
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:
|
|
3530
|
-
body:
|
|
3599
|
+
event: reviewEvent,
|
|
3600
|
+
body: finalReviewBody,
|
|
3531
3601
|
comments: [],
|
|
3532
3602
|
commit_id: commitId
|
|
3533
3603
|
});
|
|
3534
|
-
|
|
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.
|
|
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",
|
|
@@ -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
|
-
// 🟢 已修复 | 🔴
|
|
133
|
+
// 🟢 已修复 | 🔴 error数量 | 🟡 warn数量 | ⚪ 已解决(非代码修复)
|
|
134
134
|
const issuesByFile = new Map<
|
|
135
135
|
string,
|
|
136
|
-
{
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
}
|
|
156
|
+
}
|
|
157
|
+
if (issue.resolved) {
|
|
149
158
|
stats.resolved++;
|
|
150
|
-
}
|
|
151
|
-
|
|
159
|
+
}
|
|
160
|
+
if (issue.severity === "error") {
|
|
161
|
+
stats.errorCount++;
|
|
152
162
|
} else {
|
|
153
|
-
stats.
|
|
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
|
-
|
|
174
|
-
|
|
184
|
+
errorCount: 0,
|
|
185
|
+
warnCount: 0,
|
|
175
186
|
resolved: 0,
|
|
176
187
|
};
|
|
177
|
-
|
|
178
|
-
totalAll += fileTotal;
|
|
188
|
+
totalAll += stats.total;
|
|
179
189
|
totalFixed += stats.fixed;
|
|
180
|
-
totalPendingErrors += stats.
|
|
181
|
-
totalPendingWarns += stats.
|
|
190
|
+
totalPendingErrors += stats.errorCount;
|
|
191
|
+
totalPendingWarns += stats.warnCount;
|
|
182
192
|
totalResolved += stats.resolved;
|
|
183
193
|
|
|
184
194
|
lines.push(
|
|
185
|
-
`| \`${fileSummary.file}\` | ${
|
|
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
|
-
{
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
}
|
|
54
|
+
}
|
|
55
|
+
if (issue.resolved) {
|
|
47
56
|
stats.resolved++;
|
|
48
|
-
}
|
|
49
|
-
|
|
57
|
+
}
|
|
58
|
+
if (issue.severity === "error") {
|
|
59
|
+
stats.errorCount++;
|
|
50
60
|
} else {
|
|
51
|
-
stats.
|
|
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
|
-
|
|
68
|
-
|
|
78
|
+
errorCount: 0,
|
|
79
|
+
warnCount: 0,
|
|
69
80
|
resolved: 0,
|
|
70
81
|
};
|
|
71
|
-
|
|
72
|
-
totalAll += fileTotal;
|
|
82
|
+
totalAll += stats.total;
|
|
73
83
|
totalFixed += stats.fixed;
|
|
74
|
-
totalPendingErrors += stats.
|
|
75
|
-
totalPendingWarns += stats.
|
|
84
|
+
totalPendingErrors += stats.errorCount;
|
|
85
|
+
totalPendingWarns += stats.warnCount;
|
|
76
86
|
totalResolved += stats.resolved;
|
|
77
87
|
|
|
78
|
-
const totalText =
|
|
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
|
-
|
|
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)
|
package/src/review-spec/types.ts
CHANGED
|
@@ -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 /
|
|
130
|
+
/** 修复率 (0-100),仅计算代码修复:fixed / validTotal */
|
|
129
131
|
fixRate: number;
|
|
130
|
-
/** 解决率 (0-100)
|
|
132
|
+
/** 解决率 (0-100),计算已解决:resolved / validTotal */
|
|
131
133
|
resolveRate: number;
|
|
132
134
|
}
|
|
133
135
|
|
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
|
}
|
|
@@ -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
|
|
1098
|
-
const
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1101
|
-
const
|
|
1102
|
-
const
|
|
1103
|
-
|
|
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 } =
|
|
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
|
|
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:
|
|
2158
|
-
body:
|
|
2231
|
+
event: reviewEvent,
|
|
2232
|
+
body: finalReviewBody,
|
|
2159
2233
|
comments,
|
|
2160
2234
|
commit_id: commitId,
|
|
2161
2235
|
});
|
|
2162
|
-
|
|
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:
|
|
2191
|
-
body:
|
|
2286
|
+
event: reviewEvent,
|
|
2287
|
+
body: finalReviewBody,
|
|
2192
2288
|
comments: [],
|
|
2193
2289
|
commit_id: commitId,
|
|
2194
2290
|
});
|
|
2195
|
-
|
|
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
|
|