@spaceflow/review 0.30.0 → 0.32.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 +90 -0
- package/dist/index.js +161 -52
- package/package.json +3 -3
- package/src/index.ts +7 -2
- package/src/locales/en/review.json +1 -0
- package/src/locales/zh-cn/review.json +1 -0
- package/src/review.config.ts +2 -0
- package/src/review.service.spec.ts +123 -63
- package/src/review.service.ts +159 -58
package/src/review.service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
PullRequestCommit,
|
|
6
6
|
ChangedFile,
|
|
7
7
|
CreatePullReviewComment,
|
|
8
|
+
REVIEW_STATE,
|
|
8
9
|
CiConfig,
|
|
9
10
|
type LLMMode,
|
|
10
11
|
LlmProxyService,
|
|
@@ -94,6 +95,7 @@ export interface LLMReviewOptions {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
const REVIEW_COMMENT_MARKER = "<!-- spaceflow-review -->";
|
|
98
|
+
const REVIEW_LINE_COMMENTS_MARKER = "<!-- spaceflow-review-lines -->";
|
|
97
99
|
|
|
98
100
|
const REVIEW_SCHEMA: LlmJsonPutSchema = {
|
|
99
101
|
type: "object",
|
|
@@ -289,6 +291,7 @@ export class ReviewService {
|
|
|
289
291
|
retryDelay: options.retryDelay ?? reviewConf.retryDelay ?? 1000,
|
|
290
292
|
generateDescription: options.generateDescription ?? reviewConf.generateDescription ?? false,
|
|
291
293
|
showAll: options.showAll ?? false,
|
|
294
|
+
flush: options.flush ?? false,
|
|
292
295
|
eventAction: options.eventAction,
|
|
293
296
|
};
|
|
294
297
|
}
|
|
@@ -403,8 +406,8 @@ export class ReviewService {
|
|
|
403
406
|
return this.executeDeletionOnly(context);
|
|
404
407
|
}
|
|
405
408
|
|
|
406
|
-
// 如果是 closed
|
|
407
|
-
if (context.eventAction === "closed") {
|
|
409
|
+
// 如果是 closed 事件或 flush 模式,仅收集 review 状态
|
|
410
|
+
if (context.eventAction === "closed" || context.flush) {
|
|
408
411
|
return this.executeCollectOnly(context);
|
|
409
412
|
}
|
|
410
413
|
|
|
@@ -825,7 +828,7 @@ export class ReviewService {
|
|
|
825
828
|
}
|
|
826
829
|
|
|
827
830
|
/**
|
|
828
|
-
* 仅收集 review 状态模式(用于 PR
|
|
831
|
+
* 仅收集 review 状态模式(用于 PR 关闭或 --flush 指令)
|
|
829
832
|
* 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
|
|
830
833
|
*/
|
|
831
834
|
protected async executeCollectOnly(context: ReviewContext): Promise<ReviewResult> {
|
|
@@ -1873,14 +1876,14 @@ ${fileChanges || "无"}`;
|
|
|
1873
1876
|
}
|
|
1874
1877
|
}
|
|
1875
1878
|
|
|
1876
|
-
// 获取已解决的评论,同步 fixed
|
|
1879
|
+
// 获取已解决的评论,同步 fixed 状态(在更新 review 之前)
|
|
1877
1880
|
await this.syncResolvedComments(owner, repo, prNumber, result);
|
|
1878
1881
|
|
|
1879
1882
|
// 获取评论的 reactions,同步 valid 状态(👎 标记为无效)
|
|
1880
1883
|
await this.syncReactionsToIssues(owner, repo, prNumber, result, verbose);
|
|
1881
1884
|
|
|
1882
|
-
//
|
|
1883
|
-
await this.
|
|
1885
|
+
// 查找已有的 AI 评论(Issue Comment)
|
|
1886
|
+
const existingComment = await this.findExistingAiComment(owner, repo, prNumber);
|
|
1884
1887
|
|
|
1885
1888
|
// 调试:检查 issues 是否有 author
|
|
1886
1889
|
if (shouldLog(verbose, 3)) {
|
|
@@ -1901,7 +1904,55 @@ ${fileChanges || "无"}`;
|
|
|
1901
1904
|
const pr = await this.gitProvider.getPullRequest(owner, repo, prNumber);
|
|
1902
1905
|
const commitId = pr.head?.sha;
|
|
1903
1906
|
|
|
1904
|
-
//
|
|
1907
|
+
// 1. 发布或更新主评论(使用 Issue Comment API,支持删除和更新)
|
|
1908
|
+
try {
|
|
1909
|
+
if (existingComment?.id) {
|
|
1910
|
+
await this.gitProvider.updateIssueComment(owner, repo, existingComment.id, reviewBody);
|
|
1911
|
+
console.log(`✅ 已更新 AI Review 评论`);
|
|
1912
|
+
} else {
|
|
1913
|
+
await this.gitProvider.createIssueComment(owner, repo, prNumber, { body: reviewBody });
|
|
1914
|
+
console.log(`✅ 已发布 AI Review 评论`);
|
|
1915
|
+
}
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// 2. 删除旧的行级评论(逐条删除 PR Review Comment)
|
|
1921
|
+
try {
|
|
1922
|
+
const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
|
|
1923
|
+
const oldLineReviews = reviews.filter((r) => r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
1924
|
+
for (const review of oldLineReviews) {
|
|
1925
|
+
if (review.id) {
|
|
1926
|
+
const reviewComments = await this.gitProvider.listPullReviewComments(
|
|
1927
|
+
owner,
|
|
1928
|
+
repo,
|
|
1929
|
+
prNumber,
|
|
1930
|
+
review.id,
|
|
1931
|
+
);
|
|
1932
|
+
for (const comment of reviewComments) {
|
|
1933
|
+
if (comment.id) {
|
|
1934
|
+
try {
|
|
1935
|
+
await this.gitProvider.deletePullReviewComment(owner, repo, comment.id);
|
|
1936
|
+
} catch {
|
|
1937
|
+
// 删除失败忽略
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
// 评论删除后尝试删除 review 本身
|
|
1942
|
+
try {
|
|
1943
|
+
await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
|
|
1944
|
+
} catch {
|
|
1945
|
+
// 已提交的 review 无法删除,忽略
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (oldLineReviews.length > 0) {
|
|
1950
|
+
console.log(`🗑️ 已清理 ${oldLineReviews.length} 个旧的行级评论 review`);
|
|
1951
|
+
}
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
console.warn("⚠️ 清理旧行级评论失败:", error);
|
|
1954
|
+
}
|
|
1955
|
+
// 3. 发布新的行级评论(使用 PR Review API)
|
|
1905
1956
|
let comments: CreatePullReviewComment[] = [];
|
|
1906
1957
|
if (reviewConf.lineComments) {
|
|
1907
1958
|
comments = result.issues
|
|
@@ -1909,24 +1960,61 @@ ${fileChanges || "无"}`;
|
|
|
1909
1960
|
.map((issue) => this.issueToReviewComment(issue))
|
|
1910
1961
|
.filter((comment): comment is CreatePullReviewComment => comment !== null);
|
|
1911
1962
|
}
|
|
1963
|
+
if (comments.length > 0) {
|
|
1964
|
+
try {
|
|
1965
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
1966
|
+
event: REVIEW_STATE.COMMENT,
|
|
1967
|
+
body: REVIEW_LINE_COMMENTS_MARKER,
|
|
1968
|
+
comments,
|
|
1969
|
+
commit_id: commitId,
|
|
1970
|
+
});
|
|
1971
|
+
console.log(`✅ 已发布 ${comments.length} 条行级评论`);
|
|
1972
|
+
} catch {
|
|
1973
|
+
// 批量失败时逐条发布,跳过无法定位的评论
|
|
1974
|
+
console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
|
|
1975
|
+
let successCount = 0;
|
|
1976
|
+
for (const comment of comments) {
|
|
1977
|
+
try {
|
|
1978
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
1979
|
+
event: REVIEW_STATE.COMMENT,
|
|
1980
|
+
body: successCount === 0 ? REVIEW_LINE_COMMENTS_MARKER : undefined,
|
|
1981
|
+
comments: [comment],
|
|
1982
|
+
commit_id: commitId,
|
|
1983
|
+
});
|
|
1984
|
+
successCount++;
|
|
1985
|
+
} catch {
|
|
1986
|
+
console.warn(`⚠️ 跳过无法定位的评论: ${comment.path}:${comment.new_position}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (successCount > 0) {
|
|
1990
|
+
console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
|
|
1991
|
+
} else {
|
|
1992
|
+
console.warn("⚠️ 所有行级评论均无法定位,已跳过");
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1912
1997
|
|
|
1998
|
+
/**
|
|
1999
|
+
* 查找已有的 AI 评论(Issue Comment)
|
|
2000
|
+
*/
|
|
2001
|
+
protected async findExistingAiComment(
|
|
2002
|
+
owner: string,
|
|
2003
|
+
repo: string,
|
|
2004
|
+
prNumber: number,
|
|
2005
|
+
): Promise<{ id: number } | null> {
|
|
1913
2006
|
try {
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
commit_id: commitId,
|
|
1920
|
-
});
|
|
1921
|
-
const lineMsg = comments.length > 0 ? `,包含 ${comments.length} 条行级评论` : "";
|
|
1922
|
-
console.log(`✅ 已发布 AI Review${lineMsg}`);
|
|
1923
|
-
} catch (error) {
|
|
1924
|
-
console.warn("⚠️ 发布 AI Review 失败:", error);
|
|
2007
|
+
const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
|
|
2008
|
+
const aiComment = comments.find((c) => c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
2009
|
+
return aiComment?.id ? { id: aiComment.id } : null;
|
|
2010
|
+
} catch {
|
|
2011
|
+
return null;
|
|
1925
2012
|
}
|
|
1926
2013
|
}
|
|
1927
2014
|
|
|
1928
2015
|
/**
|
|
1929
|
-
*
|
|
2016
|
+
* 从 PR 的所有 resolved review threads 中同步 fixed 状态到 result.issues
|
|
2017
|
+
* 直接通过 GraphQL 查询所有 resolved threads 的 path+line,匹配 issues
|
|
1930
2018
|
*/
|
|
1931
2019
|
protected async syncResolvedComments(
|
|
1932
2020
|
owner: string,
|
|
@@ -1935,31 +2023,16 @@ ${fileChanges || "无"}`;
|
|
|
1935
2023
|
result: ReviewResult,
|
|
1936
2024
|
): Promise<void> {
|
|
1937
2025
|
try {
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
if (!aiReview?.id) {
|
|
2026
|
+
const resolvedThreads = await this.gitProvider.listResolvedThreads(owner, repo, prNumber);
|
|
2027
|
+
if (resolvedThreads.length === 0) {
|
|
1941
2028
|
return;
|
|
1942
2029
|
}
|
|
1943
|
-
// 获取该 review 的所有行级评论
|
|
1944
|
-
const reviewComments = await this.gitProvider.listPullReviewComments(
|
|
1945
|
-
owner,
|
|
1946
|
-
repo,
|
|
1947
|
-
prNumber,
|
|
1948
|
-
aiReview.id,
|
|
1949
|
-
);
|
|
1950
|
-
// 找出已解决的评论(resolver 不为 null)
|
|
1951
|
-
const resolvedComments = reviewComments.filter(
|
|
1952
|
-
(c) => c.resolver !== null && c.resolver !== undefined,
|
|
1953
|
-
);
|
|
1954
|
-
if (resolvedComments.length === 0) {
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
// 根据文件路径和行号匹配 issues,标记为已解决
|
|
1958
2030
|
const now = new Date().toISOString();
|
|
1959
|
-
for (const
|
|
2031
|
+
for (const thread of resolvedThreads) {
|
|
2032
|
+
if (!thread.path) continue;
|
|
1960
2033
|
const matchedIssue = result.issues.find(
|
|
1961
2034
|
(issue) =>
|
|
1962
|
-
issue.file ===
|
|
2035
|
+
issue.file === thread.path && this.lineMatchesPosition(issue.line, thread.line),
|
|
1963
2036
|
);
|
|
1964
2037
|
if (matchedIssue && !matchedIssue.fixed) {
|
|
1965
2038
|
matchedIssue.fixed = now;
|
|
@@ -1998,7 +2071,7 @@ ${fileChanges || "无"}`;
|
|
|
1998
2071
|
): Promise<void> {
|
|
1999
2072
|
try {
|
|
2000
2073
|
const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
|
|
2001
|
-
const aiReview = reviews.find((r) => r.body?.includes(
|
|
2074
|
+
const aiReview = reviews.find((r) => r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
2002
2075
|
if (!aiReview?.id) {
|
|
2003
2076
|
if (shouldLog(verbose, 2)) {
|
|
2004
2077
|
console.log(`[syncReactionsToIssues] No AI review found`);
|
|
@@ -2011,7 +2084,7 @@ ${fileChanges || "无"}`;
|
|
|
2011
2084
|
|
|
2012
2085
|
// 1. 从已提交的 review 中获取评审人(排除 AI bot)
|
|
2013
2086
|
for (const review of reviews) {
|
|
2014
|
-
if (review.user?.login && !review.body?.includes(
|
|
2087
|
+
if (review.user?.login && !review.body?.includes(REVIEW_LINE_COMMENTS_MARKER)) {
|
|
2015
2088
|
reviewers.add(review.user.login);
|
|
2016
2089
|
}
|
|
2017
2090
|
}
|
|
@@ -2088,7 +2161,7 @@ ${fileChanges || "无"}`;
|
|
|
2088
2161
|
commentIdToIssue.set(comment.id, matchedIssue);
|
|
2089
2162
|
}
|
|
2090
2163
|
try {
|
|
2091
|
-
const reactions = await this.gitProvider.
|
|
2164
|
+
const reactions = await this.gitProvider.getPullReviewCommentReactions(
|
|
2092
2165
|
owner,
|
|
2093
2166
|
repo,
|
|
2094
2167
|
comment.id,
|
|
@@ -2192,25 +2265,54 @@ ${fileChanges || "无"}`;
|
|
|
2192
2265
|
|
|
2193
2266
|
/**
|
|
2194
2267
|
* 删除已有的 AI review(通过 marker 识别)
|
|
2268
|
+
* - 删除行级评论的 PR Review(带 REVIEW_LINE_COMMENTS_MARKER)
|
|
2269
|
+
* - 删除主评论的 Issue Comment(带 REVIEW_COMMENT_MARKER)
|
|
2195
2270
|
*/
|
|
2196
2271
|
protected async deleteExistingAiReviews(
|
|
2197
2272
|
owner: string,
|
|
2198
2273
|
repo: string,
|
|
2199
2274
|
prNumber: number,
|
|
2200
2275
|
): Promise<void> {
|
|
2276
|
+
let deletedCount = 0;
|
|
2277
|
+
// 删除行级评论的 PR Review
|
|
2201
2278
|
try {
|
|
2202
2279
|
const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
|
|
2203
|
-
const aiReviews = reviews.filter(
|
|
2280
|
+
const aiReviews = reviews.filter(
|
|
2281
|
+
(r) =>
|
|
2282
|
+
r.body?.includes(REVIEW_LINE_COMMENTS_MARKER) || r.body?.includes(REVIEW_COMMENT_MARKER),
|
|
2283
|
+
);
|
|
2204
2284
|
for (const review of aiReviews) {
|
|
2205
2285
|
if (review.id) {
|
|
2206
|
-
|
|
2286
|
+
try {
|
|
2287
|
+
await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
|
|
2288
|
+
deletedCount++;
|
|
2289
|
+
} catch {
|
|
2290
|
+
// 已提交的 review 无法删除,忽略
|
|
2291
|
+
}
|
|
2207
2292
|
}
|
|
2208
2293
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2294
|
+
} catch (error) {
|
|
2295
|
+
console.warn("⚠️ 列出 PR reviews 失败:", error);
|
|
2296
|
+
}
|
|
2297
|
+
// 删除主评论的 Issue Comment
|
|
2298
|
+
try {
|
|
2299
|
+
const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
|
|
2300
|
+
const aiComments = comments.filter((c) => c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
2301
|
+
for (const comment of aiComments) {
|
|
2302
|
+
if (comment.id) {
|
|
2303
|
+
try {
|
|
2304
|
+
await this.gitProvider.deleteIssueComment(owner, repo, comment.id);
|
|
2305
|
+
deletedCount++;
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
console.warn(`⚠️ 删除评论 ${comment.id} 失败:`, error);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2211
2310
|
}
|
|
2212
2311
|
} catch (error) {
|
|
2213
|
-
console.warn("⚠️
|
|
2312
|
+
console.warn("⚠️ 列出 issue comments 失败:", error);
|
|
2313
|
+
}
|
|
2314
|
+
if (deletedCount > 0) {
|
|
2315
|
+
console.log(`🗑️ 已删除 ${deletedCount} 个旧的 AI review`);
|
|
2214
2316
|
}
|
|
2215
2317
|
}
|
|
2216
2318
|
|
|
@@ -2499,12 +2601,11 @@ ${fileChanges || "无"}`;
|
|
|
2499
2601
|
newIssues: ReviewIssue[],
|
|
2500
2602
|
existingIssues: ReviewIssue[],
|
|
2501
2603
|
): { filteredIssues: ReviewIssue[]; skippedCount: number } {
|
|
2502
|
-
//
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
);
|
|
2604
|
+
// 所有历史问题(无论 valid 状态)都阻止新问题重复添加
|
|
2605
|
+
// valid='false' 的问题已被评审人标记为无效,不应再次报告
|
|
2606
|
+
// valid='true' 的问题已存在,无需重复
|
|
2607
|
+
// fixed 的问题已解决,无需重复
|
|
2608
|
+
const existingKeys = new Set(existingIssues.map((issue) => this.generateIssueKey(issue)));
|
|
2508
2609
|
const filteredIssues = newIssues.filter(
|
|
2509
2610
|
(issue) => !existingKeys.has(this.generateIssueKey(issue)),
|
|
2510
2611
|
);
|
|
@@ -2518,11 +2619,11 @@ ${fileChanges || "无"}`;
|
|
|
2518
2619
|
prNumber: number,
|
|
2519
2620
|
): Promise<ReviewResult | null> {
|
|
2520
2621
|
try {
|
|
2521
|
-
// 从
|
|
2522
|
-
const
|
|
2523
|
-
const
|
|
2524
|
-
if (
|
|
2525
|
-
return this.parseExistingReviewResult(
|
|
2622
|
+
// 从 Issue Comment 获取已有的审查结果
|
|
2623
|
+
const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
|
|
2624
|
+
const existingComment = comments.find((c) => c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
2625
|
+
if (existingComment?.body) {
|
|
2626
|
+
return this.parseExistingReviewResult(existingComment.body);
|
|
2526
2627
|
}
|
|
2527
2628
|
} catch (error) {
|
|
2528
2629
|
console.warn("⚠️ 获取已有评论失败:", error);
|