@spaceflow/review 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.54.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.53.0...@spaceflow/review@0.54.0) (2026-03-02)
4
+
5
+ ### 新特性
6
+
7
+ * **review:** 为行级评论 Review 添加统计信息摘要 ([58d5b37](https://github.com/Lydanne/spaceflow/commit/58d5b37ba54daa24bd2f8396318fedc87f388c74))
8
+
9
+ ### 其他修改
10
+
11
+ * **review-summary:** released version 0.21.0 [no ci] ([11379c4](https://github.com/Lydanne/spaceflow/commit/11379c478859a12dd0340a78b1578487d9a24b31))
12
+ * **scripts:** released version 0.21.0 [no ci] ([1f0a213](https://github.com/Lydanne/spaceflow/commit/1f0a2139d155807451dc968de8213bafe2e4edb8))
13
+ * **shell:** released version 0.21.0 [no ci] ([b619af7](https://github.com/Lydanne/spaceflow/commit/b619af741e16053868a2eedd41f56d50134954d8))
14
+
15
+ ## [0.53.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.52.0...@spaceflow/review@0.53.0) (2026-03-02)
16
+
17
+ ### 修复BUG
18
+
19
+ * **core:** 重构配置 Schema 生成逻辑,使用 SpaceflowConfigSchema 作为基础 ([c73eb1c](https://github.com/Lydanne/spaceflow/commit/c73eb1ce5b6f212b8a932a15224db7e63822f8d0))
20
+
21
+ ### 其他修改
22
+
23
+ * **core:** released version 0.19.0 [no ci] ([c8bfe6b](https://github.com/Lydanne/spaceflow/commit/c8bfe6ba20893e2c3cd383ed7e7d3217b0492eb6))
24
+ * **publish:** released version 0.43.0 [no ci] ([1074b9c](https://github.com/Lydanne/spaceflow/commit/1074b9c5fb21a447093ef23300c451d790710b33))
25
+
3
26
  ## [0.52.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.51.0...@spaceflow/review@0.52.0) (2026-02-28)
4
27
 
5
28
  ### 测试用例
package/dist/index.js CHANGED
@@ -363,6 +363,10 @@ class MarkdownFormatter {
363
363
  const fixedByStr = issue.fixedBy?.login ? ` (by @${issue.fixedBy.login})` : "";
364
364
  lines.push(`- **修复时间**: ${formatDateToUTC8(issue.fixed)}${fixedByStr}`);
365
365
  }
366
+ if (issue.resolved) {
367
+ const resolvedByStr = issue.resolvedBy?.login ? ` (by @${issue.resolvedBy.login})` : "";
368
+ lines.push(`- **解决时间**: ${formatDateToUTC8(issue.resolved)}${resolvedByStr}`);
369
+ }
366
370
  if (issue.suggestion) {
367
371
  const ext = extname(issue.file).slice(1) || "";
368
372
  const cleanSuggestion = issue.suggestion.replace(/```/g, "//").trim();
@@ -568,6 +572,7 @@ class MarkdownFormatter {
568
572
  ];
569
573
  lines.push(`| 总问题数 | ${stats.total} |`);
570
574
  lines.push(`| ✅ 已修复 | ${stats.fixed} |`);
575
+ lines.push(`| 🟢 已解决 | ${stats.resolved} |`);
571
576
  lines.push(`| ❌ 无效 | ${stats.invalid} |`);
572
577
  lines.push(`| ⚠️ 待处理 | ${stats.pending} |`);
573
578
  lines.push(`| 修复率 | ${stats.fixRate}% |`);
@@ -687,6 +692,7 @@ class TerminalFormatter {
687
692
  ];
688
693
  lines.push(` 总问题数: ${stats.total}`);
689
694
  lines.push(` ${GREEN}✅ 已修复: ${stats.fixed}${RESET}`);
695
+ lines.push(` ${GREEN}🟢 已解决: ${stats.resolved}${RESET}`);
690
696
  lines.push(` ${RED}❌ 无效: ${stats.invalid}${RESET}`);
691
697
  lines.push(` ${YELLOW}⚠️ 待处理: ${stats.pending}${RESET}`);
692
698
  lines.push(` 修复率: ${stats.fixRate}%`);
@@ -1611,12 +1617,14 @@ class ReviewService {
1611
1617
  */ calculateIssueStats(issues) {
1612
1618
  const total = issues.length;
1613
1619
  const fixed = issues.filter((i)=>i.fixed).length;
1620
+ const resolved = issues.filter((i)=>i.resolved && !i.fixed).length;
1614
1621
  const invalid = issues.filter((i)=>i.valid === "false").length;
1615
- const pending = total - fixed - invalid;
1622
+ const pending = total - fixed - resolved - invalid;
1616
1623
  const fixRate = total > 0 ? Math.round(fixed / total * 100 * 10) / 10 : 0;
1617
1624
  return {
1618
1625
  total,
1619
1626
  fixed,
1627
+ resolved,
1620
1628
  invalid,
1621
1629
  pending,
1622
1630
  fixRate
@@ -2474,46 +2482,19 @@ ${fileChanges || "无"}`;
2474
2482
  } catch (error) {
2475
2483
  console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
2476
2484
  }
2477
- // 2. 删除旧的行级评论(逐条删除 PR Review Comment)
2478
- try {
2479
- const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2480
- const oldLineReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
2481
- for (const review of oldLineReviews){
2482
- if (review.id) {
2483
- const reviewComments = await this.gitProvider.listPullReviewComments(owner, repo, prNumber, review.id);
2484
- for (const comment of reviewComments){
2485
- if (comment.id) {
2486
- try {
2487
- await this.gitProvider.deletePullReviewComment(owner, repo, comment.id);
2488
- } catch {
2489
- // 删除失败忽略
2490
- }
2491
- }
2492
- }
2493
- // 评论删除后尝试删除 review 本身
2494
- try {
2495
- await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
2496
- } catch {
2497
- // 已提交的 review 无法删除,忽略
2498
- }
2499
- }
2500
- }
2501
- if (oldLineReviews.length > 0) {
2502
- console.log(`🗑️ 已清理 ${oldLineReviews.length} 个旧的行级评论 review`);
2503
- }
2504
- } catch (error) {
2505
- console.warn("⚠️ 清理旧行级评论失败:", error);
2506
- }
2507
- // 3. 发布新的行级评论(使用 PR Review API)
2485
+ // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
2486
+ let lineIssues = [];
2508
2487
  let comments = [];
2509
2488
  if (reviewConf.lineComments) {
2510
- comments = result.issues.filter((issue)=>!issue.fixed && issue.valid !== "false").map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
2489
+ lineIssues = result.issues.filter((issue)=>issue.round === result.round && !issue.fixed && !issue.resolved && issue.valid !== "false");
2490
+ comments = lineIssues.map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
2511
2491
  }
2512
2492
  if (comments.length > 0) {
2493
+ const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
2513
2494
  try {
2514
2495
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2515
2496
  event: REVIEW_STATE.COMMENT,
2516
- body: REVIEW_LINE_COMMENTS_MARKER,
2497
+ body: reviewBody,
2517
2498
  comments,
2518
2499
  commit_id: commitId
2519
2500
  });
@@ -2526,7 +2507,7 @@ ${fileChanges || "无"}`;
2526
2507
  try {
2527
2508
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
2528
2509
  event: REVIEW_STATE.COMMENT,
2529
- body: successCount === 0 ? REVIEW_LINE_COMMENTS_MARKER : undefined,
2510
+ body: successCount === 0 ? reviewBody : undefined,
2530
2511
  comments: [
2531
2512
  comment
2532
2513
  ],
@@ -2568,7 +2549,8 @@ ${fileChanges || "无"}`;
2568
2549
  }
2569
2550
  }
2570
2551
  /**
2571
- * 从 PR 的所有 resolved review threads 中同步 fixed 状态到 result.issues
2552
+ * 从 PR 的所有 resolved review threads 中同步 resolved 状态到 result.issues
2553
+ * 用户手动点击 resolve 的记录写入 resolved/resolvedBy 字段(区别于 AI 验证的 fixed/fixedBy)
2572
2554
  * 优先通过评论 body 中的 issue key 精确匹配,回退到 path+line 匹配
2573
2555
  */ async syncResolvedComments(owner, repo, prNumber, result) {
2574
2556
  try {
@@ -2596,10 +2578,10 @@ ${fileChanges || "无"}`;
2596
2578
  if (!matchedIssue) {
2597
2579
  matchedIssue = result.issues.find((issue)=>issue.file === thread.path && this.lineMatchesPosition(issue.line, thread.line));
2598
2580
  }
2599
- if (matchedIssue && !matchedIssue.fixed) {
2600
- matchedIssue.fixed = now;
2581
+ if (matchedIssue && !matchedIssue.resolved) {
2582
+ matchedIssue.resolved = now;
2601
2583
  if (thread.resolvedBy) {
2602
- matchedIssue.fixedBy = {
2584
+ matchedIssue.resolvedBy = {
2603
2585
  id: thread.resolvedBy.id?.toString(),
2604
2586
  login: thread.resolvedBy.login
2605
2587
  };
@@ -2869,6 +2851,41 @@ ${fileChanges || "无"}`;
2869
2851
  }
2870
2852
  }
2871
2853
  /**
2854
+ * 构建行级评论 Review 的 body(marker + 本轮统计 + 上轮回顾)
2855
+ */ buildLineReviewBody(issues, round, allIssues) {
2856
+ const errorCount = issues.filter((i)=>i.severity === "error").length;
2857
+ const warnCount = issues.filter((i)=>i.severity === "warn").length;
2858
+ const fileCount = new Set(issues.map((i)=>i.file)).size;
2859
+ const badges = [];
2860
+ if (errorCount > 0) badges.push(`🔴 ${errorCount}`);
2861
+ if (warnCount > 0) badges.push(`🟡 ${warnCount}`);
2862
+ const parts = [
2863
+ REVIEW_LINE_COMMENTS_MARKER
2864
+ ];
2865
+ parts.push(`### � Spaceflow Review · Round ${round}`);
2866
+ parts.push(`> **${issues.length}** 个新问题 · **${fileCount}** 个文件${badges.length > 0 ? " · " + badges.join(" ") : ""}`);
2867
+ // 上轮回顾
2868
+ if (round > 1) {
2869
+ const prevIssues = allIssues.filter((i)=>i.round === round - 1);
2870
+ if (prevIssues.length > 0) {
2871
+ const prevFixed = prevIssues.filter((i)=>i.fixed).length;
2872
+ const prevResolved = prevIssues.filter((i)=>i.resolved && !i.fixed).length;
2873
+ const prevInvalid = prevIssues.filter((i)=>i.valid === "false").length;
2874
+ const prevPending = prevIssues.length - prevFixed - prevResolved - prevInvalid;
2875
+ parts.push("");
2876
+ parts.push(`<details><summary>📊 Round ${round - 1} 回顾 (${prevIssues.length} 个问题)</summary>\n`);
2877
+ parts.push(`| 状态 | 数量 |`);
2878
+ parts.push(`|------|------|`);
2879
+ if (prevFixed > 0) parts.push(`| ✅ 已修复 | ${prevFixed} |`);
2880
+ if (prevResolved > 0) parts.push(`| 🟢 已解决 | ${prevResolved} |`);
2881
+ if (prevInvalid > 0) parts.push(`| ❌ 无效 | ${prevInvalid} |`);
2882
+ if (prevPending > 0) parts.push(`| ⚠️ 待处理 | ${prevPending} |`);
2883
+ parts.push(`\n</details>`);
2884
+ }
2885
+ }
2886
+ return parts.join("\n");
2887
+ }
2888
+ /**
2872
2889
  * 将单个 ReviewIssue 转换为 CreatePullReviewComment
2873
2890
  */ issueToReviewComment(issue) {
2874
2891
  const lineNums = this.reviewSpecService.parseLineRange(issue.line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -53,6 +53,10 @@ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportPar
53
53
  const fixedByStr = issue.fixedBy?.login ? ` (by @${issue.fixedBy.login})` : "";
54
54
  lines.push(`- **修复时间**: ${formatDateToUTC8(issue.fixed)}${fixedByStr}`);
55
55
  }
56
+ if (issue.resolved) {
57
+ const resolvedByStr = issue.resolvedBy?.login ? ` (by @${issue.resolvedBy.login})` : "";
58
+ lines.push(`- **解决时间**: ${formatDateToUTC8(issue.resolved)}${resolvedByStr}`);
59
+ }
56
60
  if (issue.suggestion) {
57
61
  const ext = extname(issue.file).slice(1) || "";
58
62
  const cleanSuggestion = issue.suggestion.replace(/```/g, "//").trim();
@@ -284,6 +288,7 @@ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportPar
284
288
  const lines = [`## 📊 ${title}\n`, `| 指标 | 数量 |`, `|------|------|`];
285
289
  lines.push(`| 总问题数 | ${stats.total} |`);
286
290
  lines.push(`| ✅ 已修复 | ${stats.fixed} |`);
291
+ lines.push(`| 🟢 已解决 | ${stats.resolved} |`);
287
292
  lines.push(`| ❌ 无效 | ${stats.invalid} |`);
288
293
  lines.push(`| ⚠️ 待处理 | ${stats.pending} |`);
289
294
  lines.push(`| 修复率 | ${stats.fixRate}% |`);
@@ -122,6 +122,7 @@ export class TerminalFormatter implements ReviewReportFormatter {
122
122
  const lines = [`\n${BOLD}${CYAN}📊 ${title}:${RESET}`];
123
123
  lines.push(` 总问题数: ${stats.total}`);
124
124
  lines.push(` ${GREEN}✅ 已修复: ${stats.fixed}${RESET}`);
125
+ lines.push(` ${GREEN}🟢 已解决: ${stats.resolved}${RESET}`);
125
126
  lines.push(` ${RED}❌ 无效: ${stats.invalid}${RESET}`);
126
127
  lines.push(` ${YELLOW}⚠️ 待处理: ${stats.pending}${RESET}`);
127
128
  lines.push(` 修复率: ${stats.fixRate}%`);
@@ -73,8 +73,10 @@ export interface ReviewIssue {
73
73
  specFile: string;
74
74
  reason: string;
75
75
  date?: string; // 发现问题的时间
76
- fixed?: string; // 修复时间
77
- fixedBy?: UserInfo; // 解决者
76
+ fixed?: string; // AI 验证修复时间
77
+ fixedBy?: UserInfo; // AI 验证修复者
78
+ resolved?: string; // 用户手动点击 resolve 的时间
79
+ resolvedBy?: UserInfo; // 手动 resolve 的操作者
78
80
  valid?: string; // 问题是否有效
79
81
  suggestion?: string;
80
82
  commit?: string;
@@ -115,8 +117,10 @@ export interface DeletionImpactResult {
115
117
  export interface ReviewStats {
116
118
  /** 总问题数 */
117
119
  total: number;
118
- /** 已修复数 */
120
+ /** AI 验证已修复数 */
119
121
  fixed: number;
122
+ /** 用户手动 resolve 数 */
123
+ resolved: number;
120
124
  /** 无效问题数 */
121
125
  invalid: number;
122
126
  /** 待处理数 */
@@ -1226,17 +1226,32 @@ describe("ReviewService", () => {
1226
1226
  describe("ReviewService.calculateIssueStats", () => {
1227
1227
  it("should calculate stats for empty array", () => {
1228
1228
  const stats = (service as any).calculateIssueStats([]);
1229
- expect(stats).toEqual({ total: 0, fixed: 0, invalid: 0, pending: 0, fixRate: 0 });
1229
+ expect(stats).toEqual({
1230
+ total: 0,
1231
+ fixed: 0,
1232
+ resolved: 0,
1233
+ invalid: 0,
1234
+ pending: 0,
1235
+ fixRate: 0,
1236
+ });
1230
1237
  });
1231
1238
 
1232
1239
  it("should calculate stats correctly", () => {
1233
- const issues = [{ fixed: "2024-01-01" }, { fixed: "2024-01-02" }, { valid: "false" }, {}, {}];
1240
+ const issues = [
1241
+ { fixed: "2024-01-01" },
1242
+ { fixed: "2024-01-02" },
1243
+ { resolved: "2024-01-03" },
1244
+ { valid: "false" },
1245
+ {},
1246
+ {},
1247
+ ];
1234
1248
  const stats = (service as any).calculateIssueStats(issues);
1235
- expect(stats.total).toBe(5);
1249
+ expect(stats.total).toBe(6);
1236
1250
  expect(stats.fixed).toBe(2);
1251
+ expect(stats.resolved).toBe(1);
1237
1252
  expect(stats.invalid).toBe(1);
1238
1253
  expect(stats.pending).toBe(2);
1239
- expect(stats.fixRate).toBe(40);
1254
+ expect(stats.fixRate).toBe(33.3);
1240
1255
  });
1241
1256
  });
1242
1257
 
@@ -1807,6 +1822,7 @@ describe("ReviewService", () => {
1807
1822
  specFile: "s.md",
1808
1823
  reason: "r",
1809
1824
  severity: "error",
1825
+ round: 1,
1810
1826
  },
1811
1827
  ],
1812
1828
  summary: [],
@@ -1820,18 +1836,18 @@ describe("ReviewService", () => {
1820
1836
  });
1821
1837
 
1822
1838
  describe("ReviewService.syncResolvedComments", () => {
1823
- it("should mark matched issues as fixed via path:line fallback", async () => {
1839
+ it("should mark matched issues as resolved via path:line fallback", async () => {
1824
1840
  mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
1825
1841
  gitProvider.listResolvedThreads.mockResolvedValue([
1826
1842
  { path: "test.ts", line: 10, resolvedBy: { login: "user1" } },
1827
1843
  ] as any);
1828
1844
  const result = { issues: [{ file: "test.ts", line: "10", ruleId: "Rule1" }] };
1829
1845
  await (service as any).syncResolvedComments("o", "r", 1, result);
1830
- expect((result.issues[0] as any).fixed).toBeDefined();
1831
- expect((result.issues[0] as any).fixedBy).toEqual({ id: undefined, login: "user1" });
1846
+ expect((result.issues[0] as any).resolved).toBeDefined();
1847
+ expect((result.issues[0] as any).resolvedBy).toEqual({ id: undefined, login: "user1" });
1832
1848
  });
1833
1849
 
1834
- it("should mark matched issues as fixed via issue key in body", async () => {
1850
+ it("should mark matched issues as resolved via issue key in body", async () => {
1835
1851
  mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
1836
1852
  gitProvider.listResolvedThreads.mockResolvedValue([
1837
1853
  {
@@ -1845,8 +1861,8 @@ describe("ReviewService", () => {
1845
1861
  issues: [{ file: "test.ts", line: "10", ruleId: "RuleA" }],
1846
1862
  };
1847
1863
  await (service as any).syncResolvedComments("o", "r", 1, result);
1848
- expect((result.issues[0] as any).fixed).toBeDefined();
1849
- expect((result.issues[0] as any).fixedBy).toEqual({ id: undefined, login: "user1" });
1864
+ expect((result.issues[0] as any).resolved).toBeDefined();
1865
+ expect((result.issues[0] as any).resolvedBy).toEqual({ id: undefined, login: "user1" });
1850
1866
  });
1851
1867
 
1852
1868
  it("should match correct issue by issue key when multiple issues at same position", async () => {
@@ -1866,17 +1882,17 @@ describe("ReviewService", () => {
1866
1882
  ],
1867
1883
  };
1868
1884
  await (service as any).syncResolvedComments("o", "r", 1, result);
1869
- expect(result.issues[0].fixed).toBeUndefined(); // RuleA 未解决
1870
- expect(result.issues[0].fixedBy).toBeUndefined();
1871
- expect(result.issues[1].fixed).toBeDefined(); // RuleB 已解决
1872
- expect(result.issues[1].fixedBy).toEqual({ id: undefined, login: "user1" });
1885
+ expect(result.issues[0].resolved).toBeUndefined(); // RuleA 未解决
1886
+ expect(result.issues[0].resolvedBy).toBeUndefined();
1887
+ expect(result.issues[1].resolved).toBeDefined(); // RuleB 已解决
1888
+ expect(result.issues[1].resolvedBy).toEqual({ id: undefined, login: "user1" });
1873
1889
  });
1874
1890
 
1875
1891
  it("should skip when no resolved threads", async () => {
1876
1892
  gitProvider.listResolvedThreads.mockResolvedValue([] as any);
1877
1893
  const result = { issues: [{ file: "test.ts", line: "10", ruleId: "Rule1" }] };
1878
1894
  await (service as any).syncResolvedComments("o", "r", 1, result);
1879
- expect((result.issues[0] as any).fixed).toBeUndefined();
1895
+ expect((result.issues[0] as any).resolved).toBeUndefined();
1880
1896
  });
1881
1897
 
1882
1898
  it("should skip threads without path", async () => {
@@ -1885,7 +1901,7 @@ describe("ReviewService", () => {
1885
1901
  ] as any);
1886
1902
  const result = { issues: [{ file: "test.ts", line: "10", ruleId: "Rule1" }] };
1887
1903
  await (service as any).syncResolvedComments("o", "r", 1, result);
1888
- expect((result.issues[0] as any).fixed).toBeUndefined();
1904
+ expect((result.issues[0] as any).resolved).toBeUndefined();
1889
1905
  });
1890
1906
 
1891
1907
  it("should handle error gracefully", async () => {
@@ -900,10 +900,11 @@ export class ReviewService {
900
900
  protected calculateIssueStats(issues: ReviewIssue[]): ReviewStats {
901
901
  const total = issues.length;
902
902
  const fixed = issues.filter((i) => i.fixed).length;
903
+ const resolved = issues.filter((i) => i.resolved && !i.fixed).length;
903
904
  const invalid = issues.filter((i) => i.valid === "false").length;
904
- const pending = total - fixed - invalid;
905
+ const pending = total - fixed - resolved - invalid;
905
906
  const fixRate = total > 0 ? Math.round((fixed / total) * 100 * 10) / 10 : 0;
906
- return { total, fixed, invalid, pending, fixRate };
907
+ return { total, fixed, resolved, invalid, pending, fixRate };
907
908
  }
908
909
 
909
910
  /**
@@ -1933,54 +1934,27 @@ ${fileChanges || "无"}`;
1933
1934
  console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
1934
1935
  }
1935
1936
 
1936
- // 2. 删除旧的行级评论(逐条删除 PR Review Comment)
1937
- try {
1938
- const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
1939
- const oldLineReviews = reviews.filter((r) => r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
1940
- for (const review of oldLineReviews) {
1941
- if (review.id) {
1942
- const reviewComments = await this.gitProvider.listPullReviewComments(
1943
- owner,
1944
- repo,
1945
- prNumber,
1946
- review.id,
1947
- );
1948
- for (const comment of reviewComments) {
1949
- if (comment.id) {
1950
- try {
1951
- await this.gitProvider.deletePullReviewComment(owner, repo, comment.id);
1952
- } catch {
1953
- // 删除失败忽略
1954
- }
1955
- }
1956
- }
1957
- // 评论删除后尝试删除 review 本身
1958
- try {
1959
- await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
1960
- } catch {
1961
- // 已提交的 review 无法删除,忽略
1962
- }
1963
- }
1964
- }
1965
- if (oldLineReviews.length > 0) {
1966
- console.log(`🗑️ 已清理 ${oldLineReviews.length} 个旧的行级评论 review`);
1967
- }
1968
- } catch (error) {
1969
- console.warn("⚠️ 清理旧行级评论失败:", error);
1970
- }
1971
- // 3. 发布新的行级评论(使用 PR Review API)
1937
+ // 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
1938
+ let lineIssues: ReviewIssue[] = [];
1972
1939
  let comments: CreatePullReviewComment[] = [];
1973
1940
  if (reviewConf.lineComments) {
1974
- comments = result.issues
1975
- .filter((issue) => !issue.fixed && issue.valid !== "false")
1941
+ lineIssues = result.issues.filter(
1942
+ (issue) =>
1943
+ issue.round === result.round &&
1944
+ !issue.fixed &&
1945
+ !issue.resolved &&
1946
+ issue.valid !== "false",
1947
+ );
1948
+ comments = lineIssues
1976
1949
  .map((issue) => this.issueToReviewComment(issue))
1977
1950
  .filter((comment): comment is CreatePullReviewComment => comment !== null);
1978
1951
  }
1979
1952
  if (comments.length > 0) {
1953
+ const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
1980
1954
  try {
1981
1955
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
1982
1956
  event: REVIEW_STATE.COMMENT,
1983
- body: REVIEW_LINE_COMMENTS_MARKER,
1957
+ body: reviewBody,
1984
1958
  comments,
1985
1959
  commit_id: commitId,
1986
1960
  });
@@ -1993,7 +1967,7 @@ ${fileChanges || "无"}`;
1993
1967
  try {
1994
1968
  await this.gitProvider.createPullReview(owner, repo, prNumber, {
1995
1969
  event: REVIEW_STATE.COMMENT,
1996
- body: successCount === 0 ? REVIEW_LINE_COMMENTS_MARKER : undefined,
1970
+ body: successCount === 0 ? reviewBody : undefined,
1997
1971
  comments: [comment],
1998
1972
  commit_id: commitId,
1999
1973
  });
@@ -2045,7 +2019,8 @@ ${fileChanges || "无"}`;
2045
2019
  }
2046
2020
 
2047
2021
  /**
2048
- * 从 PR 的所有 resolved review threads 中同步 fixed 状态到 result.issues
2022
+ * 从 PR 的所有 resolved review threads 中同步 resolved 状态到 result.issues
2023
+ * 用户手动点击 resolve 的记录写入 resolved/resolvedBy 字段(区别于 AI 验证的 fixed/fixedBy)
2049
2024
  * 优先通过评论 body 中的 issue key 精确匹配,回退到 path+line 匹配
2050
2025
  */
2051
2026
  protected async syncResolvedComments(
@@ -2082,10 +2057,10 @@ ${fileChanges || "无"}`;
2082
2057
  issue.file === thread.path && this.lineMatchesPosition(issue.line, thread.line),
2083
2058
  );
2084
2059
  }
2085
- if (matchedIssue && !matchedIssue.fixed) {
2086
- matchedIssue.fixed = now;
2060
+ if (matchedIssue && !matchedIssue.resolved) {
2061
+ matchedIssue.resolved = now;
2087
2062
  if (thread.resolvedBy) {
2088
- matchedIssue.fixedBy = {
2063
+ matchedIssue.resolvedBy = {
2089
2064
  id: thread.resolvedBy.id?.toString(),
2090
2065
  login: thread.resolvedBy.login,
2091
2066
  };
@@ -2427,6 +2402,49 @@ ${fileChanges || "无"}`;
2427
2402
  }
2428
2403
  }
2429
2404
 
2405
+ /**
2406
+ * 构建行级评论 Review 的 body(marker + 本轮统计 + 上轮回顾)
2407
+ */
2408
+ protected buildLineReviewBody(
2409
+ issues: ReviewIssue[],
2410
+ round: number,
2411
+ allIssues: ReviewIssue[],
2412
+ ): string {
2413
+ const errorCount = issues.filter((i) => i.severity === "error").length;
2414
+ const warnCount = issues.filter((i) => i.severity === "warn").length;
2415
+ const fileCount = new Set(issues.map((i) => i.file)).size;
2416
+
2417
+ const badges: string[] = [];
2418
+ if (errorCount > 0) badges.push(`🔴 ${errorCount}`);
2419
+ if (warnCount > 0) badges.push(`🟡 ${warnCount}`);
2420
+
2421
+ const parts: string[] = [REVIEW_LINE_COMMENTS_MARKER];
2422
+ parts.push(`### � Spaceflow Review · Round ${round}`);
2423
+ parts.push(`> **${issues.length}** 个新问题 · **${fileCount}** 个文件${badges.length > 0 ? " · " + badges.join(" ") : ""}`);
2424
+
2425
+ // 上轮回顾
2426
+ if (round > 1) {
2427
+ const prevIssues = allIssues.filter((i) => i.round === round - 1);
2428
+ if (prevIssues.length > 0) {
2429
+ const prevFixed = prevIssues.filter((i) => i.fixed).length;
2430
+ const prevResolved = prevIssues.filter((i) => i.resolved && !i.fixed).length;
2431
+ const prevInvalid = prevIssues.filter((i) => i.valid === "false").length;
2432
+ const prevPending = prevIssues.length - prevFixed - prevResolved - prevInvalid;
2433
+ parts.push("");
2434
+ parts.push(`<details><summary>📊 Round ${round - 1} 回顾 (${prevIssues.length} 个问题)</summary>\n`);
2435
+ parts.push(`| 状态 | 数量 |`);
2436
+ parts.push(`|------|------|`);
2437
+ if (prevFixed > 0) parts.push(`| ✅ 已修复 | ${prevFixed} |`);
2438
+ if (prevResolved > 0) parts.push(`| 🟢 已解决 | ${prevResolved} |`);
2439
+ if (prevInvalid > 0) parts.push(`| ❌ 无效 | ${prevInvalid} |`);
2440
+ if (prevPending > 0) parts.push(`| ⚠️ 待处理 | ${prevPending} |`);
2441
+ parts.push(`\n</details>`);
2442
+ }
2443
+ }
2444
+
2445
+ return parts.join("\n");
2446
+ }
2447
+
2430
2448
  /**
2431
2449
  * 将单个 ReviewIssue 转换为 CreatePullReviewComment
2432
2450
  */