@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.
@@ -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 事件,仅收集 review 状态
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 状态(在删除旧 review 之前)
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
- // 删除已有的 AI review(避免重复评论)
1883
- await this.deleteExistingAiReviews(owner, repo, prNumber);
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
- // 使用 PR Review 发布主评论 + 行级评论(合并为一个消息块)
1915
- await this.gitProvider.createPullReview(owner, repo, prNumber, {
1916
- event: "COMMENT",
1917
- body: reviewBody,
1918
- comments,
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
- * 从旧的 AI review 中获取已解决的评论,同步 fixed 状态到 result.issues
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 reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
1939
- const aiReview = reviews.find((r) => r.body?.includes(REVIEW_COMMENT_MARKER));
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 comment of resolvedComments) {
2031
+ for (const thread of resolvedThreads) {
2032
+ if (!thread.path) continue;
1960
2033
  const matchedIssue = result.issues.find(
1961
2034
  (issue) =>
1962
- issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position),
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(REVIEW_COMMENT_MARKER));
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(REVIEW_COMMENT_MARKER)) {
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.getIssueCommentReactions(
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((r) => r.body?.includes(REVIEW_COMMENT_MARKER));
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
- await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
2286
+ try {
2287
+ await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
2288
+ deletedCount++;
2289
+ } catch {
2290
+ // 已提交的 review 无法删除,忽略
2291
+ }
2207
2292
  }
2208
2293
  }
2209
- if (aiReviews.length > 0) {
2210
- console.log(`🗑️ 已删除 ${aiReviews.length} 个旧的 AI review`);
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("⚠️ 删除旧 AI review 失败:", error);
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
- // 只有 valid === 'true' 的历史问题才阻止新问题,其他情况允许覆盖
2503
- const existingKeys = new Set(
2504
- existingIssues
2505
- .filter((issue) => issue.valid === "true")
2506
- .map((issue) => this.generateIssueKey(issue)),
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
- // 从 PR Review 获取已有的审查结果
2522
- const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2523
- const existingReview = reviews.find((r) => r.body?.includes(REVIEW_COMMENT_MARKER));
2524
- if (existingReview?.body) {
2525
- return this.parseExistingReviewResult(existingReview.body);
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);