@spaceflow/review 0.51.0 → 0.53.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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.52.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.51.0...@spaceflow/review@0.52.0) (2026-02-28)
4
+
5
+ ### 测试用例
6
+
7
+ * **review:** 增强 AI 评论识别和过滤功能的测试覆盖 ([bda706b](https://github.com/Lydanne/spaceflow/commit/bda706b99aab113521afe6bcd386a590811e20a6))
8
+
9
+ ## [0.51.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.50.0...@spaceflow/review@0.51.0) (2026-02-27)
10
+
11
+ ### 其他修改
12
+
13
+ * **review-summary:** released version 0.20.0 [no ci] ([bb3f815](https://github.com/Lydanne/spaceflow/commit/bb3f81567bf6946964a19b9207b8b9beff690b8a))
14
+ * **review:** 移除 .spaceflow 目录及其配置文件 ([64b310d](https://github.com/Lydanne/spaceflow/commit/64b310d8a77614a259a8d7588a09169626efb3ae))
15
+ * **scripts:** released version 0.20.0 [no ci] ([e1fac49](https://github.com/Lydanne/spaceflow/commit/e1fac49257bf4a5902c5884ec0e054384a7859d6))
16
+ * **shell:** released version 0.20.0 [no ci] ([8b69b53](https://github.com/Lydanne/spaceflow/commit/8b69b5340fe99973add2bea3e7d53f2082d0da54))
17
+
3
18
  ## [0.50.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.49.0...@spaceflow/review@0.50.0) (2026-02-27)
4
19
 
5
20
  ### 新特性
package/dist/index.js CHANGED
@@ -2742,12 +2742,24 @@ ${fileChanges || "无"}`;
2742
2742
  return match ? match[1] : null;
2743
2743
  }
2744
2744
  /**
2745
+ * 判断评论是否为 AI 生成的评论(非用户真实回复)
2746
+ * 除 issue-key 标记外,还通过结构化格式特征识别
2747
+ */ isAiGeneratedComment(body) {
2748
+ if (!body) return false;
2749
+ // 含 issue-key 标记
2750
+ if (body.includes("<!-- issue-key:")) return true;
2751
+ // 含 AI 评论的结构化格式特征(同时包含「规则」和「文件」字段)
2752
+ if (body.includes("- **规则**:") && body.includes("- **文件**:")) return true;
2753
+ return false;
2754
+ }
2755
+ /**
2745
2756
  * 同步评论回复到对应的 issues
2746
2757
  * review 评论回复是通过同一个 review 下的后续评论实现的
2747
2758
  *
2748
2759
  * 通过 AI 评论 body 中嵌入的 issue key(`<!-- issue-key: file:line:ruleId -->`)精确匹配 issue:
2749
2760
  * - 含 issue key 的评论是 AI 自身评论,过滤掉不作为回复
2750
- * - 不含 issue key 的评论是用户真实回复,归到其前面最近的 AI 评论对应的 issue
2761
+ * - 不含 issue key 但匹配 AI 格式特征的评论也视为 AI 评论,过滤掉
2762
+ * - 其余评论是用户真实回复,归到其前面最近的 AI 评论对应的 issue
2751
2763
  */ async syncRepliesToIssues(_owner, _repo, _prNumber, reviewComments, result) {
2752
2764
  try {
2753
2765
  // 构建 issue key → issue 的映射,用于快速查找
@@ -2776,12 +2788,17 @@ ${fileChanges || "无"}`;
2776
2788
  // 遍历评论,用 issue key 精确匹配
2777
2789
  let lastIssueKey = null;
2778
2790
  for (const comment of comments){
2779
- const issueKey = this.extractIssueKeyFromBody(comment.body || "");
2791
+ const commentBody = comment.body || "";
2792
+ const issueKey = this.extractIssueKeyFromBody(commentBody);
2780
2793
  if (issueKey) {
2781
- // AI 自身评论,记录 issue key 但不作为回复
2794
+ // AI 自身评论(含 issue-key),记录 issue key 但不作为回复
2782
2795
  lastIssueKey = issueKey;
2783
2796
  continue;
2784
2797
  }
2798
+ // 跳过不含 issue-key 但匹配 AI 格式特征的评论(如其他轮次的 bot 评论)
2799
+ if (this.isAiGeneratedComment(commentBody)) {
2800
+ continue;
2801
+ }
2785
2802
  // 用户真实回复,通过前面最近的 AI 评论的 issue key 精确匹配
2786
2803
  let matchedIssue = lastIssueKey ? issueByKey.get(lastIssueKey) ?? null : null;
2787
2804
  // 回退:如果 issue key 匹配失败,使用 path:position 匹配
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -28,7 +28,7 @@
28
28
  "@spaceflow/cli": "0.38.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@spaceflow/core": "0.18.0"
31
+ "@spaceflow/core": "0.19.0"
32
32
  },
33
33
  "spaceflow": {
34
34
  "type": "flow",
@@ -2499,6 +2499,29 @@ describe("ReviewService", () => {
2499
2499
  });
2500
2500
  });
2501
2501
 
2502
+ describe("ReviewService.isAiGeneratedComment", () => {
2503
+ it("should detect comment with issue-key marker", () => {
2504
+ const body = `🟡 **问题**\n<!-- issue-key: test.ts:10:Rule1 -->`;
2505
+ expect((service as any).isAiGeneratedComment(body)).toBe(true);
2506
+ });
2507
+
2508
+ it("should detect comment with structured AI format (规则 + 文件)", () => {
2509
+ const body = ` **魔法字符串问题**\n- **文件**: \`test.ts:64-98\`\n- **规则**: \`JsTs.Base.NoMagicStringsAndNumbers\` (来自 \`js&ts.base.md\`)`;
2510
+ expect((service as any).isAiGeneratedComment(body)).toBe(true);
2511
+ });
2512
+
2513
+ it("should return false for normal user reply", () => {
2514
+ expect((service as any).isAiGeneratedComment("这个问题已经修复了")).toBe(false);
2515
+ expect((service as any).isAiGeneratedComment("LGTM")).toBe(false);
2516
+ expect((service as any).isAiGeneratedComment("")).toBe(false);
2517
+ });
2518
+
2519
+ it("should return false for partial match (only 规则 or only 文件)", () => {
2520
+ expect((service as any).isAiGeneratedComment("- **规则**: something")).toBe(false);
2521
+ expect((service as any).isAiGeneratedComment("- **文件**: something")).toBe(false);
2522
+ });
2523
+ });
2524
+
2502
2525
  describe("ReviewService.syncRepliesToIssues", () => {
2503
2526
  it("should sync user replies to matched issues and filter out AI comments", async () => {
2504
2527
  mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
@@ -2618,6 +2641,44 @@ describe("ReviewService", () => {
2618
2641
  // 两条都不含 issue key,都会通过 fallback path:position 匹配
2619
2642
  expect(result.issues[0].replies).toHaveLength(2);
2620
2643
  });
2644
+
2645
+ it("should filter out bot comments with AI structured format but without issue-key", async () => {
2646
+ mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
2647
+ const reviewComments = [
2648
+ {
2649
+ id: 1,
2650
+ path: "test.ts",
2651
+ position: 10,
2652
+ body: `🟡 **问题描述**\n<!-- issue-key: test.ts:10:JsTs.Base.ComplexFunc -->`,
2653
+ user: { id: 1, login: "bot" },
2654
+ created_at: "2024-01-01T01:00:00Z",
2655
+ },
2656
+ {
2657
+ id: 2,
2658
+ path: "test.ts",
2659
+ position: 10,
2660
+ body: ` **魔法字符串问题**\n- **文件**: \`test.ts:64-98\`\n- **规则**: \`JsTs.Base.NoMagicStringsAndNumbers\` (来自 \`js&ts.base.md\`)\n- **Commit**: 3390baa\n- **建议**:\n\`\`\`ts\nconst UNKNOWN = '未知';\n\`\`\``,
2661
+ user: { id: 12, login: "GiteaActions" },
2662
+ created_at: "2024-01-01T02:00:00Z",
2663
+ },
2664
+ {
2665
+ id: 3,
2666
+ path: "test.ts",
2667
+ position: 10,
2668
+ body: "已修复,谢谢",
2669
+ user: { id: 5, login: "dev" },
2670
+ created_at: "2024-01-01T03:00:00Z",
2671
+ },
2672
+ ];
2673
+ const result = {
2674
+ issues: [{ file: "test.ts", line: "10", ruleId: "JsTs.Base.ComplexFunc" } as any],
2675
+ };
2676
+ await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
2677
+ // bot 的结构化评论应被过滤,只保留用户的真实回复
2678
+ expect(result.issues[0].replies).toHaveLength(1);
2679
+ expect(result.issues[0].replies[0].body).toBe("已修复,谢谢");
2680
+ expect(result.issues[0].replies[0].user.login).toBe("dev");
2681
+ });
2621
2682
  });
2622
2683
 
2623
2684
  describe("ReviewService.execute - CI with existingResult", () => {
@@ -2269,13 +2269,27 @@ ${fileChanges || "无"}`;
2269
2269
  return match ? match[1] : null;
2270
2270
  }
2271
2271
 
2272
+ /**
2273
+ * 判断评论是否为 AI 生成的评论(非用户真实回复)
2274
+ * 除 issue-key 标记外,还通过结构化格式特征识别
2275
+ */
2276
+ protected isAiGeneratedComment(body: string): boolean {
2277
+ if (!body) return false;
2278
+ // 含 issue-key 标记
2279
+ if (body.includes("<!-- issue-key:")) return true;
2280
+ // 含 AI 评论的结构化格式特征(同时包含「规则」和「文件」字段)
2281
+ if (body.includes("- **规则**:") && body.includes("- **文件**:")) return true;
2282
+ return false;
2283
+ }
2284
+
2272
2285
  /**
2273
2286
  * 同步评论回复到对应的 issues
2274
2287
  * review 评论回复是通过同一个 review 下的后续评论实现的
2275
2288
  *
2276
2289
  * 通过 AI 评论 body 中嵌入的 issue key(`<!-- issue-key: file:line:ruleId -->`)精确匹配 issue:
2277
2290
  * - 含 issue key 的评论是 AI 自身评论,过滤掉不作为回复
2278
- * - 不含 issue key 的评论是用户真实回复,归到其前面最近的 AI 评论对应的 issue
2291
+ * - 不含 issue key 但匹配 AI 格式特征的评论也视为 AI 评论,过滤掉
2292
+ * - 其余评论是用户真实回复,归到其前面最近的 AI 评论对应的 issue
2279
2293
  */
2280
2294
  protected async syncRepliesToIssues(
2281
2295
  _owner: string,
@@ -2318,12 +2332,17 @@ ${fileChanges || "无"}`;
2318
2332
  // 遍历评论,用 issue key 精确匹配
2319
2333
  let lastIssueKey: string | null = null;
2320
2334
  for (const comment of comments) {
2321
- const issueKey = this.extractIssueKeyFromBody(comment.body || "");
2335
+ const commentBody = comment.body || "";
2336
+ const issueKey = this.extractIssueKeyFromBody(commentBody);
2322
2337
  if (issueKey) {
2323
- // AI 自身评论,记录 issue key 但不作为回复
2338
+ // AI 自身评论(含 issue-key),记录 issue key 但不作为回复
2324
2339
  lastIssueKey = issueKey;
2325
2340
  continue;
2326
2341
  }
2342
+ // 跳过不含 issue-key 但匹配 AI 格式特征的评论(如其他轮次的 bot 评论)
2343
+ if (this.isAiGeneratedComment(commentBody)) {
2344
+ continue;
2345
+ }
2327
2346
  // 用户真实回复,通过前面最近的 AI 评论的 issue key 精确匹配
2328
2347
  let matchedIssue = lastIssueKey ? (issueByKey.get(lastIssueKey) ?? null) : null;
2329
2348
  // 回退:如果 issue key 匹配失败,使用 path:position 匹配