@spaceflow/review 0.78.0 → 0.79.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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.78.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.77.0...@spaceflow/review@0.78.0) (2026-04-07)
4
+
5
+ ### 新特性
6
+
7
+ * **review:** 新增 code-* 代码结构过滤语法,支持按函数/类/接口等类型审查新增代码 ([f3b17d3](https://github.com/Lydanne/spaceflow/commit/f3b17d36c6360269a2fd4075311ae764e85cf428))
8
+
9
+ ### 代码重构
10
+
11
+ * **ci:** 将 PR 审查工作流日志级别从 -vv 提升至 -vvv ([43bd563](https://github.com/Lydanne/spaceflow/commit/43bd5632dbd7151e11977520ca0634fdbc393279))
12
+ * **review:** 优化 syncReactionsToIssues 中的 issue 匹配逻辑,优先使用 issue-key 精确匹配并回退到 path+position 匹配 ([eb8ea47](https://github.com/Lydanne/spaceflow/commit/eb8ea47c56466aa25d58358a09f1e0c9094aa28d))
13
+ * **review:** 修改 buildLinesWithNumbers 忽略占位符格式,从 `...... ..| ignore X-Y code` 改为 `....... ignore X-Y line .......` ([a45dc1a](https://github.com/Lydanne/spaceflow/commit/a45dc1a5b92d50f75682782cec24b0100732e434))
14
+ * **review:** 将 code-* 语法从 includes 迁移至独立 filterCodeBlocks 配置项 ([ed4b921](https://github.com/Lydanne/spaceflow/commit/ed4b9217a563d992b76d1e55af21f9b364329737))
15
+ * **review:** 将 filterCodeBlocks 重命名为 whenModifiedCode,简化代码结构过滤语法 ([951c570](https://github.com/Lydanne/spaceflow/commit/951c570ceb7c04d25265d043c3948687bbb230f1))
16
+ * **review:** 将 prompt 构建逻辑提取到独立的 prompt 模块,新增类型验证和 schema 定义 ([5f21356](https://github.com/Lydanne/spaceflow/commit/5f21356964ad4f08004e229ac7bd538c0301b37d))
17
+ * **review:** 将 review-pr-comment-utils 重构为 utils/review-pr-comment,新增完整测试覆盖 ([29041b6](https://github.com/Lydanne/spaceflow/commit/29041b635e3a6ecbd2b2ae49ad1b657aa1a66e9c))
18
+ * **review:** 将 skipDuplicateWorkflow 重构为 duplicateWorkflowResolved,支持 skip/delete 两种处理模式 ([37238f9](https://github.com/Lydanne/spaceflow/commit/37238f9ae8ed38c67f46acb963e3ebb795c0ef71))
19
+ * **review:** 将无效 commit hash 的 issue 标记为 invalid 并清除 commit 字段,移除 code-* 前缀语法支持 ([0a89136](https://github.com/Lydanne/spaceflow/commit/0a89136b32cf49a70dbdfa26683e97ee13dc207e))
20
+ * **review:** 提前检测无效 commit hash,在 fillIssueAuthors 中对已有 author 的 issue 也进行校验 ([c166496](https://github.com/Lydanne/spaceflow/commit/c1664966066bca032bd936b6c1d2c8f320949f9c))
21
+ * **review:** 新增 systemRules 静态规则系统,支持 maxLinesPerFile 限制并跳过超限文件的 LLM 审查 ([3721d0f](https://github.com/Lydanne/spaceflow/commit/3721d0f46d40f71230aceb060350682a3d5642d7))
22
+ * **review:** 新增 whenModifiedCode 过滤逻辑,跳过无匹配代码块的文件并记录日志 ([3d0c1e1](https://github.com/Lydanne/spaceflow/commit/3d0c1e1a4b492e62104af92d16208c206934ad10))
23
+ * **review:** 简化 includes glob 模式,移除冗余的 `*/**` 前缀 ([56cf145](https://github.com/Lydanne/spaceflow/commit/56cf145fbf8f1311df9d538fffc61581d4ead400))
24
+
25
+ ### 测试用例
26
+
27
+ * **review:** 新增全量 diff 语义测试,验证 added| 对分支首次引入文件的持续匹配行为 ([5b72577](https://github.com/Lydanne/spaceflow/commit/5b725772091d1d4fe5207af1b7d176e705bdb8d3))
28
+
29
+ ### 其他修改
30
+
31
+ * **review-summary:** released version 0.46.0 [no ci] ([72fa783](https://github.com/Lydanne/spaceflow/commit/72fa783ef5c104d654a29481300d791b333df0ee))
32
+ * **scripts:** released version 0.32.0 [no ci] ([7871aa9](https://github.com/Lydanne/spaceflow/commit/7871aa94a7f1227d6f09fc43ea69fb7d4e193ec4))
33
+ * **shell:** released version 0.32.0 [no ci] ([88bdc81](https://github.com/Lydanne/spaceflow/commit/88bdc81cb0ec7809fbd76c7b157e0177b2f4db20))
34
+
3
35
  ## [0.77.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.76.0...@spaceflow/review@0.77.0) (2026-04-07)
4
36
 
5
37
  ### 新特性
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
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 { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
3
- import { basename, dirname, extname, isAbsolute, join, relative } from "path";
3
+ import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "path";
4
4
  import { homedir } from "os";
5
5
  import { execSync, spawn } from "child_process";
6
6
  import micromatch_0 from "micromatch";
@@ -3239,11 +3239,14 @@ class ReviewContextBuilder {
3239
3239
  if (reviewConf.references?.length) {
3240
3240
  specSources.push(...reviewConf.references);
3241
3241
  }
3242
+ const normalizedFiles = this.normalizeFilePaths(options.files);
3242
3243
  // 解析本地模式:非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
3244
+ // 当显式指定 files 时,强制走“按文件审查模式”,不进入本地未提交模式
3243
3245
  const localMode = this.resolveLocalMode(options, {
3244
3246
  ci: options.ci,
3245
3247
  hasPrNumber: !!prNumber,
3246
- hasBaseHead: !!(options.base || options.head)
3248
+ hasBaseHead: !!(options.base || options.head),
3249
+ hasFiles: !!normalizedFiles?.length
3247
3250
  });
3248
3251
  // 当没有 PR 且没有指定 base/head 且不是本地模式时,自动获取默认值
3249
3252
  let baseRef = options.base;
@@ -3273,7 +3276,7 @@ class ReviewContextBuilder {
3273
3276
  includes: ctxIncludes,
3274
3277
  whenModifiedCode: options.whenModifiedCode ?? reviewConf.whenModifiedCode,
3275
3278
  llmMode: options.llmMode ?? titleOptions.llmMode ?? reviewConf.llmMode,
3276
- files: this.normalizeFilePaths(options.files),
3279
+ files: normalizedFiles,
3277
3280
  commits: options.commits,
3278
3281
  verifyFixes: options.verifyFixes ?? titleOptions.verifyFixes ?? reviewConf.verifyFixes ?? true,
3279
3282
  verifyConcurrency: options.verifyConcurrency ?? reviewConf.verifyFixesConcurrency ?? 10,
@@ -3303,6 +3306,10 @@ class ReviewContextBuilder {
3303
3306
  * - 显式指定 --no-local 时禁用
3304
3307
  * - 非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
3305
3308
  */ resolveLocalMode(options, env) {
3309
+ // 显式指定了 files,优先进入按文件审查模式
3310
+ if (env.hasFiles) {
3311
+ return false;
3312
+ }
3306
3313
  // 显式指定了 --no-local
3307
3314
  if (options.local === false) {
3308
3315
  return false;
@@ -3328,13 +3335,17 @@ class ReviewContextBuilder {
3328
3335
  */ normalizeFilePaths(files) {
3329
3336
  if (!files || files.length === 0) return files;
3330
3337
  const cwd = process.cwd();
3331
- return files.map((file)=>{
3332
- if (isAbsolute(file)) {
3333
- // 绝对路径转换为相对路径
3334
- return relative(cwd, file);
3335
- }
3336
- return file;
3337
- });
3338
+ return files.map((file)=>this.normalizeSingleFilePath(file, cwd));
3339
+ }
3340
+ /**
3341
+ * 规范化单个文件路径为仓库相对路径:
3342
+ * - 绝对路径转相对路径
3343
+ * - 统一分隔符为 /
3344
+ * - 移除前导 ./
3345
+ */ normalizeSingleFilePath(file, cwd) {
3346
+ const normalizedInput = normalize(file);
3347
+ const relativePath = isAbsolute(normalizedInput) ? relative(cwd, normalizedInput) : normalizedInput;
3348
+ return relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
3338
3349
  }
3339
3350
  /**
3340
3351
  * 根据 AnalyzeDeletionsMode 和当前环境解析是否启用删除代码分析
@@ -5147,6 +5158,10 @@ class ReviewService {
5147
5158
  const source = await this.resolveSourceData(context);
5148
5159
  if (source.earlyReturn) return source.earlyReturn;
5149
5160
  const { prModel, commits, changedFiles, headSha, isDirectFileMode } = source;
5161
+ const effectiveWhenModifiedCode = isDirectFileMode ? undefined : context.whenModifiedCode;
5162
+ if (isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
5163
+ console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
5164
+ }
5150
5165
  // 2. 规则匹配
5151
5166
  const specs = await this.issueFilter.loadSpecs(specSources, verbose);
5152
5167
  const applicableSpecs = this.reviewSpecService.filterApplicableSpecs(specs, changedFiles);
@@ -5174,7 +5189,7 @@ class ReviewService {
5174
5189
  if (shouldLog(verbose, 1)) {
5175
5190
  console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
5176
5191
  }
5177
- const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null, context.whenModifiedCode, verbose, context.systemRules);
5192
+ const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null, effectiveWhenModifiedCode, verbose, context.systemRules);
5178
5193
  const result = await this.runLLMReview(llmMode, reviewPrompt, {
5179
5194
  verbose,
5180
5195
  concurrency: context.concurrency,
@@ -5230,7 +5245,7 @@ class ReviewService {
5230
5245
  * 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
5231
5246
  */ async resolveSourceData(context) {
5232
5247
  const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode, duplicateWorkflowResolved } = context;
5233
- const isDirectFileMode = !!(files && files.length > 0 && baseRef === headRef);
5248
+ const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
5234
5249
  let isLocalMode = !!localMode;
5235
5250
  let effectiveBaseRef = baseRef;
5236
5251
  let effectiveHeadRef = headRef;
@@ -5289,8 +5304,17 @@ class ReviewService {
5289
5304
  }
5290
5305
  }
5291
5306
  }
5292
- // PR 模式、分支比较模式、或本地模式回退后的分支比较
5293
- if (prNumber) {
5307
+ // 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
5308
+ if (isDirectFileMode) {
5309
+ if (shouldLog(verbose, 1)) {
5310
+ console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
5311
+ }
5312
+ changedFiles = files.map((f)=>({
5313
+ filename: f,
5314
+ status: "modified"
5315
+ }));
5316
+ isLocalMode = true;
5317
+ } else if (prNumber) {
5294
5318
  if (shouldLog(verbose, 1)) {
5295
5319
  console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
5296
5320
  }
@@ -5319,15 +5343,7 @@ class ReviewService {
5319
5343
  }
5320
5344
  }
5321
5345
  } else if (effectiveBaseRef && effectiveHeadRef) {
5322
- if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
5323
- if (shouldLog(verbose, 1)) {
5324
- console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
5325
- }
5326
- changedFiles = files.map((f)=>({
5327
- filename: f,
5328
- status: "modified"
5329
- }));
5330
- } else if (changedFiles.length === 0) {
5346
+ if (changedFiles.length === 0) {
5331
5347
  if (shouldLog(verbose, 1)) {
5332
5348
  console.log(`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`);
5333
5349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.78.0",
3
+ "version": "0.79.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -15,7 +15,7 @@ import { parseTitleOptions } from "./parse-title-options";
15
15
  import { type ReviewIssue, type UserInfo } from "./review-spec";
16
16
  import { readFile } from "fs/promises";
17
17
  import { join } from "path";
18
- import { isAbsolute, relative } from "path";
18
+ import { isAbsolute, normalize, relative } from "path";
19
19
  import { homedir } from "os";
20
20
  import type { ReportFormat } from "./review-report";
21
21
 
@@ -137,11 +137,15 @@ export class ReviewContextBuilder {
137
137
  specSources.push(...reviewConf.references);
138
138
  }
139
139
 
140
+ const normalizedFiles = this.normalizeFilePaths(options.files);
141
+
140
142
  // 解析本地模式:非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
143
+ // 当显式指定 files 时,强制走“按文件审查模式”,不进入本地未提交模式
141
144
  const localMode = this.resolveLocalMode(options, {
142
145
  ci: options.ci,
143
146
  hasPrNumber: !!prNumber,
144
147
  hasBaseHead: !!(options.base || options.head),
148
+ hasFiles: !!normalizedFiles?.length,
145
149
  });
146
150
 
147
151
  // 当没有 PR 且没有指定 base/head 且不是本地模式时,自动获取默认值
@@ -175,7 +179,7 @@ export class ReviewContextBuilder {
175
179
  includes: ctxIncludes,
176
180
  whenModifiedCode: options.whenModifiedCode ?? reviewConf.whenModifiedCode,
177
181
  llmMode: options.llmMode ?? titleOptions.llmMode ?? reviewConf.llmMode,
178
- files: this.normalizeFilePaths(options.files),
182
+ files: normalizedFiles,
179
183
  commits: options.commits,
180
184
  verifyFixes:
181
185
  options.verifyFixes ?? titleOptions.verifyFixes ?? reviewConf.verifyFixes ?? true,
@@ -219,8 +223,12 @@ export class ReviewContextBuilder {
219
223
  */
220
224
  resolveLocalMode(
221
225
  options: ReviewOptions,
222
- env: { ci: boolean; hasPrNumber: boolean; hasBaseHead: boolean },
226
+ env: { ci: boolean; hasPrNumber: boolean; hasBaseHead: boolean; hasFiles: boolean },
223
227
  ): "uncommitted" | "staged" | false {
228
+ // 显式指定了 files,优先进入按文件审查模式
229
+ if (env.hasFiles) {
230
+ return false;
231
+ }
224
232
  // 显式指定了 --no-local
225
233
  if (options.local === false) {
226
234
  return false;
@@ -249,13 +257,19 @@ export class ReviewContextBuilder {
249
257
  if (!files || files.length === 0) return files;
250
258
 
251
259
  const cwd = process.cwd();
252
- return files.map((file) => {
253
- if (isAbsolute(file)) {
254
- // 绝对路径转换为相对路径
255
- return relative(cwd, file);
256
- }
257
- return file;
258
- });
260
+ return files.map((file) => this.normalizeSingleFilePath(file, cwd));
261
+ }
262
+
263
+ /**
264
+ * 规范化单个文件路径为仓库相对路径:
265
+ * - 绝对路径转相对路径
266
+ * - 统一分隔符为 /
267
+ * - 移除前导 ./
268
+ */
269
+ private normalizeSingleFilePath(file: string, cwd: string): string {
270
+ const normalizedInput = normalize(file);
271
+ const relativePath = isAbsolute(normalizedInput) ? relative(cwd, normalizedInput) : normalizedInput;
272
+ return relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
259
273
  }
260
274
 
261
275
  /**
@@ -127,6 +127,10 @@ describe("ReviewService", () => {
127
127
  parseDiffText: vi.fn().mockReturnValue([]),
128
128
  getRemoteUrl: vi.fn().mockReturnValue(null),
129
129
  parseRepositoryFromRemoteUrl: vi.fn().mockReturnValue(null),
130
+ getUncommittedFiles: vi.fn().mockReturnValue([]),
131
+ getStagedFiles: vi.fn().mockReturnValue([]),
132
+ getUncommittedDiff: vi.fn().mockReturnValue([]),
133
+ getStagedDiff: vi.fn().mockReturnValue([]),
130
134
  getChangedFilesBetweenRefs: vi.fn().mockResolvedValue([]),
131
135
  getCommitsBetweenRefs: vi.fn().mockResolvedValue([]),
132
136
  getDiffBetweenRefs: vi.fn().mockResolvedValue([]),
@@ -419,6 +423,26 @@ describe("ReviewService", () => {
419
423
  expect(result.success).toBe(true);
420
424
  });
421
425
 
426
+ it("should ignore whenModifiedCode in direct file mode", async () => {
427
+ const context: ReviewContext = {
428
+ owner: "owner",
429
+ repo: "repo",
430
+ files: ["src/app.ts"],
431
+ specSources: ["/spec/dir"],
432
+ dryRun: true,
433
+ ci: false,
434
+ llmMode: "openai",
435
+ whenModifiedCode: ["function", "class"],
436
+ };
437
+ const buildReviewPromptSpy = vi.spyOn(service as any, "buildReviewPrompt");
438
+
439
+ const result = await service.execute(context);
440
+
441
+ expect(result.success).toBe(true);
442
+ expect(buildReviewPromptSpy).toHaveBeenCalled();
443
+ expect(buildReviewPromptSpy.mock.calls[0][5]).toBeUndefined();
444
+ });
445
+
422
446
  it("should filter files by includes pattern", async () => {
423
447
  const context: ReviewContext = {
424
448
  owner: "owner",
@@ -1363,14 +1387,28 @@ describe("ReviewService", () => {
1363
1387
 
1364
1388
  it("should normalize absolute file paths", async () => {
1365
1389
  configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
1390
+ const absPath = `${process.cwd()}/src/file.ts`;
1366
1391
  const options = {
1367
1392
  dryRun: false,
1368
1393
  ci: false,
1369
- files: ["/absolute/path/to/file.ts", "relative.ts"],
1394
+ files: [absPath, "./relative.ts"],
1370
1395
  };
1371
1396
  const context = await service.getContextFromEnv(options as any);
1372
1397
  expect(context.files).toBeDefined();
1373
- expect(context.files![1]).toBe("relative.ts");
1398
+ expect(context.files).toEqual(["src/file.ts", "relative.ts"]);
1399
+ });
1400
+
1401
+ it("should force direct file mode when files are specified", async () => {
1402
+ configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
1403
+ const options = {
1404
+ dryRun: false,
1405
+ ci: false,
1406
+ local: "uncommitted" as const,
1407
+ files: ["./miniprogram/utils/asyncSharedUtilsLoader.js"],
1408
+ };
1409
+ const context = await service.getContextFromEnv(options as any);
1410
+ expect(context.localMode).toBe(false);
1411
+ expect(context.files).toEqual(["miniprogram/utils/asyncSharedUtilsLoader.js"]);
1374
1412
  });
1375
1413
 
1376
1414
  it("should auto-detect base/head with verbose logging", async () => {
@@ -1429,6 +1467,30 @@ describe("ReviewService", () => {
1429
1467
  });
1430
1468
  });
1431
1469
 
1470
+ describe("ReviewService.resolveSourceData - direct file mode", () => {
1471
+ it("should bypass local uncommitted scanning when files are specified", async () => {
1472
+ const context: ReviewContext = {
1473
+ owner: "o",
1474
+ repo: "r",
1475
+ dryRun: true,
1476
+ ci: false,
1477
+ specSources: ["/spec"],
1478
+ files: ["miniprogram/utils/asyncSharedUtilsLoader.js"],
1479
+ localMode: false,
1480
+ };
1481
+
1482
+ const result = await (service as any).resolveSourceData(context);
1483
+
1484
+ expect(result.isDirectFileMode).toBe(true);
1485
+ expect(result.isLocalMode).toBe(true);
1486
+ expect(result.changedFiles).toEqual([
1487
+ { filename: "miniprogram/utils/asyncSharedUtilsLoader.js", status: "modified" },
1488
+ ]);
1489
+ expect(mockGitSdkService.getUncommittedFiles).not.toHaveBeenCalled();
1490
+ expect(mockGitSdkService.getStagedFiles).not.toHaveBeenCalled();
1491
+ });
1492
+ });
1493
+
1432
1494
  describe("ReviewService.getFilesForCommit", () => {
1433
1495
  it("should return files from git sdk", async () => {
1434
1496
  mockGitSdkService.getFilesForCommit.mockResolvedValue([
@@ -97,6 +97,10 @@ export class ReviewService {
97
97
  if (source.earlyReturn) return source.earlyReturn;
98
98
 
99
99
  const { prModel, commits, changedFiles, headSha, isDirectFileMode } = source;
100
+ const effectiveWhenModifiedCode = isDirectFileMode ? undefined : context.whenModifiedCode;
101
+ if (isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
102
+ console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
103
+ }
100
104
 
101
105
  // 2. 规则匹配
102
106
  const specs = await this.issueFilter.loadSpecs(specSources, verbose);
@@ -147,7 +151,7 @@ export class ReviewService {
147
151
  fileContents,
148
152
  commits,
149
153
  existingResultModel?.result ?? null,
150
- context.whenModifiedCode,
154
+ effectiveWhenModifiedCode,
151
155
  verbose,
152
156
  context.systemRules,
153
157
  );
@@ -234,7 +238,7 @@ export class ReviewService {
234
238
  duplicateWorkflowResolved,
235
239
  } = context;
236
240
 
237
- const isDirectFileMode = !!(files && files.length > 0 && baseRef === headRef);
241
+ const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
238
242
  let isLocalMode = !!localMode;
239
243
  let effectiveBaseRef = baseRef;
240
244
  let effectiveHeadRef = headRef;
@@ -294,8 +298,16 @@ export class ReviewService {
294
298
  }
295
299
  }
296
300
 
301
+ // 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
302
+ if (isDirectFileMode) {
303
+ if (shouldLog(verbose, 1)) {
304
+ console.log(`📥 直接审查指定文件模式 (${files!.length} 个文件)`);
305
+ }
306
+ changedFiles = files!.map((f) => ({ filename: f, status: "modified" as const }));
307
+ isLocalMode = true;
308
+ }
297
309
  // PR 模式、分支比较模式、或本地模式回退后的分支比较
298
- if (prNumber) {
310
+ else if (prNumber) {
299
311
  if (shouldLog(verbose, 1)) {
300
312
  console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
301
313
  }
@@ -330,12 +342,7 @@ export class ReviewService {
330
342
  }
331
343
  }
332
344
  } else if (effectiveBaseRef && effectiveHeadRef) {
333
- if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
334
- if (shouldLog(verbose, 1)) {
335
- console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
336
- }
337
- changedFiles = files.map((f) => ({ filename: f, status: "modified" as const }));
338
- } else if (changedFiles.length === 0) {
345
+ if (changedFiles.length === 0) {
339
346
  if (shouldLog(verbose, 1)) {
340
347
  console.log(
341
348
  `📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`,