@spaceflow/review 0.29.2 → 0.30.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 +26 -0
- package/dist/index.js +4488 -5043
- package/package.json +3 -10
- package/src/deletion-impact.service.spec.ts +8 -19
- package/src/deletion-impact.service.ts +0 -2
- package/src/index.ts +118 -24
- package/src/issue-verify.service.spec.ts +7 -23
- package/src/issue-verify.service.ts +0 -2
- package/src/mcp/index.ts +191 -0
- package/src/review-report/index.ts +0 -1
- package/src/review-report/review-report.service.ts +0 -2
- package/src/review-spec/index.ts +0 -1
- package/src/review-spec/review-spec.service.spec.ts +3 -8
- package/src/review-spec/review-spec.service.ts +1 -4
- package/src/review.config.ts +59 -21
- package/src/review.service.spec.ts +33 -81
- package/src/review.service.ts +4 -6
- package/tsconfig.json +1 -1
- package/src/dto/mcp.dto.ts +0 -42
- package/src/review-report/review-report.module.ts +0 -8
- package/src/review-spec/review-spec.module.ts +0 -10
- package/src/review.command.ts +0 -244
- package/src/review.mcp.ts +0 -184
- package/src/review.module.ts +0 -52
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
import { vi, type
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
ConfigService,
|
|
5
|
-
ConfigReaderService,
|
|
6
|
-
GitProviderService,
|
|
7
|
-
ClaudeSetupService,
|
|
8
|
-
LlmProxyService,
|
|
9
|
-
GitSdkService,
|
|
10
|
-
parseChangedLinesFromPatch,
|
|
11
|
-
} from "@spaceflow/core";
|
|
12
|
-
import { ReviewSpecService } from "./review-spec";
|
|
13
|
-
import { ReviewReportService } from "./review-report";
|
|
1
|
+
import { vi, type Mock } from "vitest";
|
|
2
|
+
import { parseChangedLinesFromPatch } from "@spaceflow/core";
|
|
14
3
|
import { readFile } from "fs/promises";
|
|
15
4
|
import { ReviewService, ReviewContext, ReviewPrompt } from "./review.service";
|
|
16
|
-
import {
|
|
17
|
-
import { DeletionImpactService } from "./deletion-impact.service";
|
|
18
|
-
import type { ReviewOptions } from "./review.command";
|
|
5
|
+
import type { ReviewOptions } from "./review.config";
|
|
19
6
|
|
|
20
7
|
vi.mock("c12");
|
|
21
8
|
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
@@ -56,14 +43,17 @@ vi.mock("openai", () => {
|
|
|
56
43
|
|
|
57
44
|
describe("ReviewService", () => {
|
|
58
45
|
let service: ReviewService;
|
|
59
|
-
let gitProvider:
|
|
60
|
-
let configService:
|
|
46
|
+
let gitProvider: any;
|
|
47
|
+
let configService: any;
|
|
61
48
|
let mockReviewSpecService: any;
|
|
62
49
|
let mockDeletionImpactService: any;
|
|
63
50
|
let mockGitSdkService: any;
|
|
51
|
+
let mockLlmProxyService: any;
|
|
52
|
+
let mockConfigReaderService: any;
|
|
64
53
|
|
|
65
|
-
beforeEach(
|
|
66
|
-
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
gitProvider = {
|
|
67
57
|
validateConfig: vi.fn(),
|
|
68
58
|
getPullRequest: vi.fn(),
|
|
69
59
|
getCommit: vi.fn(),
|
|
@@ -80,14 +70,10 @@ describe("ReviewService", () => {
|
|
|
80
70
|
getIssueCommentReactions: vi.fn().mockResolvedValue([]),
|
|
81
71
|
};
|
|
82
72
|
|
|
83
|
-
|
|
73
|
+
configService = {
|
|
84
74
|
get: vi.fn(),
|
|
85
75
|
};
|
|
86
76
|
|
|
87
|
-
const mockClaudeSetupService = {
|
|
88
|
-
configure: vi.fn(),
|
|
89
|
-
};
|
|
90
|
-
|
|
91
77
|
mockReviewSpecService = {
|
|
92
78
|
resolveSpecSources: vi.fn().mockResolvedValue(["/mock/spec/dir"]),
|
|
93
79
|
loadReviewSpecs: vi.fn().mockResolvedValue([
|
|
@@ -139,63 +125,29 @@ describe("ReviewService", () => {
|
|
|
139
125
|
getCommitDiff: vi.fn().mockReturnValue([]),
|
|
140
126
|
};
|
|
141
127
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{
|
|
154
|
-
provide: ClaudeSetupService,
|
|
155
|
-
useValue: mockClaudeSetupService,
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
provide: ReviewSpecService,
|
|
159
|
-
useValue: mockReviewSpecService,
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
provide: LlmProxyService,
|
|
163
|
-
useValue: {
|
|
164
|
-
chat: vi.fn(),
|
|
165
|
-
chatStream: vi.fn(),
|
|
166
|
-
createSession: vi.fn(),
|
|
167
|
-
getAvailableAdapters: vi.fn().mockReturnValue(["claude-code", "openai"]),
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
provide: ReviewReportService,
|
|
172
|
-
useValue: mockReviewReportService,
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
provide: IssueVerifyService,
|
|
176
|
-
useValue: mockIssueVerifyService,
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
provide: DeletionImpactService,
|
|
180
|
-
useValue: mockDeletionImpactService,
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
provide: GitSdkService,
|
|
184
|
-
useValue: mockGitSdkService,
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
provide: ConfigReaderService,
|
|
188
|
-
useValue: {
|
|
189
|
-
getPluginConfig: vi.fn().mockReturnValue({}),
|
|
190
|
-
getSystemConfig: vi.fn().mockReturnValue({}),
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
}).compile();
|
|
128
|
+
mockLlmProxyService = {
|
|
129
|
+
chat: vi.fn(),
|
|
130
|
+
chatStream: vi.fn(),
|
|
131
|
+
createSession: vi.fn(),
|
|
132
|
+
getAvailableAdapters: vi.fn().mockReturnValue(["claude-code", "openai"]),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
mockConfigReaderService = {
|
|
136
|
+
getPluginConfig: vi.fn().mockReturnValue({}),
|
|
137
|
+
getSystemConfig: vi.fn().mockReturnValue({}),
|
|
138
|
+
};
|
|
195
139
|
|
|
196
|
-
service =
|
|
197
|
-
|
|
198
|
-
|
|
140
|
+
service = new ReviewService(
|
|
141
|
+
gitProvider as any,
|
|
142
|
+
configService as any,
|
|
143
|
+
mockConfigReaderService as any,
|
|
144
|
+
mockReviewSpecService as any,
|
|
145
|
+
mockLlmProxyService as any,
|
|
146
|
+
mockReviewReportService as any,
|
|
147
|
+
mockIssueVerifyService as any,
|
|
148
|
+
mockDeletionImpactService as any,
|
|
149
|
+
mockGitSdkService as any,
|
|
150
|
+
);
|
|
199
151
|
});
|
|
200
152
|
|
|
201
153
|
afterEach(() => {
|
package/src/review.service.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Injectable,
|
|
3
|
-
ConfigService,
|
|
4
2
|
ConfigReaderService,
|
|
5
3
|
GitProviderService,
|
|
6
4
|
PullRequest,
|
|
@@ -24,6 +22,7 @@ import {
|
|
|
24
22
|
parseHunksFromPatch,
|
|
25
23
|
calculateNewLineNumber,
|
|
26
24
|
} from "@spaceflow/core";
|
|
25
|
+
import type { IConfigReader } from "@spaceflow/core";
|
|
27
26
|
import { type AnalyzeDeletionsMode, type ReviewConfig } from "./review.config";
|
|
28
27
|
import {
|
|
29
28
|
ReviewSpecService,
|
|
@@ -41,7 +40,7 @@ import { execSync } from "child_process";
|
|
|
41
40
|
import { readFile, readdir } from "fs/promises";
|
|
42
41
|
import { join, dirname, extname, relative, isAbsolute } from "path";
|
|
43
42
|
import micromatch from "micromatch";
|
|
44
|
-
import { ReviewOptions } from "./review.
|
|
43
|
+
import { ReviewOptions } from "./review.config";
|
|
45
44
|
import { IssueVerifyService } from "./issue-verify.service";
|
|
46
45
|
import { DeletionImpactService } from "./deletion-impact.service";
|
|
47
46
|
import { parseTitleOptions } from "./parse-title-options";
|
|
@@ -138,13 +137,12 @@ const REVIEW_SCHEMA: LlmJsonPutSchema = {
|
|
|
138
137
|
additionalProperties: false,
|
|
139
138
|
};
|
|
140
139
|
|
|
141
|
-
@Injectable()
|
|
142
140
|
export class ReviewService {
|
|
143
141
|
protected readonly llmJsonPut: LlmJsonPut<ReviewResult>;
|
|
144
142
|
|
|
145
143
|
constructor(
|
|
146
144
|
protected readonly gitProvider: GitProviderService,
|
|
147
|
-
protected readonly
|
|
145
|
+
protected readonly config: IConfigReader,
|
|
148
146
|
protected readonly configReader: ConfigReaderService,
|
|
149
147
|
protected readonly reviewSpecService: ReviewSpecService,
|
|
150
148
|
protected readonly llmProxyService: LlmProxyService,
|
|
@@ -172,7 +170,7 @@ export class ReviewService {
|
|
|
172
170
|
|
|
173
171
|
async getContextFromEnv(options: ReviewOptions): Promise<ReviewContext> {
|
|
174
172
|
const reviewConf = this.configReader.getPluginConfig<ReviewConfig>("review");
|
|
175
|
-
const ciConf = this.
|
|
173
|
+
const ciConf = this.config.get<CiConfig>("ci");
|
|
176
174
|
const repository = ciConf?.repository;
|
|
177
175
|
|
|
178
176
|
if (options.ci) {
|
package/tsconfig.json
CHANGED
package/src/dto/mcp.dto.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ApiProperty,
|
|
3
|
-
ApiPropertyOptional,
|
|
4
|
-
IsString,
|
|
5
|
-
IsBoolean,
|
|
6
|
-
IsOptional,
|
|
7
|
-
t,
|
|
8
|
-
} from "@spaceflow/core";
|
|
9
|
-
|
|
10
|
-
export class ListRulesInput {
|
|
11
|
-
@ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
|
|
12
|
-
@IsString()
|
|
13
|
-
@IsOptional()
|
|
14
|
-
cwd?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class GetRulesForFileInput {
|
|
18
|
-
@ApiProperty({ description: t("review:mcp.dto.filePath") })
|
|
19
|
-
@IsString()
|
|
20
|
-
filePath!: string;
|
|
21
|
-
|
|
22
|
-
@ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
|
|
23
|
-
@IsString()
|
|
24
|
-
@IsOptional()
|
|
25
|
-
cwd?: string;
|
|
26
|
-
|
|
27
|
-
@ApiPropertyOptional({ description: t("review:mcp.dto.includeExamples") })
|
|
28
|
-
@IsBoolean()
|
|
29
|
-
@IsOptional()
|
|
30
|
-
includeExamples?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class GetRuleDetailInput {
|
|
34
|
-
@ApiProperty({ description: t("review:mcp.dto.ruleId") })
|
|
35
|
-
@IsString()
|
|
36
|
-
ruleId!: string;
|
|
37
|
-
|
|
38
|
-
@ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
|
|
39
|
-
@IsString()
|
|
40
|
-
@IsOptional()
|
|
41
|
-
cwd?: string;
|
|
42
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Module } from "@nestjs/common";
|
|
2
|
-
import { GitProviderModule } from "@spaceflow/core";
|
|
3
|
-
import { ReviewSpecService } from "./review-spec.service";
|
|
4
|
-
|
|
5
|
-
@Module({
|
|
6
|
-
imports: [GitProviderModule.forFeature()],
|
|
7
|
-
providers: [ReviewSpecService],
|
|
8
|
-
exports: [ReviewSpecService],
|
|
9
|
-
})
|
|
10
|
-
export class ReviewSpecModule {}
|
package/src/review.command.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { Command, CommandRunner, Option, t } from "@spaceflow/core";
|
|
2
|
-
import type { LLMMode, VerboseLevel } from "@spaceflow/core";
|
|
3
|
-
import type { AnalyzeDeletionsMode } from "./review.config";
|
|
4
|
-
import type { ReportFormat } from "./review-report";
|
|
5
|
-
import { ReviewService } from "./review.service";
|
|
6
|
-
|
|
7
|
-
export interface ReviewOptions {
|
|
8
|
-
dryRun: boolean;
|
|
9
|
-
ci: boolean;
|
|
10
|
-
prNumber?: number;
|
|
11
|
-
base?: string;
|
|
12
|
-
head?: string;
|
|
13
|
-
references?: string[];
|
|
14
|
-
verbose?: VerboseLevel;
|
|
15
|
-
includes?: string[];
|
|
16
|
-
llmMode?: LLMMode;
|
|
17
|
-
files?: string[];
|
|
18
|
-
commits?: string[];
|
|
19
|
-
verifyFixes?: boolean;
|
|
20
|
-
verifyConcurrency?: number;
|
|
21
|
-
analyzeDeletions?: AnalyzeDeletionsMode;
|
|
22
|
-
/** 仅执行删除代码分析,跳过常规代码审查 */
|
|
23
|
-
deletionOnly?: boolean;
|
|
24
|
-
/** 删除代码分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式 */
|
|
25
|
-
deletionAnalysisMode?: LLMMode;
|
|
26
|
-
/** 输出格式:markdown, terminal, json。不指定则智能选择 */
|
|
27
|
-
outputFormat?: ReportFormat;
|
|
28
|
-
/** 是否使用 AI 生成 PR 功能描述 */
|
|
29
|
-
generateDescription?: boolean;
|
|
30
|
-
/** 显示所有问题,不过滤非变更行的问题 */
|
|
31
|
-
showAll?: boolean;
|
|
32
|
-
/** PR 事件类型(opened, synchronize, closed 等) */
|
|
33
|
-
eventAction?: string;
|
|
34
|
-
concurrency?: number;
|
|
35
|
-
timeout?: number;
|
|
36
|
-
retries?: number;
|
|
37
|
-
retryDelay?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Review 命令
|
|
42
|
-
*
|
|
43
|
-
* 在 GitHub Actions 中执行,用于自动代码审查
|
|
44
|
-
*
|
|
45
|
-
* 环境变量:
|
|
46
|
-
* - GITHUB_TOKEN: GitHub API Token
|
|
47
|
-
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo 格式)
|
|
48
|
-
* - GITHUB_REF_NAME: 当前分支名称
|
|
49
|
-
* - GITHUB_EVENT_PATH: 事件文件路径(包含 PR 信息)
|
|
50
|
-
*/
|
|
51
|
-
@Command({
|
|
52
|
-
name: "review",
|
|
53
|
-
description: t("review:description"),
|
|
54
|
-
})
|
|
55
|
-
export class ReviewCommand extends CommandRunner {
|
|
56
|
-
constructor(protected readonly reviewService: ReviewService) {
|
|
57
|
-
super();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async run(_passedParams: string[], options: ReviewOptions): Promise<void> {
|
|
61
|
-
try {
|
|
62
|
-
const context = await this.reviewService.getContextFromEnv(options);
|
|
63
|
-
await this.reviewService.execute(context);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (error instanceof Error) {
|
|
66
|
-
console.error(t("common.executionFailed", { error: error.message }));
|
|
67
|
-
if (error.stack) {
|
|
68
|
-
console.error(t("common.stackTrace", { stack: error.stack }));
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
console.error(t("common.executionFailed", { error }));
|
|
72
|
-
}
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@Option({
|
|
78
|
-
flags: "-d, --dry-run",
|
|
79
|
-
description: t("review:options.dryRun"),
|
|
80
|
-
})
|
|
81
|
-
parseDryRun(val: boolean): boolean {
|
|
82
|
-
return val;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@Option({
|
|
86
|
-
flags: "-c, --ci",
|
|
87
|
-
description: t("common.options.ci"),
|
|
88
|
-
})
|
|
89
|
-
parseCi(val: boolean): boolean {
|
|
90
|
-
return val;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
@Option({
|
|
94
|
-
flags: "-p, --pr-number <number>",
|
|
95
|
-
description: t("review:options.prNumber"),
|
|
96
|
-
})
|
|
97
|
-
parsePrNumber(val: string): number {
|
|
98
|
-
return parseInt(val, 10);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@Option({
|
|
102
|
-
flags: "-b, --base <ref>",
|
|
103
|
-
description: t("review:options.base"),
|
|
104
|
-
})
|
|
105
|
-
parseBase(val: string): string {
|
|
106
|
-
return val;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@Option({
|
|
110
|
-
flags: "--head <ref>",
|
|
111
|
-
description: t("review:options.head"),
|
|
112
|
-
})
|
|
113
|
-
parseHead(val: string): string {
|
|
114
|
-
return val;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
@Option({
|
|
118
|
-
flags: "-v, --verbose",
|
|
119
|
-
description: t("common.options.verboseDebug"),
|
|
120
|
-
})
|
|
121
|
-
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
|
|
122
|
-
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
|
|
123
|
-
return Math.min(current + 1, 3) as VerboseLevel;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
@Option({
|
|
127
|
-
flags: "-i, --includes <patterns...>",
|
|
128
|
-
description: t("review:options.includes"),
|
|
129
|
-
})
|
|
130
|
-
parseIncludes(val: string, previous: string[] = []): string[] {
|
|
131
|
-
return [...previous, val];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
@Option({
|
|
135
|
-
flags: "-l, --llm-mode <mode>",
|
|
136
|
-
description: t("review:options.llmMode"),
|
|
137
|
-
choices: ["claude-code", "openai", "gemini"],
|
|
138
|
-
})
|
|
139
|
-
parseLlmMode(val: string): LLMMode {
|
|
140
|
-
return val as LLMMode;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
@Option({
|
|
144
|
-
flags: "-f, --files <files...>",
|
|
145
|
-
description: t("review:options.files"),
|
|
146
|
-
})
|
|
147
|
-
parseFiles(val: string, previous: string[] = []): string[] {
|
|
148
|
-
return [...previous, val];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
@Option({
|
|
152
|
-
flags: "--commits <commits...>",
|
|
153
|
-
description: t("review:options.commits"),
|
|
154
|
-
})
|
|
155
|
-
parseCommits(val: string, previous: string[] = []): string[] {
|
|
156
|
-
return [...previous, val];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@Option({
|
|
160
|
-
flags: "--verify-fixes",
|
|
161
|
-
description: t("review:options.verifyFixes"),
|
|
162
|
-
})
|
|
163
|
-
parseVerifyFixes(val: boolean): boolean {
|
|
164
|
-
return val;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
@Option({
|
|
168
|
-
flags: "--no-verify-fixes",
|
|
169
|
-
description: t("review:options.noVerifyFixes"),
|
|
170
|
-
})
|
|
171
|
-
parseNoVerifyFixes(val: boolean): boolean {
|
|
172
|
-
return !val;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
@Option({
|
|
176
|
-
flags: "--verify-concurrency <number>",
|
|
177
|
-
description: t("review:options.verifyConcurrency"),
|
|
178
|
-
})
|
|
179
|
-
parseVerifyConcurrency(val: string): number {
|
|
180
|
-
return parseInt(val, 10);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
@Option({
|
|
184
|
-
flags: "--analyze-deletions [mode]",
|
|
185
|
-
description: t("review:options.analyzeDeletions"),
|
|
186
|
-
})
|
|
187
|
-
parseAnalyzeDeletions(val: string | boolean): AnalyzeDeletionsMode {
|
|
188
|
-
if (val === true || val === "true") return true;
|
|
189
|
-
if (val === false || val === "false") return false;
|
|
190
|
-
if (val === "ci" || val === "pr" || val === "terminal") return val;
|
|
191
|
-
// 默认为 true(当只传 --analyze-deletions 不带值时)
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
@Option({
|
|
196
|
-
flags: "--deletion-analysis-mode <mode>",
|
|
197
|
-
description: t("review:options.deletionAnalysisMode"),
|
|
198
|
-
choices: ["openai", "claude-code"],
|
|
199
|
-
})
|
|
200
|
-
parseDeletionAnalysisMode(val: string): LLMMode {
|
|
201
|
-
return val as LLMMode;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
@Option({
|
|
205
|
-
flags: "--deletion-only",
|
|
206
|
-
description: t("review:options.deletionOnly"),
|
|
207
|
-
})
|
|
208
|
-
parseDeletionOnly(val: boolean): boolean {
|
|
209
|
-
return val;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
@Option({
|
|
213
|
-
flags: "-o, --output-format <format>",
|
|
214
|
-
description: t("review:options.outputFormat"),
|
|
215
|
-
choices: ["markdown", "terminal", "json"],
|
|
216
|
-
})
|
|
217
|
-
parseOutputFormat(val: string): ReportFormat {
|
|
218
|
-
return val as ReportFormat;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
@Option({
|
|
222
|
-
flags: "--generate-description",
|
|
223
|
-
description: t("review:options.generateDescription"),
|
|
224
|
-
})
|
|
225
|
-
parseGenerateDescription(val: boolean): boolean {
|
|
226
|
-
return val;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
@Option({
|
|
230
|
-
flags: "--show-all",
|
|
231
|
-
description: t("review:options.showAll"),
|
|
232
|
-
})
|
|
233
|
-
parseShowAll(val: boolean): boolean {
|
|
234
|
-
return val;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
@Option({
|
|
238
|
-
flags: "--event-action <action>",
|
|
239
|
-
description: t("review:options.eventAction"),
|
|
240
|
-
})
|
|
241
|
-
parseEventAction(val: string): string {
|
|
242
|
-
return val;
|
|
243
|
-
}
|
|
244
|
-
}
|
package/src/review.mcp.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Review MCP 服务
|
|
3
|
-
* 提供代码审查规则查询的 MCP 工具
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { McpServer, McpTool, ConfigReaderService, t } from "@spaceflow/core";
|
|
7
|
-
import { ReviewSpecService } from "./review-spec/review-spec.service";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
import { existsSync } from "fs";
|
|
10
|
-
import { ListRulesInput, GetRulesForFileInput, GetRuleDetailInput } from "./dto/mcp.dto";
|
|
11
|
-
import type { ReviewConfig } from "./review.config";
|
|
12
|
-
|
|
13
|
-
@McpServer({ name: "review-mcp", version: "1.0.0", description: t("review:mcp.serverDescription") })
|
|
14
|
-
export class ReviewMcp {
|
|
15
|
-
constructor(
|
|
16
|
-
private readonly specService: ReviewSpecService,
|
|
17
|
-
private readonly configReader: ConfigReaderService,
|
|
18
|
-
) {}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 获取项目的规则目录
|
|
22
|
-
*/
|
|
23
|
-
private async getSpecDirs(cwd: string): Promise<string[]> {
|
|
24
|
-
const dirs: string[] = [];
|
|
25
|
-
|
|
26
|
-
// 1. 通过 ConfigReaderService 读取 review 配置
|
|
27
|
-
try {
|
|
28
|
-
const reviewConfig = this.configReader.getPluginConfig<ReviewConfig>("review");
|
|
29
|
-
if (reviewConfig?.references?.length) {
|
|
30
|
-
const resolved = await this.specService.resolveSpecSources(reviewConfig.references);
|
|
31
|
-
dirs.push(...resolved);
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// 忽略配置读取错误
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 2. 检查默认目录
|
|
38
|
-
const defaultDirs = [
|
|
39
|
-
join(cwd, ".claude", "skills"),
|
|
40
|
-
join(cwd, ".cursor", "skills"),
|
|
41
|
-
join(cwd, "review-specs"),
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
for (const dir of defaultDirs) {
|
|
45
|
-
if (existsSync(dir)) {
|
|
46
|
-
dirs.push(dir);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return [...new Set(dirs)]; // 去重
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 加载所有规则
|
|
55
|
-
*/
|
|
56
|
-
private async loadAllSpecs(cwd: string) {
|
|
57
|
-
const specDirs = await this.getSpecDirs(cwd);
|
|
58
|
-
const allSpecs = [];
|
|
59
|
-
|
|
60
|
-
for (const dir of specDirs) {
|
|
61
|
-
const specs = await this.specService.loadReviewSpecs(dir);
|
|
62
|
-
allSpecs.push(...specs);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 只去重,不应用 override(MCP 工具应返回所有规则,override 在实际审查时应用)
|
|
66
|
-
return this.specService.deduplicateSpecs(allSpecs);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@McpTool({
|
|
70
|
-
name: "list_rules",
|
|
71
|
-
description: t("review:mcp.listRules"),
|
|
72
|
-
dto: ListRulesInput,
|
|
73
|
-
})
|
|
74
|
-
async listRules(input: ListRulesInput) {
|
|
75
|
-
const workDir = input.cwd || process.cwd();
|
|
76
|
-
const specs = await this.loadAllSpecs(workDir);
|
|
77
|
-
|
|
78
|
-
const rules = specs.flatMap((spec) =>
|
|
79
|
-
spec.rules.map((rule) => ({
|
|
80
|
-
id: rule.id,
|
|
81
|
-
title: rule.title,
|
|
82
|
-
description: rule.description.slice(0, 200) + (rule.description.length > 200 ? "..." : ""),
|
|
83
|
-
severity: rule.severity || spec.severity,
|
|
84
|
-
extensions: spec.extensions,
|
|
85
|
-
specFile: spec.filename,
|
|
86
|
-
includes: spec.includes,
|
|
87
|
-
hasExamples: rule.examples.length > 0,
|
|
88
|
-
})),
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
total: rules.length,
|
|
93
|
-
rules,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@McpTool({
|
|
98
|
-
name: "get_rules_for_file",
|
|
99
|
-
description: t("review:mcp.getRulesForFile"),
|
|
100
|
-
dto: GetRulesForFileInput,
|
|
101
|
-
})
|
|
102
|
-
async getRulesForFile(input: GetRulesForFileInput) {
|
|
103
|
-
const workDir = input.cwd || process.cwd();
|
|
104
|
-
const allSpecs = await this.loadAllSpecs(workDir);
|
|
105
|
-
|
|
106
|
-
// 根据文件过滤适用的规则
|
|
107
|
-
const applicableSpecs = this.specService.filterApplicableSpecs(allSpecs, [
|
|
108
|
-
{ filename: input.filePath },
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
// 进一步根据 includes 过滤(支持规则级 includes 覆盖文件级)
|
|
112
|
-
const micromatchModule = await import("micromatch");
|
|
113
|
-
const micromatch = micromatchModule.default || micromatchModule;
|
|
114
|
-
|
|
115
|
-
const rules = applicableSpecs.flatMap((spec) =>
|
|
116
|
-
spec.rules
|
|
117
|
-
.filter((rule) => {
|
|
118
|
-
// 规则级 includes 优先于文件级
|
|
119
|
-
const includes = rule.includes || spec.includes;
|
|
120
|
-
if (includes.length === 0) return true;
|
|
121
|
-
return micromatch.isMatch(input.filePath, includes, { matchBase: true });
|
|
122
|
-
})
|
|
123
|
-
.map((rule) => ({
|
|
124
|
-
id: rule.id,
|
|
125
|
-
title: rule.title,
|
|
126
|
-
description: rule.description,
|
|
127
|
-
severity: rule.severity || spec.severity,
|
|
128
|
-
specFile: spec.filename,
|
|
129
|
-
...(input.includeExamples && rule.examples.length > 0
|
|
130
|
-
? {
|
|
131
|
-
examples: rule.examples.map((ex) => ({
|
|
132
|
-
type: ex.type,
|
|
133
|
-
lang: ex.lang,
|
|
134
|
-
code: ex.code,
|
|
135
|
-
})),
|
|
136
|
-
}
|
|
137
|
-
: {}),
|
|
138
|
-
})),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
file: input.filePath,
|
|
143
|
-
total: rules.length,
|
|
144
|
-
rules,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@McpTool({
|
|
149
|
-
name: "get_rule_detail",
|
|
150
|
-
description: t("review:mcp.getRuleDetail"),
|
|
151
|
-
dto: GetRuleDetailInput,
|
|
152
|
-
})
|
|
153
|
-
async getRuleDetail(input: GetRuleDetailInput) {
|
|
154
|
-
const workDir = input.cwd || process.cwd();
|
|
155
|
-
const specs = await this.loadAllSpecs(workDir);
|
|
156
|
-
|
|
157
|
-
const result = this.specService.findRuleById(input.ruleId, specs);
|
|
158
|
-
|
|
159
|
-
if (!result) {
|
|
160
|
-
return { error: t("review:mcp.ruleNotFound", { ruleId: input.ruleId }) };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const { rule, spec } = result;
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
id: rule.id,
|
|
167
|
-
title: rule.title,
|
|
168
|
-
description: rule.description,
|
|
169
|
-
severity: rule.severity || spec.severity,
|
|
170
|
-
specFile: spec.filename,
|
|
171
|
-
extensions: spec.extensions,
|
|
172
|
-
includes: spec.includes,
|
|
173
|
-
overrides: rule.overrides,
|
|
174
|
-
examples: rule.examples.map((ex) => ({
|
|
175
|
-
type: ex.type,
|
|
176
|
-
lang: ex.lang,
|
|
177
|
-
code: ex.code,
|
|
178
|
-
})),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ReviewMcpService 类已通过 @McpServer 装饰器标记
|
|
184
|
-
// CLI 的 `spaceflow mcp` 命令会自动扫描并发现该类
|