@spaceflow/review 5.0.1 → 5.0.3

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.0.2](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@5.0.1...@spaceflow/review@5.0.2) (2026-04-14)
4
+
5
+ ### 代码重构
6
+
7
+ * **review:** 重构问题统计逻辑,修正 pending 计算并复用 calculateIssueStats ([23ddcac](https://github.com/Lydanne/spaceflow/commit/23ddcac7f4d8cf59b295b9b370e15d3a7058eecf))
8
+
9
+ ### 其他修改
10
+
11
+ * **review-summary:** released version 5.0.1 [no ci] ([41d770e](https://github.com/Lydanne/spaceflow/commit/41d770e9800defbf1f37d15de6a889df598d45ae))
12
+ * **scripts:** released version 5.0.1 [no ci] ([f527047](https://github.com/Lydanne/spaceflow/commit/f5270478d4a85ec19c2cc2ad6028972909fd7320))
13
+ * **shell:** released version 5.0.1 [no ci] ([e04c907](https://github.com/Lydanne/spaceflow/commit/e04c907ae9bd38a73506f9f93ca21ee97a05c606))
14
+
15
+ ## [5.0.1](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@5.0.0...@spaceflow/review@5.0.1) (2026-04-14)
16
+
17
+ ### 修复BUG
18
+
19
+ * **review:** 修正已解决问题统计逻辑,避免与已修复问题重复计数 ([6d191bb](https://github.com/Lydanne/spaceflow/commit/6d191bbd71b8ce6194ce56a8d008dd966173541b))
20
+
21
+ ### 代码重构
22
+
23
+ * **shared:** 移除 Claude Code 相关配置和文档,统一使用 OpenAI/Gemini/OpenCode ([b0754d6](https://github.com/Lydanne/spaceflow/commit/b0754d64f8991a10708730c191cf0f335e8aa6ca))
24
+
25
+ ### 其他修改
26
+
27
+ * **cli:** released version 5.0.1 [no ci] ([f3009ea](https://github.com/Lydanne/spaceflow/commit/f3009eaaf26759d708541f54e707882b4927ebbd))
28
+ * **core:** released version 5.0.1 [no ci] ([7904b9b](https://github.com/Lydanne/spaceflow/commit/7904b9bbc619c91a3a4fa1c8ea04d9dc548495ad))
29
+ * **core:** 移除 @anthropic-ai/claude-agent-sdk 依赖及相关 sharp 图像处理包 ([7b50e06](https://github.com/Lydanne/spaceflow/commit/7b50e066df97d408eb63707eb27273592a28d34d))
30
+ * **publish:** released version 5.0.1 [no ci] ([b14849f](https://github.com/Lydanne/spaceflow/commit/b14849ffafd38f230a3e69c2d63c468cb1946388))
31
+ * **shared:** released version 5.0.1 [no ci] ([f70cd93](https://github.com/Lydanne/spaceflow/commit/f70cd930b01ffa09187f66522c8d6b18f835ec53))
32
+
3
33
  ## [4.0.1](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@4.0.0...@spaceflow/review@4.0.1) (2026-04-13)
4
34
 
5
35
  ### 新特性
package/dist/index.js CHANGED
@@ -2254,7 +2254,7 @@ function generateIssueKey(issue) {
2254
2254
  const fixed = validIssue.filter((i)=>i.fixed).length;
2255
2255
  const resolved = validIssue.filter((i)=>i.resolved).length;
2256
2256
  const invalid = total - validTotal;
2257
- const pending = validTotal - fixed - resolved;
2257
+ const pending = validTotal - validIssue.filter((i)=>i.fixed || i.resolved).length;
2258
2258
  const fixRate = validTotal > 0 ? Math.round(fixed / validTotal * 100 * 10) / 10 : 0;
2259
2259
  const resolveRate = validTotal > 0 ? Math.round(resolved / validTotal * 100 * 10) / 10 : 0;
2260
2260
  return {
@@ -2544,7 +2544,7 @@ function generateIssueKey(issue) {
2544
2544
  }
2545
2545
  try {
2546
2546
  const reactions = await this.pr.getReviewCommentReactions(comment.id);
2547
- if (reactions.length === 0 || !matchedIssue) continue;
2547
+ if (!reactions || reactions.length === 0 || !matchedIssue) continue;
2548
2548
  // 按 content 分组,收集每种 reaction 的用户列表
2549
2549
  const reactionMap = new Map();
2550
2550
  for (const r of reactions){
@@ -2720,8 +2720,10 @@ function generateIssueKey(issue) {
2720
2720
  outputFormat: "markdown",
2721
2721
  ci: true
2722
2722
  });
2723
- // 获取 PR 信息以获取 head commit SHA
2724
- const commitId = await this.pr.getHeadSha();
2723
+ // 获取 PR 信息以获取 head commit SHA(同时缓存 PR 状态,供后续 closed 检查复用)
2724
+ const prInfo = await this.pr.getInfo().catch(()=>null);
2725
+ const prIsClosed = prInfo?.state === "closed" || !!prInfo?.merged;
2726
+ const commitId = prInfo?.head?.sha || "HEAD";
2725
2727
  // 1. 发布或更新主评论(使用 Issue Comment API,支持删除和更新)
2726
2728
  try {
2727
2729
  if (existingComments.length > 0) {
@@ -2747,6 +2749,11 @@ function generateIssueKey(issue) {
2747
2749
  console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
2748
2750
  }
2749
2751
  // 2. 发布本轮新发现的行级评论(使用 PR Review API)
2752
+ // PR 已关闭/合并时无法提交行级 Review,直接跳过
2753
+ if (prIsClosed) {
2754
+ console.log(`ℹ️ PR #${this.pr.number} 已关闭/合并,跳过行级 Review 提交`);
2755
+ return;
2756
+ }
2750
2757
  // 保留旧轮次的 review 历史,但清理同轮次的旧 AI review(重复触发场景)
2751
2758
  // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
2752
2759
  await this.cleanupDuplicateRoundReviews(this._result.round, verbose);
@@ -3131,10 +3138,7 @@ function generateIssueKey(issue) {
3131
3138
  if (round > 1) {
3132
3139
  const prevIssues = allIssues.filter((i)=>i.round === round - 1);
3133
3140
  if (prevIssues.length > 0) {
3134
- const prevFixed = prevIssues.filter((i)=>i.fixed).length;
3135
- const prevResolved = prevIssues.filter((i)=>i.resolved && !i.fixed).length;
3136
- const prevInvalid = prevIssues.filter((i)=>i.valid === "false" && !i.fixed && !i.resolved).length;
3137
- const prevPending = prevIssues.length - prevFixed - prevResolved - prevInvalid;
3141
+ const { fixed: prevFixed, resolved: prevResolved, invalid: prevInvalid, pending: prevPending } = calculateIssueStats(prevIssues);
3138
3142
  parts.push("");
3139
3143
  parts.push(`<details><summary>📊 Round ${round - 1} 回顾 (${prevIssues.length} 个问题)</summary>\n`);
3140
3144
  parts.push(`| 状态 | 数量 |`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -368,7 +368,7 @@ export class ReviewResultModel {
368
368
  }
369
369
  try {
370
370
  const reactions = await this.pr.getReviewCommentReactions(comment.id);
371
- if (reactions.length === 0 || !matchedIssue) continue;
371
+ if (!reactions || reactions.length === 0 || !matchedIssue) continue;
372
372
  // 按 content 分组,收集每种 reaction 的用户列表
373
373
  const reactionMap = new Map<string, string[]>();
374
374
  for (const r of reactions) {
@@ -585,8 +585,10 @@ export class ReviewResultModel {
585
585
  ci: true,
586
586
  });
587
587
 
588
- // 获取 PR 信息以获取 head commit SHA
589
- const commitId = await this.pr.getHeadSha();
588
+ // 获取 PR 信息以获取 head commit SHA(同时缓存 PR 状态,供后续 closed 检查复用)
589
+ const prInfo = await this.pr.getInfo().catch(() => null);
590
+ const prIsClosed = prInfo?.state === "closed" || !!prInfo?.merged;
591
+ const commitId = prInfo?.head?.sha || "HEAD";
590
592
 
591
593
  // 1. 发布或更新主评论(使用 Issue Comment API,支持删除和更新)
592
594
  try {
@@ -612,6 +614,12 @@ export class ReviewResultModel {
612
614
  }
613
615
 
614
616
  // 2. 发布本轮新发现的行级评论(使用 PR Review API)
617
+ // PR 已关闭/合并时无法提交行级 Review,直接跳过
618
+ if (prIsClosed) {
619
+ console.log(`ℹ️ PR #${this.pr.number} 已关闭/合并,跳过行级 Review 提交`);
620
+ return;
621
+ }
622
+
615
623
  // 保留旧轮次的 review 历史,但清理同轮次的旧 AI review(重复触发场景)
616
624
  // 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
617
625
  await this.cleanupDuplicateRoundReviews(this._result.round, verbose);
@@ -1069,12 +1077,12 @@ export class ReviewResultModel {
1069
1077
  if (round > 1) {
1070
1078
  const prevIssues = allIssues.filter((i) => i.round === round - 1);
1071
1079
  if (prevIssues.length > 0) {
1072
- const prevFixed = prevIssues.filter((i) => i.fixed).length;
1073
- const prevResolved = prevIssues.filter((i) => i.resolved && !i.fixed).length;
1074
- const prevInvalid = prevIssues.filter(
1075
- (i) => i.valid === "false" && !i.fixed && !i.resolved,
1076
- ).length;
1077
- const prevPending = prevIssues.length - prevFixed - prevResolved - prevInvalid;
1080
+ const {
1081
+ fixed: prevFixed,
1082
+ resolved: prevResolved,
1083
+ invalid: prevInvalid,
1084
+ pending: prevPending,
1085
+ } = calculateIssueStats(prevIssues);
1078
1086
  parts.push("");
1079
1087
  parts.push(
1080
1088
  `<details><summary>📊 Round ${round - 1} 回顾 (${prevIssues.length} 个问题)</summary>\n`,
@@ -27,9 +27,7 @@ describe("utils/review-pr-comment", () => {
27
27
 
28
28
  describe("extractIssueKeyFromBody", () => {
29
29
  it("提取标准格式的 issue key", () => {
30
- expect(extractIssueKeyFromBody("<!-- issue-key: src/a.ts:10:R1 -->")).toBe(
31
- "src/a.ts:10:R1",
32
- );
30
+ expect(extractIssueKeyFromBody("<!-- issue-key: src/a.ts:10:R1 -->")).toBe("src/a.ts:10:R1");
33
31
  });
34
32
 
35
33
  it("body 不含 issue-key 时返回 null", () => {
@@ -69,9 +67,9 @@ describe("utils/review-pr-comment", () => {
69
67
 
70
68
  describe("generateIssueKey", () => {
71
69
  it("拼接 file:line:ruleId", () => {
72
- expect(
73
- generateIssueKey({ file: "src/a.ts", line: "10", ruleId: "R1" } as any),
74
- ).toBe("src/a.ts:10:R1");
70
+ expect(generateIssueKey({ file: "src/a.ts", line: "10", ruleId: "R1" } as any)).toBe(
71
+ "src/a.ts:10:R1",
72
+ );
75
73
  });
76
74
  });
77
75
 
@@ -129,21 +127,20 @@ describe("utils/review-pr-comment", () => {
129
127
  });
130
128
 
131
129
  it("resolved(非 fixed)计入 resolved,不计入 pending", () => {
132
- const issues = [
133
- { file: "a.ts", line: "1", ruleId: "R1", resolved: "2024-01-01" },
134
- ] as any[];
130
+ const issues = [{ file: "a.ts", line: "1", ruleId: "R1", resolved: "2024-01-01" }] as any[];
135
131
  const stats = calculateIssueStats(issues);
136
132
  expect(stats.resolved).toBe(1);
137
133
  expect(stats.pending).toBe(0);
138
134
  });
139
135
 
140
- it("fixed 同时有 resolved 时只计入 fixed", () => {
136
+ it("fixed resolved 同时存在时各自独立计数,pending 不重复减", () => {
141
137
  const issues = [
142
138
  { file: "a.ts", line: "1", ruleId: "R1", fixed: "2024-01-01", resolved: "2024-01-01" },
143
139
  ] as any[];
144
140
  const stats = calculateIssueStats(issues);
145
141
  expect(stats.fixed).toBe(1);
146
- expect(stats.resolved).toBe(0);
142
+ expect(stats.resolved).toBe(1);
143
+ expect(stats.pending).toBe(0);
147
144
  });
148
145
 
149
146
  it("fixRate 计算正确(2/4 = 50%)", () => {
@@ -179,7 +179,7 @@ export function calculateIssueStats(issues: ReviewIssue[]): ReviewStats {
179
179
  const fixed = validIssue.filter((i) => i.fixed).length;
180
180
  const resolved = validIssue.filter((i) => i.resolved).length;
181
181
  const invalid = total - validTotal;
182
- const pending = validTotal - fixed - resolved;
182
+ const pending = validTotal - validIssue.filter((i) => i.fixed || i.resolved).length;
183
183
  const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
184
184
  const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
185
185
  return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };