@spaceflow/review 0.51.0 → 0.52.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 +9 -0
- package/dist/index.js +20 -3
- package/package.json +1 -1
- package/src/review.service.spec.ts +61 -0
- package/src/review.service.ts +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.51.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.50.0...@spaceflow/review@0.51.0) (2026-02-27)
|
|
4
|
+
|
|
5
|
+
### 其他修改
|
|
6
|
+
|
|
7
|
+
* **review-summary:** released version 0.20.0 [no ci] ([bb3f815](https://github.com/Lydanne/spaceflow/commit/bb3f81567bf6946964a19b9207b8b9beff690b8a))
|
|
8
|
+
* **review:** 移除 .spaceflow 目录及其配置文件 ([64b310d](https://github.com/Lydanne/spaceflow/commit/64b310d8a77614a259a8d7588a09169626efb3ae))
|
|
9
|
+
* **scripts:** released version 0.20.0 [no ci] ([e1fac49](https://github.com/Lydanne/spaceflow/commit/e1fac49257bf4a5902c5884ec0e054384a7859d6))
|
|
10
|
+
* **shell:** released version 0.20.0 [no ci] ([8b69b53](https://github.com/Lydanne/spaceflow/commit/8b69b5340fe99973add2bea3e7d53f2082d0da54))
|
|
11
|
+
|
|
3
12
|
## [0.50.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.49.0...@spaceflow/review@0.50.0) (2026-02-27)
|
|
4
13
|
|
|
5
14
|
### 新特性
|
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
|
|
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
|
|
2791
|
+
const commentBody = comment.body || "";
|
|
2792
|
+
const issueKey = this.extractIssueKeyFromBody(commentBody);
|
|
2780
2793
|
if (issueKey) {
|
|
2781
|
-
// AI
|
|
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
|
@@ -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", () => {
|
package/src/review.service.ts
CHANGED
|
@@ -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
|
|
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
|
|
2335
|
+
const commentBody = comment.body || "";
|
|
2336
|
+
const issueKey = this.extractIssueKeyFromBody(commentBody);
|
|
2322
2337
|
if (issueKey) {
|
|
2323
|
-
// AI
|
|
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 匹配
|