@spaceflow/review 0.77.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 +67 -0
- package/dist/index.js +1095 -500
- package/package.json +1 -1
- package/src/deletion-impact.service.ts +13 -128
- package/src/issue-verify.service.ts +18 -82
- package/src/mcp/index.ts +4 -1
- package/src/prompt/code-review.ts +95 -0
- package/src/prompt/deletion-impact.ts +105 -0
- package/src/prompt/index.ts +37 -0
- package/src/prompt/issue-verify.ts +86 -0
- package/src/prompt/pr-description.ts +149 -0
- package/src/prompt/schemas.ts +106 -0
- package/src/prompt/types.ts +53 -0
- package/src/review-context.ts +53 -15
- package/src/review-includes-filter.spec.ts +36 -0
- package/src/review-includes-filter.ts +59 -7
- package/src/review-issue-filter.ts +1 -1
- package/src/review-llm.ts +116 -207
- package/src/review-result-model.ts +28 -6
- package/src/review-spec/review-spec.service.ts +26 -5
- package/src/review.config.ts +31 -5
- package/src/review.service.spec.ts +75 -3
- package/src/review.service.ts +120 -13
- package/src/system-rules/index.ts +48 -0
- package/src/system-rules/max-lines-per-file.ts +57 -0
- package/src/types/review-llm.ts +2 -0
- package/src/utils/review-llm.spec.ts +277 -0
- package/src/utils/review-llm.ts +152 -7
- package/src/utils/review-pr-comment.spec.ts +340 -0
- package/src/{review-pr-comment-utils.ts → utils/review-pr-comment.ts} +2 -2
package/src/review-llm.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
createStreamLoggerState,
|
|
8
8
|
shouldLog,
|
|
9
9
|
type VerboseLevel,
|
|
10
|
-
type LlmJsonPutSchema,
|
|
11
10
|
LlmJsonPut,
|
|
12
11
|
parallel,
|
|
13
12
|
} from "@spaceflow/core";
|
|
@@ -23,52 +22,19 @@ import { readdir } from "fs/promises";
|
|
|
23
22
|
import { dirname, extname } from "path";
|
|
24
23
|
import micromatch from "micromatch";
|
|
25
24
|
import type { FileReviewPrompt, ReviewPrompt, LLMReviewOptions } from "./types/review-llm";
|
|
26
|
-
import { buildLinesWithNumbers, buildCommitsSection } from "./utils/review-llm";
|
|
25
|
+
import { buildLinesWithNumbers, buildCommitsSection, extractCodeBlocks } from "./utils/review-llm";
|
|
26
|
+
import { extractCodeBlockTypes, extractGlobsFromIncludes } from "./review-includes-filter";
|
|
27
|
+
import {
|
|
28
|
+
REVIEW_SCHEMA,
|
|
29
|
+
buildFileReviewPrompt,
|
|
30
|
+
buildPrDescriptionPrompt,
|
|
31
|
+
buildPrTitlePrompt,
|
|
32
|
+
} from "./prompt";
|
|
33
|
+
import type { SystemRules } from "./review.config";
|
|
34
|
+
import { applyStaticRules } from "./system-rules";
|
|
27
35
|
|
|
28
36
|
export type { FileReviewPrompt, ReviewPrompt, LLMReviewOptions } from "./types/review-llm";
|
|
29
37
|
|
|
30
|
-
const REVIEW_SCHEMA: LlmJsonPutSchema = {
|
|
31
|
-
type: "object",
|
|
32
|
-
properties: {
|
|
33
|
-
issues: {
|
|
34
|
-
type: "array",
|
|
35
|
-
items: {
|
|
36
|
-
type: "object",
|
|
37
|
-
properties: {
|
|
38
|
-
file: { type: "string", description: "发生问题的文件路径" },
|
|
39
|
-
line: {
|
|
40
|
-
type: "string",
|
|
41
|
-
description:
|
|
42
|
-
"问题所在的行号,只支持单行或多行 (如 123 或 123-125),不允许使用 `,` 分隔多个行号",
|
|
43
|
-
},
|
|
44
|
-
ruleId: { type: "string", description: "违反的规则 ID(如 JsTs.FileName.UpperCamel)" },
|
|
45
|
-
specFile: {
|
|
46
|
-
type: "string",
|
|
47
|
-
description: "规则来源的规范文件名(如 js&ts.file-name.md)",
|
|
48
|
-
},
|
|
49
|
-
reason: { type: "string", description: "问题的简要概括" },
|
|
50
|
-
suggestion: {
|
|
51
|
-
type: "string",
|
|
52
|
-
description:
|
|
53
|
-
"修改后的完整代码片段。要求以代码为主体,并在代码中使用详细的中文注释解释逻辑改进点。不要包含 Markdown 反引号。",
|
|
54
|
-
},
|
|
55
|
-
commit: { type: "string", description: "相关的 7 位 commit SHA" },
|
|
56
|
-
severity: {
|
|
57
|
-
type: "string",
|
|
58
|
-
description: "问题严重程度,根据规则文档中的 severity 标记确定",
|
|
59
|
-
enum: ["error", "warn"],
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
required: ["file", "line", "ruleId", "specFile", "reason"],
|
|
63
|
-
additionalProperties: false,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
summary: { type: "string", description: "本次代码审查的整体总结" },
|
|
67
|
-
},
|
|
68
|
-
required: ["issues", "summary"],
|
|
69
|
-
additionalProperties: false,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
38
|
export class ReviewLlmProcessor {
|
|
73
39
|
readonly llmJsonPut: LlmJsonPut<ReviewResult>;
|
|
74
40
|
|
|
@@ -109,8 +75,11 @@ export class ReviewLlmProcessor {
|
|
|
109
75
|
}
|
|
110
76
|
|
|
111
77
|
// 如果有 includes 配置,检查文件名是否匹配 includes 模式
|
|
78
|
+
// 需先提取纯 glob(去掉 added|/modified| 前缀,过滤 code-* 空串),避免 micromatch 报错
|
|
112
79
|
if (spec.includes.length > 0) {
|
|
113
|
-
|
|
80
|
+
const globs = extractGlobsFromIncludes(spec.includes);
|
|
81
|
+
if (globs.length === 0) return true;
|
|
82
|
+
return micromatch.isMatch(filename, globs, { matchBase: true });
|
|
114
83
|
}
|
|
115
84
|
|
|
116
85
|
// 没有 includes 配置,扩展名匹配即可
|
|
@@ -118,122 +87,121 @@ export class ReviewLlmProcessor {
|
|
|
118
87
|
});
|
|
119
88
|
}
|
|
120
89
|
|
|
121
|
-
/**
|
|
122
|
-
* 构建 systemPrompt
|
|
123
|
-
*/
|
|
124
|
-
buildSystemPrompt(specsSection: string): string {
|
|
125
|
-
return `你是一个专业的代码审查专家,负责根据团队的编码规范对代码进行严格审查。
|
|
126
|
-
|
|
127
|
-
## 审查规范
|
|
128
|
-
|
|
129
|
-
${specsSection}
|
|
130
|
-
|
|
131
|
-
## 审查要求
|
|
132
|
-
|
|
133
|
-
1. **严格遵循规范**:只按照上述审查规范进行审查,不要添加规范之外的要求
|
|
134
|
-
2. **精准定位问题**:每个问题必须指明具体的行号,行号从文件内容中的 "行号|" 格式获取
|
|
135
|
-
3. **避免重复报告**:如果提示词中包含"上一次审查结果",请不要重复报告已存在的问题
|
|
136
|
-
4. **提供可行建议**:对于每个问题,提供具体的修改建议代码
|
|
137
|
-
|
|
138
|
-
## 注意事项
|
|
139
|
-
|
|
140
|
-
- 变更文件内容已在上下文中提供,无需调用读取工具
|
|
141
|
-
- 你可以读取项目中的其他文件以了解上下文
|
|
142
|
-
- 不要调用编辑工具修改文件,你的职责是审查而非修改
|
|
143
|
-
- 文件内容格式为 "CommitHash 行号| 代码",输出的 line 字段应对应原始行号
|
|
144
|
-
|
|
145
|
-
## 输出要求
|
|
146
|
-
|
|
147
|
-
- 发现问题时:在 issues 数组中列出所有问题,每个问题包含 file、line、ruleId、specFile、reason、suggestion、severity
|
|
148
|
-
- 无论是否发现问题:都必须在 summary 中提供该文件的审查总结,简要说明审查结果`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
90
|
async buildReviewPrompt(
|
|
152
91
|
specs: ReviewSpec[],
|
|
153
92
|
changedFiles: ChangedFile[],
|
|
154
93
|
fileContents: FileContentsMap,
|
|
155
94
|
commits: PullRequestCommit[],
|
|
156
95
|
existingResult?: ReviewResult | null,
|
|
96
|
+
whenModifiedCode?: string[],
|
|
97
|
+
verbose?: VerboseLevel,
|
|
98
|
+
systemRules?: SystemRules,
|
|
157
99
|
): Promise<ReviewPrompt> {
|
|
100
|
+
const round = (existingResult?.round ?? 0) + 1;
|
|
101
|
+
const { staticIssues, skippedFiles } = applyStaticRules(
|
|
102
|
+
changedFiles,
|
|
103
|
+
fileContents,
|
|
104
|
+
systemRules,
|
|
105
|
+
round,
|
|
106
|
+
verbose,
|
|
107
|
+
);
|
|
108
|
+
|
|
158
109
|
const fileDataList = changedFiles
|
|
159
110
|
.filter((f) => f.status !== "deleted" && f.filename)
|
|
160
111
|
.map((file) => {
|
|
161
112
|
const filename = file.filename!;
|
|
113
|
+
if (skippedFiles.has(filename)) return null;
|
|
162
114
|
const contentLines = fileContents.get(filename);
|
|
163
115
|
if (!contentLines) {
|
|
164
|
-
return {
|
|
165
|
-
filename,
|
|
166
|
-
file,
|
|
167
|
-
linesWithNumbers: "(无法获取内容)",
|
|
168
|
-
commitsSection: "- 无相关 commits",
|
|
169
|
-
};
|
|
116
|
+
return { filename, file, contentLines: null, commitsSection: "- 无相关 commits" };
|
|
170
117
|
}
|
|
171
|
-
const linesWithNumbers = buildLinesWithNumbers(contentLines);
|
|
172
118
|
const commitsSection = buildCommitsSection(contentLines, commits);
|
|
173
|
-
return { filename, file,
|
|
119
|
+
return { filename, file, contentLines, commitsSection };
|
|
174
120
|
});
|
|
175
121
|
|
|
176
|
-
const filePrompts: FileReviewPrompt[] = await Promise.all(
|
|
177
|
-
fileDataList
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
: "
|
|
199
|
-
|
|
122
|
+
const filePrompts: (FileReviewPrompt | null)[] = await Promise.all(
|
|
123
|
+
fileDataList
|
|
124
|
+
.filter((item): item is NonNullable<typeof item> => item !== null)
|
|
125
|
+
.map(async ({ filename, file, contentLines, commitsSection }) => {
|
|
126
|
+
const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
|
|
127
|
+
|
|
128
|
+
// 根据文件过滤 specs,只注入与当前文件匹配的规则
|
|
129
|
+
const fileSpecs = this.filterSpecsForFile(specs, filename);
|
|
130
|
+
|
|
131
|
+
// 从全局 whenModifiedCode 配置中解析代码结构过滤类型
|
|
132
|
+
const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
|
|
133
|
+
|
|
134
|
+
// 构建带行号的内容:有 code-* 过滤时只输出匹配的代码块范围
|
|
135
|
+
let linesWithNumbers: string;
|
|
136
|
+
if (!contentLines) {
|
|
137
|
+
linesWithNumbers = "(无法获取内容)";
|
|
138
|
+
} else if (codeBlockTypes.length > 0) {
|
|
139
|
+
const visibleRanges = extractCodeBlocks(contentLines, codeBlockTypes);
|
|
140
|
+
// 如果配置了 whenModifiedCode 但没有匹配的代码块,跳过这个文件
|
|
141
|
+
if (visibleRanges.length === 0) {
|
|
142
|
+
if (shouldLog(verbose, 2)) {
|
|
143
|
+
console.log(
|
|
144
|
+
`[buildReviewPrompt] ${filename}: 没有匹配的 ${codeBlockTypes.join(", ")} 代码块,跳过审查`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
200
148
|
}
|
|
201
|
-
|
|
202
|
-
|
|
149
|
+
linesWithNumbers = buildLinesWithNumbers(contentLines, visibleRanges);
|
|
150
|
+
} else {
|
|
151
|
+
linesWithNumbers = buildLinesWithNumbers(contentLines);
|
|
203
152
|
}
|
|
204
|
-
previousReviewSection = parts.join("\n");
|
|
205
|
-
}
|
|
206
153
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
154
|
+
// 获取该文件上一次的审查结果
|
|
155
|
+
const existingFileSummary = existingResult?.summary?.find((s) => s.file === filename);
|
|
156
|
+
const existingFileIssues =
|
|
157
|
+
existingResult?.issues?.filter((i) => i.file === filename) ?? [];
|
|
158
|
+
|
|
159
|
+
let previousReviewSection = "";
|
|
160
|
+
if (existingFileSummary || existingFileIssues.length > 0) {
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
if (existingFileSummary?.summary) {
|
|
163
|
+
parts.push(`**总结**:\n`);
|
|
164
|
+
parts.push(`${existingFileSummary.summary}\n`);
|
|
165
|
+
}
|
|
166
|
+
if (existingFileIssues.length > 0) {
|
|
167
|
+
parts.push(`**已发现的问题** (${existingFileIssues.length} 个):\n`);
|
|
168
|
+
for (const issue of existingFileIssues) {
|
|
169
|
+
const status = issue.fixed
|
|
170
|
+
? "✅ 已修复"
|
|
171
|
+
: issue.valid === "false"
|
|
172
|
+
? "❌ 无效"
|
|
173
|
+
: "⚠️ 待处理";
|
|
174
|
+
parts.push(
|
|
175
|
+
`- [${status}] 行 ${issue.line}: ${issue.reason} (规则: ${issue.ruleId})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
parts.push("");
|
|
179
|
+
// parts.push("请注意:不要重复报告上述已发现的问题,除非代码有新的变更导致问题复现。\n");
|
|
180
|
+
}
|
|
181
|
+
previousReviewSection = parts.join("\n");
|
|
182
|
+
}
|
|
231
183
|
|
|
232
|
-
|
|
233
|
-
|
|
184
|
+
const specsSection = this.reviewSpecService.buildSpecsSection(fileSpecs);
|
|
185
|
+
const { systemPrompt, userPrompt } = buildFileReviewPrompt({
|
|
186
|
+
filename,
|
|
187
|
+
status: file.status,
|
|
188
|
+
linesWithNumbers,
|
|
189
|
+
commitsSection,
|
|
190
|
+
fileDirectoryInfo,
|
|
191
|
+
previousReviewSection,
|
|
192
|
+
specsSection,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return { filename, systemPrompt, userPrompt };
|
|
196
|
+
}),
|
|
234
197
|
);
|
|
235
198
|
|
|
236
|
-
|
|
199
|
+
// 过滤掉 null 值(跳过的文件)
|
|
200
|
+
const validFilePrompts = filePrompts.filter((fp): fp is FileReviewPrompt => fp !== null);
|
|
201
|
+
return {
|
|
202
|
+
filePrompts: validFilePrompts,
|
|
203
|
+
staticIssues: staticIssues.length > 0 ? staticIssues : undefined,
|
|
204
|
+
};
|
|
237
205
|
}
|
|
238
206
|
|
|
239
207
|
async runLLMReview(
|
|
@@ -475,54 +443,14 @@ ${previousReviewSection}`;
|
|
|
475
443
|
fileContents?: FileContentsMap,
|
|
476
444
|
verbose?: VerboseLevel,
|
|
477
445
|
): Promise<{ title: string; description: string }> {
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
.join("\n");
|
|
485
|
-
// 构建代码变更内容(只包含变更行,限制总长度)
|
|
486
|
-
let codeChangesSection = "";
|
|
487
|
-
if (fileContents && fileContents.size > 0) {
|
|
488
|
-
const codeSnippets: string[] = [];
|
|
489
|
-
let totalLength = 0;
|
|
490
|
-
const maxTotalLength = 8000; // 限制代码总长度
|
|
491
|
-
for (const [filename, lines] of fileContents) {
|
|
492
|
-
if (totalLength >= maxTotalLength) break;
|
|
493
|
-
// 只提取有变更的行(commitHash 不是 "-------")
|
|
494
|
-
const changedLines = lines
|
|
495
|
-
.map(([hash, code], idx) => (hash !== "-------" ? `${idx + 1}: ${code}` : null))
|
|
496
|
-
.filter(Boolean);
|
|
497
|
-
if (changedLines.length > 0) {
|
|
498
|
-
const snippet = `### ${filename}\n\`\`\`\n${changedLines.slice(0, 50).join("\n")}\n\`\`\``;
|
|
499
|
-
if (totalLength + snippet.length <= maxTotalLength) {
|
|
500
|
-
codeSnippets.push(snippet);
|
|
501
|
-
totalLength += snippet.length;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (codeSnippets.length > 0) {
|
|
506
|
-
codeChangesSection = `\n\n## 代码变更内容\n${codeSnippets.join("\n\n")}`;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const prompt = `请根据以下 PR 的 commit 记录、文件变更和代码内容,用简洁的中文总结这个 PR 实现了什么功能。
|
|
510
|
-
要求:
|
|
511
|
-
1. 第一行输出 PR 标题,格式必须是: Feat xxx 或 Fix xxx 或 Refactor xxx(根据变更类型选择,整体不超过 50 个字符)
|
|
512
|
-
2. 空一行后输出详细描述
|
|
513
|
-
3. 描述应该简明扼要,突出核心功能点
|
|
514
|
-
4. 使用 Markdown 格式
|
|
515
|
-
5. 不要逐条列出 commit,而是归纳总结
|
|
516
|
-
6. 重点分析代码变更的实际功能
|
|
517
|
-
|
|
518
|
-
## Commit 记录 (${commits.length} 个)
|
|
519
|
-
${commitMessages || "无"}
|
|
520
|
-
|
|
521
|
-
## 文件变更 (${changedFiles.length} 个文件)
|
|
522
|
-
${fileChanges || "无"}
|
|
523
|
-
${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` : ""}${codeChangesSection}`;
|
|
446
|
+
const { userPrompt } = buildPrDescriptionPrompt({
|
|
447
|
+
commits,
|
|
448
|
+
changedFiles,
|
|
449
|
+
fileContents,
|
|
450
|
+
});
|
|
451
|
+
|
|
524
452
|
try {
|
|
525
|
-
const stream = this.llmProxyService.chatStream([{ role: "user", content:
|
|
453
|
+
const stream = this.llmProxyService.chatStream([{ role: "user", content: userPrompt }], {
|
|
526
454
|
adapter: llmMode,
|
|
527
455
|
});
|
|
528
456
|
let content = "";
|
|
@@ -553,29 +481,10 @@ ${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` :
|
|
|
553
481
|
commits: PullRequestCommit[],
|
|
554
482
|
changedFiles: ChangedFile[],
|
|
555
483
|
): Promise<string> {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
.map((c) => c.commit?.message?.split("\n")[0])
|
|
559
|
-
.filter(Boolean)
|
|
560
|
-
.join("\n");
|
|
561
|
-
const fileChanges = changedFiles
|
|
562
|
-
.slice(0, 20)
|
|
563
|
-
.map((f) => `${f.filename} (${f.status})`)
|
|
564
|
-
.join("\n");
|
|
565
|
-
const prompt = `请根据以下 commit 记录和文件变更,生成一个简短的 PR 标题。
|
|
566
|
-
要求:
|
|
567
|
-
1. 格式必须是: Feat: xxx 或 Fix: xxx 或 Refactor: xxx
|
|
568
|
-
2. 根据变更内容选择合适的前缀(新功能用 Feat,修复用 Fix,重构用 Refactor)
|
|
569
|
-
3. xxx 部分用简短的中文描述(整体不超过 50 个字符)
|
|
570
|
-
4. 只输出标题,不要加任何解释
|
|
571
|
-
|
|
572
|
-
Commit 记录:
|
|
573
|
-
${commitMessages || "无"}
|
|
574
|
-
|
|
575
|
-
文件变更:
|
|
576
|
-
${fileChanges || "无"}`;
|
|
484
|
+
const { userPrompt } = buildPrTitlePrompt({ commits, changedFiles });
|
|
485
|
+
|
|
577
486
|
try {
|
|
578
|
-
const stream = this.llmProxyService.chatStream([{ role: "user", content:
|
|
487
|
+
const stream = this.llmProxyService.chatStream([{ role: "user", content: userPrompt }], {
|
|
579
488
|
adapter: "openai",
|
|
580
489
|
});
|
|
581
490
|
let title = "";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
calculateIssueStats,
|
|
20
20
|
REVIEW_COMMENT_MARKER,
|
|
21
21
|
REVIEW_LINE_COMMENTS_MARKER,
|
|
22
|
-
} from "./review-pr-comment
|
|
22
|
+
} from "./utils/review-pr-comment";
|
|
23
23
|
|
|
24
24
|
export interface ReviewResultSaveOptions {
|
|
25
25
|
verbose?: VerboseLevel;
|
|
@@ -325,11 +325,33 @@ export class ReviewResultModel {
|
|
|
325
325
|
// 遍历每个评论,获取其 reactions
|
|
326
326
|
for (const comment of reviewComments) {
|
|
327
327
|
if (!comment.id) continue;
|
|
328
|
-
// 找到对应的 issue
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
// 找到对应的 issue:优先通过 issue-key 精确匹配,回退到 path+line 匹配
|
|
329
|
+
let matchedIssue: ReviewIssue | undefined;
|
|
330
|
+
if (comment.body) {
|
|
331
|
+
const issueKey = extractIssueKeyFromBody(comment.body);
|
|
332
|
+
if (issueKey) {
|
|
333
|
+
matchedIssue = this._result.issues.find(
|
|
334
|
+
(issue) => generateIssueKey(issue) === issueKey,
|
|
335
|
+
);
|
|
336
|
+
if (shouldLog(verbose, 3)) {
|
|
337
|
+
console.log(
|
|
338
|
+
`[syncReactionsToIssues] comment ${comment.id}: issue-key=${issueKey}, matched=${matchedIssue ? "yes" : "no"}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// 如果 issue-key 匹配失败,使用 path+position 回退匹配
|
|
344
|
+
if (!matchedIssue) {
|
|
345
|
+
matchedIssue = this._result.issues.find(
|
|
346
|
+
(issue) =>
|
|
347
|
+
issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position),
|
|
348
|
+
);
|
|
349
|
+
if (shouldLog(verbose, 3)) {
|
|
350
|
+
console.log(
|
|
351
|
+
`[syncReactionsToIssues] comment ${comment.id}: fallback matching path=${comment.path}, position=${comment.position}, matched=${matchedIssue ? "yes" : "no"}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
333
355
|
if (matchedIssue) {
|
|
334
356
|
commentIdToIssue.set(comment.id, matchedIssue);
|
|
335
357
|
}
|
|
@@ -13,6 +13,7 @@ import { homedir } from "os";
|
|
|
13
13
|
import { execSync } from "child_process";
|
|
14
14
|
import micromatch from "micromatch";
|
|
15
15
|
import { ReviewSpec, ReviewRule, RuleExample, Severity } from "./types";
|
|
16
|
+
import { extractGlobsFromIncludes } from "../review-includes-filter";
|
|
16
17
|
|
|
17
18
|
/** 远程规则缓存 TTL(毫秒),默认 5 分钟 */
|
|
18
19
|
const REMOTE_SPEC_CACHE_TTL = 5 * 60 * 1000;
|
|
@@ -69,7 +70,23 @@ export class ReviewSpecService {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
console.log(
|
|
74
|
+
`[filterApplicableSpecs] changedExtensions=${JSON.stringify([...changedExtensions])}, specs count=${specs.length}`,
|
|
75
|
+
);
|
|
76
|
+
const result = specs.filter((spec) => {
|
|
77
|
+
const matches = spec.extensions.some((ext) => changedExtensions.has(ext));
|
|
78
|
+
if (!matches) {
|
|
79
|
+
console.log(
|
|
80
|
+
`[filterApplicableSpecs] spec ${spec.filename} (ext: ${JSON.stringify(spec.extensions)}) NOT matched`,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(
|
|
84
|
+
`[filterApplicableSpecs] spec ${spec.filename} (ext: ${JSON.stringify(spec.extensions)}) MATCHED`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return matches;
|
|
88
|
+
});
|
|
89
|
+
return result;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
async loadReviewSpecs(specDir: string): Promise<ReviewSpec[]> {
|
|
@@ -524,8 +541,10 @@ export class ReviewSpecService {
|
|
|
524
541
|
return true;
|
|
525
542
|
}
|
|
526
543
|
|
|
527
|
-
// 检查文件是否匹配 includes
|
|
528
|
-
const
|
|
544
|
+
// 检查文件是否匹配 includes 模式(转换为纯 glob,避免 status| 前缀和 code-* 空串传入 micromatch)
|
|
545
|
+
const globs = extractGlobsFromIncludes(includes);
|
|
546
|
+
if (globs.length === 0) return true;
|
|
547
|
+
const matches = micromatch.isMatch(issue.file, globs, { matchBase: true });
|
|
529
548
|
if (!matches) {
|
|
530
549
|
// console.log(` Issue [${issue.ruleId}] 在文件 ${issue.file} 不匹配 includes 模式,跳过`);
|
|
531
550
|
}
|
|
@@ -638,8 +657,10 @@ export class ReviewSpecService {
|
|
|
638
657
|
if (scoped.includes.length === 0) {
|
|
639
658
|
return true;
|
|
640
659
|
}
|
|
641
|
-
// 使用 micromatch 检查文件是否匹配 includes
|
|
642
|
-
|
|
660
|
+
// 使用 micromatch 检查文件是否匹配 includes 模式(转换为纯 glob)
|
|
661
|
+
const globs = extractGlobsFromIncludes(scoped.includes);
|
|
662
|
+
if (globs.length === 0) return true;
|
|
663
|
+
return issueFile && micromatch.isMatch(issueFile, globs, { matchBase: true });
|
|
643
664
|
});
|
|
644
665
|
|
|
645
666
|
if (matched) {
|
package/src/review.config.ts
CHANGED
|
@@ -27,6 +27,15 @@ export type AnalyzeDeletionsMode = z.infer<typeof analyzeDeletionsModeSchema>;
|
|
|
27
27
|
/** 审查规则严重级别 */
|
|
28
28
|
export type Severity = z.infer<typeof severitySchema>;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 系统规则配置,不依赖 LLM,在构建 prompt 前直接检查并生成系统问题。
|
|
32
|
+
* 格式为 [阈值, severity]
|
|
33
|
+
*/
|
|
34
|
+
export interface SystemRules {
|
|
35
|
+
/** 单文件最大审查行数,超过时跳过 LLM 并生成系统问题。格式: [maxLine, severity] */
|
|
36
|
+
maxLinesPerFile?: [number, Severity];
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* 变更文件处理策略
|
|
32
41
|
* - 'invalidate': 将变更文件的历史问题标记为无效(默认)
|
|
@@ -47,6 +56,11 @@ export interface ReviewOptions {
|
|
|
47
56
|
references?: string[];
|
|
48
57
|
verbose?: VerboseLevel;
|
|
49
58
|
includes?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* 代码结构过滤配置,指定在代码审查时要关注的代码结构类型
|
|
61
|
+
* 支持格式:"function"、"class"、"interface"、"type"、"method"
|
|
62
|
+
*/
|
|
63
|
+
whenModifiedCode?: string[];
|
|
50
64
|
llmMode?: LLMMode;
|
|
51
65
|
files?: string[];
|
|
52
66
|
commits?: string[];
|
|
@@ -80,11 +94,12 @@ export interface ReviewOptions {
|
|
|
80
94
|
*/
|
|
81
95
|
local?: LocalReviewMode;
|
|
82
96
|
/**
|
|
83
|
-
*
|
|
84
|
-
* -
|
|
85
|
-
* -
|
|
97
|
+
* 处理重复 workflow 的策略
|
|
98
|
+
* - 'off': 禁用检查
|
|
99
|
+
* - 'skip': 检测到同名 workflow 正在运行时跳过审查
|
|
100
|
+
* - 'delete': 检测到同名 workflow 时删除旧的 AI Review 评论和 PR Review(默认)
|
|
86
101
|
*/
|
|
87
|
-
|
|
102
|
+
duplicateWorkflowResolved?: "off" | "skip" | "delete";
|
|
88
103
|
/**
|
|
89
104
|
* 自动批准合并
|
|
90
105
|
* - true: 当所有问题都已解决时,自动提交 APPROVE review
|
|
@@ -99,6 +114,8 @@ export interface ReviewOptions {
|
|
|
99
114
|
* - 'warn+error': 有未解决的 warn 或 error 级别问题时抛出异常
|
|
100
115
|
*/
|
|
101
116
|
failOnIssues?: "off" | "warn" | "error" | "warn+error";
|
|
117
|
+
/** 系统规则配置,不依赖 LLM,直接在检查阶段生成系统问题 */
|
|
118
|
+
systemRules?: SystemRules;
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
/** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
|
|
@@ -107,6 +124,7 @@ export const reviewSchema = () =>
|
|
|
107
124
|
references: z.array(z.string()).optional(),
|
|
108
125
|
llmMode: llmModeSchema.default("openai").optional(),
|
|
109
126
|
includes: z.array(z.string()).optional(),
|
|
127
|
+
whenModifiedCode: z.array(z.string()).optional(),
|
|
110
128
|
rules: z.record(z.string(), severitySchema).optional(),
|
|
111
129
|
verifyFixes: z.boolean().default(false),
|
|
112
130
|
verifyFixesConcurrency: z.number().default(10).optional(),
|
|
@@ -120,9 +138,17 @@ export const reviewSchema = () =>
|
|
|
120
138
|
retries: z.number().default(0).optional(),
|
|
121
139
|
retryDelay: z.number().default(1000).optional(),
|
|
122
140
|
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
|
|
123
|
-
|
|
141
|
+
duplicateWorkflowResolved: z.enum(["off", "skip", "delete"]).default("delete").optional(),
|
|
124
142
|
autoApprove: z.boolean().default(false).optional(),
|
|
125
143
|
failOnIssues: z.enum(["off", "warn", "error", "warn+error"]).default("off").optional(),
|
|
144
|
+
systemRules: z
|
|
145
|
+
.object({
|
|
146
|
+
maxLinesPerFile: z
|
|
147
|
+
.tuple([z.number(), severitySchema])
|
|
148
|
+
.transform((v): [number, Severity] => [v[0], v[1]])
|
|
149
|
+
.optional(),
|
|
150
|
+
})
|
|
151
|
+
.optional(),
|
|
126
152
|
});
|
|
127
153
|
|
|
128
154
|
/** review 配置类型(从 schema 推导) */
|