@spaceflow/review 0.80.0 → 0.82.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 +27 -0
- package/dist/index.js +1048 -762
- package/package.json +3 -3
- package/src/README.md +0 -1
- package/src/changed-file-collection.ts +87 -0
- package/src/mcp/index.ts +5 -1
- package/src/prompt/issue-verify.ts +8 -3
- package/src/review-context.spec.ts +214 -0
- package/src/review-context.ts +4 -2
- package/src/review-issue-filter.spec.ts +742 -0
- package/src/review-issue-filter.ts +21 -280
- package/src/review-llm.spec.ts +287 -0
- package/src/review-llm.ts +19 -23
- package/src/review-report/formatters/markdown.formatter.ts +6 -7
- package/src/review-result-model.spec.ts +35 -4
- package/src/review-result-model.ts +58 -10
- package/src/review-source-resolver.ts +636 -0
- package/src/review-spec/review-spec.service.spec.ts +94 -12
- package/src/review-spec/review-spec.service.ts +289 -59
- package/src/review.service.spec.ts +142 -1154
- package/src/review.service.ts +177 -534
- package/src/types/changed-file-collection.ts +5 -0
- package/src/types/review-source-resolver.ts +55 -0
package/src/review.service.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GitProviderService,
|
|
3
3
|
PullRequestCommit,
|
|
4
|
-
ChangedFile,
|
|
5
4
|
type LLMMode,
|
|
6
5
|
LlmProxyService,
|
|
7
6
|
type VerboseLevel,
|
|
@@ -10,29 +9,37 @@ import {
|
|
|
10
9
|
} from "@spaceflow/core";
|
|
11
10
|
import type { IConfigReader } from "@spaceflow/core";
|
|
12
11
|
import { type ReviewConfig } from "./review.config";
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
ReviewSpecService,
|
|
14
|
+
ReviewResult,
|
|
15
|
+
FileSummary,
|
|
16
|
+
ReviewSpec,
|
|
17
|
+
FileContentsMap,
|
|
18
|
+
} from "./review-spec";
|
|
19
|
+
import { ChangedFileCollection } from "./changed-file-collection";
|
|
14
20
|
import { MarkdownFormatter, ReviewReportService } from "./review-report";
|
|
15
|
-
import micromatch from "micromatch";
|
|
16
21
|
import { ReviewOptions } from "./review.config";
|
|
17
22
|
import { IssueVerifyService } from "./issue-verify.service";
|
|
18
23
|
import { DeletionImpactService } from "./deletion-impact.service";
|
|
19
24
|
import { execSync } from "child_process";
|
|
20
25
|
import { ReviewContextBuilder, type ReviewContext } from "./review-context";
|
|
21
26
|
import { ReviewIssueFilter } from "./review-issue-filter";
|
|
22
|
-
import { filterFilesByIncludes
|
|
27
|
+
import { filterFilesByIncludes } from "./review-includes-filter";
|
|
23
28
|
import { ReviewLlmProcessor } from "./review-llm";
|
|
24
29
|
import { PullRequestModel } from "./pull-request-model";
|
|
25
30
|
import { ReviewResultModel, type ReviewResultModelDeps } from "./review-result-model";
|
|
26
|
-
import {
|
|
31
|
+
import { ReviewSourceResolver, type SourceData } from "./review-source-resolver";
|
|
27
32
|
|
|
28
33
|
export type { ReviewContext } from "./review-context";
|
|
29
34
|
export type { FileReviewPrompt, ReviewPrompt, LLMReviewOptions } from "./review-llm";
|
|
35
|
+
export type { SourceData } from "./review-source-resolver";
|
|
30
36
|
|
|
31
37
|
export class ReviewService {
|
|
32
38
|
protected readonly contextBuilder: ReviewContextBuilder;
|
|
33
39
|
protected readonly issueFilter: ReviewIssueFilter;
|
|
34
40
|
protected readonly llmProcessor: ReviewLlmProcessor;
|
|
35
41
|
protected readonly resultModelDeps: ReviewResultModelDeps;
|
|
42
|
+
protected readonly sourceResolver: ReviewSourceResolver;
|
|
36
43
|
|
|
37
44
|
constructor(
|
|
38
45
|
protected readonly gitProvider: GitProviderService,
|
|
@@ -53,6 +60,7 @@ export class ReviewService {
|
|
|
53
60
|
gitSdk,
|
|
54
61
|
);
|
|
55
62
|
this.llmProcessor = new ReviewLlmProcessor(llmProxyService, reviewSpecService);
|
|
63
|
+
this.sourceResolver = new ReviewSourceResolver(gitProvider, gitSdk, this.issueFilter);
|
|
56
64
|
this.resultModelDeps = {
|
|
57
65
|
gitProvider,
|
|
58
66
|
config,
|
|
@@ -96,47 +104,42 @@ export class ReviewService {
|
|
|
96
104
|
const source = await this.resolveSourceData(context);
|
|
97
105
|
if (source.earlyReturn) return source.earlyReturn;
|
|
98
106
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
const effectiveWhenModifiedCode = source.isDirectFileMode
|
|
108
|
+
? undefined
|
|
109
|
+
: context.whenModifiedCode;
|
|
110
|
+
if (source.isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
|
|
102
111
|
console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
// 2. 规则匹配
|
|
106
|
-
const
|
|
107
|
-
const
|
|
115
|
+
const allSpecs = await this.issueFilter.loadSpecs(specSources, verbose);
|
|
116
|
+
const specs = this.reviewSpecService.filterApplicableSpecs(allSpecs, source.changedFiles);
|
|
108
117
|
if (shouldLog(verbose, 2)) {
|
|
109
118
|
console.log(
|
|
110
119
|
`[execute] loadSpecs: loaded ${specs.length} specs from sources: ${JSON.stringify(specSources)}`,
|
|
111
120
|
);
|
|
112
121
|
console.log(
|
|
113
|
-
`[execute] filterApplicableSpecs: ${
|
|
122
|
+
`[execute] filterApplicableSpecs: ${specs.length} applicable out of ${allSpecs.length}, changedFiles=${JSON.stringify(source.changedFiles.filenames())}`,
|
|
114
123
|
);
|
|
115
124
|
}
|
|
116
125
|
if (shouldLog(verbose, 1)) {
|
|
117
|
-
console.log(` 适用的规则文件: ${
|
|
126
|
+
console.log(` 适用的规则文件: ${specs.length}`);
|
|
118
127
|
}
|
|
119
|
-
if (
|
|
120
|
-
return this.handleNoApplicableSpecs(context,
|
|
128
|
+
if (specs.length === 0 || source.changedFiles.length === 0) {
|
|
129
|
+
return this.handleNoApplicableSpecs(context, specs, source.changedFiles, source.commits);
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
// 3.
|
|
124
|
-
const fileContents =
|
|
125
|
-
context.owner,
|
|
126
|
-
context.repo,
|
|
127
|
-
changedFiles,
|
|
128
|
-
commits,
|
|
129
|
-
headSha,
|
|
130
|
-
context.prNumber,
|
|
131
|
-
verbose,
|
|
132
|
-
source.isLocalMode,
|
|
133
|
-
);
|
|
132
|
+
// 3. LLM 审查
|
|
133
|
+
const { fileContents } = source;
|
|
134
134
|
if (!llmMode) throw new Error("必须指定 LLM 类型");
|
|
135
135
|
|
|
136
136
|
// 获取上一次的审查结果(用于提示词优化和轮次推进)
|
|
137
137
|
let existingResultModel: ReviewResultModel | null = null;
|
|
138
|
-
if (context.ci && prModel) {
|
|
139
|
-
existingResultModel = await ReviewResultModel.loadFromPr(
|
|
138
|
+
if (context.ci && source.prModel) {
|
|
139
|
+
existingResultModel = await ReviewResultModel.loadFromPr(
|
|
140
|
+
source.prModel,
|
|
141
|
+
this.resultModelDeps,
|
|
142
|
+
);
|
|
140
143
|
if (existingResultModel && shouldLog(verbose, 1)) {
|
|
141
144
|
console.log(`📋 获取到上一次审查结果,包含 ${existingResultModel.issues.length} 个问题`);
|
|
142
145
|
}
|
|
@@ -145,17 +148,67 @@ export class ReviewService {
|
|
|
145
148
|
console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
const reviewPrompt = await this.buildReviewPrompt(
|
|
151
|
+
const reviewPrompt = await this.llmProcessor.buildReviewPrompt(
|
|
149
152
|
specs,
|
|
150
|
-
changedFiles,
|
|
153
|
+
source.changedFiles,
|
|
151
154
|
fileContents,
|
|
152
|
-
commits,
|
|
155
|
+
source.commits,
|
|
153
156
|
existingResultModel?.result ?? null,
|
|
154
157
|
effectiveWhenModifiedCode,
|
|
155
158
|
verbose,
|
|
156
159
|
context.systemRules,
|
|
157
160
|
);
|
|
158
|
-
|
|
161
|
+
// 4. 运行 LLM 审查 + 过滤新 issues
|
|
162
|
+
const result = await this.buildReviewResult(context, reviewPrompt, llmMode, {
|
|
163
|
+
specs,
|
|
164
|
+
fileContents,
|
|
165
|
+
changedFiles: source.changedFiles,
|
|
166
|
+
commits: source.commits,
|
|
167
|
+
isDirectFileMode: source.isDirectFileMode,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 5. 构建最终的 ReviewResultModel
|
|
171
|
+
const finalModel = await this.buildFinalModel(
|
|
172
|
+
context,
|
|
173
|
+
result,
|
|
174
|
+
{
|
|
175
|
+
prModel: source.prModel,
|
|
176
|
+
commits: source.commits,
|
|
177
|
+
headSha: source.headSha,
|
|
178
|
+
specs,
|
|
179
|
+
fileContents,
|
|
180
|
+
},
|
|
181
|
+
existingResultModel,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// 6. 保存 + 输出
|
|
185
|
+
await this.saveAndOutput(context, finalModel, source.commits);
|
|
186
|
+
return finalModel.result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 运行 LLM 审查并构建过滤后的 ReviewResult:
|
|
191
|
+
* - 调用 LLM 生成问题列表
|
|
192
|
+
* - 填充 PR 标题/描述
|
|
193
|
+
* - 过滤新 issues(去重、commit 范围等)
|
|
194
|
+
* - 合并静态规则问题
|
|
195
|
+
*/
|
|
196
|
+
protected async buildReviewResult(
|
|
197
|
+
context: ReviewContext,
|
|
198
|
+
reviewPrompt: Awaited<ReturnType<typeof this.llmProcessor.buildReviewPrompt>>,
|
|
199
|
+
llmMode: LLMMode,
|
|
200
|
+
source: {
|
|
201
|
+
specs: ReviewSpec[];
|
|
202
|
+
fileContents: FileContentsMap;
|
|
203
|
+
changedFiles: ChangedFileCollection;
|
|
204
|
+
commits: PullRequestCommit[];
|
|
205
|
+
isDirectFileMode: boolean;
|
|
206
|
+
},
|
|
207
|
+
): Promise<ReviewResult> {
|
|
208
|
+
const { verbose } = context;
|
|
209
|
+
const { specs, fileContents, changedFiles, commits, isDirectFileMode } = source;
|
|
210
|
+
|
|
211
|
+
const result = await this.llmProcessor.runLLMReview(llmMode, reviewPrompt, {
|
|
159
212
|
verbose,
|
|
160
213
|
concurrency: context.concurrency,
|
|
161
214
|
timeout: context.timeout,
|
|
@@ -165,17 +218,22 @@ export class ReviewService {
|
|
|
165
218
|
|
|
166
219
|
// 填充 PR 功能描述和标题
|
|
167
220
|
const prInfo = context.generateDescription
|
|
168
|
-
? await this.generatePrDescription(
|
|
169
|
-
|
|
221
|
+
? await this.llmProcessor.generatePrDescription(
|
|
222
|
+
commits,
|
|
223
|
+
changedFiles,
|
|
224
|
+
llmMode,
|
|
225
|
+
fileContents,
|
|
226
|
+
verbose,
|
|
227
|
+
)
|
|
228
|
+
: await this.llmProcessor.buildBasicDescription(commits, changedFiles);
|
|
170
229
|
result.title = prInfo.title;
|
|
171
230
|
result.description = prInfo.description;
|
|
172
231
|
if (shouldLog(verbose, 1)) {
|
|
173
232
|
console.log(`📝 LLM 审查完成,发现 ${result.issues.length} 个问题`);
|
|
174
233
|
}
|
|
175
234
|
|
|
176
|
-
|
|
177
|
-
result.issues =
|
|
178
|
-
result.issues = this.filterNewIssues(result.issues, specs, applicableSpecs, {
|
|
235
|
+
result.issues = await this.issueFilter.fillIssueCode(result.issues, fileContents);
|
|
236
|
+
result.issues = this.filterNewIssues(result.issues, specs, {
|
|
179
237
|
commits,
|
|
180
238
|
fileContents,
|
|
181
239
|
changedFiles,
|
|
@@ -194,263 +252,15 @@ export class ReviewService {
|
|
|
194
252
|
console.log(`📝 最终发现 ${result.issues.length} 个问题`);
|
|
195
253
|
}
|
|
196
254
|
|
|
197
|
-
|
|
198
|
-
const finalModel = await this.buildFinalModel(
|
|
199
|
-
context,
|
|
200
|
-
result,
|
|
201
|
-
{ prModel, commits, headSha, specs, fileContents },
|
|
202
|
-
existingResultModel,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// 6. 保存 + 输出
|
|
206
|
-
await this.saveAndOutput(context, finalModel, commits);
|
|
207
|
-
return finalModel.result;
|
|
255
|
+
return result;
|
|
208
256
|
}
|
|
209
257
|
|
|
210
|
-
// ─── 提取的子方法 ──────────────────────────────────────
|
|
211
|
-
|
|
212
258
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
|
|
259
|
+
* 解析输入数据:委托给 ReviewSourceResolver。
|
|
260
|
+
* @see ReviewSourceResolver#resolve
|
|
216
261
|
*/
|
|
217
|
-
protected async resolveSourceData(context: ReviewContext): Promise<{
|
|
218
|
-
|
|
219
|
-
commits: PullRequestCommit[];
|
|
220
|
-
changedFiles: ChangedFile[];
|
|
221
|
-
headSha: string;
|
|
222
|
-
isLocalMode: boolean;
|
|
223
|
-
isDirectFileMode: boolean;
|
|
224
|
-
earlyReturn?: ReviewResult;
|
|
225
|
-
}> {
|
|
226
|
-
const {
|
|
227
|
-
owner,
|
|
228
|
-
repo,
|
|
229
|
-
prNumber,
|
|
230
|
-
baseRef,
|
|
231
|
-
headRef,
|
|
232
|
-
verbose,
|
|
233
|
-
ci,
|
|
234
|
-
includes,
|
|
235
|
-
files,
|
|
236
|
-
commits: filterCommits,
|
|
237
|
-
localMode,
|
|
238
|
-
duplicateWorkflowResolved,
|
|
239
|
-
} = context;
|
|
240
|
-
|
|
241
|
-
const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
|
|
242
|
-
let isLocalMode = !!localMode;
|
|
243
|
-
let effectiveBaseRef = baseRef;
|
|
244
|
-
let effectiveHeadRef = headRef;
|
|
245
|
-
|
|
246
|
-
let prModel: PullRequestModel | undefined;
|
|
247
|
-
let commits: PullRequestCommit[] = [];
|
|
248
|
-
let changedFiles: ChangedFile[] = [];
|
|
249
|
-
|
|
250
|
-
if (isLocalMode) {
|
|
251
|
-
// 本地模式:从 git 获取未提交/暂存区的变更
|
|
252
|
-
if (shouldLog(verbose, 1)) {
|
|
253
|
-
console.log(`📥 本地模式: 获取${localMode === "staged" ? "暂存区" : "未提交"}的代码变更`);
|
|
254
|
-
}
|
|
255
|
-
const localFiles =
|
|
256
|
-
localMode === "staged" ? this.gitSdk.getStagedFiles() : this.gitSdk.getUncommittedFiles();
|
|
257
|
-
|
|
258
|
-
if (localFiles.length === 0) {
|
|
259
|
-
// 本地无变更,回退到分支比较模式
|
|
260
|
-
if (shouldLog(verbose, 1)) {
|
|
261
|
-
console.log(
|
|
262
|
-
`ℹ️ 没有${localMode === "staged" ? "暂存区" : "未提交"}的代码变更,回退到分支比较模式`,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
isLocalMode = false;
|
|
266
|
-
effectiveHeadRef = this.gitSdk.getCurrentBranch() ?? "HEAD";
|
|
267
|
-
effectiveBaseRef = this.gitSdk.getDefaultBranch();
|
|
268
|
-
if (shouldLog(verbose, 1)) {
|
|
269
|
-
console.log(`📌 自动检测分支: base=${effectiveBaseRef}, head=${effectiveHeadRef}`);
|
|
270
|
-
}
|
|
271
|
-
// 同分支无法比较,提前返回
|
|
272
|
-
if (effectiveBaseRef === effectiveHeadRef) {
|
|
273
|
-
console.log(`ℹ️ 当前分支 ${effectiveHeadRef} 与默认分支相同,没有可审查的代码变更`);
|
|
274
|
-
return {
|
|
275
|
-
commits: [],
|
|
276
|
-
changedFiles: [],
|
|
277
|
-
headSha: "HEAD",
|
|
278
|
-
isLocalMode: false,
|
|
279
|
-
isDirectFileMode: false,
|
|
280
|
-
earlyReturn: { success: true, description: "", issues: [], summary: [], round: 1 },
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
// 一次性获取所有 diff,避免每个文件调用一次 git 命令
|
|
285
|
-
const localDiffs =
|
|
286
|
-
localMode === "staged" ? this.gitSdk.getStagedDiff() : this.gitSdk.getUncommittedDiff();
|
|
287
|
-
const diffMap = new Map(localDiffs.map((d) => [d.filename, d.patch]));
|
|
288
|
-
|
|
289
|
-
changedFiles = localFiles.map((f) => ({
|
|
290
|
-
filename: f.filename,
|
|
291
|
-
status: f.status as ChangedFile["status"],
|
|
292
|
-
patch: diffMap.get(f.filename),
|
|
293
|
-
}));
|
|
294
|
-
|
|
295
|
-
if (shouldLog(verbose, 1)) {
|
|
296
|
-
console.log(` Changed files: ${changedFiles.length}`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
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
|
-
}
|
|
309
|
-
// PR 模式、分支比较模式、或本地模式回退后的分支比较
|
|
310
|
-
else if (prNumber) {
|
|
311
|
-
if (shouldLog(verbose, 1)) {
|
|
312
|
-
console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
|
|
313
|
-
}
|
|
314
|
-
prModel = new PullRequestModel(this.gitProvider, owner, repo, prNumber);
|
|
315
|
-
const prInfo = await prModel.getInfo();
|
|
316
|
-
commits = await prModel.getCommits();
|
|
317
|
-
changedFiles = await prModel.getFiles();
|
|
318
|
-
if (shouldLog(verbose, 1)) {
|
|
319
|
-
console.log(` PR: ${prInfo?.title}`);
|
|
320
|
-
console.log(` Commits: ${commits.length}`);
|
|
321
|
-
console.log(` Changed files: ${changedFiles.length}`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// 检查是否有其他同名 review workflow 正在运行中
|
|
325
|
-
if (duplicateWorkflowResolved !== "off" && ci && prInfo?.head?.sha) {
|
|
326
|
-
const duplicateResult = await this.checkDuplicateWorkflow(
|
|
327
|
-
prModel,
|
|
328
|
-
prInfo.head.sha,
|
|
329
|
-
duplicateWorkflowResolved,
|
|
330
|
-
verbose,
|
|
331
|
-
);
|
|
332
|
-
if (duplicateResult) {
|
|
333
|
-
return {
|
|
334
|
-
prModel,
|
|
335
|
-
commits,
|
|
336
|
-
changedFiles,
|
|
337
|
-
headSha: prInfo.head.sha,
|
|
338
|
-
isLocalMode,
|
|
339
|
-
isDirectFileMode,
|
|
340
|
-
earlyReturn: duplicateResult,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
345
|
-
if (changedFiles.length === 0) {
|
|
346
|
-
if (shouldLog(verbose, 1)) {
|
|
347
|
-
console.log(
|
|
348
|
-
`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
changedFiles = await this.getChangedFilesBetweenRefs(
|
|
352
|
-
owner,
|
|
353
|
-
repo,
|
|
354
|
-
effectiveBaseRef,
|
|
355
|
-
effectiveHeadRef,
|
|
356
|
-
);
|
|
357
|
-
commits = await this.getCommitsBetweenRefs(effectiveBaseRef, effectiveHeadRef);
|
|
358
|
-
if (shouldLog(verbose, 1)) {
|
|
359
|
-
console.log(` Changed files: ${changedFiles.length}`);
|
|
360
|
-
console.log(` Commits: ${commits.length}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
} else if (!isLocalMode) {
|
|
364
|
-
if (shouldLog(verbose, 1)) {
|
|
365
|
-
console.log(`❌ 错误: 缺少 prNumber 或 baseRef/headRef`, { prNumber, baseRef, headRef });
|
|
366
|
-
}
|
|
367
|
-
throw new Error("必须指定 PR 编号或者 base/head 分支");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// ── 前置过滤 ──────────────────────────────────────────
|
|
371
|
-
|
|
372
|
-
// 0. 过滤掉 merge commit
|
|
373
|
-
{
|
|
374
|
-
const before = commits.length;
|
|
375
|
-
commits = commits.filter((c) => {
|
|
376
|
-
const message = c.commit?.message || "";
|
|
377
|
-
return !message.startsWith("Merge ");
|
|
378
|
-
});
|
|
379
|
-
if (before !== commits.length && shouldLog(verbose, 1)) {
|
|
380
|
-
console.log(` 跳过 Merge Commits: ${before} -> ${commits.length} 个`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// 1. 按指定的 files 过滤
|
|
385
|
-
if (files && files.length > 0) {
|
|
386
|
-
const before = changedFiles.length;
|
|
387
|
-
changedFiles = changedFiles.filter((f) => files.includes(f.filename || ""));
|
|
388
|
-
if (shouldLog(verbose, 1)) {
|
|
389
|
-
console.log(` Files 过滤文件: ${before} -> ${changedFiles.length} 个文件`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// 2. 按指定的 commits 过滤
|
|
394
|
-
if (filterCommits && filterCommits.length > 0) {
|
|
395
|
-
const beforeCommits = commits.length;
|
|
396
|
-
commits = commits.filter((c) => filterCommits.some((fc) => fc && c.sha?.startsWith(fc)));
|
|
397
|
-
if (shouldLog(verbose, 1)) {
|
|
398
|
-
console.log(` Commits 过滤: ${beforeCommits} -> ${commits.length} 个`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const beforeFiles = changedFiles.length;
|
|
402
|
-
const commitFilenames = new Set<string>();
|
|
403
|
-
for (const commit of commits) {
|
|
404
|
-
if (!commit.sha) continue;
|
|
405
|
-
const commitFiles = await this.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
406
|
-
commitFiles.forEach((f) => commitFilenames.add(f));
|
|
407
|
-
}
|
|
408
|
-
changedFiles = changedFiles.filter((f) => commitFilenames.has(f.filename || ""));
|
|
409
|
-
if (shouldLog(verbose, 1)) {
|
|
410
|
-
console.log(` 按 Commits 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// 3. 使用 includes 过滤文件和 commits(支持 added|/modified|/deleted| 前缀语法)
|
|
415
|
-
if (isDirectFileMode && includes && includes.length > 0) {
|
|
416
|
-
if (shouldLog(verbose, 1)) {
|
|
417
|
-
console.log(`ℹ️ 直接文件模式下忽略 includes 过滤`);
|
|
418
|
-
}
|
|
419
|
-
} else if (includes && includes.length > 0) {
|
|
420
|
-
const beforeFiles = changedFiles.length;
|
|
421
|
-
if (shouldLog(verbose, 2)) {
|
|
422
|
-
console.log(
|
|
423
|
-
`[resolveSourceData] filterFilesByIncludes: before=${JSON.stringify(changedFiles.map((f) => ({ filename: f.filename, status: f.status })))}, includes=${JSON.stringify(includes)}`,
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
changedFiles = filterFilesByIncludes(changedFiles, includes);
|
|
427
|
-
if (shouldLog(verbose, 1)) {
|
|
428
|
-
console.log(` Includes 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
429
|
-
}
|
|
430
|
-
if (shouldLog(verbose, 2)) {
|
|
431
|
-
console.log(
|
|
432
|
-
`[resolveSourceData] filterFilesByIncludes: after=${JSON.stringify(changedFiles.map((f) => f.filename))}`,
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const globs = extractGlobsFromIncludes(includes);
|
|
437
|
-
const beforeCommits = commits.length;
|
|
438
|
-
const filteredCommits: PullRequestCommit[] = [];
|
|
439
|
-
for (const commit of commits) {
|
|
440
|
-
if (!commit.sha) continue;
|
|
441
|
-
const commitFiles = await this.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
442
|
-
if (micromatch.some(commitFiles, globs)) {
|
|
443
|
-
filteredCommits.push(commit);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
commits = filteredCommits;
|
|
447
|
-
if (shouldLog(verbose, 1)) {
|
|
448
|
-
console.log(` Includes 过滤 Commits: ${beforeCommits} -> ${commits.length} 个`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const headSha = prModel ? await prModel.getHeadSha() : headRef || "HEAD";
|
|
453
|
-
return { prModel, commits, changedFiles, headSha, isLocalMode, isDirectFileMode };
|
|
262
|
+
protected async resolveSourceData(context: ReviewContext): Promise<SourceData> {
|
|
263
|
+
return this.sourceResolver.resolve(context);
|
|
454
264
|
}
|
|
455
265
|
|
|
456
266
|
/**
|
|
@@ -460,11 +270,10 @@ export class ReviewService {
|
|
|
460
270
|
protected filterNewIssues(
|
|
461
271
|
issues: ReviewResult["issues"],
|
|
462
272
|
specs: any[],
|
|
463
|
-
applicableSpecs: any[],
|
|
464
273
|
opts: {
|
|
465
274
|
commits: PullRequestCommit[];
|
|
466
275
|
fileContents: any;
|
|
467
|
-
changedFiles:
|
|
276
|
+
changedFiles: ChangedFileCollection;
|
|
468
277
|
isDirectFileMode: boolean;
|
|
469
278
|
context: ReviewContext;
|
|
470
279
|
},
|
|
@@ -472,7 +281,7 @@ export class ReviewService {
|
|
|
472
281
|
const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
|
|
473
282
|
const { verbose } = context;
|
|
474
283
|
|
|
475
|
-
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues,
|
|
284
|
+
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
|
|
476
285
|
if (shouldLog(verbose, 1)) {
|
|
477
286
|
console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
|
|
478
287
|
}
|
|
@@ -482,7 +291,7 @@ export class ReviewService {
|
|
|
482
291
|
console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
|
|
483
292
|
}
|
|
484
293
|
|
|
485
|
-
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered,
|
|
294
|
+
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
|
|
486
295
|
|
|
487
296
|
// 变更行过滤
|
|
488
297
|
if (shouldLog(verbose, 3)) {
|
|
@@ -491,21 +300,27 @@ export class ReviewService {
|
|
|
491
300
|
` showAll=${context.showAll}, isDirectFileMode=${isDirectFileMode}, commits.length=${commits.length}`,
|
|
492
301
|
);
|
|
493
302
|
}
|
|
494
|
-
if (!context.showAll && !isDirectFileMode
|
|
303
|
+
if (!context.showAll && !isDirectFileMode) {
|
|
495
304
|
if (shouldLog(verbose, 2)) {
|
|
496
305
|
console.log(` 🔍 开始变更行过滤,当前 ${filtered.length} 个问题`);
|
|
497
306
|
}
|
|
498
|
-
filtered = this.filterIssuesByValidCommits(
|
|
307
|
+
filtered = this.issueFilter.filterIssuesByValidCommits(
|
|
308
|
+
filtered,
|
|
309
|
+
commits,
|
|
310
|
+
fileContents,
|
|
311
|
+
verbose,
|
|
312
|
+
);
|
|
499
313
|
if (shouldLog(verbose, 2)) {
|
|
500
314
|
console.log(` 🔍 变更行过滤完成,剩余 ${filtered.length} 个问题`);
|
|
501
315
|
}
|
|
502
316
|
} else if (shouldLog(verbose, 1)) {
|
|
503
|
-
console.log(
|
|
504
|
-
` 跳过变更行过滤 (${context.showAll ? "showAll=true" : isDirectFileMode ? "直接审查文件模式" : "commits.length=0"})`,
|
|
505
|
-
);
|
|
317
|
+
console.log(` 跳过变更行过滤 (${context.showAll ? "showAll=true" : "直接审查文件模式"})`);
|
|
506
318
|
}
|
|
507
319
|
|
|
508
|
-
filtered = this.reviewSpecService.formatIssues(filtered, {
|
|
320
|
+
filtered = this.reviewSpecService.formatIssues(filtered, {
|
|
321
|
+
specs,
|
|
322
|
+
changedFiles: changedFiles.toArray(),
|
|
323
|
+
});
|
|
509
324
|
if (shouldLog(verbose, 1)) {
|
|
510
325
|
console.log(` 应用格式化后: ${filtered.length} 个问题`);
|
|
511
326
|
}
|
|
@@ -530,6 +345,7 @@ export class ReviewService {
|
|
|
530
345
|
): Promise<ReviewResultModel> {
|
|
531
346
|
const { prModel, commits, headSha, specs, fileContents } = source;
|
|
532
347
|
const { verbose, ci } = context;
|
|
348
|
+
result.headSha = headSha;
|
|
533
349
|
|
|
534
350
|
if (ci && prModel && existingResultModel && existingResultModel.issues.length > 0) {
|
|
535
351
|
if (shouldLog(verbose, 1)) {
|
|
@@ -545,7 +361,7 @@ export class ReviewService {
|
|
|
545
361
|
reviewConf.invalidateChangedFiles !== "off" &&
|
|
546
362
|
reviewConf.invalidateChangedFiles !== "keep"
|
|
547
363
|
) {
|
|
548
|
-
await existingResultModel.invalidateChangedFiles(headSha, verbose);
|
|
364
|
+
await existingResultModel.invalidateChangedFiles(headSha, fileContents, verbose);
|
|
549
365
|
}
|
|
550
366
|
|
|
551
367
|
// 验证历史问题是否已修复
|
|
@@ -555,7 +371,6 @@ export class ReviewService {
|
|
|
555
371
|
existingResultModel.issues,
|
|
556
372
|
commits,
|
|
557
373
|
{ specs, fileContents },
|
|
558
|
-
prModel,
|
|
559
374
|
);
|
|
560
375
|
} else {
|
|
561
376
|
if (shouldLog(verbose, 1)) {
|
|
@@ -563,24 +378,12 @@ export class ReviewService {
|
|
|
563
378
|
}
|
|
564
379
|
}
|
|
565
380
|
|
|
566
|
-
//
|
|
567
|
-
const { filteredIssues: newIssues, skippedCount } = this.filterDuplicateIssues(
|
|
568
|
-
result.issues,
|
|
569
|
-
existingResultModel.issues,
|
|
570
|
-
);
|
|
571
|
-
if (skippedCount > 0 && shouldLog(verbose, 1)) {
|
|
572
|
-
console.log(` 跳过 ${skippedCount} 个重复问题,新增 ${newIssues.length} 个问题`);
|
|
573
|
-
}
|
|
574
|
-
result.issues = newIssues;
|
|
575
|
-
result.headSha = headSha;
|
|
576
|
-
|
|
577
|
-
// 自动 round 递增 + issues 合并
|
|
381
|
+
// 自动 round 递增 + 去重 + issues 合并
|
|
578
382
|
return existingResultModel.nextRound(result);
|
|
579
383
|
}
|
|
580
384
|
|
|
581
385
|
// 首次审查或无历史结果
|
|
582
386
|
result.round = 1;
|
|
583
|
-
result.headSha = headSha;
|
|
584
387
|
result.issues = result.issues.map((issue) => ({ ...issue, round: 1 }));
|
|
585
388
|
return prModel
|
|
586
389
|
? ReviewResultModel.create(prModel, result, this.resultModelDeps)
|
|
@@ -612,7 +415,7 @@ export class ReviewService {
|
|
|
612
415
|
|
|
613
416
|
// 填充 author 信息
|
|
614
417
|
if (commits.length > 0) {
|
|
615
|
-
finalModel.issues = await this.fillIssueAuthors(
|
|
418
|
+
finalModel.issues = await this.contextBuilder.fillIssueAuthors(
|
|
616
419
|
finalModel.issues,
|
|
617
420
|
commits,
|
|
618
421
|
owner,
|
|
@@ -694,7 +497,7 @@ export class ReviewService {
|
|
|
694
497
|
|
|
695
498
|
// 2. 获取 commits 并填充 author 信息
|
|
696
499
|
const commits = await prModel.getCommits();
|
|
697
|
-
resultModel.issues = await this.fillIssueAuthors(
|
|
500
|
+
resultModel.issues = await this.contextBuilder.fillIssueAuthors(
|
|
698
501
|
resultModel.issues,
|
|
699
502
|
commits,
|
|
700
503
|
owner,
|
|
@@ -709,16 +512,32 @@ export class ReviewService {
|
|
|
709
512
|
await resultModel.syncReactions(verbose);
|
|
710
513
|
|
|
711
514
|
// 5. LLM 验证历史问题是否已修复
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
515
|
+
if (context.verifyFixes && context.specSources?.length) {
|
|
516
|
+
try {
|
|
517
|
+
const changedFiles = await prModel.getFiles();
|
|
518
|
+
const headSha = await prModel.getHeadSha();
|
|
519
|
+
const verifySpecs = await this.issueFilter.loadSpecs(context.specSources, verbose);
|
|
520
|
+
const verifyFileContents = await this.sourceResolver.getFileContents(
|
|
521
|
+
owner,
|
|
522
|
+
repo,
|
|
523
|
+
changedFiles,
|
|
524
|
+
commits,
|
|
525
|
+
headSha,
|
|
526
|
+
prNumber,
|
|
527
|
+
false,
|
|
528
|
+
verbose,
|
|
529
|
+
);
|
|
530
|
+
resultModel.issues = await this.issueFilter.verifyAndUpdateIssues(
|
|
531
|
+
context,
|
|
532
|
+
resultModel.issues,
|
|
533
|
+
commits,
|
|
534
|
+
{ specs: verifySpecs, fileContents: verifyFileContents },
|
|
535
|
+
);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.warn("⚠️ LLM 验证修复状态失败,跳过:", error);
|
|
538
|
+
}
|
|
539
|
+
} else if (!context.verifyFixes && shouldLog(verbose, 1)) {
|
|
540
|
+
console.log(` ⏭️ 跳过历史问题验证 (verifyFixes=false)`);
|
|
722
541
|
}
|
|
723
542
|
|
|
724
543
|
// 6. 统计问题状态并设置到 result
|
|
@@ -773,24 +592,34 @@ export class ReviewService {
|
|
|
773
592
|
// 获取 commits 和 changedFiles 用于生成描述
|
|
774
593
|
let prModel: PullRequestModel | undefined;
|
|
775
594
|
let commits: PullRequestCommit[] = [];
|
|
776
|
-
let changedFiles:
|
|
595
|
+
let changedFiles: ChangedFileCollection = ChangedFileCollection.empty();
|
|
777
596
|
if (prNumber) {
|
|
778
597
|
prModel = new PullRequestModel(this.gitProvider, owner, repo, prNumber);
|
|
779
598
|
commits = await prModel.getCommits();
|
|
780
|
-
changedFiles = await prModel.getFiles();
|
|
599
|
+
changedFiles = ChangedFileCollection.from(await prModel.getFiles());
|
|
781
600
|
} else if (baseRef && headRef) {
|
|
782
|
-
changedFiles =
|
|
783
|
-
|
|
601
|
+
changedFiles = ChangedFileCollection.from(
|
|
602
|
+
await this.issueFilter.getChangedFilesBetweenRefs(owner, repo, baseRef, headRef),
|
|
603
|
+
);
|
|
604
|
+
commits = await this.issueFilter.getCommitsBetweenRefs(baseRef, headRef);
|
|
784
605
|
}
|
|
785
606
|
|
|
786
607
|
// 使用 includes 过滤文件(支持 added|/modified|/deleted| 前缀语法)
|
|
787
608
|
if (context.includes && context.includes.length > 0) {
|
|
788
|
-
changedFiles =
|
|
609
|
+
changedFiles = ChangedFileCollection.from(
|
|
610
|
+
filterFilesByIncludes(changedFiles.toArray(), context.includes),
|
|
611
|
+
);
|
|
789
612
|
}
|
|
790
613
|
|
|
791
614
|
const prDesc = context.generateDescription
|
|
792
|
-
? await this.generatePrDescription(
|
|
793
|
-
|
|
615
|
+
? await this.llmProcessor.generatePrDescription(
|
|
616
|
+
commits,
|
|
617
|
+
changedFiles,
|
|
618
|
+
llmMode,
|
|
619
|
+
undefined,
|
|
620
|
+
verbose,
|
|
621
|
+
)
|
|
622
|
+
: await this.llmProcessor.buildBasicDescription(commits, changedFiles);
|
|
794
623
|
const result: ReviewResult = {
|
|
795
624
|
success: true,
|
|
796
625
|
title: prDesc.title,
|
|
@@ -832,7 +661,7 @@ export class ReviewService {
|
|
|
832
661
|
private async handleNoApplicableSpecs(
|
|
833
662
|
context: ReviewContext,
|
|
834
663
|
applicableSpecs: any[],
|
|
835
|
-
changedFiles:
|
|
664
|
+
changedFiles: ChangedFileCollection,
|
|
836
665
|
commits: PullRequestCommit[],
|
|
837
666
|
): Promise<ReviewResult> {
|
|
838
667
|
const { ci, prNumber, verbose, dryRun, llmMode, autoApprove } = context;
|
|
@@ -851,18 +680,22 @@ export class ReviewService {
|
|
|
851
680
|
const currentRound = (existingResultModel?.round ?? 0) + 1;
|
|
852
681
|
|
|
853
682
|
// 即使没有适用的规则,也为每个变更文件生成摘要
|
|
854
|
-
const summary: FileSummary[] = changedFiles
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
summary: applicableSpecs.length === 0 ? "无适用的审查规则" : "已跳过",
|
|
861
|
-
}));
|
|
683
|
+
const summary: FileSummary[] = changedFiles.nonDeletedFiles().map((f) => ({
|
|
684
|
+
file: f.filename!,
|
|
685
|
+
resolved: 0,
|
|
686
|
+
unresolved: 0,
|
|
687
|
+
summary: applicableSpecs.length === 0 ? "无适用的审查规则" : "已跳过",
|
|
688
|
+
}));
|
|
862
689
|
const prDesc =
|
|
863
690
|
context.generateDescription && llmMode
|
|
864
|
-
? await this.generatePrDescription(
|
|
865
|
-
|
|
691
|
+
? await this.llmProcessor.generatePrDescription(
|
|
692
|
+
commits,
|
|
693
|
+
changedFiles,
|
|
694
|
+
llmMode,
|
|
695
|
+
undefined,
|
|
696
|
+
verbose,
|
|
697
|
+
)
|
|
698
|
+
: await this.llmProcessor.buildBasicDescription(commits, changedFiles);
|
|
866
699
|
const result: ReviewResult = {
|
|
867
700
|
success: true,
|
|
868
701
|
title: prDesc.title,
|
|
@@ -887,196 +720,6 @@ export class ReviewService {
|
|
|
887
720
|
return result;
|
|
888
721
|
}
|
|
889
722
|
|
|
890
|
-
/**
|
|
891
|
-
* 检查是否有其他同名 review workflow 正在运行中
|
|
892
|
-
* 根据 duplicateWorkflowResolved 配置决定是跳过还是删除旧评论
|
|
893
|
-
*/
|
|
894
|
-
private async checkDuplicateWorkflow(
|
|
895
|
-
prModel: PullRequestModel,
|
|
896
|
-
headSha: string,
|
|
897
|
-
mode: "skip" | "delete",
|
|
898
|
-
verbose?: VerboseLevel,
|
|
899
|
-
): Promise<ReviewResult | null> {
|
|
900
|
-
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
901
|
-
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
902
|
-
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prModel.number;
|
|
903
|
-
|
|
904
|
-
try {
|
|
905
|
-
const runningWorkflows = await prModel.listWorkflowRuns({
|
|
906
|
-
status: "in_progress",
|
|
907
|
-
});
|
|
908
|
-
const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
|
|
909
|
-
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
910
|
-
const duplicateReviewRuns = runningWorkflows.filter(
|
|
911
|
-
(w) =>
|
|
912
|
-
w.sha === headSha &&
|
|
913
|
-
w.name === currentWorkflowName &&
|
|
914
|
-
(!currentRunId || String(w.id) !== currentRunId),
|
|
915
|
-
);
|
|
916
|
-
if (duplicateReviewRuns.length > 0) {
|
|
917
|
-
if (mode === "delete") {
|
|
918
|
-
// 删除模式:清理旧的 AI Review 评论和 PR Review
|
|
919
|
-
if (shouldLog(verbose, 1)) {
|
|
920
|
-
console.log(
|
|
921
|
-
`🗑️ 检测到 ${duplicateReviewRuns.length} 个同名 workflow,清理旧的 AI Review 评论...`,
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
await this.cleanupDuplicateAiReviews(prModel, verbose);
|
|
925
|
-
// 清理后继续执行当前审查
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// 跳过模式(默认)
|
|
930
|
-
if (shouldLog(verbose, 1)) {
|
|
931
|
-
console.log(
|
|
932
|
-
`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`,
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
return {
|
|
936
|
-
success: true,
|
|
937
|
-
description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
|
|
938
|
-
issues: [],
|
|
939
|
-
summary: [],
|
|
940
|
-
round: 1,
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
} catch (error) {
|
|
944
|
-
if (shouldLog(verbose, 1)) {
|
|
945
|
-
console.warn(
|
|
946
|
-
`⚠️ 无法检查重复 workflow(可能缺少 repo owner 权限),跳过此检查:`,
|
|
947
|
-
error instanceof Error ? error.message : error,
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
return null;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/**
|
|
955
|
-
* 清理重复的 AI Review 评论(Issue Comments 和 PR Reviews)
|
|
956
|
-
*/
|
|
957
|
-
private async cleanupDuplicateAiReviews(
|
|
958
|
-
prModel: PullRequestModel,
|
|
959
|
-
verbose?: VerboseLevel,
|
|
960
|
-
): Promise<void> {
|
|
961
|
-
try {
|
|
962
|
-
// 删除 Issue Comments(主评论)
|
|
963
|
-
const comments = await prModel.getComments();
|
|
964
|
-
const aiComments = comments.filter((c) => c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
965
|
-
let deletedComments = 0;
|
|
966
|
-
for (const comment of aiComments) {
|
|
967
|
-
if (comment.id) {
|
|
968
|
-
try {
|
|
969
|
-
await prModel.deleteComment(comment.id);
|
|
970
|
-
deletedComments++;
|
|
971
|
-
} catch {
|
|
972
|
-
// 忽略删除失败
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (deletedComments > 0 && shouldLog(verbose, 1)) {
|
|
977
|
-
console.log(` 已删除 ${deletedComments} 个重复的 AI Review 主评论`);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// 删除 PR Reviews(行级评论)
|
|
981
|
-
const reviews = await prModel.getReviews();
|
|
982
|
-
const aiReviews = reviews.filter((r) => r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
983
|
-
let deletedReviews = 0;
|
|
984
|
-
for (const review of aiReviews) {
|
|
985
|
-
if (review.id) {
|
|
986
|
-
try {
|
|
987
|
-
await prModel.deleteReview(review.id);
|
|
988
|
-
deletedReviews++;
|
|
989
|
-
} catch {
|
|
990
|
-
// 已提交的 review 无法删除,忽略
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
if (deletedReviews > 0 && shouldLog(verbose, 1)) {
|
|
995
|
-
console.log(` 已删除 ${deletedReviews} 个重复的 AI Review PR Review`);
|
|
996
|
-
}
|
|
997
|
-
} catch (error) {
|
|
998
|
-
if (shouldLog(verbose, 1)) {
|
|
999
|
-
console.warn(`⚠️ 清理旧评论失败:`, error instanceof Error ? error.message : error);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// --- Delegation methods for backward compatibility with tests ---
|
|
1005
|
-
|
|
1006
|
-
protected async fillIssueAuthors(...args: Parameters<ReviewContextBuilder["fillIssueAuthors"]>) {
|
|
1007
|
-
return this.contextBuilder.fillIssueAuthors(...args);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
protected async getFileContents(...args: Parameters<ReviewIssueFilter["getFileContents"]>) {
|
|
1011
|
-
return this.issueFilter.getFileContents(...args);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
protected async getFilesForCommit(...args: Parameters<ReviewIssueFilter["getFilesForCommit"]>) {
|
|
1015
|
-
return this.issueFilter.getFilesForCommit(...args);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
protected async getChangedFilesBetweenRefs(
|
|
1019
|
-
...args: Parameters<ReviewIssueFilter["getChangedFilesBetweenRefs"]>
|
|
1020
|
-
) {
|
|
1021
|
-
return this.issueFilter.getChangedFilesBetweenRefs(...args);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
protected async getCommitsBetweenRefs(
|
|
1025
|
-
...args: Parameters<ReviewIssueFilter["getCommitsBetweenRefs"]>
|
|
1026
|
-
) {
|
|
1027
|
-
return this.issueFilter.getCommitsBetweenRefs(...args);
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
protected filterIssuesByValidCommits(
|
|
1031
|
-
...args: Parameters<ReviewIssueFilter["filterIssuesByValidCommits"]>
|
|
1032
|
-
) {
|
|
1033
|
-
return this.issueFilter.filterIssuesByValidCommits(...args);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
protected filterDuplicateIssues(...args: Parameters<ReviewIssueFilter["filterDuplicateIssues"]>) {
|
|
1037
|
-
return this.issueFilter.filterDuplicateIssues(...args);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
protected async fillIssueCode(...args: Parameters<ReviewIssueFilter["fillIssueCode"]>) {
|
|
1041
|
-
return this.issueFilter.fillIssueCode(...args);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
protected async runLLMReview(...args: Parameters<ReviewLlmProcessor["runLLMReview"]>) {
|
|
1045
|
-
return this.llmProcessor.runLLMReview(...args);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
protected async buildReviewPrompt(...args: Parameters<ReviewLlmProcessor["buildReviewPrompt"]>) {
|
|
1049
|
-
return this.llmProcessor.buildReviewPrompt(...args);
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
protected async generatePrDescription(
|
|
1053
|
-
...args: Parameters<ReviewLlmProcessor["generatePrDescription"]>
|
|
1054
|
-
) {
|
|
1055
|
-
return this.llmProcessor.generatePrDescription(...args);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
protected async buildBasicDescription(
|
|
1059
|
-
...args: Parameters<ReviewLlmProcessor["buildBasicDescription"]>
|
|
1060
|
-
) {
|
|
1061
|
-
return this.llmProcessor.buildBasicDescription(...args);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
protected normalizeFilePaths(...args: Parameters<ReviewContextBuilder["normalizeFilePaths"]>) {
|
|
1065
|
-
return this.contextBuilder.normalizeFilePaths(...args);
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
protected resolveAnalyzeDeletions(
|
|
1069
|
-
...args: Parameters<ReviewContextBuilder["resolveAnalyzeDeletions"]>
|
|
1070
|
-
) {
|
|
1071
|
-
return this.contextBuilder.resolveAnalyzeDeletions(...args);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
protected async getPrNumberFromEvent(
|
|
1075
|
-
...args: Parameters<ReviewContextBuilder["getPrNumberFromEvent"]>
|
|
1076
|
-
) {
|
|
1077
|
-
return this.contextBuilder.getPrNumberFromEvent(...args);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
723
|
/**
|
|
1081
724
|
* 确保 Claude CLI 已安装
|
|
1082
725
|
*/
|