@spaceflow/review 0.30.0 → 0.31.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,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.29.3...@spaceflow/review@0.30.0) (2026-02-16)
4
+
5
+ ### 新特性
6
+
7
+ * **ci-scripts:** 迁移到新架构格式 ([5df3630](https://github.com/Lydanne/spaceflow/commit/5df3630208894e6543d34f9985fcf0f612a78d7e))
8
+ * **ci-shell:** 迁移到新架构格式 ([b7d92fd](https://github.com/Lydanne/spaceflow/commit/b7d92fd5f83e9a5e8edeb0ce4971b19190078a7f))
9
+ * **cli:** 修复 commander.js 集成问题 ([8e6aeca](https://github.com/Lydanne/spaceflow/commit/8e6aecadb06e6168050371ec5da3b0a3c40d51c2))
10
+ * **cli:** 阶段3 - 创建新 CLI 架构基础 ([deeaf0d](https://github.com/Lydanne/spaceflow/commit/deeaf0dfeedb3e8c9a8f30fcd05c37e9db18cb55))
11
+ * **core:** 阶段2 - 去除核心服务的 NestJS 装饰器 ([684ec07](https://github.com/Lydanne/spaceflow/commit/684ec0723a90383a6c5fb84fc9198d8b908a89d8))
12
+ * **period-summary:** 迁移到新架构格式 ([b6c7a92](https://github.com/Lydanne/spaceflow/commit/b6c7a927963c84349637884e8fab37d550daa91f))
13
+ * **publish:** 迁移到新架构格式 ([5df49a8](https://github.com/Lydanne/spaceflow/commit/5df49a8b3df6906e7ff65fd2a3bae4ee5ef31bc5))
14
+ * **review:** 迁移到新架构格式 ([6bda477](https://github.com/Lydanne/spaceflow/commit/6bda4773eed9cdbc82593ee29888d1d2cce6efef))
15
+
16
+ ### 修复BUG
17
+
18
+ * 修复 ReviewSpecService 构造函数调用,传入 gitProvider 参数 ([747b87e](https://github.com/Lydanne/spaceflow/commit/747b87e7e4621b8976c6444e0350e71600c20801))
19
+ * 修复 StorageServiceOptions 类型错误 ([0bd2820](https://github.com/Lydanne/spaceflow/commit/0bd2820af594f2f30473e57ba4843197e3e7a2ff))
20
+ * 修复 TypeScript 编译配置 ([8c3a4f1](https://github.com/Lydanne/spaceflow/commit/8c3a4f148ccbd5598fb553608f4832ec6a4dc40b))
21
+ * 修复扩展 tsconfig.json 中的路径错误 ([ff42b24](https://github.com/Lydanne/spaceflow/commit/ff42b244645128b1c31714e6e6cf6c71fa4bbc80))
22
+ * 清理 list.service.ts 中的 NestJS 依赖 ([f483bc1](https://github.com/Lydanne/spaceflow/commit/f483bc1edcaf472acf2bdc38902d992840d004c8))
23
+ * 清理命令服务中的 NestJS 依赖 ([a5ca870](https://github.com/Lydanne/spaceflow/commit/a5ca870782eaf4117ecc4f58a689e001df34ce5e))
24
+ * 清理命令服务中的 NestJS 依赖 ([2bebe2a](https://github.com/Lydanne/spaceflow/commit/2bebe2ab23367d661706368c37c01bc6360e5e36))
25
+
26
+ ### 代码重构
27
+
28
+ * 使用 ciConfig 函数替代配置读取,简化 CI 配置获取 ([9356752](https://github.com/Lydanne/spaceflow/commit/93567520181a95398f36413a68e8334342d01e51))
29
+ * 删除不再使用的 cli.module.ts ([5b86cb2](https://github.com/Lydanne/spaceflow/commit/5b86cb21d0b55aa4ed8934a51c7bba88f3892ece))
30
+ * 删除所有不再使用的 .module.ts 文件 ([7049a59](https://github.com/Lydanne/spaceflow/commit/7049a59fca05906e163a58a2bc1ad64b51dbccf1))
31
+ * 增强 MCP 工具收集的日志输出 ([e889c2b](https://github.com/Lydanne/spaceflow/commit/e889c2bad0c2dba833905299af2cb4a76126ed0e))
32
+ * 实现 MCP 工具收集和 Inspector 模式支持 ([0702d83](https://github.com/Lydanne/spaceflow/commit/0702d83791e9836ed54576b6d3d373a0fc5085b4))
33
+ * 支持 verbose 计数选项,实现 -vvv 累加级别 ([42ab32d](https://github.com/Lydanne/spaceflow/commit/42ab32dbbf42cff1774d27a1ba2fedab02f33038))
34
+ * 清理 NestJS 依赖和模块文件 ([3efe7d7](https://github.com/Lydanne/spaceflow/commit/3efe7d7555746f93737d91508b06a0061ba2295f))
35
+ * 清理剩余的 NestJS 依赖 ([a3104b3](https://github.com/Lydanne/spaceflow/commit/a3104b36cecb0c93bc839472dba63b88e71bb662))
36
+ * 移除 dtoToJsonSchema 中的 class-validator 元数据推断逻辑 ([0dd1c8e](https://github.com/Lydanne/spaceflow/commit/0dd1c8e1424e1de57ca40b8e317f3bad00d08e7d))
37
+ * 移除 MonorepoService 中的 NestJS 依赖 ([c6a3243](https://github.com/Lydanne/spaceflow/commit/c6a32439da97efb287239e16723f4cb982bdf208))
38
+ * 移除测试文件中的 NestJS 依赖,改用直接实例化 ([325f2e3](https://github.com/Lydanne/spaceflow/commit/325f2e36b434ebb4e471c17b5e2ef2f9a4db2456))
39
+ * 简化 list 命令,只显示外部扩展 ([babcb24](https://github.com/Lydanne/spaceflow/commit/babcb245f527c427103726e290ff889444c1e8da))
40
+ * 统一配置管理,支持环境变量自动合并 ([f6b09a3](https://github.com/Lydanne/spaceflow/commit/f6b09a35c273f1f96f5a07546f2bf58ceb6942f6))
41
+ * 迁移 ci-scripts、ci-shell、publish、review 扩展到新架构 ([4630116](https://github.com/Lydanne/spaceflow/commit/4630116c96699a6a3c36aa439badaa3567cc4c06))
42
+ * 重构服务容器为懒加载依赖注入架构 ([c74a346](https://github.com/Lydanne/spaceflow/commit/c74a346ef27540836b06a8fd825283155722d4af))
43
+
44
+ ### 文档更新
45
+
46
+ * **architecture:** 添加 Spaceflow 架构重设计方案 v2 ([21b252c](https://github.com/Lydanne/spaceflow/commit/21b252c97aaf4561cc5837b68229356e5ffa8d36))
47
+
48
+ ### 其他修改
49
+
50
+ * **ci-scripts:** released version 0.20.0 [no ci] ([ed8d88d](https://github.com/Lydanne/spaceflow/commit/ed8d88df09c7d119df092793e4c83d451d67a6b8))
51
+ * **ci-shell:** released version 0.20.0 [no ci] ([5109d94](https://github.com/Lydanne/spaceflow/commit/5109d944bfbd95596e71d6e11e56d3e3599f8297))
52
+ * **cli:** released version 0.20.0 [no ci] ([7cb015c](https://github.com/Lydanne/spaceflow/commit/7cb015c9fba3b9b4a8a170f66597505300e35e10))
53
+ * **core:** released version 0.2.0 [no ci] ([6176e7e](https://github.com/Lydanne/spaceflow/commit/6176e7e5755dd594dee7d4e0016dfb89b391d824))
54
+ * **period-summary:** released version 0.20.0 [no ci] ([54feb4a](https://github.com/Lydanne/spaceflow/commit/54feb4adaf0d72d402287bef84fd9433db673ed6))
55
+ * **publish:** released version 0.22.0 [no ci] ([2e39f34](https://github.com/Lydanne/spaceflow/commit/2e39f347c514490be5da690c896849fc6dbfd513))
56
+
3
57
  ## [0.29.3](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.29.2...@spaceflow/review@0.29.3) (2026-02-16)
4
58
 
5
59
  ### 修复BUG
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LlmJsonPut, addLocaleResources, calculateNewLineNumber, createStreamLoggerState, defineExtension, logStreamEvent, normalizeVerbose, parallel, parseChangedLinesFromPatch, parseDiffText, parseHunksFromPatch, parseRepoUrl, parseVerbose, shouldLog, t, z } from "@spaceflow/core";
1
+ import { LlmJsonPut, REVIEW_STATE, addLocaleResources, calculateNewLineNumber, createStreamLoggerState, defineExtension, logStreamEvent, normalizeVerbose, parallel, parseChangedLinesFromPatch, parseDiffText, parseHunksFromPatch, parseRepoUrl, parseVerbose, shouldLog, t, z } from "@spaceflow/core";
2
2
  import { basename, dirname, extname, isAbsolute, join, relative } from "path";
3
3
  import { execSync, spawn } from "child_process";
4
4
  import { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
@@ -143,9 +143,9 @@ __webpack_require__.d(__webpack_exports__, {
143
143
  ;// CONCATENATED MODULE: external "@spaceflow/core"
144
144
 
145
145
  ;// CONCATENATED MODULE: ./src/locales/zh-cn/review.json
146
- var review_namespaceObject = JSON.parse('{"description":"代码审查命令,使用 LLM 对 PR 代码进行自动审查","options.dryRun":"仅打印将要执行的操作,不实际提交评论","options.prNumber":"PR 编号,如果不指定则从环境变量获取","options.base":"基准分支/tag,用于比较差异","options.head":"目标分支/tag,用于比较差异","options.includes":"要审查的文件 glob 模式,如 *.ts *.js(可多次指定)","options.llmMode":"使用的 LLM 模式: claude-code, openai, gemini","options.files":"仅审查指定的文件(空格分隔)","options.commits":"仅审查指定的 commits(空格分隔)","options.verifyFixes":"是否验证历史问题是否已修复(默认从配置文件读取)","options.noVerifyFixes":"禁用历史问题验证","options.verifyConcurrency":"验证历史问题的并发数(默认 10)","options.analyzeDeletions":"分析删除代码可能带来的影响 (true, false, ci, pr, terminal)","options.deletionAnalysisMode":"删除代码分析模式: openai (标准模式) 或 claude-code (Agent 模式,可使用工具)","options.deletionOnly":"仅执行删除代码分析,跳过常规代码审查","options.outputFormat":"输出格式: markdown, terminal, json。不指定则智能选择(PR 用 markdown,终端用 terminal)","options.generateDescription":"使用 AI 生成 PR 功能描述","options.showAll":"显示所有发现的问题,不过滤非变更行的问题","options.eventAction":"PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查","extensionDescription":"代码审查命令,使用 LLM 对 PR 代码进行自动审查","mcp.serverDescription":"代码审查规则查询服务","mcp.listRules":"获取当前项目的所有代码审查规则,返回规则列表包含 ID、标题、描述、适用的文件扩展名等信息","mcp.getRulesForFile":"获取某个文件应该使用的代码审查规则,根据文件扩展名和 includes 配置过滤","mcp.getRuleDetail":"获取某个规则的完整详情,包括描述、示例代码等","mcp.ruleNotFound":"规则 {{ruleId}} 不存在","mcp.dto.cwd":"项目根目录路径,默认为当前工作目录","mcp.dto.filePath":"文件路径,可以是相对路径或绝对路径","mcp.dto.includeExamples":"是否包含规则示例代码,默认 false","mcp.dto.ruleId":"规则 ID,如 JsTs.Naming.FileName"}')
146
+ var review_namespaceObject = JSON.parse('{"description":"代码审查命令,使用 LLM 对 PR 代码进行自动审查","options.dryRun":"仅打印将要执行的操作,不实际提交评论","options.prNumber":"PR 编号,如果不指定则从环境变量获取","options.base":"基准分支/tag,用于比较差异","options.head":"目标分支/tag,用于比较差异","options.includes":"要审查的文件 glob 模式,如 *.ts *.js(可多次指定)","options.llmMode":"使用的 LLM 模式: claude-code, openai, gemini","options.files":"仅审查指定的文件(空格分隔)","options.commits":"仅审查指定的 commits(空格分隔)","options.verifyFixes":"是否验证历史问题是否已修复(默认从配置文件读取)","options.noVerifyFixes":"禁用历史问题验证","options.verifyConcurrency":"验证历史问题的并发数(默认 10)","options.analyzeDeletions":"分析删除代码可能带来的影响 (true, false, ci, pr, terminal)","options.deletionAnalysisMode":"删除代码分析模式: openai (标准模式) 或 claude-code (Agent 模式,可使用工具)","options.deletionOnly":"仅执行删除代码分析,跳过常规代码审查","options.outputFormat":"输出格式: markdown, terminal, json。不指定则智能选择(PR 用 markdown,终端用 terminal)","options.generateDescription":"使用 AI 生成 PR 功能描述","options.showAll":"显示所有发现的问题,不过滤非变更行的问题","options.flush":"仅刷新状态(同步 reactions、resolved conversations、replies),不执行 LLM 审查","options.eventAction":"PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查","extensionDescription":"代码审查命令,使用 LLM 对 PR 代码进行自动审查","mcp.serverDescription":"代码审查规则查询服务","mcp.listRules":"获取当前项目的所有代码审查规则,返回规则列表包含 ID、标题、描述、适用的文件扩展名等信息","mcp.getRulesForFile":"获取某个文件应该使用的代码审查规则,根据文件扩展名和 includes 配置过滤","mcp.getRuleDetail":"获取某个规则的完整详情,包括描述、示例代码等","mcp.ruleNotFound":"规则 {{ruleId}} 不存在","mcp.dto.cwd":"项目根目录路径,默认为当前工作目录","mcp.dto.filePath":"文件路径,可以是相对路径或绝对路径","mcp.dto.includeExamples":"是否包含规则示例代码,默认 false","mcp.dto.ruleId":"规则 ID,如 JsTs.Naming.FileName"}')
147
147
  ;// CONCATENATED MODULE: ./src/locales/en/review.json
148
- var en_review_namespaceObject = JSON.parse('{"description":"Code review command using LLM for automated PR review","options.dryRun":"Only print actions without posting comments","options.prNumber":"PR number, auto-detected from env if not specified","options.base":"Base branch/tag for diff comparison","options.head":"Head branch/tag for diff comparison","options.includes":"File glob patterns to review, e.g. *.ts *.js (can be specified multiple times)","options.llmMode":"LLM mode: claude-code, openai, gemini","options.files":"Only review specified files (space-separated)","options.commits":"Only review specified commits (space-separated)","options.verifyFixes":"Verify if historical issues are fixed (default from config)","options.noVerifyFixes":"Disable historical issue verification","options.verifyConcurrency":"Concurrency for verifying historical issues (default 10)","options.analyzeDeletions":"Analyze impact of deleted code (true, false, ci, pr, terminal)","options.deletionAnalysisMode":"Deletion analysis mode: openai (standard) or claude-code (Agent mode with tools)","options.deletionOnly":"Only run deletion analysis, skip regular code review","options.outputFormat":"Output format: markdown, terminal, json. Auto-selected if not specified (markdown for PR, terminal for CLI)","options.generateDescription":"Generate PR description using AI","options.showAll":"Show all issues found, including those on unchanged lines","options.eventAction":"PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review","extensionDescription":"Code review command using LLM for automated PR review","mcp.serverDescription":"Code review rules query service","mcp.listRules":"List all code review rules for the current project, returning rule list with ID, title, description, applicable file extensions, etc.","mcp.getRulesForFile":"Get applicable code review rules for a specific file, filtered by file extension and includes configuration","mcp.getRuleDetail":"Get full details of a specific rule, including description, example code, etc.","mcp.ruleNotFound":"Rule {{ruleId}} not found","mcp.dto.cwd":"Project root directory path, defaults to current working directory","mcp.dto.filePath":"File path, can be relative or absolute","mcp.dto.includeExamples":"Whether to include rule example code, defaults to false","mcp.dto.ruleId":"Rule ID, e.g. JsTs.Naming.FileName"}')
148
+ var en_review_namespaceObject = JSON.parse('{"description":"Code review command using LLM for automated PR review","options.dryRun":"Only print actions without posting comments","options.prNumber":"PR number, auto-detected from env if not specified","options.base":"Base branch/tag for diff comparison","options.head":"Head branch/tag for diff comparison","options.includes":"File glob patterns to review, e.g. *.ts *.js (can be specified multiple times)","options.llmMode":"LLM mode: claude-code, openai, gemini","options.files":"Only review specified files (space-separated)","options.commits":"Only review specified commits (space-separated)","options.verifyFixes":"Verify if historical issues are fixed (default from config)","options.noVerifyFixes":"Disable historical issue verification","options.verifyConcurrency":"Concurrency for verifying historical issues (default 10)","options.analyzeDeletions":"Analyze impact of deleted code (true, false, ci, pr, terminal)","options.deletionAnalysisMode":"Deletion analysis mode: openai (standard) or claude-code (Agent mode with tools)","options.deletionOnly":"Only run deletion analysis, skip regular code review","options.outputFormat":"Output format: markdown, terminal, json. Auto-selected if not specified (markdown for PR, terminal for CLI)","options.generateDescription":"Generate PR description using AI","options.showAll":"Show all issues found, including those on unchanged lines","options.flush":"Only sync status (reactions, resolved conversations, replies), skip LLM review","options.eventAction":"PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review","extensionDescription":"Code review command using LLM for automated PR review","mcp.serverDescription":"Code review rules query service","mcp.listRules":"List all code review rules for the current project, returning rule list with ID, title, description, applicable file extensions, etc.","mcp.getRulesForFile":"Get applicable code review rules for a specific file, filtered by file extension and includes configuration","mcp.getRuleDetail":"Get full details of a specific rule, including description, example code, etc.","mcp.ruleNotFound":"Rule {{ruleId}} not found","mcp.dto.cwd":"Project root directory path, defaults to current working directory","mcp.dto.filePath":"File path, can be relative or absolute","mcp.dto.includeExamples":"Whether to include rule example code, defaults to false","mcp.dto.ruleId":"Rule ID, e.g. JsTs.Naming.FileName"}')
149
149
  ;// CONCATENATED MODULE: ./src/locales/index.ts
150
150
 
151
151
 
@@ -939,6 +939,7 @@ function parseAnalyzeDeletionsValue(value) {
939
939
 
940
940
 
941
941
  const REVIEW_COMMENT_MARKER = "<!-- spaceflow-review -->";
942
+ const REVIEW_LINE_COMMENTS_MARKER = "<!-- spaceflow-review-lines -->";
942
943
  const REVIEW_SCHEMA = {
943
944
  type: "object",
944
945
  properties: {
@@ -1147,6 +1148,7 @@ class ReviewService {
1147
1148
  retryDelay: options.retryDelay ?? reviewConf.retryDelay ?? 1000,
1148
1149
  generateDescription: options.generateDescription ?? reviewConf.generateDescription ?? false,
1149
1150
  showAll: options.showAll ?? false,
1151
+ flush: options.flush ?? false,
1150
1152
  eventAction: options.eventAction
1151
1153
  };
1152
1154
  }
@@ -1229,8 +1231,8 @@ class ReviewService {
1229
1231
  if (deletionOnly) {
1230
1232
  return this.executeDeletionOnly(context);
1231
1233
  }
1232
- // 如果是 closed 事件,仅收集 review 状态
1233
- if (context.eventAction === "closed") {
1234
+ // 如果是 closed 事件或 flush 模式,仅收集 review 状态
1235
+ if (context.eventAction === "closed" || context.flush) {
1234
1236
  return this.executeCollectOnly(context);
1235
1237
  }
1236
1238
  if (shouldLog(verbose, 1)) {
@@ -1556,7 +1558,7 @@ class ReviewService {
1556
1558
  return result;
1557
1559
  }
1558
1560
  /**
1559
- * 仅收集 review 状态模式(用于 PR 关闭时)
1561
+ * 仅收集 review 状态模式(用于 PR 关闭或 --flush 指令)
1560
1562
  * 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
1561
1563
  */ async executeCollectOnly(context) {
1562
1564
  const { owner, repo, prNumber, verbose, ci, dryRun } = context;
@@ -2423,12 +2425,12 @@ ${fileChanges || "无"}`;
2423
2425
  console.warn("⚠️ 更新 PR 标题失败:", error);
2424
2426
  }
2425
2427
  }
2426
- // 获取已解决的评论,同步 fixed 状态(在删除旧 review 之前)
2428
+ // 获取已解决的评论,同步 fixed 状态(在更新 review 之前)
2427
2429
  await this.syncResolvedComments(owner, repo, prNumber, result);
2428
2430
  // 获取评论的 reactions,同步 valid 状态(👎 标记为无效)
2429
2431
  await this.syncReactionsToIssues(owner, repo, prNumber, result, verbose);
2430
- // 删除已有的 AI review(避免重复评论)
2431
- await this.deleteExistingAiReviews(owner, repo, prNumber);
2432
+ // 查找已有的 AI 评论(Issue Comment)
2433
+ const existingComment = await this.findExistingAiComment(owner, repo, prNumber);
2432
2434
  // 调试:检查 issues 是否有 author
2433
2435
  if (shouldLog(verbose, 3)) {
2434
2436
  for (const issue of result.issues.slice(0, 3)){
@@ -2443,45 +2445,117 @@ ${fileChanges || "无"}`;
2443
2445
  // 获取 PR 信息以获取 head commit SHA
2444
2446
  const pr = await this.gitProvider.getPullRequest(owner, repo, prNumber);
2445
2447
  const commitId = pr.head?.sha;
2446
- // 构建行级评论(根据配置决定是否启用)
2448
+ // 1. 发布或更新主评论(使用 Issue Comment API,支持删除和更新)
2449
+ try {
2450
+ if (existingComment?.id) {
2451
+ await this.gitProvider.updateIssueComment(owner, repo, existingComment.id, reviewBody);
2452
+ console.log(`✅ 已更新 AI Review 评论`);
2453
+ } else {
2454
+ await this.gitProvider.createIssueComment(owner, repo, prNumber, {
2455
+ body: reviewBody
2456
+ });
2457
+ console.log(`✅ 已发布 AI Review 评论`);
2458
+ }
2459
+ } catch (error) {
2460
+ console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
2461
+ }
2462
+ // 2. 删除旧的行级评论(逐条删除 PR Review Comment)
2463
+ try {
2464
+ const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2465
+ const oldLineReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
2466
+ for (const review of oldLineReviews){
2467
+ if (review.id) {
2468
+ const reviewComments = await this.gitProvider.listPullReviewComments(owner, repo, prNumber, review.id);
2469
+ for (const comment of reviewComments){
2470
+ if (comment.id) {
2471
+ try {
2472
+ await this.gitProvider.deletePullReviewComment(owner, repo, comment.id);
2473
+ } catch {
2474
+ // 删除失败忽略
2475
+ }
2476
+ }
2477
+ }
2478
+ // 评论删除后尝试删除 review 本身
2479
+ try {
2480
+ await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
2481
+ } catch {
2482
+ // 已提交的 review 无法删除,忽略
2483
+ }
2484
+ }
2485
+ }
2486
+ if (oldLineReviews.length > 0) {
2487
+ console.log(`🗑️ 已清理 ${oldLineReviews.length} 个旧的行级评论 review`);
2488
+ }
2489
+ } catch (error) {
2490
+ console.warn("⚠️ 清理旧行级评论失败:", error);
2491
+ }
2492
+ // 3. 发布新的行级评论(使用 PR Review API)
2447
2493
  let comments = [];
2448
2494
  if (reviewConf.lineComments) {
2449
2495
  comments = result.issues.filter((issue)=>!issue.fixed && issue.valid !== "false").map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
2450
2496
  }
2497
+ if (comments.length > 0) {
2498
+ try {
2499
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
2500
+ event: REVIEW_STATE.COMMENT,
2501
+ body: REVIEW_LINE_COMMENTS_MARKER,
2502
+ comments,
2503
+ commit_id: commitId
2504
+ });
2505
+ console.log(`✅ 已发布 ${comments.length} 条行级评论`);
2506
+ } catch {
2507
+ // 批量失败时逐条发布,跳过无法定位的评论
2508
+ console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
2509
+ let successCount = 0;
2510
+ for (const comment of comments){
2511
+ try {
2512
+ await this.gitProvider.createPullReview(owner, repo, prNumber, {
2513
+ event: REVIEW_STATE.COMMENT,
2514
+ body: successCount === 0 ? REVIEW_LINE_COMMENTS_MARKER : undefined,
2515
+ comments: [
2516
+ comment
2517
+ ],
2518
+ commit_id: commitId
2519
+ });
2520
+ successCount++;
2521
+ } catch {
2522
+ console.warn(`⚠️ 跳过无法定位的评论: ${comment.path}:${comment.new_position}`);
2523
+ }
2524
+ }
2525
+ if (successCount > 0) {
2526
+ console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
2527
+ } else {
2528
+ console.warn("⚠️ 所有行级评论均无法定位,已跳过");
2529
+ }
2530
+ }
2531
+ }
2532
+ }
2533
+ /**
2534
+ * 查找已有的 AI 评论(Issue Comment)
2535
+ */ async findExistingAiComment(owner, repo, prNumber) {
2451
2536
  try {
2452
- // 使用 PR Review 发布主评论 + 行级评论(合并为一个消息块)
2453
- await this.gitProvider.createPullReview(owner, repo, prNumber, {
2454
- event: "COMMENT",
2455
- body: reviewBody,
2456
- comments,
2457
- commit_id: commitId
2458
- });
2459
- const lineMsg = comments.length > 0 ? `,包含 ${comments.length} 条行级评论` : "";
2460
- console.log(`✅ 已发布 AI Review${lineMsg}`);
2461
- } catch (error) {
2462
- console.warn("⚠️ 发布 AI Review 失败:", error);
2537
+ const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
2538
+ const aiComment = comments.find((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
2539
+ return aiComment?.id ? {
2540
+ id: aiComment.id
2541
+ } : null;
2542
+ } catch {
2543
+ return null;
2463
2544
  }
2464
2545
  }
2465
2546
  /**
2466
- * 从旧的 AI review 中获取已解决的评论,同步 fixed 状态到 result.issues
2547
+ * PR 的所有 resolved review threads 中同步 fixed 状态到 result.issues
2548
+ * 直接通过 GraphQL 查询所有 resolved threads 的 path+line,匹配 issues
2467
2549
  */ async syncResolvedComments(owner, repo, prNumber, result) {
2468
2550
  try {
2469
- const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2470
- const aiReview = reviews.find((r)=>r.body?.includes(REVIEW_COMMENT_MARKER));
2471
- if (!aiReview?.id) {
2472
- return;
2473
- }
2474
- // 获取该 review 的所有行级评论
2475
- const reviewComments = await this.gitProvider.listPullReviewComments(owner, repo, prNumber, aiReview.id);
2476
- // 找出已解决的评论(resolver 不为 null)
2477
- const resolvedComments = reviewComments.filter((c)=>c.resolver !== null && c.resolver !== undefined);
2478
- if (resolvedComments.length === 0) {
2551
+ const resolvedThreads = await this.gitProvider.listResolvedThreads(owner, repo, prNumber);
2552
+ if (resolvedThreads.length === 0) {
2479
2553
  return;
2480
2554
  }
2481
- // 根据文件路径和行号匹配 issues,标记为已解决
2482
2555
  const now = new Date().toISOString();
2483
- for (const comment of resolvedComments){
2484
- const matchedIssue = result.issues.find((issue)=>issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position));
2556
+ for (const thread of resolvedThreads){
2557
+ if (!thread.path) continue;
2558
+ const matchedIssue = result.issues.find((issue)=>issue.file === thread.path && this.lineMatchesPosition(issue.line, thread.line));
2485
2559
  if (matchedIssue && !matchedIssue.fixed) {
2486
2560
  matchedIssue.fixed = now;
2487
2561
  console.log(`🟢 问题已标记为已解决: ${matchedIssue.file}:${matchedIssue.line}`);
@@ -2509,7 +2583,7 @@ ${fileChanges || "无"}`;
2509
2583
  */ async syncReactionsToIssues(owner, repo, prNumber, result, verbose) {
2510
2584
  try {
2511
2585
  const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2512
- const aiReview = reviews.find((r)=>r.body?.includes(REVIEW_COMMENT_MARKER));
2586
+ const aiReview = reviews.find((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
2513
2587
  if (!aiReview?.id) {
2514
2588
  if (shouldLog(verbose, 2)) {
2515
2589
  console.log(`[syncReactionsToIssues] No AI review found`);
@@ -2520,7 +2594,7 @@ ${fileChanges || "无"}`;
2520
2594
  const reviewers = new Set();
2521
2595
  // 1. 从已提交的 review 中获取评审人(排除 AI bot)
2522
2596
  for (const review of reviews){
2523
- if (review.user?.login && !review.body?.includes(REVIEW_COMMENT_MARKER)) {
2597
+ if (review.user?.login && !review.body?.includes(REVIEW_LINE_COMMENTS_MARKER)) {
2524
2598
  reviewers.add(review.user.login);
2525
2599
  }
2526
2600
  }
@@ -2579,7 +2653,7 @@ ${fileChanges || "无"}`;
2579
2653
  commentIdToIssue.set(comment.id, matchedIssue);
2580
2654
  }
2581
2655
  try {
2582
- const reactions = await this.gitProvider.getIssueCommentReactions(owner, repo, comment.id);
2656
+ const reactions = await this.gitProvider.getPullReviewCommentReactions(owner, repo, comment.id);
2583
2657
  if (reactions.length === 0 || !matchedIssue) continue;
2584
2658
  // 按 content 分组,收集每种 reaction 的用户列表
2585
2659
  const reactionMap = new Map();
@@ -2657,20 +2731,46 @@ ${fileChanges || "无"}`;
2657
2731
  }
2658
2732
  /**
2659
2733
  * 删除已有的 AI review(通过 marker 识别)
2734
+ * - 删除行级评论的 PR Review(带 REVIEW_LINE_COMMENTS_MARKER)
2735
+ * - 删除主评论的 Issue Comment(带 REVIEW_COMMENT_MARKER)
2660
2736
  */ async deleteExistingAiReviews(owner, repo, prNumber) {
2737
+ let deletedCount = 0;
2738
+ // 删除行级评论的 PR Review
2661
2739
  try {
2662
2740
  const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2663
- const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_COMMENT_MARKER));
2741
+ const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER) || r.body?.includes(REVIEW_COMMENT_MARKER));
2664
2742
  for (const review of aiReviews){
2665
2743
  if (review.id) {
2666
- await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
2744
+ try {
2745
+ await this.gitProvider.deletePullReview(owner, repo, prNumber, review.id);
2746
+ deletedCount++;
2747
+ } catch {
2748
+ // 已提交的 review 无法删除,忽略
2749
+ }
2667
2750
  }
2668
2751
  }
2669
- if (aiReviews.length > 0) {
2670
- console.log(`🗑️ 已删除 ${aiReviews.length} 个旧的 AI review`);
2752
+ } catch (error) {
2753
+ console.warn("⚠️ 列出 PR reviews 失败:", error);
2754
+ }
2755
+ // 删除主评论的 Issue Comment
2756
+ try {
2757
+ const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
2758
+ const aiComments = comments.filter((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
2759
+ for (const comment of aiComments){
2760
+ if (comment.id) {
2761
+ try {
2762
+ await this.gitProvider.deleteIssueComment(owner, repo, comment.id);
2763
+ deletedCount++;
2764
+ } catch (error) {
2765
+ console.warn(`⚠️ 删除评论 ${comment.id} 失败:`, error);
2766
+ }
2767
+ }
2671
2768
  }
2672
2769
  } catch (error) {
2673
- console.warn("⚠️ 删除旧 AI review 失败:", error);
2770
+ console.warn("⚠️ 列出 issue comments 失败:", error);
2771
+ }
2772
+ if (deletedCount > 0) {
2773
+ console.log(`🗑️ 已删除 ${deletedCount} 个旧的 AI review`);
2674
2774
  }
2675
2775
  }
2676
2776
  /**
@@ -2918,8 +3018,11 @@ ${fileChanges || "无"}`;
2918
3018
  return filtered;
2919
3019
  }
2920
3020
  filterDuplicateIssues(newIssues, existingIssues) {
2921
- // 只有 valid === 'true' 的历史问题才阻止新问题,其他情况允许覆盖
2922
- const existingKeys = new Set(existingIssues.filter((issue)=>issue.valid === "true").map((issue)=>this.generateIssueKey(issue)));
3021
+ // 所有历史问题(无论 valid 状态)都阻止新问题重复添加
3022
+ // valid='false' 的问题已被评审人标记为无效,不应再次报告
3023
+ // valid='true' 的问题已存在,无需重复
3024
+ // fixed 的问题已解决,无需重复
3025
+ const existingKeys = new Set(existingIssues.map((issue)=>this.generateIssueKey(issue)));
2923
3026
  const filteredIssues = newIssues.filter((issue)=>!existingKeys.has(this.generateIssueKey(issue)));
2924
3027
  const skippedCount = newIssues.length - filteredIssues.length;
2925
3028
  return {
@@ -2929,11 +3032,11 @@ ${fileChanges || "无"}`;
2929
3032
  }
2930
3033
  async getExistingReviewResult(owner, repo, prNumber) {
2931
3034
  try {
2932
- // 从 PR Review 获取已有的审查结果
2933
- const reviews = await this.gitProvider.listPullReviews(owner, repo, prNumber);
2934
- const existingReview = reviews.find((r)=>r.body?.includes(REVIEW_COMMENT_MARKER));
2935
- if (existingReview?.body) {
2936
- return this.parseExistingReviewResult(existingReview.body);
3035
+ // 从 Issue Comment 获取已有的审查结果
3036
+ const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
3037
+ const existingComment = comments.find((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
3038
+ if (existingComment?.body) {
3039
+ return this.parseExistingReviewResult(existingComment.body);
2937
3040
  }
2938
3041
  } catch (error) {
2939
3042
  console.warn("⚠️ 获取已有评论失败:", error);
@@ -5080,23 +5183,28 @@ const extension = defineExtension({
5080
5183
  flags: "--show-all",
5081
5184
  description: t("review:options.showAll")
5082
5185
  },
5186
+ {
5187
+ flags: "--flush",
5188
+ description: t("review:options.flush")
5189
+ },
5083
5190
  {
5084
5191
  flags: "--event-action <action>",
5085
5192
  description: t("review:options.eventAction")
5086
5193
  }
5087
5194
  ],
5088
5195
  run: async (_args, options, ctx)=>{
5196
+ const isFlush = !!options?.flush;
5089
5197
  if (!ctx.hasService("gitProvider")) {
5090
5198
  ctx.output.error("review 命令需要配置 Git Provider,请在 spaceflow.json 中配置 gitProvider 字段");
5091
5199
  process.exit(1);
5092
5200
  }
5093
- if (!ctx.hasService("llmProxy")) {
5201
+ if (!isFlush && !ctx.hasService("llmProxy")) {
5094
5202
  ctx.output.error("review 命令需要配置 LLM 服务,请在 spaceflow.json 中配置 llm 字段");
5095
5203
  process.exit(1);
5096
5204
  }
5097
5205
  const gitProvider = ctx.getService("gitProvider");
5098
5206
  const configReader = ctx.getService("config");
5099
- const llmProxy = ctx.getService("llmProxy");
5207
+ const llmProxy = ctx.hasService("llmProxy") ? ctx.getService("llmProxy") : undefined;
5100
5208
  const gitSdk = ctx.hasService("gitSdk") ? ctx.getService("gitSdk") : undefined;
5101
5209
  const reviewSpecService = new ReviewSpecService(gitProvider);
5102
5210
  const reviewReportService = new ReviewReportService();
@@ -5121,6 +5229,7 @@ const extension = defineExtension({
5121
5229
  outputFormat: options?.outputFormat,
5122
5230
  generateDescription: !!options?.generateDescription,
5123
5231
  showAll: !!options?.showAll,
5232
+ flush: isFlush,
5124
5233
  eventAction: options?.eventAction
5125
5234
  };
5126
5235
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -25,10 +25,10 @@
25
25
  "@vitest/coverage-v8": "^4.0.18",
26
26
  "unplugin-swc": "^1.5.9",
27
27
  "vitest": "^4.0.18",
28
- "@spaceflow/cli": "0.20.0"
28
+ "@spaceflow/cli": "0.21.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@spaceflow/core": "0.2.0"
31
+ "@spaceflow/core": "0.3.0"
32
32
  },
33
33
  "spaceflow": {
34
34
  "type": "flow",
package/src/index.ts CHANGED
@@ -48,23 +48,27 @@ export const extension = defineExtension({
48
48
  { flags: "-o, --output-format <format>", description: t("review:options.outputFormat") },
49
49
  { flags: "--generate-description", description: t("review:options.generateDescription") },
50
50
  { flags: "--show-all", description: t("review:options.showAll") },
51
+ { flags: "--flush", description: t("review:options.flush") },
51
52
  { flags: "--event-action <action>", description: t("review:options.eventAction") },
52
53
  ],
53
54
  run: async (_args, options, ctx) => {
55
+ const isFlush = !!options?.flush;
54
56
  if (!ctx.hasService("gitProvider")) {
55
57
  ctx.output.error(
56
58
  "review 命令需要配置 Git Provider,请在 spaceflow.json 中配置 gitProvider 字段",
57
59
  );
58
60
  process.exit(1);
59
61
  }
60
- if (!ctx.hasService("llmProxy")) {
62
+ if (!isFlush && !ctx.hasService("llmProxy")) {
61
63
  ctx.output.error("review 命令需要配置 LLM 服务,请在 spaceflow.json 中配置 llm 字段");
62
64
  process.exit(1);
63
65
  }
64
66
 
65
67
  const gitProvider = ctx.getService<GitProviderService>("gitProvider");
66
68
  const configReader = ctx.getService<ConfigReaderService>("config");
67
- const llmProxy = ctx.getService<LlmProxyService>("llmProxy");
69
+ const llmProxy = ctx.hasService("llmProxy")
70
+ ? ctx.getService<LlmProxyService>("llmProxy")
71
+ : (undefined as unknown as LlmProxyService);
68
72
  const gitSdk = ctx.hasService("gitSdk")
69
73
  ? ctx.getService<GitSdkService>("gitSdk")
70
74
  : undefined;
@@ -104,6 +108,7 @@ export const extension = defineExtension({
104
108
  outputFormat: options?.outputFormat as ReportFormat,
105
109
  generateDescription: !!options?.generateDescription,
106
110
  showAll: !!options?.showAll,
111
+ flush: isFlush,
107
112
  eventAction: options?.eventAction as string,
108
113
  };
109
114
 
@@ -17,6 +17,7 @@
17
17
  "options.outputFormat": "Output format: markdown, terminal, json. Auto-selected if not specified (markdown for PR, terminal for CLI)",
18
18
  "options.generateDescription": "Generate PR description using AI",
19
19
  "options.showAll": "Show all issues found, including those on unchanged lines",
20
+ "options.flush": "Only sync status (reactions, resolved conversations, replies), skip LLM review",
20
21
  "options.eventAction": "PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review",
21
22
  "extensionDescription": "Code review command using LLM for automated PR review",
22
23
  "mcp.serverDescription": "Code review rules query service",
@@ -17,6 +17,7 @@
17
17
  "options.outputFormat": "输出格式: markdown, terminal, json。不指定则智能选择(PR 用 markdown,终端用 terminal)",
18
18
  "options.generateDescription": "使用 AI 生成 PR 功能描述",
19
19
  "options.showAll": "显示所有发现的问题,不过滤非变更行的问题",
20
+ "options.flush": "仅刷新状态(同步 reactions、resolved conversations、replies),不执行 LLM 审查",
20
21
  "options.eventAction": "PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查",
21
22
  "extensionDescription": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
22
23
  "mcp.serverDescription": "代码审查规则查询服务",
@@ -63,6 +63,8 @@ export interface ReviewOptions {
63
63
  generateDescription?: boolean;
64
64
  /** 显示所有问题,不过滤非变更行的问题 */
65
65
  showAll?: boolean;
66
+ /** 仅刷新状态(同步 reactions、resolved 等),不执行 LLM 审查 */
67
+ flush?: boolean;
66
68
  /** PR 事件类型(opened, synchronize, closed 等) */
67
69
  eventAction?: string;
68
70
  concurrency?: number;