@spaceflow/review 0.55.0 → 0.57.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 +17 -0
- package/dist/index.js +3589 -3444
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/locales/en/review.json +3 -1
- package/src/locales/zh-cn/review.json +3 -1
- package/src/mcp/index.ts +54 -0
- package/src/review-report/formatters/markdown.formatter.ts +10 -7
- package/src/review-report/formatters/terminal.formatter.ts +10 -7
- package/src/review-spec/types.ts +3 -1
- package/src/review.service.ts +171 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spaceflow/review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lydanne",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@spaceflow/cli": "0.38.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@spaceflow/core": "0.
|
|
31
|
+
"@spaceflow/core": "0.20.0"
|
|
32
32
|
},
|
|
33
33
|
"spaceflow": {
|
|
34
34
|
"type": "flow",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import "./locales";
|
|
2
|
+
export * from "./review-spec";
|
|
3
|
+
export * from "./review-report";
|
|
2
4
|
import { defineExtension, t } from "@spaceflow/core";
|
|
3
5
|
import type { GitProviderService, LlmProxyService, GitSdkService, LLMMode } from "@spaceflow/core";
|
|
4
6
|
import { parseVerbose } from "@spaceflow/core";
|
|
@@ -28,5 +28,7 @@
|
|
|
28
28
|
"mcp.dto.cwd": "Project root directory path, defaults to current working directory",
|
|
29
29
|
"mcp.dto.filePath": "File path, can be relative or absolute",
|
|
30
30
|
"mcp.dto.includeExamples": "Whether to include rule example code, defaults to false",
|
|
31
|
-
"mcp.dto.ruleId": "Rule ID, e.g. JsTs.Naming.FileName"
|
|
31
|
+
"mcp.dto.ruleId": "Rule ID, e.g. JsTs.Naming.FileName",
|
|
32
|
+
"mcp.getRulesFromDir": "Load code review rules from a specific directory. Reads all .md files in the directory, parses them into rules, and deduplicates by rule ID (later rules override earlier ones)",
|
|
33
|
+
"mcp.dto.dirPath": "Directory path containing rule .md files, can be relative or absolute"
|
|
32
34
|
}
|
|
@@ -28,5 +28,7 @@
|
|
|
28
28
|
"mcp.dto.cwd": "项目根目录路径,默认为当前工作目录",
|
|
29
29
|
"mcp.dto.filePath": "文件路径,可以是相对路径或绝对路径",
|
|
30
30
|
"mcp.dto.includeExamples": "是否包含规则示例代码,默认 false",
|
|
31
|
-
"mcp.dto.ruleId": "规则 ID,如 JsTs.Naming.FileName"
|
|
31
|
+
"mcp.dto.ruleId": "规则 ID,如 JsTs.Naming.FileName",
|
|
32
|
+
"mcp.getRulesFromDir": "从指定目录加载代码审查规则。读取目录下所有 .md 文件,解析为规则并按规则 ID 去重(后加载的覆盖先加载的)",
|
|
33
|
+
"mcp.dto.dirPath": "包含规则 .md 文件的目录路径,可以是相对路径或绝对路径"
|
|
32
34
|
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -16,6 +16,11 @@ export const getRuleDetailInputSchema = z.object({
|
|
|
16
16
|
ruleId: z.string().describe(t("review:mcp.dto.ruleId")),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
export const getRulesFromDirInputSchema = z.object({
|
|
20
|
+
dirPath: z.string().describe(t("review:mcp.dto.dirPath")),
|
|
21
|
+
includeExamples: z.boolean().optional().describe(t("review:mcp.dto.includeExamples")),
|
|
22
|
+
});
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* 获取 GitProviderService(可选)
|
|
21
26
|
*/
|
|
@@ -165,4 +170,53 @@ export const tools = [
|
|
|
165
170
|
};
|
|
166
171
|
},
|
|
167
172
|
},
|
|
173
|
+
{
|
|
174
|
+
name: "get_rules_from_dir",
|
|
175
|
+
description: t("review:mcp.getRulesFromDir"),
|
|
176
|
+
inputSchema: getRulesFromDirInputSchema,
|
|
177
|
+
handler: async (input, ctx) => {
|
|
178
|
+
const { dirPath, includeExamples } = input as z.infer<typeof getRulesFromDirInputSchema>;
|
|
179
|
+
const workDir = ctx.cwd;
|
|
180
|
+
const resolvedDir = dirPath.startsWith("/") ? dirPath : join(workDir, dirPath);
|
|
181
|
+
|
|
182
|
+
if (!existsSync(resolvedDir)) {
|
|
183
|
+
return { error: `Directory not found: ${resolvedDir}` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const gitProvider = getGitProvider(ctx);
|
|
187
|
+
const specService = new ReviewSpecService(gitProvider);
|
|
188
|
+
const specs = await specService.loadReviewSpecs(resolvedDir);
|
|
189
|
+
const dedupedSpecs = specService.deduplicateSpecs(specs);
|
|
190
|
+
|
|
191
|
+
const rules = dedupedSpecs.flatMap((spec) =>
|
|
192
|
+
spec.rules.map((rule) => ({
|
|
193
|
+
id: rule.id,
|
|
194
|
+
title: rule.title,
|
|
195
|
+
description: includeExamples
|
|
196
|
+
? rule.description
|
|
197
|
+
: rule.description.slice(0, 200) + (rule.description.length > 200 ? "..." : ""),
|
|
198
|
+
severity: rule.severity || spec.severity,
|
|
199
|
+
extensions: spec.extensions,
|
|
200
|
+
specFile: spec.filename,
|
|
201
|
+
includes: spec.includes,
|
|
202
|
+
...(includeExamples && rule.examples.length > 0
|
|
203
|
+
? {
|
|
204
|
+
examples: rule.examples.map((ex) => ({
|
|
205
|
+
type: ex.type,
|
|
206
|
+
lang: ex.lang,
|
|
207
|
+
code: ex.code,
|
|
208
|
+
})),
|
|
209
|
+
}
|
|
210
|
+
: { hasExamples: rule.examples.length > 0 }),
|
|
211
|
+
})),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
dir: resolvedDir,
|
|
216
|
+
specFiles: dedupedSpecs.length,
|
|
217
|
+
total: rules.length,
|
|
218
|
+
rules,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
},
|
|
168
222
|
];
|
|
@@ -130,30 +130,32 @@ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportPar
|
|
|
130
130
|
return "没有需要审查的文件";
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const issuesByFile = new Map<string, { resolved: number;
|
|
133
|
+
const issuesByFile = new Map<string, { resolved: number; errors: number; warns: number }>();
|
|
134
134
|
for (const issue of issues) {
|
|
135
135
|
if (issue.valid === "false") continue;
|
|
136
|
-
const stats = issuesByFile.get(issue.file) || { resolved: 0,
|
|
136
|
+
const stats = issuesByFile.get(issue.file) || { resolved: 0, errors: 0, warns: 0 };
|
|
137
137
|
if (issue.fixed) {
|
|
138
138
|
stats.resolved++;
|
|
139
|
+
} else if (issue.severity === "error") {
|
|
140
|
+
stats.errors++;
|
|
139
141
|
} else {
|
|
140
|
-
stats.
|
|
142
|
+
stats.warns++;
|
|
141
143
|
}
|
|
142
144
|
issuesByFile.set(issue.file, stats);
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
const lines: string[] = [];
|
|
146
|
-
lines.push("| 文件 | 🟢 | 🔴 | 总结 |");
|
|
147
|
-
lines.push("
|
|
148
|
+
lines.push("| 文件 | 🟢 | 🔴 | 🟡 | 总结 |");
|
|
149
|
+
lines.push("|------|----|----|----|----|");
|
|
148
150
|
|
|
149
151
|
for (const fileSummary of summaries) {
|
|
150
|
-
const stats = issuesByFile.get(fileSummary.file) || { resolved: 0,
|
|
152
|
+
const stats = issuesByFile.get(fileSummary.file) || { resolved: 0, errors: 0, warns: 0 };
|
|
151
153
|
const summaryText = fileSummary.summary
|
|
152
154
|
.split("\n")
|
|
153
155
|
.filter((line) => line.trim())
|
|
154
156
|
.join("<br>");
|
|
155
157
|
lines.push(
|
|
156
|
-
`| \`${fileSummary.file}\` | ${stats.resolved} | ${stats.
|
|
158
|
+
`| \`${fileSummary.file}\` | ${stats.resolved} | ${stats.errors} | ${stats.warns} | ${summaryText} |`,
|
|
157
159
|
);
|
|
158
160
|
}
|
|
159
161
|
|
|
@@ -292,6 +294,7 @@ export class MarkdownFormatter implements ReviewReportFormatter, ReviewReportPar
|
|
|
292
294
|
lines.push(`| ❌ 无效 | ${stats.invalid} |`);
|
|
293
295
|
lines.push(`| ⚠️ 待处理 | ${stats.pending} |`);
|
|
294
296
|
lines.push(`| 修复率 | ${stats.fixRate}% |`);
|
|
297
|
+
lines.push(`| 解决率 | ${stats.resolveRate}% |`);
|
|
295
298
|
return lines.join("\n");
|
|
296
299
|
}
|
|
297
300
|
}
|
|
@@ -28,24 +28,26 @@ export class TerminalFormatter implements ReviewReportFormatter {
|
|
|
28
28
|
return "没有需要审查的文件";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const issuesByFile = new Map<string, { resolved: number;
|
|
31
|
+
const issuesByFile = new Map<string, { resolved: number; errors: number; warns: number }>();
|
|
32
32
|
for (const issue of issues) {
|
|
33
|
-
const stats = issuesByFile.get(issue.file) || { resolved: 0,
|
|
33
|
+
const stats = issuesByFile.get(issue.file) || { resolved: 0, errors: 0, warns: 0 };
|
|
34
34
|
if (issue.fixed) {
|
|
35
35
|
stats.resolved++;
|
|
36
|
+
} else if (issue.severity === "error") {
|
|
37
|
+
stats.errors++;
|
|
36
38
|
} else {
|
|
37
|
-
stats.
|
|
39
|
+
stats.warns++;
|
|
38
40
|
}
|
|
39
41
|
issuesByFile.set(issue.file, stats);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
const lines: string[] = [];
|
|
43
45
|
for (const fileSummary of summaries) {
|
|
44
|
-
const stats = issuesByFile.get(fileSummary.file) || { resolved: 0,
|
|
46
|
+
const stats = issuesByFile.get(fileSummary.file) || { resolved: 0, errors: 0, warns: 0 };
|
|
45
47
|
const resolvedText = stats.resolved > 0 ? `${GREEN}✅ ${stats.resolved} 已解决${RESET}` : "";
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const statsText = [resolvedText,
|
|
48
|
+
const errorText = stats.errors > 0 ? `${RED}🔴 ${stats.errors} error${RESET}` : "";
|
|
49
|
+
const warnText = stats.warns > 0 ? `${YELLOW}🟡 ${stats.warns} warn${RESET}` : "";
|
|
50
|
+
const statsText = [resolvedText, errorText, warnText].filter(Boolean).join(" / ");
|
|
49
51
|
|
|
50
52
|
if (statsText) {
|
|
51
53
|
lines.push(`${BOLD}${fileSummary.file}${RESET} (${statsText}): ${fileSummary.summary}`);
|
|
@@ -126,6 +128,7 @@ export class TerminalFormatter implements ReviewReportFormatter {
|
|
|
126
128
|
lines.push(` ${RED}❌ 无效: ${stats.invalid}${RESET}`);
|
|
127
129
|
lines.push(` ${YELLOW}⚠️ 待处理: ${stats.pending}${RESET}`);
|
|
128
130
|
lines.push(` 修复率: ${stats.fixRate}%`);
|
|
131
|
+
lines.push(` 解决率: ${stats.resolveRate}%`);
|
|
129
132
|
return lines.join("\n");
|
|
130
133
|
}
|
|
131
134
|
}
|
package/src/review-spec/types.ts
CHANGED
|
@@ -125,8 +125,10 @@ export interface ReviewStats {
|
|
|
125
125
|
invalid: number;
|
|
126
126
|
/** 待处理数 */
|
|
127
127
|
pending: number;
|
|
128
|
-
/** 修复率 (0-100) */
|
|
128
|
+
/** 修复率 (0-100),仅计算代码修复:fixed / total */
|
|
129
129
|
fixRate: number;
|
|
130
|
+
/** 解决率 (0-100),计算修复+解决:(fixed + resolved) / total */
|
|
131
|
+
resolveRate: number;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
export interface ReviewResult {
|
package/src/review.service.ts
CHANGED
|
@@ -409,30 +409,7 @@ export class ReviewService {
|
|
|
409
409
|
return this.executeCollectOnly(context);
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
-
|
|
413
|
-
console.log(`📂 解析规则来源: ${specSources.length} 个`);
|
|
414
|
-
}
|
|
415
|
-
const specDirs = await this.reviewSpecService.resolveSpecSources(specSources);
|
|
416
|
-
if (shouldLog(verbose, 2)) {
|
|
417
|
-
console.log(` 解析到 ${specDirs.length} 个规则目录`, specDirs);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
let specs: ReviewSpec[] = [];
|
|
421
|
-
for (const specDir of specDirs) {
|
|
422
|
-
const dirSpecs = await this.reviewSpecService.loadReviewSpecs(specDir);
|
|
423
|
-
specs.push(...dirSpecs);
|
|
424
|
-
}
|
|
425
|
-
if (shouldLog(verbose, 1)) {
|
|
426
|
-
console.log(` 找到 ${specs.length} 个规则文件`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// 去重规则:后加载的覆盖先加载的
|
|
430
|
-
const beforeDedup = specs.reduce((sum, s) => sum + s.rules.length, 0);
|
|
431
|
-
specs = this.reviewSpecService.deduplicateSpecs(specs);
|
|
432
|
-
const afterDedup = specs.reduce((sum, s) => sum + s.rules.length, 0);
|
|
433
|
-
if (beforeDedup !== afterDedup && shouldLog(verbose, 1)) {
|
|
434
|
-
console.log(` 去重规则: ${beforeDedup} -> ${afterDedup} 条`);
|
|
435
|
-
}
|
|
412
|
+
const specs = await this.loadSpecs(specSources, verbose);
|
|
436
413
|
|
|
437
414
|
let pr: PullRequest | undefined;
|
|
438
415
|
let commits: PullRequestCommit[] = [];
|
|
@@ -719,19 +696,12 @@ export class ReviewService {
|
|
|
719
696
|
|
|
720
697
|
// 验证历史问题是否已修复
|
|
721
698
|
if (context.verifyFixes) {
|
|
722
|
-
|
|
723
|
-
|
|
699
|
+
existingIssues = await this.verifyAndUpdateIssues(
|
|
700
|
+
context,
|
|
701
|
+
existingIssues,
|
|
702
|
+
commits,
|
|
703
|
+
{ specs, fileContents },
|
|
724
704
|
);
|
|
725
|
-
if (unfixedExistingIssues.length > 0 && llmMode) {
|
|
726
|
-
existingIssues = await this.issueVerifyService.verifyIssueFixes(
|
|
727
|
-
existingIssues,
|
|
728
|
-
fileContents,
|
|
729
|
-
specs,
|
|
730
|
-
llmMode,
|
|
731
|
-
verbose,
|
|
732
|
-
context.verifyConcurrency,
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
705
|
} else {
|
|
736
706
|
if (shouldLog(verbose, 1)) {
|
|
737
707
|
console.log(` ⏭️ 跳过历史问题验证 (verifyFixes=false)`);
|
|
@@ -873,14 +843,25 @@ export class ReviewService {
|
|
|
873
843
|
// 4. 同步评论 reactions(👍/👎)
|
|
874
844
|
await this.syncReactionsToIssues(owner, repo, prNumber, existingResult, verbose);
|
|
875
845
|
|
|
876
|
-
// 5.
|
|
846
|
+
// 5. LLM 验证历史问题是否已修复
|
|
847
|
+
try {
|
|
848
|
+
existingResult.issues = await this.verifyAndUpdateIssues(
|
|
849
|
+
context,
|
|
850
|
+
existingResult.issues,
|
|
851
|
+
commits,
|
|
852
|
+
);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
console.warn("⚠️ LLM 验证修复状态失败,跳过:", error);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 6. 统计问题状态并设置到 result
|
|
877
858
|
const stats = this.calculateIssueStats(existingResult.issues);
|
|
878
859
|
existingResult.stats = stats;
|
|
879
860
|
|
|
880
|
-
//
|
|
861
|
+
// 7. 输出统计信息
|
|
881
862
|
console.log(this.reviewReportService.formatStatsTerminal(stats, prNumber));
|
|
882
863
|
|
|
883
|
-
//
|
|
864
|
+
// 8. 更新 PR 评论(如果不是 dry-run)
|
|
884
865
|
if (ci && !dryRun) {
|
|
885
866
|
if (shouldLog(verbose, 1)) {
|
|
886
867
|
console.log(`💬 更新 PR 评论...`);
|
|
@@ -894,6 +875,106 @@ export class ReviewService {
|
|
|
894
875
|
return existingResult;
|
|
895
876
|
}
|
|
896
877
|
|
|
878
|
+
/**
|
|
879
|
+
* 加载并去重审查规则
|
|
880
|
+
*/
|
|
881
|
+
protected async loadSpecs(specSources: string[], verbose?: VerboseLevel): Promise<ReviewSpec[]> {
|
|
882
|
+
if (shouldLog(verbose, 1)) {
|
|
883
|
+
console.log(`📂 解析规则来源: ${specSources.length} 个`);
|
|
884
|
+
}
|
|
885
|
+
const specDirs = await this.reviewSpecService.resolveSpecSources(specSources);
|
|
886
|
+
if (shouldLog(verbose, 2)) {
|
|
887
|
+
console.log(` 解析到 ${specDirs.length} 个规则目录`, specDirs);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
let specs: ReviewSpec[] = [];
|
|
891
|
+
for (const specDir of specDirs) {
|
|
892
|
+
const dirSpecs = await this.reviewSpecService.loadReviewSpecs(specDir);
|
|
893
|
+
specs.push(...dirSpecs);
|
|
894
|
+
}
|
|
895
|
+
if (shouldLog(verbose, 1)) {
|
|
896
|
+
console.log(` 找到 ${specs.length} 个规则文件`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const beforeDedup = specs.reduce((sum, s) => sum + s.rules.length, 0);
|
|
900
|
+
specs = this.reviewSpecService.deduplicateSpecs(specs);
|
|
901
|
+
const afterDedup = specs.reduce((sum, s) => sum + s.rules.length, 0);
|
|
902
|
+
if (beforeDedup !== afterDedup && shouldLog(verbose, 1)) {
|
|
903
|
+
console.log(` 去重规则: ${beforeDedup} -> ${afterDedup} 条`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return specs;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* LLM 验证历史问题是否已修复
|
|
911
|
+
* 如果传入 preloaded(specs/fileContents),直接使用;否则从 PR 获取
|
|
912
|
+
*/
|
|
913
|
+
protected async verifyAndUpdateIssues(
|
|
914
|
+
context: ReviewContext,
|
|
915
|
+
issues: ReviewIssue[],
|
|
916
|
+
commits: PullRequestCommit[],
|
|
917
|
+
preloaded?: { specs: ReviewSpec[]; fileContents: FileContentsMap },
|
|
918
|
+
): Promise<ReviewIssue[]> {
|
|
919
|
+
const { owner, repo, prNumber, llmMode, specSources, verbose } = context;
|
|
920
|
+
const unfixedIssues = issues.filter(
|
|
921
|
+
(i) => i.valid !== "false" && !i.fixed,
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
if (unfixedIssues.length === 0) {
|
|
925
|
+
return issues;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (!llmMode) {
|
|
929
|
+
if (shouldLog(verbose, 1)) {
|
|
930
|
+
console.log(` ⏭️ 跳过 LLM 验证(缺少 llmMode)`);
|
|
931
|
+
}
|
|
932
|
+
return issues;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (!preloaded && (!specSources?.length || !prNumber)) {
|
|
936
|
+
if (shouldLog(verbose, 1)) {
|
|
937
|
+
console.log(` ⏭️ 跳过 LLM 验证(缺少 specSources 或 prNumber)`);
|
|
938
|
+
}
|
|
939
|
+
return issues;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (shouldLog(verbose, 1)) {
|
|
943
|
+
console.log(`\n🔍 开始 LLM 验证 ${unfixedIssues.length} 个未修复问题...`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let specs: ReviewSpec[];
|
|
947
|
+
let fileContents: FileContentsMap;
|
|
948
|
+
|
|
949
|
+
if (preloaded) {
|
|
950
|
+
specs = preloaded.specs;
|
|
951
|
+
fileContents = preloaded.fileContents;
|
|
952
|
+
} else {
|
|
953
|
+
const pr = await this.gitProvider.getPullRequest(owner, repo, prNumber!);
|
|
954
|
+
const changedFiles = await this.gitProvider.getPullRequestFiles(owner, repo, prNumber!);
|
|
955
|
+
const headSha = pr?.head?.sha || "HEAD";
|
|
956
|
+
specs = await this.loadSpecs(specSources, verbose);
|
|
957
|
+
fileContents = await this.getFileContents(
|
|
958
|
+
owner,
|
|
959
|
+
repo,
|
|
960
|
+
changedFiles,
|
|
961
|
+
commits,
|
|
962
|
+
headSha,
|
|
963
|
+
prNumber!,
|
|
964
|
+
verbose,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return this.issueVerifyService.verifyIssueFixes(
|
|
969
|
+
issues,
|
|
970
|
+
fileContents,
|
|
971
|
+
specs,
|
|
972
|
+
llmMode,
|
|
973
|
+
verbose,
|
|
974
|
+
context.verifyConcurrency,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
897
978
|
/**
|
|
898
979
|
* 计算问题状态统计
|
|
899
980
|
*/
|
|
@@ -904,7 +985,8 @@ export class ReviewService {
|
|
|
904
985
|
const invalid = issues.filter((i) => i.valid === "false").length;
|
|
905
986
|
const pending = total - fixed - resolved - invalid;
|
|
906
987
|
const fixRate = total > 0 ? Math.round((fixed / total) * 100 * 10) / 10 : 0;
|
|
907
|
-
|
|
988
|
+
const resolveRate = total > 0 ? Math.round(((fixed + resolved) / total) * 100 * 10) / 10 : 0;
|
|
989
|
+
return { total, fixed, resolved, invalid, pending, fixRate, resolveRate };
|
|
908
990
|
}
|
|
909
991
|
|
|
910
992
|
/**
|
|
@@ -1949,37 +2031,52 @@ ${fileChanges || "无"}`;
|
|
|
1949
2031
|
.map((issue) => this.issueToReviewComment(issue))
|
|
1950
2032
|
.filter((comment): comment is CreatePullReviewComment => comment !== null);
|
|
1951
2033
|
}
|
|
1952
|
-
if (
|
|
2034
|
+
if (reviewConf.lineComments) {
|
|
1953
2035
|
const reviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2036
|
+
if (comments.length > 0) {
|
|
2037
|
+
try {
|
|
2038
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2039
|
+
event: REVIEW_STATE.COMMENT,
|
|
2040
|
+
body: reviewBody,
|
|
2041
|
+
comments,
|
|
2042
|
+
commit_id: commitId,
|
|
2043
|
+
});
|
|
2044
|
+
console.log(`✅ 已发布 ${comments.length} 条行级评论`);
|
|
2045
|
+
} catch {
|
|
2046
|
+
// 批量失败时逐条发布,跳过无法定位的评论
|
|
2047
|
+
console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
|
|
2048
|
+
let successCount = 0;
|
|
2049
|
+
for (const comment of comments) {
|
|
2050
|
+
try {
|
|
2051
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2052
|
+
event: REVIEW_STATE.COMMENT,
|
|
2053
|
+
body: successCount === 0 ? reviewBody : undefined,
|
|
2054
|
+
comments: [comment],
|
|
2055
|
+
commit_id: commitId,
|
|
2056
|
+
});
|
|
2057
|
+
successCount++;
|
|
2058
|
+
} catch {
|
|
2059
|
+
console.warn(`⚠️ 跳过无法定位的评论: ${comment.path}:${comment.new_position}`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (successCount > 0) {
|
|
2063
|
+
console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
|
|
2064
|
+
} else {
|
|
2065
|
+
console.warn("⚠️ 所有行级评论均无法定位,已跳过");
|
|
1977
2066
|
}
|
|
1978
2067
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2068
|
+
} else {
|
|
2069
|
+
// 本轮无新问题,仍发布 Round 状态(含上轮回顾)
|
|
2070
|
+
try {
|
|
2071
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2072
|
+
event: REVIEW_STATE.COMMENT,
|
|
2073
|
+
body: reviewBody,
|
|
2074
|
+
comments: [],
|
|
2075
|
+
commit_id: commitId,
|
|
2076
|
+
});
|
|
2077
|
+
console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
console.warn("⚠️ 发布审查状态失败:", error);
|
|
1983
2080
|
}
|
|
1984
2081
|
}
|
|
1985
2082
|
}
|
|
@@ -2419,8 +2516,12 @@ ${fileChanges || "无"}`;
|
|
|
2419
2516
|
if (warnCount > 0) badges.push(`🟡 ${warnCount}`);
|
|
2420
2517
|
|
|
2421
2518
|
const parts: string[] = [REVIEW_LINE_COMMENTS_MARKER];
|
|
2422
|
-
parts.push(`###
|
|
2423
|
-
|
|
2519
|
+
parts.push(`### 🚀 Spaceflow Review · Round ${round}`);
|
|
2520
|
+
if (issues.length === 0) {
|
|
2521
|
+
parts.push(`> ✅ 未发现新问题`);
|
|
2522
|
+
} else {
|
|
2523
|
+
parts.push(`> **${issues.length}** 个新问题 · **${fileCount}** 个文件${badges.length > 0 ? " · " + badges.join(" ") : ""}`);
|
|
2524
|
+
}
|
|
2424
2525
|
|
|
2425
2526
|
// 上轮回顾
|
|
2426
2527
|
if (round > 1) {
|