@spaceflow/review 0.76.0 → 0.77.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 +12 -0
- package/dist/index.js +2654 -1872
- package/package.json +2 -2
- package/src/deletion-impact.service.ts +4 -2
- package/src/index.ts +34 -2
- package/src/locales/en/review.json +2 -1
- package/src/locales/zh-cn/review.json +2 -1
- package/src/pull-request-model.ts +236 -0
- package/src/review-context.ts +409 -0
- package/src/review-includes-filter.spec.ts +248 -0
- package/src/review-includes-filter.ts +144 -0
- package/src/review-issue-filter.ts +523 -0
- package/src/review-llm.ts +634 -0
- package/src/review-pr-comment-utils.ts +186 -0
- package/src/review-result-model.spec.ts +657 -0
- package/src/review-result-model.ts +1024 -0
- package/src/review-spec/types.ts +2 -0
- package/src/review.config.ts +9 -0
- package/src/review.service.spec.ts +93 -1626
- package/src/review.service.ts +531 -2765
- package/src/types/review-llm.ts +19 -0
- package/src/utils/review-llm.ts +32 -0
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spaceflow/review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lydanne",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@spaceflow/cli": "0.40.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@spaceflow/core": "0.
|
|
31
|
+
"@spaceflow/core": "0.30.0"
|
|
32
32
|
},
|
|
33
33
|
"spaceflow": {
|
|
34
34
|
"type": "flow",
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@spaceflow/core";
|
|
13
13
|
import micromatch from "micromatch";
|
|
14
14
|
import type { DeletionImpactResult } from "./review-spec";
|
|
15
|
+
import { extractGlobsFromIncludes } from "./review-includes-filter";
|
|
15
16
|
import { spawn } from "child_process";
|
|
16
17
|
|
|
17
18
|
export interface DeletedCodeBlock {
|
|
@@ -160,11 +161,12 @@ export class DeletionImpactService {
|
|
|
160
161
|
console.log(` 📦 发现 ${deletedBlocks.length} 个删除的代码块`);
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
// 1.5 使用 includes
|
|
164
|
+
// 1.5 使用 includes 过滤文件(删除分析模式中只按 glob 匹配,不区分 status)
|
|
164
165
|
if (context.includes && context.includes.length > 0) {
|
|
165
166
|
const beforeCount = deletedBlocks.length;
|
|
167
|
+
const globs = extractGlobsFromIncludes(context.includes);
|
|
166
168
|
const filenames = deletedBlocks.map((b) => b.file);
|
|
167
|
-
const matchedFilenames = micromatch(filenames,
|
|
169
|
+
const matchedFilenames = micromatch(filenames, globs);
|
|
168
170
|
deletedBlocks = deletedBlocks.filter((b) => matchedFilenames.includes(b.file));
|
|
169
171
|
if (shouldLog(verbose, 1)) {
|
|
170
172
|
console.log(` 🔍 Includes 过滤: ${beforeCount} -> ${deletedBlocks.length} 个删除块`);
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import "./locales";
|
|
2
2
|
export * from "./review-spec";
|
|
3
3
|
export * from "./review-report";
|
|
4
|
+
export { PullRequestModel } from "./pull-request-model";
|
|
5
|
+
export { ReviewResultModel } from "./review-result-model";
|
|
6
|
+
export type { ReviewResultModelDeps, ReviewResultSaveOptions } from "./review-result-model";
|
|
4
7
|
import { defineExtension, t } from "@spaceflow/core";
|
|
5
8
|
import type {
|
|
6
9
|
GitProviderService,
|
|
@@ -10,7 +13,7 @@ import type {
|
|
|
10
13
|
LocalReviewMode,
|
|
11
14
|
} from "@spaceflow/core";
|
|
12
15
|
import { parseVerbose } from "@spaceflow/core";
|
|
13
|
-
import { reviewSchema, type AnalyzeDeletionsMode } from "./review.config";
|
|
16
|
+
import { reviewSchema, type AnalyzeDeletionsMode, type ReviewConfig } from "./review.config";
|
|
14
17
|
import { ReviewService } from "./review.service";
|
|
15
18
|
import { ReviewSpecService } from "./review-spec";
|
|
16
19
|
import { ReviewReportService, type ReportFormat } from "./review-report";
|
|
@@ -54,6 +57,7 @@ export const extension = defineExtension({
|
|
|
54
57
|
{ flags: "--event-action <action>", description: t("review:options.eventAction") },
|
|
55
58
|
{ flags: "--local [mode]", description: t("review:options.local") },
|
|
56
59
|
{ flags: "--no-local", description: t("review:options.noLocal") },
|
|
60
|
+
{ flags: "--fail-on-issues [mode]", description: t("review:options.failOnIssues") },
|
|
57
61
|
],
|
|
58
62
|
run: async (_args, options, ctx) => {
|
|
59
63
|
const isFlush = !!options?.flush;
|
|
@@ -113,8 +117,18 @@ export const extension = defineExtension({
|
|
|
113
117
|
flush: isFlush,
|
|
114
118
|
eventAction: options?.eventAction as string,
|
|
115
119
|
local: parseLocalOption(options?.local),
|
|
120
|
+
failOnIssues: parseFailOnIssues(options?.failOnIssues),
|
|
116
121
|
};
|
|
117
122
|
|
|
123
|
+
function parseFailOnIssues(
|
|
124
|
+
value: unknown,
|
|
125
|
+
): "off" | "warn" | "error" | "warn+error" | undefined {
|
|
126
|
+
if (value === true || value === "") return "error";
|
|
127
|
+
if (value === "warn" || value === "error" || value === "off" || value === "warn+error")
|
|
128
|
+
return value;
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function parseLocalOption(value: unknown): LocalReviewMode | undefined {
|
|
119
133
|
if (value === false) return false;
|
|
120
134
|
if (value === true || value === undefined || value === "") return undefined;
|
|
@@ -124,7 +138,25 @@ export const extension = defineExtension({
|
|
|
124
138
|
|
|
125
139
|
try {
|
|
126
140
|
const context = await reviewService.getContextFromEnv(reviewOptions);
|
|
127
|
-
await reviewService.execute(context);
|
|
141
|
+
const result = await reviewService.execute(context);
|
|
142
|
+
const effectiveFailOnIssues: "off" | "warn" | "error" | "warn+error" =
|
|
143
|
+
reviewOptions.failOnIssues ??
|
|
144
|
+
ctx.config.getPluginConfig<ReviewConfig>("review")?.failOnIssues ??
|
|
145
|
+
"off";
|
|
146
|
+
if (effectiveFailOnIssues !== "off") {
|
|
147
|
+
const blockers = result.issues.filter((issue) => {
|
|
148
|
+
if (issue.fixed || issue.resolved || issue.valid === "false") return false;
|
|
149
|
+
if (effectiveFailOnIssues === "warn") return issue.severity === "warn";
|
|
150
|
+
if (effectiveFailOnIssues === "error") return issue.severity === "error";
|
|
151
|
+
return issue.severity === "error" || issue.severity === "warn"; // warn+error
|
|
152
|
+
});
|
|
153
|
+
if (blockers.length > 0) {
|
|
154
|
+
ctx.output.error(
|
|
155
|
+
`审核不通过:存在 ${blockers.length} 个未解决的问题(模式: ${effectiveFailOnIssues})`,
|
|
156
|
+
);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
128
160
|
} catch (error) {
|
|
129
161
|
if (error instanceof Error) {
|
|
130
162
|
ctx.output.error(t("common.executionFailed", { error: error.message }));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"options.prNumber": "PR number, auto-detected from env if not specified",
|
|
5
5
|
"options.base": "Base branch/tag for diff comparison",
|
|
6
6
|
"options.head": "Head branch/tag for diff comparison",
|
|
7
|
-
"options.includes": "File glob patterns to review, e.g. *.ts *.js (can be specified multiple times)",
|
|
7
|
+
"options.includes": "File glob patterns to review, e.g. *.ts *.js (can be specified multiple times). Supports change-type prefixes: added|*.ts (new files only), modified|*.ts (modified only), deleted|*.ts (deleted only)",
|
|
8
8
|
"options.llmMode": "LLM mode: claude-code, openai, gemini",
|
|
9
9
|
"options.files": "Only review specified files (space-separated)",
|
|
10
10
|
"options.commits": "Only review specified commits (space-separated)",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"options.eventAction": "PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review",
|
|
22
22
|
"options.local": "Review local uncommitted code. Mode: 'uncommitted' (default, staged + working) or 'staged' (only staged)",
|
|
23
23
|
"options.noLocal": "Disable local mode, use branch comparison instead",
|
|
24
|
+
"options.failOnIssues": "Fail the workflow when unresolved issues exist. Mode: warn (warn-level only) / error (error-level only) / warn+error (either warn or error). Passing no mode is equivalent to error",
|
|
24
25
|
"extensionDescription": "Code review command using LLM for automated PR review",
|
|
25
26
|
"mcp.serverDescription": "Code review rules query service",
|
|
26
27
|
"mcp.listRules": "List all code review rules for the current project, returning rule list with ID, title, description, applicable file extensions, etc.",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"options.prNumber": "PR 编号,如果不指定则从环境变量获取",
|
|
5
5
|
"options.base": "基准分支/tag,用于比较差异",
|
|
6
6
|
"options.head": "目标分支/tag,用于比较差异",
|
|
7
|
-
"options.includes": "要审查的文件 glob 模式,如 *.ts *.js
|
|
7
|
+
"options.includes": "要审查的文件 glob 模式,如 *.ts *.js(可多次指定)。支持变更类型前缀: added|*.ts(仅新增)、modified|*.ts(仅修改)、deleted|*.ts(仅删除)",
|
|
8
8
|
"options.llmMode": "使用的 LLM 模式: claude-code, openai, gemini",
|
|
9
9
|
"options.files": "仅审查指定的文件(空格分隔)",
|
|
10
10
|
"options.commits": "仅审查指定的 commits(空格分隔)",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"options.eventAction": "PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查",
|
|
22
22
|
"options.local": "审查本地未提交的代码。模式: 'uncommitted'(默认,暂存区+工作区)或 'staged'(仅暂存区)",
|
|
23
23
|
"options.noLocal": "禁用本地模式,使用分支比较",
|
|
24
|
+
"options.failOnIssues": "存在未解决问题时工作流抛出异常。模式: warn(仅 warn 问题)/ error(仅 error 问题)/ warn&error(warn 或 error 问题)。不加模式等同 error",
|
|
24
25
|
"extensionDescription": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
|
|
25
26
|
"mcp.serverDescription": "代码审查规则查询服务",
|
|
26
27
|
"mcp.listRules": "获取当前项目的所有代码审查规则,返回规则列表包含 ID、标题、描述、适用的文件扩展名等信息",
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GitProviderService,
|
|
3
|
+
PullRequest,
|
|
4
|
+
PullRequestCommit,
|
|
5
|
+
ChangedFile,
|
|
6
|
+
CommitInfo,
|
|
7
|
+
IssueComment,
|
|
8
|
+
CreateIssueCommentOption,
|
|
9
|
+
CreatePullReviewOption,
|
|
10
|
+
PullReview,
|
|
11
|
+
PullReviewComment,
|
|
12
|
+
EditPullRequestOption,
|
|
13
|
+
Reaction,
|
|
14
|
+
ResolvedThread,
|
|
15
|
+
} from "@spaceflow/core";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PR 数据模型映射层
|
|
19
|
+
* 封装所有 PR 相关的读写操作,消除 owner/repo/prNumber 三元组的散传
|
|
20
|
+
* 读操作带懒加载缓存,写操作直通 gitProvider
|
|
21
|
+
*/
|
|
22
|
+
export class PullRequestModel {
|
|
23
|
+
private _info: PullRequest | null = null;
|
|
24
|
+
private _commits: PullRequestCommit[] | null = null;
|
|
25
|
+
private _files: ChangedFile[] | null = null;
|
|
26
|
+
private _diff: string | null = null;
|
|
27
|
+
private _comments: IssueComment[] | null = null;
|
|
28
|
+
private _reviews: PullReview[] | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly gitProvider: GitProviderService,
|
|
32
|
+
readonly owner: string,
|
|
33
|
+
readonly repo: string,
|
|
34
|
+
readonly number: number,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
// ============ 懒加载数据(带缓存) ============
|
|
38
|
+
|
|
39
|
+
/** 获取 PR 基本信息(懒加载 + 缓存) */
|
|
40
|
+
async getInfo(): Promise<PullRequest> {
|
|
41
|
+
if (!this._info) {
|
|
42
|
+
this._info = await this.gitProvider.getPullRequest(this.owner, this.repo, this.number);
|
|
43
|
+
}
|
|
44
|
+
return this._info;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 获取 head commit SHA */
|
|
48
|
+
async getHeadSha(): Promise<string> {
|
|
49
|
+
const info = await this.getInfo();
|
|
50
|
+
return info.head?.sha || "HEAD";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 获取 PR 的所有 commits(懒加载 + 缓存) */
|
|
54
|
+
async getCommits(): Promise<PullRequestCommit[]> {
|
|
55
|
+
if (!this._commits) {
|
|
56
|
+
this._commits = await this.gitProvider.getPullRequestCommits(
|
|
57
|
+
this.owner,
|
|
58
|
+
this.repo,
|
|
59
|
+
this.number,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return [...this._commits];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 获取 PR 的变更文件列表(懒加载 + 缓存) */
|
|
66
|
+
async getFiles(): Promise<ChangedFile[]> {
|
|
67
|
+
if (!this._files) {
|
|
68
|
+
this._files = await this.gitProvider.getPullRequestFiles(this.owner, this.repo, this.number);
|
|
69
|
+
}
|
|
70
|
+
return [...this._files];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 获取 PR 的 diff 文本(懒加载 + 缓存) */
|
|
74
|
+
async getDiff(): Promise<string> {
|
|
75
|
+
if (!this._diff) {
|
|
76
|
+
this._diff = await this.gitProvider.getPullRequestDiff(this.owner, this.repo, this.number);
|
|
77
|
+
}
|
|
78
|
+
return this._diff;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============ PR 编辑 ============
|
|
82
|
+
|
|
83
|
+
/** 编辑 PR(标题、描述等),自动 invalidate info 缓存 */
|
|
84
|
+
async edit(options: EditPullRequestOption): Promise<PullRequest> {
|
|
85
|
+
const result = await this.gitProvider.editPullRequest(
|
|
86
|
+
this.owner,
|
|
87
|
+
this.repo,
|
|
88
|
+
this.number,
|
|
89
|
+
options,
|
|
90
|
+
);
|
|
91
|
+
this._info = result;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============ Issue Comments(PR 主评论) ============
|
|
96
|
+
|
|
97
|
+
/** 列出 PR 的所有 Issue Comments(懒加载 + 缓存) */
|
|
98
|
+
async getComments(): Promise<IssueComment[]> {
|
|
99
|
+
if (!this._comments) {
|
|
100
|
+
this._comments = await this.gitProvider.listIssueComments(this.owner, this.repo, this.number);
|
|
101
|
+
}
|
|
102
|
+
return [...this._comments];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 创建 Issue Comment */
|
|
106
|
+
async createComment(options: CreateIssueCommentOption): Promise<IssueComment> {
|
|
107
|
+
const result = await this.gitProvider.createIssueComment(
|
|
108
|
+
this.owner,
|
|
109
|
+
this.repo,
|
|
110
|
+
this.number,
|
|
111
|
+
options,
|
|
112
|
+
);
|
|
113
|
+
this._comments = null; // invalidate
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 更新 Issue Comment */
|
|
118
|
+
async updateComment(commentId: number, body: string): Promise<IssueComment> {
|
|
119
|
+
const result = await this.gitProvider.updateIssueComment(
|
|
120
|
+
this.owner,
|
|
121
|
+
this.repo,
|
|
122
|
+
commentId,
|
|
123
|
+
body,
|
|
124
|
+
);
|
|
125
|
+
this._comments = null; // invalidate
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 删除 Issue Comment */
|
|
130
|
+
async deleteComment(commentId: number): Promise<void> {
|
|
131
|
+
await this.gitProvider.deleteIssueComment(this.owner, this.repo, commentId);
|
|
132
|
+
this._comments = null; // invalidate
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============ PR Reviews(行级评论) ============
|
|
136
|
+
|
|
137
|
+
/** 列出 PR 的所有 Reviews(懒加载 + 缓存) */
|
|
138
|
+
async getReviews(): Promise<PullReview[]> {
|
|
139
|
+
if (!this._reviews) {
|
|
140
|
+
this._reviews = await this.gitProvider.listPullReviews(this.owner, this.repo, this.number);
|
|
141
|
+
}
|
|
142
|
+
return [...this._reviews];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 创建 PR Review */
|
|
146
|
+
async createReview(options: CreatePullReviewOption): Promise<PullReview> {
|
|
147
|
+
const result = await this.gitProvider.createPullReview(
|
|
148
|
+
this.owner,
|
|
149
|
+
this.repo,
|
|
150
|
+
this.number,
|
|
151
|
+
options,
|
|
152
|
+
);
|
|
153
|
+
this._reviews = null; // invalidate
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 删除 PR Review */
|
|
158
|
+
async deleteReview(reviewId: number): Promise<void> {
|
|
159
|
+
await this.gitProvider.deletePullReview(this.owner, this.repo, this.number, reviewId);
|
|
160
|
+
this._reviews = null; // invalidate
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 获取 PR Review 的行级评论列表 */
|
|
164
|
+
async getReviewComments(reviewId: number): Promise<PullReviewComment[]> {
|
|
165
|
+
return this.gitProvider.listPullReviewComments(this.owner, this.repo, this.number, reviewId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 获取已解决的 review threads */
|
|
169
|
+
async getResolvedThreads(): Promise<ResolvedThread[]> {
|
|
170
|
+
return this.gitProvider.listResolvedThreads(this.owner, this.repo, this.number);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============ Reaction 操作 ============
|
|
174
|
+
|
|
175
|
+
/** 获取 PR Review 行级评论的 reactions */
|
|
176
|
+
async getReviewCommentReactions(commentId: number): Promise<Reaction[]> {
|
|
177
|
+
return this.gitProvider.getPullReviewCommentReactions(this.owner, this.repo, commentId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============ 仓库级操作(需要 owner/repo 但不需要 prNumber) ============
|
|
181
|
+
|
|
182
|
+
/** 获取文件内容 */
|
|
183
|
+
async getFileContent(filepath: string, ref?: string): Promise<string> {
|
|
184
|
+
return this.gitProvider.getFileContent(this.owner, this.repo, filepath, ref);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 获取单个 commit 信息 */
|
|
188
|
+
async getCommit(sha: string): Promise<CommitInfo> {
|
|
189
|
+
return this.gitProvider.getCommit(this.owner, this.repo, sha);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 获取单个 commit 的 diff */
|
|
193
|
+
async getCommitDiff(sha: string): Promise<string> {
|
|
194
|
+
return this.gitProvider.getCommitDiff(this.owner, this.repo, sha);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 列出 workflow runs */
|
|
198
|
+
async listWorkflowRuns(options?: { status?: string; sha?: string }) {
|
|
199
|
+
return this.gitProvider.listWorkflowRuns(this.owner, this.repo, options);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============ 缓存控制 ============
|
|
203
|
+
|
|
204
|
+
/** 清除指定或所有缓存 */
|
|
205
|
+
invalidate(key?: "info" | "commits" | "files" | "diff" | "comments" | "reviews"): void {
|
|
206
|
+
if (key) {
|
|
207
|
+
switch (key) {
|
|
208
|
+
case "info":
|
|
209
|
+
this._info = null;
|
|
210
|
+
break;
|
|
211
|
+
case "commits":
|
|
212
|
+
this._commits = null;
|
|
213
|
+
break;
|
|
214
|
+
case "files":
|
|
215
|
+
this._files = null;
|
|
216
|
+
break;
|
|
217
|
+
case "diff":
|
|
218
|
+
this._diff = null;
|
|
219
|
+
break;
|
|
220
|
+
case "comments":
|
|
221
|
+
this._comments = null;
|
|
222
|
+
break;
|
|
223
|
+
case "reviews":
|
|
224
|
+
this._reviews = null;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
this._info = null;
|
|
229
|
+
this._commits = null;
|
|
230
|
+
this._files = null;
|
|
231
|
+
this._diff = null;
|
|
232
|
+
this._comments = null;
|
|
233
|
+
this._reviews = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|