@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/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmJsonPut, REVIEW_STATE, addLocaleResources, calculateNewLineNumber, createStreamLoggerState, defineExtension, logStreamEvent, normalizeVerbose, parallel, parseChangedLinesFromPatch, parseDiffText, parseHunksFromPatch, parseRepoUrl, parseVerbose, shouldLog, t, z } from "@spaceflow/core";
|
|
2
2
|
import { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative } from "path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { execSync, spawn } from "child_process";
|
|
6
6
|
import micromatch_0 from "micromatch";
|
|
@@ -182,6 +182,148 @@ const SEVERITY_EMOJI = {
|
|
|
182
182
|
|
|
183
183
|
;// CONCATENATED MODULE: external "micromatch"
|
|
184
184
|
|
|
185
|
+
;// CONCATENATED MODULE: ./src/review-includes-filter.ts
|
|
186
|
+
|
|
187
|
+
const CODE_BLOCK_TYPES = [
|
|
188
|
+
"function",
|
|
189
|
+
"class",
|
|
190
|
+
"interface",
|
|
191
|
+
"type",
|
|
192
|
+
"method"
|
|
193
|
+
];
|
|
194
|
+
/** status 值到前缀的映射(兼容 GitHub/GitLab/Gitea 各平台) */ const STATUS_ALIAS = {
|
|
195
|
+
added: "added",
|
|
196
|
+
created: "added",
|
|
197
|
+
renamed: "modified",
|
|
198
|
+
modified: "modified",
|
|
199
|
+
changed: "modified",
|
|
200
|
+
removed: "deleted",
|
|
201
|
+
deleted: "deleted"
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* 解析单条 include 模式,拆分 status 前缀和 glob。
|
|
205
|
+
*
|
|
206
|
+
* 只有当 `|` 前面的部分是已知 status 关键字时才视为前缀,否则当作普通 glob 处理(容错),
|
|
207
|
+
* 这样可以避免误解析 extglob 语法中含 `|` 的模式(如 `+(*.ts|*.js)`)。
|
|
208
|
+
* 排除模式(以 `!` 开头)始终作为普通 glob 处理。
|
|
209
|
+
*/ function parseIncludePattern(pattern) {
|
|
210
|
+
if (pattern.startsWith("!")) {
|
|
211
|
+
return {
|
|
212
|
+
status: undefined,
|
|
213
|
+
glob: pattern
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const separatorIndex = pattern.indexOf("|");
|
|
217
|
+
if (separatorIndex === -1) {
|
|
218
|
+
return {
|
|
219
|
+
status: undefined,
|
|
220
|
+
glob: pattern
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const prefix = pattern.slice(0, separatorIndex).trim().toLowerCase();
|
|
224
|
+
const glob = pattern.slice(separatorIndex + 1).trim();
|
|
225
|
+
const status = STATUS_ALIAS[prefix];
|
|
226
|
+
if (!status) {
|
|
227
|
+
// 前缀无法识别(如 extglob 中的 `|`),当作普通 glob 处理
|
|
228
|
+
return {
|
|
229
|
+
status: undefined,
|
|
230
|
+
glob: pattern
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
status,
|
|
235
|
+
glob
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 根据 includes 模式列表过滤文件,支持 `status|glob` 前缀语法。
|
|
240
|
+
*
|
|
241
|
+
* 算法:
|
|
242
|
+
* 1. 将 includes 拆分为:排除模式(`!`)、无前缀正向 glob、有 status 前缀 glob
|
|
243
|
+
* 2. 每个文件先检查是否命中任意正向条件(无前缀 glob 或匹配 status 的前缀 glob)
|
|
244
|
+
* 3. 最后用排除模式做全局过滤(排除模式始终优先)
|
|
245
|
+
*
|
|
246
|
+
* @param files 待过滤的文件列表
|
|
247
|
+
* @param includes include 模式列表,支持 `added|*.ts`、`modified|*.ts`、`deleted|*.ts` 前缀
|
|
248
|
+
* @returns 匹配的文件列表
|
|
249
|
+
*/ function filterFilesByIncludes(files, includes) {
|
|
250
|
+
if (!includes || includes.length === 0) return files;
|
|
251
|
+
const parsed = includes.map(parseIncludePattern);
|
|
252
|
+
console.log(`[filterFilesByIncludes] parsed patterns=${JSON.stringify(parsed)}`);
|
|
253
|
+
// 排除模式(以 ! 开头),用于最终全局过滤
|
|
254
|
+
const negativeGlobs = parsed.filter((p)=>p.status === undefined && p.glob.startsWith("!")).map((p)=>p.glob.slice(1)); // 去掉 ! 前缀,用 micromatch.not 处理
|
|
255
|
+
// 无前缀的正向 globs
|
|
256
|
+
const plainGlobs = parsed.filter((p)=>p.status === undefined && !p.glob.startsWith("!")).map((p)=>p.glob);
|
|
257
|
+
// 有 status 前缀的 patterns
|
|
258
|
+
const statusPatterns = parsed.filter((p)=>p.status !== undefined);
|
|
259
|
+
console.log(`[filterFilesByIncludes] negativeGlobs=${JSON.stringify(negativeGlobs)}, plainGlobs=${JSON.stringify(plainGlobs)}, statusPatterns=${JSON.stringify(statusPatterns)}`);
|
|
260
|
+
return files.filter((file)=>{
|
|
261
|
+
const filename = file.filename ?? "";
|
|
262
|
+
const fileStatus = STATUS_ALIAS[file.status?.toLowerCase() ?? ""] ?? "modified";
|
|
263
|
+
if (!filename) return false;
|
|
264
|
+
// 最终排除:命中排除模式的文件直接过滤掉
|
|
265
|
+
if (negativeGlobs.length > 0 && micromatch_0.isMatch(filename, negativeGlobs, {
|
|
266
|
+
matchBase: true
|
|
267
|
+
})) {
|
|
268
|
+
console.log(`[filterFilesByIncludes] ${filename} excluded by negativeGlobs`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
// 正向匹配:无前缀 glob
|
|
272
|
+
if (plainGlobs.length > 0 && micromatch_0.isMatch(filename, plainGlobs, {
|
|
273
|
+
matchBase: true
|
|
274
|
+
})) {
|
|
275
|
+
console.log(`[filterFilesByIncludes] ${filename} matched plainGlobs`);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// 正向匹配:有 status 前缀的 glob,按文件实际 status 过滤
|
|
279
|
+
// glob 可以带 ! 前缀表示在该 status 范围内排除,如 added|!**/*.spec.ts
|
|
280
|
+
if (statusPatterns.length > 0) {
|
|
281
|
+
// 按 status 分组,每组内正向 glob + 排除 glob 合并后批量匹配
|
|
282
|
+
const matchingStatusGlobs = statusPatterns.filter(({ status })=>status === fileStatus).map(({ glob })=>glob);
|
|
283
|
+
if (matchingStatusGlobs.length > 0) {
|
|
284
|
+
// 有正向 glob 才有意义,纯排除 glob 组合 micromatch 会视为全匹配再排除
|
|
285
|
+
const positiveGlobs = matchingStatusGlobs.filter((g)=>!g.startsWith("!"));
|
|
286
|
+
const negativeStatusGlobs = matchingStatusGlobs.filter((g)=>g.startsWith("!")).map((g)=>g.slice(1));
|
|
287
|
+
if (positiveGlobs.length > 0) {
|
|
288
|
+
const matchesPositive = micromatch_0.isMatch(filename, positiveGlobs, {
|
|
289
|
+
matchBase: true
|
|
290
|
+
});
|
|
291
|
+
const matchesNegative = negativeStatusGlobs.length > 0 && micromatch_0.isMatch(filename, negativeStatusGlobs, {
|
|
292
|
+
matchBase: true
|
|
293
|
+
});
|
|
294
|
+
if (matchesPositive && !matchesNegative) {
|
|
295
|
+
console.log(`[filterFilesByIncludes] ${filename} (status=${fileStatus}) matched statusPatterns`);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
console.log(`[filterFilesByIncludes] ${filename} (status=${fileStatus}) NOT matched`);
|
|
302
|
+
return false;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 从 includes 模式列表中提取纯 glob(用于 commit 过滤,commit 没有 status 概念)。
|
|
307
|
+
* 带 status 前缀的模式会去掉前缀,仅保留 glob 部分。
|
|
308
|
+
*/ function extractGlobsFromIncludes(includes) {
|
|
309
|
+
return includes.map((p)=>parseIncludePattern(p).glob).filter((g)=>g.length > 0);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 从 whenModifiedCode 配置中解析代码结构过滤类型。
|
|
313
|
+
* 只接受简单的类型名称,如 "function"、"class"、"interface"、"type"、"method"
|
|
314
|
+
*/ function extractCodeBlockTypes(whenModifiedCode) {
|
|
315
|
+
const types = new Set();
|
|
316
|
+
for (const entry of whenModifiedCode){
|
|
317
|
+
const trimmed = entry.trim();
|
|
318
|
+
if (CODE_BLOCK_TYPES.includes(trimmed)) {
|
|
319
|
+
types.add(trimmed);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return [
|
|
323
|
+
...types
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
|
|
185
327
|
;// CONCATENATED MODULE: ./src/review-spec/review-spec.service.ts
|
|
186
328
|
|
|
187
329
|
|
|
@@ -189,6 +331,7 @@ const SEVERITY_EMOJI = {
|
|
|
189
331
|
|
|
190
332
|
|
|
191
333
|
|
|
334
|
+
|
|
192
335
|
/** 远程规则缓存 TTL(毫秒),默认 5 分钟 */ const REMOTE_SPEC_CACHE_TTL = 5 * 60 * 1000;
|
|
193
336
|
class ReviewSpecService {
|
|
194
337
|
gitProvider;
|
|
@@ -237,7 +380,19 @@ class ReviewSpecService {
|
|
|
237
380
|
}
|
|
238
381
|
}
|
|
239
382
|
}
|
|
240
|
-
|
|
383
|
+
console.log(`[filterApplicableSpecs] changedExtensions=${JSON.stringify([
|
|
384
|
+
...changedExtensions
|
|
385
|
+
])}, specs count=${specs.length}`);
|
|
386
|
+
const result = specs.filter((spec)=>{
|
|
387
|
+
const matches = spec.extensions.some((ext)=>changedExtensions.has(ext));
|
|
388
|
+
if (!matches) {
|
|
389
|
+
console.log(`[filterApplicableSpecs] spec ${spec.filename} (ext: ${JSON.stringify(spec.extensions)}) NOT matched`);
|
|
390
|
+
} else {
|
|
391
|
+
console.log(`[filterApplicableSpecs] spec ${spec.filename} (ext: ${JSON.stringify(spec.extensions)}) MATCHED`);
|
|
392
|
+
}
|
|
393
|
+
return matches;
|
|
394
|
+
});
|
|
395
|
+
return result;
|
|
241
396
|
}
|
|
242
397
|
async loadReviewSpecs(specDir) {
|
|
243
398
|
const specs = [];
|
|
@@ -623,8 +778,10 @@ class ReviewSpecService {
|
|
|
623
778
|
if (includes.length === 0) {
|
|
624
779
|
return true;
|
|
625
780
|
}
|
|
626
|
-
// 检查文件是否匹配 includes
|
|
627
|
-
const
|
|
781
|
+
// 检查文件是否匹配 includes 模式(转换为纯 glob,避免 status| 前缀和 code-* 空串传入 micromatch)
|
|
782
|
+
const globs = extractGlobsFromIncludes(includes);
|
|
783
|
+
if (globs.length === 0) return true;
|
|
784
|
+
const matches = micromatch_0.isMatch(issue.file, globs, {
|
|
628
785
|
matchBase: true
|
|
629
786
|
});
|
|
630
787
|
if (!matches) {
|
|
@@ -719,8 +876,10 @@ class ReviewSpecService {
|
|
|
719
876
|
if (scoped.includes.length === 0) {
|
|
720
877
|
return true;
|
|
721
878
|
}
|
|
722
|
-
// 使用 micromatch 检查文件是否匹配 includes
|
|
723
|
-
|
|
879
|
+
// 使用 micromatch 检查文件是否匹配 includes 模式(转换为纯 glob)
|
|
880
|
+
const globs = extractGlobsFromIncludes(scoped.includes);
|
|
881
|
+
if (globs.length === 0) return true;
|
|
882
|
+
return issueFile && micromatch_0.isMatch(issueFile, globs, {
|
|
724
883
|
matchBase: true
|
|
725
884
|
});
|
|
726
885
|
});
|
|
@@ -1749,7 +1908,7 @@ class ReviewReportService {
|
|
|
1749
1908
|
}
|
|
1750
1909
|
}
|
|
1751
1910
|
|
|
1752
|
-
;// CONCATENATED MODULE: ./src/review-pr-comment
|
|
1911
|
+
;// CONCATENATED MODULE: ./src/utils/review-pr-comment.ts
|
|
1753
1912
|
const REVIEW_COMMENT_MARKER = "<!-- spaceflow-review -->";
|
|
1754
1913
|
const REVIEW_LINE_COMMENTS_MARKER = "<!-- spaceflow-review-lines -->";
|
|
1755
1914
|
/**
|
|
@@ -2163,8 +2322,24 @@ function generateIssueKey(issue) {
|
|
|
2163
2322
|
// 遍历每个评论,获取其 reactions
|
|
2164
2323
|
for (const comment of reviewComments){
|
|
2165
2324
|
if (!comment.id) continue;
|
|
2166
|
-
// 找到对应的 issue
|
|
2167
|
-
|
|
2325
|
+
// 找到对应的 issue:优先通过 issue-key 精确匹配,回退到 path+line 匹配
|
|
2326
|
+
let matchedIssue;
|
|
2327
|
+
if (comment.body) {
|
|
2328
|
+
const issueKey = extractIssueKeyFromBody(comment.body);
|
|
2329
|
+
if (issueKey) {
|
|
2330
|
+
matchedIssue = this._result.issues.find((issue)=>generateIssueKey(issue) === issueKey);
|
|
2331
|
+
if (shouldLog(verbose, 3)) {
|
|
2332
|
+
console.log(`[syncReactionsToIssues] comment ${comment.id}: issue-key=${issueKey}, matched=${matchedIssue ? "yes" : "no"}`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
// 如果 issue-key 匹配失败,使用 path+position 回退匹配
|
|
2337
|
+
if (!matchedIssue) {
|
|
2338
|
+
matchedIssue = this._result.issues.find((issue)=>issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position));
|
|
2339
|
+
if (shouldLog(verbose, 3)) {
|
|
2340
|
+
console.log(`[syncReactionsToIssues] comment ${comment.id}: fallback matching path=${comment.path}, position=${comment.position}, matched=${matchedIssue ? "yes" : "no"}`);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2168
2343
|
if (matchedIssue) {
|
|
2169
2344
|
commentIdToIssue.set(comment.id, matchedIssue);
|
|
2170
2345
|
}
|
|
@@ -2783,6 +2958,7 @@ function generateIssueKey(issue) {
|
|
|
2783
2958
|
references: z.array(z.string()).optional(),
|
|
2784
2959
|
llmMode: llmModeSchema.default("openai").optional(),
|
|
2785
2960
|
includes: z.array(z.string()).optional(),
|
|
2961
|
+
whenModifiedCode: z.array(z.string()).optional(),
|
|
2786
2962
|
rules: z.record(z.string(), severitySchema).optional(),
|
|
2787
2963
|
verifyFixes: z.boolean().default(false),
|
|
2788
2964
|
verifyFixesConcurrency: z.number().default(10).optional(),
|
|
@@ -2796,14 +2972,27 @@ function generateIssueKey(issue) {
|
|
|
2796
2972
|
retries: z.number().default(0).optional(),
|
|
2797
2973
|
retryDelay: z.number().default(1000).optional(),
|
|
2798
2974
|
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
|
|
2799
|
-
|
|
2975
|
+
duplicateWorkflowResolved: z["enum"]([
|
|
2976
|
+
"off",
|
|
2977
|
+
"skip",
|
|
2978
|
+
"delete"
|
|
2979
|
+
]).default("delete").optional(),
|
|
2800
2980
|
autoApprove: z.boolean().default(false).optional(),
|
|
2801
2981
|
failOnIssues: z["enum"]([
|
|
2802
2982
|
"off",
|
|
2803
2983
|
"warn",
|
|
2804
2984
|
"error",
|
|
2805
2985
|
"warn+error"
|
|
2806
|
-
]).default("off").optional()
|
|
2986
|
+
]).default("off").optional(),
|
|
2987
|
+
systemRules: z.object({
|
|
2988
|
+
maxLinesPerFile: z.tuple([
|
|
2989
|
+
z.number(),
|
|
2990
|
+
severitySchema
|
|
2991
|
+
]).transform((v)=>[
|
|
2992
|
+
v[0],
|
|
2993
|
+
v[1]
|
|
2994
|
+
]).optional()
|
|
2995
|
+
}).optional()
|
|
2807
2996
|
});
|
|
2808
2997
|
|
|
2809
2998
|
;// CONCATENATED MODULE: ./src/parse-title-options.ts
|
|
@@ -2988,6 +3177,9 @@ class ReviewContextBuilder {
|
|
|
2988
3177
|
}
|
|
2989
3178
|
async getContextFromEnv(options) {
|
|
2990
3179
|
const reviewConf = this.config.getPluginConfig("review");
|
|
3180
|
+
if (shouldLog(options.verbose, 2)) {
|
|
3181
|
+
console.log(`[getContextFromEnv] reviewConf: ${JSON.stringify(reviewConf)}`);
|
|
3182
|
+
}
|
|
2991
3183
|
const ciConf = this.config.get("ci");
|
|
2992
3184
|
const repository = ciConf?.repository;
|
|
2993
3185
|
if (options.ci) {
|
|
@@ -3047,11 +3239,14 @@ class ReviewContextBuilder {
|
|
|
3047
3239
|
if (reviewConf.references?.length) {
|
|
3048
3240
|
specSources.push(...reviewConf.references);
|
|
3049
3241
|
}
|
|
3242
|
+
const normalizedFiles = this.normalizeFilePaths(options.files);
|
|
3050
3243
|
// 解析本地模式:非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
|
|
3244
|
+
// 当显式指定 files 时,强制走“按文件审查模式”,不进入本地未提交模式
|
|
3051
3245
|
const localMode = this.resolveLocalMode(options, {
|
|
3052
3246
|
ci: options.ci,
|
|
3053
3247
|
hasPrNumber: !!prNumber,
|
|
3054
|
-
hasBaseHead: !!(options.base || options.head)
|
|
3248
|
+
hasBaseHead: !!(options.base || options.head),
|
|
3249
|
+
hasFiles: !!normalizedFiles?.length
|
|
3055
3250
|
});
|
|
3056
3251
|
// 当没有 PR 且没有指定 base/head 且不是本地模式时,自动获取默认值
|
|
3057
3252
|
let baseRef = options.base;
|
|
@@ -3064,6 +3259,10 @@ class ReviewContextBuilder {
|
|
|
3064
3259
|
}
|
|
3065
3260
|
}
|
|
3066
3261
|
// 合并参数优先级:命令行 > PR 标题 > 配置文件 > 默认值
|
|
3262
|
+
const ctxIncludes = options.includes ?? titleOptions.includes ?? reviewConf.includes;
|
|
3263
|
+
if (shouldLog(options.verbose, 2)) {
|
|
3264
|
+
console.log(`[getContextFromEnv] includes: commandLine=${JSON.stringify(options.includes)}, title=${JSON.stringify(titleOptions.includes)}, config=${JSON.stringify(reviewConf.includes)}, final=${JSON.stringify(ctxIncludes)}`);
|
|
3265
|
+
}
|
|
3067
3266
|
return {
|
|
3068
3267
|
owner,
|
|
3069
3268
|
repo,
|
|
@@ -3074,9 +3273,10 @@ class ReviewContextBuilder {
|
|
|
3074
3273
|
dryRun: options.dryRun || titleOptions.dryRun || false,
|
|
3075
3274
|
ci: options.ci ?? false,
|
|
3076
3275
|
verbose: normalizeVerbose(options.verbose ?? titleOptions.verbose),
|
|
3077
|
-
includes:
|
|
3276
|
+
includes: ctxIncludes,
|
|
3277
|
+
whenModifiedCode: options.whenModifiedCode ?? reviewConf.whenModifiedCode,
|
|
3078
3278
|
llmMode: options.llmMode ?? titleOptions.llmMode ?? reviewConf.llmMode,
|
|
3079
|
-
files:
|
|
3279
|
+
files: normalizedFiles,
|
|
3080
3280
|
commits: options.commits,
|
|
3081
3281
|
verifyFixes: options.verifyFixes ?? titleOptions.verifyFixes ?? reviewConf.verifyFixes ?? true,
|
|
3082
3282
|
verifyConcurrency: options.verifyConcurrency ?? reviewConf.verifyFixesConcurrency ?? 10,
|
|
@@ -3095,8 +3295,9 @@ class ReviewContextBuilder {
|
|
|
3095
3295
|
flush: options.flush ?? false,
|
|
3096
3296
|
eventAction: options.eventAction,
|
|
3097
3297
|
localMode,
|
|
3098
|
-
|
|
3099
|
-
autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false
|
|
3298
|
+
duplicateWorkflowResolved: options.duplicateWorkflowResolved ?? reviewConf.duplicateWorkflowResolved ?? "delete",
|
|
3299
|
+
autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false,
|
|
3300
|
+
systemRules: options.systemRules ?? reviewConf.systemRules
|
|
3100
3301
|
};
|
|
3101
3302
|
}
|
|
3102
3303
|
/**
|
|
@@ -3105,6 +3306,10 @@ class ReviewContextBuilder {
|
|
|
3105
3306
|
* - 显式指定 --no-local 时禁用
|
|
3106
3307
|
* - 非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
|
|
3107
3308
|
*/ resolveLocalMode(options, env) {
|
|
3309
|
+
// 显式指定了 files,优先进入按文件审查模式
|
|
3310
|
+
if (env.hasFiles) {
|
|
3311
|
+
return false;
|
|
3312
|
+
}
|
|
3108
3313
|
// 显式指定了 --no-local
|
|
3109
3314
|
if (options.local === false) {
|
|
3110
3315
|
return false;
|
|
@@ -3130,13 +3335,17 @@ class ReviewContextBuilder {
|
|
|
3130
3335
|
*/ normalizeFilePaths(files) {
|
|
3131
3336
|
if (!files || files.length === 0) return files;
|
|
3132
3337
|
const cwd = process.cwd();
|
|
3133
|
-
return files.map((file)=>
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3338
|
+
return files.map((file)=>this.normalizeSingleFilePath(file, cwd));
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* 规范化单个文件路径为仓库相对路径:
|
|
3342
|
+
* - 绝对路径转相对路径
|
|
3343
|
+
* - 统一分隔符为 /
|
|
3344
|
+
* - 移除前导 ./
|
|
3345
|
+
*/ normalizeSingleFilePath(file, cwd) {
|
|
3346
|
+
const normalizedInput = normalize(file);
|
|
3347
|
+
const relativePath = isAbsolute(normalizedInput) ? relative(cwd, normalizedInput) : normalizedInput;
|
|
3348
|
+
return relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
3140
3349
|
}
|
|
3141
3350
|
/**
|
|
3142
3351
|
* 根据 AnalyzeDeletionsMode 和当前环境解析是否启用删除代码分析
|
|
@@ -3268,13 +3477,32 @@ class ReviewContextBuilder {
|
|
|
3268
3477
|
// 为每个 issue 填充 author
|
|
3269
3478
|
return issues.map((issue)=>{
|
|
3270
3479
|
if (issue.author) {
|
|
3480
|
+
const shortHash = issue.commit?.slice(0, 7);
|
|
3481
|
+
if (shortHash?.includes("---")) {
|
|
3482
|
+
return {
|
|
3483
|
+
...issue,
|
|
3484
|
+
commit: undefined,
|
|
3485
|
+
valid: "false"
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3271
3488
|
if (shouldLog(verbose, 2)) {
|
|
3272
3489
|
console.log(`[fillIssueAuthors] issue already has author: ${issue.author.login}`);
|
|
3273
3490
|
}
|
|
3274
3491
|
return issue;
|
|
3275
3492
|
}
|
|
3276
3493
|
const shortHash = issue.commit?.slice(0, 7);
|
|
3277
|
-
const
|
|
3494
|
+
const isValidHash = Boolean(shortHash && !shortHash.includes("---"));
|
|
3495
|
+
if (!isValidHash) {
|
|
3496
|
+
if (shouldLog(verbose, 2)) {
|
|
3497
|
+
console.log(`[fillIssueAuthors] issue: file=${issue.file}, commit=${issue.commit} is invalid hash, marking as invalid`);
|
|
3498
|
+
}
|
|
3499
|
+
return {
|
|
3500
|
+
...issue,
|
|
3501
|
+
commit: undefined,
|
|
3502
|
+
valid: "false"
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
const author = commitAuthorMap.get(shortHash);
|
|
3278
3506
|
if (shouldLog(verbose, 2)) {
|
|
3279
3507
|
console.log(`[fillIssueAuthors] issue: file=${issue.file}, commit=${issue.commit}, shortHash=${shortHash}, foundAuthor=${author?.login}, finalAuthor=${(author || defaultAuthor)?.login}`);
|
|
3280
3508
|
}
|
|
@@ -3677,125 +3905,139 @@ class ReviewIssueFilter {
|
|
|
3677
3905
|
}
|
|
3678
3906
|
}
|
|
3679
3907
|
|
|
3680
|
-
;// CONCATENATED MODULE: ./src/review-
|
|
3681
|
-
|
|
3682
|
-
/** status 值到前缀的映射(兼容 GitHub/GitLab/Gitea 各平台) */ const STATUS_ALIAS = {
|
|
3683
|
-
added: "added",
|
|
3684
|
-
created: "added",
|
|
3685
|
-
renamed: "modified",
|
|
3686
|
-
modified: "modified",
|
|
3687
|
-
changed: "modified",
|
|
3688
|
-
removed: "deleted",
|
|
3689
|
-
deleted: "deleted"
|
|
3690
|
-
};
|
|
3908
|
+
;// CONCATENATED MODULE: ./src/utils/review-llm.ts
|
|
3691
3909
|
/**
|
|
3692
|
-
*
|
|
3910
|
+
* 构建带行号的文件内容字符串。
|
|
3693
3911
|
*
|
|
3694
|
-
*
|
|
3695
|
-
*
|
|
3696
|
-
*
|
|
3697
|
-
*/ function
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3912
|
+
* @param contentLines [hash, code] 行数组
|
|
3913
|
+
* @param visibleRanges 可选,指定需要输出的行号区间 [startLine, endLine](不含则输出全文)
|
|
3914
|
+
* 被跳过的连续行用 `...... ..| ignore {start}-{end} code` 占位
|
|
3915
|
+
*/ function buildLinesWithNumbers(contentLines, visibleRanges) {
|
|
3916
|
+
const padWidth = String(contentLines.length).length;
|
|
3917
|
+
if (!visibleRanges || visibleRanges.length === 0) {
|
|
3918
|
+
return contentLines.map(([hash, line], index)=>{
|
|
3919
|
+
const lineNum = index + 1;
|
|
3920
|
+
return `${hash} ${String(lineNum).padStart(padWidth)}| ${line}`;
|
|
3921
|
+
}).join("\n");
|
|
3703
3922
|
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3923
|
+
// 将 ranges 按起始行号排序并合并重叠区间
|
|
3924
|
+
const sorted = [
|
|
3925
|
+
...visibleRanges
|
|
3926
|
+
].sort((a, b)=>a[0] - b[0]);
|
|
3927
|
+
const merged = [];
|
|
3928
|
+
for (const range of sorted){
|
|
3929
|
+
if (merged.length > 0 && range[0] <= merged[merged.length - 1][1] + 1) {
|
|
3930
|
+
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], range[1]);
|
|
3931
|
+
} else {
|
|
3932
|
+
merged.push([
|
|
3933
|
+
...range
|
|
3934
|
+
]);
|
|
3935
|
+
}
|
|
3710
3936
|
}
|
|
3711
|
-
const
|
|
3712
|
-
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
}
|
|
3937
|
+
const output = [];
|
|
3938
|
+
let prevEnd = 0;
|
|
3939
|
+
for (const [start, end] of merged){
|
|
3940
|
+
const clampedStart = Math.max(1, start);
|
|
3941
|
+
const clampedEnd = Math.min(contentLines.length, end);
|
|
3942
|
+
// 被忽略的前缀区间
|
|
3943
|
+
if (clampedStart > prevEnd + 1) {
|
|
3944
|
+
output.push(`....... ignore ${prevEnd + 1}-${clampedStart - 1} line .......`);
|
|
3945
|
+
}
|
|
3946
|
+
// 输出可见行
|
|
3947
|
+
for(let i = clampedStart - 1; i < clampedEnd; i++){
|
|
3948
|
+
const [hash, line] = contentLines[i];
|
|
3949
|
+
const lineNum = i + 1;
|
|
3950
|
+
output.push(`${hash} ${String(lineNum).padStart(padWidth)}| ${line}`);
|
|
3951
|
+
}
|
|
3952
|
+
prevEnd = clampedEnd;
|
|
3720
3953
|
}
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
}
|
|
3954
|
+
// 被忽略的末尾区间
|
|
3955
|
+
if (prevEnd < contentLines.length) {
|
|
3956
|
+
output.push(`....... ignore ${prevEnd + 1}-${contentLines.length} line .......`);
|
|
3957
|
+
}
|
|
3958
|
+
return output.join("\n");
|
|
3725
3959
|
}
|
|
3726
3960
|
/**
|
|
3727
|
-
*
|
|
3961
|
+
* 从 contentLines 中提取新增代码里的指定结构类型的行号范围。
|
|
3728
3962
|
*
|
|
3729
|
-
*
|
|
3730
|
-
* 1.
|
|
3731
|
-
* 2.
|
|
3732
|
-
* 3.
|
|
3963
|
+
* 逻辑:
|
|
3964
|
+
* 1. 只考虑 hash !== "-------" 的新增行
|
|
3965
|
+
* 2. 用各类型的正则匹配结构开头行,再用层级计数找到结尾行
|
|
3966
|
+
* 3. 返回行号范围列表 [startLine, endLine](从 1 计)
|
|
3733
3967
|
*
|
|
3734
|
-
* @param
|
|
3735
|
-
* @param
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3968
|
+
* @param contentLines 文件的 [hash, code] 行列表
|
|
3969
|
+
* @param types 要提取的结构类型
|
|
3970
|
+
*/ function extractCodeBlocks(contentLines, types) {
|
|
3971
|
+
if (types.length === 0) return [];
|
|
3972
|
+
const ranges = [];
|
|
3973
|
+
// 将所有行的实际代码组成文本(用于层级计数)
|
|
3974
|
+
const fullLines = contentLines.map(([, code])=>code);
|
|
3975
|
+
// 各类型的开头識别正则(匹配行首)
|
|
3976
|
+
const PATTERNS = {
|
|
3977
|
+
function: /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
3978
|
+
class: /^\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/,
|
|
3979
|
+
interface: /^\s*(?:export\s+)?interface\s+\w+/,
|
|
3980
|
+
type: /^\s*(?:export\s+)?type\s+\w+\s*[=<]/,
|
|
3981
|
+
method: /^\s*(?:(?:public|protected|private|static|async|override|readonly|abstract)\s+)*(?!(?:if|for|while|switch|return|const|let|var|throw|new)\b)(\w+)\s*[(<]/
|
|
3982
|
+
};
|
|
3983
|
+
for(let i = 0; i < fullLines.length; i++){
|
|
3984
|
+
const lineNum = i + 1;
|
|
3985
|
+
const isAdded = contentLines[i][0] !== "-------";
|
|
3986
|
+
if (!isAdded) continue;
|
|
3987
|
+
for (const type of types){
|
|
3988
|
+
const pattern = PATTERNS[type];
|
|
3989
|
+
if (!pattern.test(fullLines[i])) continue;
|
|
3990
|
+
// 找到结构开头,用层级计数找封闭括号结尾
|
|
3991
|
+
const endLine = findBlockEnd(fullLines, i);
|
|
3992
|
+
ranges.push([
|
|
3993
|
+
lineNum,
|
|
3994
|
+
endLine
|
|
3995
|
+
]);
|
|
3996
|
+
break; // 同一行只匹配一种类型
|
|
3760
3997
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3998
|
+
}
|
|
3999
|
+
return mergeRanges(ranges);
|
|
4000
|
+
}
|
|
4001
|
+
/**
|
|
4002
|
+
* 从开始行向下层级计数,找到匹配的封闭括号位置(行号从 1 计)。
|
|
4003
|
+
* 如果没有找到匹配括号,返回开始行到文件末尾。
|
|
4004
|
+
*/ function findBlockEnd(lines, startIndex) {
|
|
4005
|
+
let depth = 0;
|
|
4006
|
+
let foundOpen = false;
|
|
4007
|
+
for(let i = startIndex; i < lines.length; i++){
|
|
4008
|
+
for (const ch of lines[i]){
|
|
4009
|
+
if (ch === "{") {
|
|
4010
|
+
depth++;
|
|
4011
|
+
foundOpen = true;
|
|
4012
|
+
} else if (ch === "}") {
|
|
4013
|
+
depth--;
|
|
4014
|
+
if (foundOpen && depth === 0) {
|
|
4015
|
+
return i + 1; // 行号从 1 计
|
|
3779
4016
|
}
|
|
3780
4017
|
}
|
|
3781
4018
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
}
|
|
3785
|
-
/**
|
|
3786
|
-
* 从 includes 模式列表中提取纯 glob(用于 commit 过滤,commit 没有 status 概念)。
|
|
3787
|
-
* 带 status 前缀的模式会去掉前缀,仅保留 glob 部分。
|
|
3788
|
-
*/ function extractGlobsFromIncludes(includes) {
|
|
3789
|
-
return includes.map((p)=>parseIncludePattern(p).glob);
|
|
4019
|
+
}
|
|
4020
|
+
return lines.length; // 没有找到匹配括号,返回文件末尾
|
|
3790
4021
|
}
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
4022
|
+
function mergeRanges(ranges) {
|
|
4023
|
+
if (ranges.length === 0) return [];
|
|
4024
|
+
const sorted = [
|
|
4025
|
+
...ranges
|
|
4026
|
+
].sort((a, b)=>a[0] - b[0]);
|
|
4027
|
+
const merged = [
|
|
4028
|
+
sorted[0]
|
|
4029
|
+
];
|
|
4030
|
+
for(let i = 1; i < sorted.length; i++){
|
|
4031
|
+
const last = merged[merged.length - 1];
|
|
4032
|
+
if (sorted[i][0] <= last[1] + 1) {
|
|
4033
|
+
last[1] = Math.max(last[1], sorted[i][1]);
|
|
4034
|
+
} else {
|
|
4035
|
+
merged.push([
|
|
4036
|
+
...sorted[i]
|
|
4037
|
+
]);
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
return merged;
|
|
3799
4041
|
}
|
|
3800
4042
|
function buildCommitsSection(contentLines, commits) {
|
|
3801
4043
|
const fileCommitHashes = new Set();
|
|
@@ -3811,13 +4053,10 @@ function buildCommitsSection(contentLines, commits) {
|
|
|
3811
4053
|
return relatedCommits.length > 0 ? relatedCommits.map((c)=>`- \`${c.sha?.slice(0, 7)}\` ${c.commit?.message?.split("\n")[0]}`).join("\n") : "- 无相关 commits";
|
|
3812
4054
|
}
|
|
3813
4055
|
|
|
3814
|
-
;// CONCATENATED MODULE: ./src/
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
const REVIEW_SCHEMA = {
|
|
4056
|
+
;// CONCATENATED MODULE: ./src/prompt/schemas.ts
|
|
4057
|
+
/**
|
|
4058
|
+
* 代码审查结果 JSON Schema
|
|
4059
|
+
*/ const REVIEW_SCHEMA = {
|
|
3821
4060
|
type: "object",
|
|
3822
4061
|
properties: {
|
|
3823
4062
|
issues: {
|
|
@@ -3883,6 +4122,532 @@ const REVIEW_SCHEMA = {
|
|
|
3883
4122
|
],
|
|
3884
4123
|
additionalProperties: false
|
|
3885
4124
|
};
|
|
4125
|
+
/**
|
|
4126
|
+
* 删除影响分析结果 JSON Schema
|
|
4127
|
+
*/ const DELETION_IMPACT_SCHEMA = {
|
|
4128
|
+
type: "object",
|
|
4129
|
+
properties: {
|
|
4130
|
+
impacts: {
|
|
4131
|
+
type: "array",
|
|
4132
|
+
items: {
|
|
4133
|
+
type: "object",
|
|
4134
|
+
properties: {
|
|
4135
|
+
file: {
|
|
4136
|
+
type: "string",
|
|
4137
|
+
description: "被删除代码所在的文件路径"
|
|
4138
|
+
},
|
|
4139
|
+
deletedCode: {
|
|
4140
|
+
type: "string",
|
|
4141
|
+
description: "被删除的代码片段摘要(前50字符)"
|
|
4142
|
+
},
|
|
4143
|
+
riskLevel: {
|
|
4144
|
+
type: "string",
|
|
4145
|
+
enum: [
|
|
4146
|
+
"high",
|
|
4147
|
+
"medium",
|
|
4148
|
+
"low",
|
|
4149
|
+
"none"
|
|
4150
|
+
],
|
|
4151
|
+
description: "风险等级:high=可能导致功能异常,medium=可能影响部分功能,low=影响较小,none=无影响"
|
|
4152
|
+
},
|
|
4153
|
+
affectedFiles: {
|
|
4154
|
+
type: "array",
|
|
4155
|
+
items: {
|
|
4156
|
+
type: "string"
|
|
4157
|
+
},
|
|
4158
|
+
description: "可能受影响的文件列表"
|
|
4159
|
+
},
|
|
4160
|
+
reason: {
|
|
4161
|
+
type: "string",
|
|
4162
|
+
description: "影响分析的详细说明"
|
|
4163
|
+
},
|
|
4164
|
+
suggestion: {
|
|
4165
|
+
type: "string",
|
|
4166
|
+
description: "建议的处理方式"
|
|
4167
|
+
}
|
|
4168
|
+
},
|
|
4169
|
+
required: [
|
|
4170
|
+
"file",
|
|
4171
|
+
"deletedCode",
|
|
4172
|
+
"riskLevel",
|
|
4173
|
+
"affectedFiles",
|
|
4174
|
+
"reason"
|
|
4175
|
+
],
|
|
4176
|
+
additionalProperties: false
|
|
4177
|
+
}
|
|
4178
|
+
},
|
|
4179
|
+
summary: {
|
|
4180
|
+
type: "string",
|
|
4181
|
+
description: "删除代码影响的整体总结"
|
|
4182
|
+
}
|
|
4183
|
+
},
|
|
4184
|
+
required: [
|
|
4185
|
+
"impacts",
|
|
4186
|
+
"summary"
|
|
4187
|
+
],
|
|
4188
|
+
additionalProperties: false
|
|
4189
|
+
};
|
|
4190
|
+
/**
|
|
4191
|
+
* 问题验证结果 JSON Schema
|
|
4192
|
+
*/ const VERIFY_SCHEMA = {
|
|
4193
|
+
type: "object",
|
|
4194
|
+
properties: {
|
|
4195
|
+
fixed: {
|
|
4196
|
+
type: "boolean",
|
|
4197
|
+
description: "问题是否已被修复"
|
|
4198
|
+
},
|
|
4199
|
+
valid: {
|
|
4200
|
+
type: "boolean",
|
|
4201
|
+
description: "问题是否有效,有效的条件就是你需要看看代码是否符合规范"
|
|
4202
|
+
},
|
|
4203
|
+
reason: {
|
|
4204
|
+
type: "string",
|
|
4205
|
+
description: "判断依据,说明为什么认为问题已修复或仍存在"
|
|
4206
|
+
}
|
|
4207
|
+
},
|
|
4208
|
+
required: [
|
|
4209
|
+
"fixed",
|
|
4210
|
+
"valid",
|
|
4211
|
+
"reason"
|
|
4212
|
+
],
|
|
4213
|
+
additionalProperties: false
|
|
4214
|
+
};
|
|
4215
|
+
|
|
4216
|
+
;// CONCATENATED MODULE: ./src/prompt/types.ts
|
|
4217
|
+
/**
|
|
4218
|
+
* 提示词回调函数类型定义
|
|
4219
|
+
*/ /**
|
|
4220
|
+
* 输入验证错误类
|
|
4221
|
+
*/ class PromptValidationError extends Error {
|
|
4222
|
+
constructor(message){
|
|
4223
|
+
super(message);
|
|
4224
|
+
this.name = "PromptValidationError";
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
/**
|
|
4228
|
+
* 输入验证工具函数
|
|
4229
|
+
*/ function validateRequired(value, fieldName) {
|
|
4230
|
+
if (value === undefined || value === null) {
|
|
4231
|
+
throw new PromptValidationError(`${fieldName} is required but was ${value}`);
|
|
4232
|
+
}
|
|
4233
|
+
return value;
|
|
4234
|
+
}
|
|
4235
|
+
function types_validateNonEmptyString(value, fieldName) {
|
|
4236
|
+
if (value === undefined || value === null || value.trim() === "") {
|
|
4237
|
+
throw new PromptValidationError(`${fieldName} is required and cannot be empty`);
|
|
4238
|
+
}
|
|
4239
|
+
return value;
|
|
4240
|
+
}
|
|
4241
|
+
function validateArray(value, fieldName) {
|
|
4242
|
+
if (!Array.isArray(value)) {
|
|
4243
|
+
throw new PromptValidationError(`${fieldName} must be an array but was ${typeof value}`);
|
|
4244
|
+
}
|
|
4245
|
+
return value;
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4248
|
+
;// CONCATENATED MODULE: ./src/prompt/code-review.ts
|
|
4249
|
+
|
|
4250
|
+
/**
|
|
4251
|
+
* 代码审查公共系统提示词基础
|
|
4252
|
+
*/ const CODE_REVIEW_BASE_SYSTEM_PROMPT = `你是一个专业的代码审查专家,负责根据团队的编码规范对代码进行严格审查。
|
|
4253
|
+
|
|
4254
|
+
## 审查要求
|
|
4255
|
+
|
|
4256
|
+
1. **严格遵循规范**:只按照上述审查规范进行审查,不要添加规范之外的要求
|
|
4257
|
+
2. **精准定位问题**:每个问题必须指明具体的行号,行号从文件内容中的 "行号|" 格式获取
|
|
4258
|
+
3. **避免重复报告**:如果提示词中包含"上一次审查结果",请不要重复报告已存在的问题
|
|
4259
|
+
4. **提供可行建议**:对于每个问题,提供具体的修改建议代码
|
|
4260
|
+
|
|
4261
|
+
## 注意事项
|
|
4262
|
+
|
|
4263
|
+
- 变更文件内容已在上下文中提供,无需调用读取工具
|
|
4264
|
+
- 你可以读取项目中的其他文件以了解上下文
|
|
4265
|
+
- 不要调用编辑工具修改文件,你的职责是审查而非修改
|
|
4266
|
+
- 文件内容格式为 "CommitHash 行号| 代码",输出的 line 字段应对应原始行号
|
|
4267
|
+
|
|
4268
|
+
## 输出要求
|
|
4269
|
+
|
|
4270
|
+
- 发现问题时:在 issues 数组中列出所有问题,每个问题包含 file、line、ruleId、specFile、reason、suggestion、severity
|
|
4271
|
+
- 无论是否发现问题:都必须在 summary 中提供该文件的审查总结,简要说明审查结果`;
|
|
4272
|
+
const buildCodeReviewSystemPrompt = (ctx)=>{
|
|
4273
|
+
validateNonEmptyString(ctx.specsSection, "specsSection");
|
|
4274
|
+
return {
|
|
4275
|
+
systemPrompt: `${CODE_REVIEW_BASE_SYSTEM_PROMPT}
|
|
4276
|
+
|
|
4277
|
+
## 审查规范
|
|
4278
|
+
|
|
4279
|
+
${ctx.specsSection}`,
|
|
4280
|
+
userPrompt: ""
|
|
4281
|
+
};
|
|
4282
|
+
};
|
|
4283
|
+
const buildFileReviewPrompt = (ctx)=>{
|
|
4284
|
+
// 验证必需的输入参数
|
|
4285
|
+
types_validateNonEmptyString(ctx.filename, "filename");
|
|
4286
|
+
types_validateNonEmptyString(ctx.status, "status");
|
|
4287
|
+
types_validateNonEmptyString(ctx.linesWithNumbers, "linesWithNumbers");
|
|
4288
|
+
validateRequired(ctx.specsSection, "specsSection");
|
|
4289
|
+
return {
|
|
4290
|
+
systemPrompt: `${CODE_REVIEW_BASE_SYSTEM_PROMPT}
|
|
4291
|
+
|
|
4292
|
+
## 审查规范
|
|
4293
|
+
|
|
4294
|
+
${ctx.specsSection}`,
|
|
4295
|
+
userPrompt: `## ${ctx.filename} (${ctx.status})
|
|
4296
|
+
|
|
4297
|
+
### 文件内容
|
|
4298
|
+
|
|
4299
|
+
\`\`\`
|
|
4300
|
+
${ctx.linesWithNumbers}
|
|
4301
|
+
\`\`\`
|
|
4302
|
+
|
|
4303
|
+
### 该文件的相关 Commits
|
|
4304
|
+
|
|
4305
|
+
${ctx.commitsSection}
|
|
4306
|
+
|
|
4307
|
+
### 该文件所在的目录树
|
|
4308
|
+
|
|
4309
|
+
${ctx.fileDirectoryInfo}
|
|
4310
|
+
|
|
4311
|
+
### 上一次审查结果
|
|
4312
|
+
|
|
4313
|
+
${ctx.previousReviewSection}`
|
|
4314
|
+
};
|
|
4315
|
+
};
|
|
4316
|
+
|
|
4317
|
+
;// CONCATENATED MODULE: ./src/prompt/pr-description.ts
|
|
4318
|
+
|
|
4319
|
+
/**
|
|
4320
|
+
* 内存使用限制常量
|
|
4321
|
+
*/ const MEMORY_LIMITS = {
|
|
4322
|
+
MAX_TOTAL_LENGTH: 8000,
|
|
4323
|
+
MAX_FILES: 30,
|
|
4324
|
+
MAX_SNIPPET_LENGTH: 50,
|
|
4325
|
+
MAX_COMMITS: 10,
|
|
4326
|
+
MAX_FILES_FOR_TITLE: 20
|
|
4327
|
+
};
|
|
4328
|
+
const buildPrDescriptionPrompt = (ctx)=>{
|
|
4329
|
+
// 验证必需的输入参数
|
|
4330
|
+
validateArray(ctx.commits, "commits");
|
|
4331
|
+
validateArray(ctx.changedFiles, "changedFiles");
|
|
4332
|
+
const commitMessages = ctx.commits.map((c)=>`- ${c.sha?.slice(0, 7)}: ${c.commit?.message?.split("\n")[0]}`).join("\n");
|
|
4333
|
+
const fileChanges = ctx.changedFiles.slice(0, MEMORY_LIMITS.MAX_FILES).map((f)=>`- ${f.filename} (${f.status})`).join("\n");
|
|
4334
|
+
// 构建代码变更内容(只包含变更行,优化内存使用)
|
|
4335
|
+
let codeChangesSection = "";
|
|
4336
|
+
if (ctx.fileContents && ctx.fileContents.size > 0) {
|
|
4337
|
+
const codeSnippets = [];
|
|
4338
|
+
let totalLength = 0;
|
|
4339
|
+
// 使用 Map.entries() 进行更高效的迭代
|
|
4340
|
+
for (const [filename, lines] of ctx.fileContents){
|
|
4341
|
+
if (totalLength >= MEMORY_LIMITS.MAX_TOTAL_LENGTH) break;
|
|
4342
|
+
// 只提取有变更的行(commitHash 不是 "-------")
|
|
4343
|
+
const changedLines = lines.map(([hash, code], idx)=>hash !== "-------" ? `${idx + 1}: ${code}` : null).filter(Boolean);
|
|
4344
|
+
if (changedLines.length > 0) {
|
|
4345
|
+
// 限制每个文件的代码行数,避免单个文件占用过多内存
|
|
4346
|
+
const limitedLines = changedLines.slice(0, MEMORY_LIMITS.MAX_SNIPPET_LENGTH);
|
|
4347
|
+
const snippet = `### ${filename}\n\`\`\`\n${limitedLines.join("\n")}\n\`\`\``;
|
|
4348
|
+
// 检查添加此片段是否会超过内存限制
|
|
4349
|
+
if (totalLength + snippet.length <= MEMORY_LIMITS.MAX_TOTAL_LENGTH) {
|
|
4350
|
+
codeSnippets.push(snippet);
|
|
4351
|
+
totalLength += snippet.length;
|
|
4352
|
+
} else {
|
|
4353
|
+
// 如果添加当前片段会超过限制,尝试截断它
|
|
4354
|
+
const remainingLength = MEMORY_LIMITS.MAX_TOTAL_LENGTH - totalLength;
|
|
4355
|
+
if (remainingLength > 100) {
|
|
4356
|
+
// 至少保留 100 字符的片段
|
|
4357
|
+
// snippet 格式为 "### filename\n```\ncode\n```"
|
|
4358
|
+
// 截断时去掉结尾的 ``` 再追加,避免双重代码块
|
|
4359
|
+
const closingTag = "\n```";
|
|
4360
|
+
const contentEnd = snippet.lastIndexOf(closingTag);
|
|
4361
|
+
const truncateAt = Math.max(0, contentEnd > 0 ? Math.min(remainingLength - 20, contentEnd) : remainingLength - 20);
|
|
4362
|
+
const truncatedSnippet = snippet.substring(0, truncateAt) + "\n..." + closingTag;
|
|
4363
|
+
codeSnippets.push(truncatedSnippet);
|
|
4364
|
+
break;
|
|
4365
|
+
}
|
|
4366
|
+
break;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
if (codeSnippets.length > 0) {
|
|
4371
|
+
codeChangesSection = `\n\n## 代码变更内容\n${codeSnippets.join("\n\n")}`;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
return {
|
|
4375
|
+
systemPrompt: "",
|
|
4376
|
+
userPrompt: `请根据以下 PR 的 commit 记录、文件变更和代码内容,用简洁的中文总结这个 PR 实现了什么功能。
|
|
4377
|
+
要求:
|
|
4378
|
+
1. 第一行输出 PR 标题,格式必须是: Feat xxx 或 Fix xxx 或 Refactor xxx(根据变更类型选择,整体不超过 50 个字符)
|
|
4379
|
+
2. 空一行后输出详细描述
|
|
4380
|
+
3. 描述应该简明扼要,突出核心功能点
|
|
4381
|
+
4. 使用 Markdown 格式
|
|
4382
|
+
5. 不要逐条列出 commit,而是归纳总结
|
|
4383
|
+
6. 重点分析代码变更的实际功能
|
|
4384
|
+
|
|
4385
|
+
## Commit 记录 (${ctx.commits.length} 个)
|
|
4386
|
+
${commitMessages || "无"}
|
|
4387
|
+
|
|
4388
|
+
## 文件变更 (${ctx.changedFiles.length} 个文件)
|
|
4389
|
+
${fileChanges || "无"}${ctx.changedFiles.length > MEMORY_LIMITS.MAX_FILES ? `\n... 等 ${ctx.changedFiles.length - MEMORY_LIMITS.MAX_FILES} 个文件` : ""}${codeChangesSection}`
|
|
4390
|
+
};
|
|
4391
|
+
};
|
|
4392
|
+
const buildPrTitlePrompt = (ctx)=>{
|
|
4393
|
+
// 验证必需的输入参数
|
|
4394
|
+
validateArray(ctx.commits, "commits");
|
|
4395
|
+
validateArray(ctx.changedFiles, "changedFiles");
|
|
4396
|
+
const commitMessages = ctx.commits.slice(0, MEMORY_LIMITS.MAX_COMMITS).map((c)=>c.commit?.message?.split("\n")[0]).filter(Boolean).join("\n");
|
|
4397
|
+
const fileChanges = ctx.changedFiles.slice(0, MEMORY_LIMITS.MAX_FILES_FOR_TITLE).map((f)=>`${f.filename} (${f.status})`).join("\n");
|
|
4398
|
+
return {
|
|
4399
|
+
systemPrompt: "",
|
|
4400
|
+
userPrompt: `请根据以下 commit 记录和文件变更,生成一个简短的 PR 标题。
|
|
4401
|
+
要求:
|
|
4402
|
+
1. 格式必须是: Feat: xxx 或 Fix: xxx 或 Refactor: xxx
|
|
4403
|
+
2. 根据变更内容选择合适的前缀(新功能用 Feat,修复用 Fix,重构用 Refactor)
|
|
4404
|
+
3. xxx 部分用简短的中文描述(整体不超过 50 个字符)
|
|
4405
|
+
4. 只输出标题,不要加任何解释
|
|
4406
|
+
|
|
4407
|
+
Commit 记录:
|
|
4408
|
+
${commitMessages || "无"}
|
|
4409
|
+
|
|
4410
|
+
文件变更:
|
|
4411
|
+
${fileChanges || "无"}`
|
|
4412
|
+
};
|
|
4413
|
+
};
|
|
4414
|
+
|
|
4415
|
+
;// CONCATENATED MODULE: ./src/prompt/deletion-impact.ts
|
|
4416
|
+
|
|
4417
|
+
const DELETION_IMPACT_SYSTEM = `你是一个代码审查专家,专门分析删除代码可能带来的影响。
|
|
4418
|
+
|
|
4419
|
+
## 任务
|
|
4420
|
+
分析以下被删除的代码块,判断删除这些代码是否会影响到其他功能。
|
|
4421
|
+
|
|
4422
|
+
## 分析要点
|
|
4423
|
+
1. **功能依赖**: 被删除的代码是否被其他模块调用或依赖
|
|
4424
|
+
2. **接口变更**: 删除是否会导致 API 或接口不兼容
|
|
4425
|
+
3. **副作用**: 删除是否会影响系统的其他行为
|
|
4426
|
+
4. **数据流**: 删除是否会中断数据处理流程
|
|
4427
|
+
|
|
4428
|
+
## 风险等级判断标准
|
|
4429
|
+
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
4430
|
+
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
4431
|
+
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
4432
|
+
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
4433
|
+
|
|
4434
|
+
## 输出要求
|
|
4435
|
+
- 对每个有风险的删除块给出详细分析
|
|
4436
|
+
- 如果删除是安全的,也要说明原因
|
|
4437
|
+
- 提供具体的建议`;
|
|
4438
|
+
function buildDeletedCodeSection(ctx) {
|
|
4439
|
+
return ctx.deletedBlocks.map((block, index)=>{
|
|
4440
|
+
const refs = ctx.references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
|
4441
|
+
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}\n\n\`\`\`\n${block.content}\n\`\`\`\n\n可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}\n`;
|
|
4442
|
+
}).join("\n");
|
|
4443
|
+
}
|
|
4444
|
+
const buildDeletionImpactPrompt = (ctx)=>{
|
|
4445
|
+
validateArray(ctx.deletedBlocks, "deletedBlocks");
|
|
4446
|
+
validateRequired(ctx.references, "references");
|
|
4447
|
+
return {
|
|
4448
|
+
systemPrompt: DELETION_IMPACT_SYSTEM,
|
|
4449
|
+
userPrompt: `## 被删除的代码块\n\n${buildDeletedCodeSection(ctx)}\n请分析这些删除操作可能带来的影响。`
|
|
4450
|
+
};
|
|
4451
|
+
};
|
|
4452
|
+
/** @deprecated 使用 buildDeletionImpactPrompt */ const buildDeletionImpactSystemPrompt = (ctx)=>buildDeletionImpactPrompt(ctx);
|
|
4453
|
+
/** @deprecated 使用 buildDeletionImpactPrompt */ const buildDeletionImpactUserPrompt = (ctx)=>buildDeletionImpactPrompt(ctx);
|
|
4454
|
+
const DELETION_IMPACT_AGENT_SYSTEM = `你是一个资深代码架构师,擅长分析代码变更的影响范围和潜在风险。
|
|
4455
|
+
|
|
4456
|
+
## 任务
|
|
4457
|
+
深入分析以下被删除的代码块,评估删除操作对代码库的影响。
|
|
4458
|
+
|
|
4459
|
+
## 你的能力
|
|
4460
|
+
你可以使用以下工具来深入分析代码:
|
|
4461
|
+
- **Read**: 读取文件内容,查看被删除代码的完整上下文
|
|
4462
|
+
- **Grep**: 搜索代码库,查找对被删除代码的引用
|
|
4463
|
+
- **Glob**: 查找匹配模式的文件
|
|
4464
|
+
|
|
4465
|
+
## 分析流程
|
|
4466
|
+
1. 首先阅读被删除代码的上下文,理解其功能
|
|
4467
|
+
2. 使用 Grep 搜索代码库中对这些代码的引用
|
|
4468
|
+
3. 分析引用处的代码,判断删除后的影响
|
|
4469
|
+
4. 给出风险评估和建议
|
|
4470
|
+
|
|
4471
|
+
## 风险等级判断标准
|
|
4472
|
+
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
4473
|
+
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
4474
|
+
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
4475
|
+
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
4476
|
+
|
|
4477
|
+
## 输出要求
|
|
4478
|
+
- 对每个有风险的删除块给出详细分析
|
|
4479
|
+
- 如果删除是安全的,也要说明原因
|
|
4480
|
+
- 提供具体的建议`;
|
|
4481
|
+
const buildDeletionImpactAgentPrompt = (ctx)=>{
|
|
4482
|
+
validateArray(ctx.deletedBlocks, "deletedBlocks");
|
|
4483
|
+
validateRequired(ctx.references, "references");
|
|
4484
|
+
return {
|
|
4485
|
+
systemPrompt: DELETION_IMPACT_AGENT_SYSTEM,
|
|
4486
|
+
userPrompt: `## 被删除的代码块\n\n${buildDeletedCodeSection(ctx)}\n## 补充说明\n\n请使用你的工具能力深入分析这些删除操作可能带来的影响。\n- 如果需要查看更多上下文,请读取相关文件\n- 如果需要确认引用关系,请搜索代码库\n- 分析完成后,给出结构化的影响评估`
|
|
4487
|
+
};
|
|
4488
|
+
};
|
|
4489
|
+
/** @deprecated 使用 buildDeletionImpactAgentPrompt */ const buildDeletionImpactAgentSystemPrompt = (ctx)=>buildDeletionImpactAgentPrompt(ctx);
|
|
4490
|
+
/** @deprecated 使用 buildDeletionImpactAgentPrompt */ const buildDeletionImpactAgentUserPrompt = (ctx)=>buildDeletionImpactAgentPrompt(ctx);
|
|
4491
|
+
|
|
4492
|
+
;// CONCATENATED MODULE: ./src/prompt/issue-verify.ts
|
|
4493
|
+
|
|
4494
|
+
/**
|
|
4495
|
+
* 构建问题验证提示词
|
|
4496
|
+
*/ const buildIssueVerifyPrompt = (ctx)=>{
|
|
4497
|
+
// 验证必需的输入参数
|
|
4498
|
+
validateRequired(ctx.issue, "issue");
|
|
4499
|
+
validateArray(ctx.fileContent, "fileContent");
|
|
4500
|
+
const padWidth = String(ctx.fileContent.length).length;
|
|
4501
|
+
const linesWithNumbers = ctx.fileContent.map(([, line], index)=>`${String(index + 1).padStart(padWidth)}| ${line}`).join("\n");
|
|
4502
|
+
const systemPrompt = `你是一个代码审查专家。你的任务是判断之前发现的一个代码问题:
|
|
4503
|
+
1. 是否有效(是否真的违反了规则)
|
|
4504
|
+
2. 是否已经被修复
|
|
4505
|
+
|
|
4506
|
+
请仔细分析当前的代码内容。
|
|
4507
|
+
|
|
4508
|
+
## 输出要求
|
|
4509
|
+
- valid: 布尔值,true 表示问题有效(代码确实违反了规则),false 表示问题无效(误报)
|
|
4510
|
+
- fixed: 布尔值,true 表示问题已经被修复,false 表示问题仍然存在
|
|
4511
|
+
- reason: 判断依据
|
|
4512
|
+
|
|
4513
|
+
## 判断标准
|
|
4514
|
+
|
|
4515
|
+
### valid 判断
|
|
4516
|
+
- 根据规则 ID 和问题描述,判断代码是否真的违反了该规则
|
|
4517
|
+
- 如果问题描述与实际代码不符,valid 为 false
|
|
4518
|
+
- 如果规则不适用于该代码场景,valid 为 false
|
|
4519
|
+
|
|
4520
|
+
### fixed 判断
|
|
4521
|
+
- 只有当问题所在的代码已被修改,且修改后的代码不再违反规则时,fixed 才为 true
|
|
4522
|
+
- 如果问题所在的代码仍然存在且仍违反规则,fixed 必须为 false
|
|
4523
|
+
- 如果代码行号发生变化但问题本质仍存在,fixed 必须为 false
|
|
4524
|
+
|
|
4525
|
+
## 重要提醒
|
|
4526
|
+
- valid=false 时,fixed 的值无意义(无效问题无需修复)
|
|
4527
|
+
- 请确保 valid 和 fixed 的值与 reason 的描述一致!`;
|
|
4528
|
+
// 构建规则定义部分
|
|
4529
|
+
let ruleSection = "";
|
|
4530
|
+
if (ctx.specsSection) {
|
|
4531
|
+
ruleSection = ctx.specsSection;
|
|
4532
|
+
} else if (ctx.ruleInfo) {
|
|
4533
|
+
const { spec, rule } = ctx.ruleInfo;
|
|
4534
|
+
ruleSection = `### ${spec.filename} (${spec.type})\n\n${spec.content.slice(0, 200)}...\n\n#### 规则\n- ${rule.id}: ${rule.title}\n ${rule.description}`;
|
|
4535
|
+
}
|
|
4536
|
+
const userPrompt = `## 规则定义
|
|
4537
|
+
|
|
4538
|
+
${ruleSection}
|
|
4539
|
+
|
|
4540
|
+
## 之前发现的问题
|
|
4541
|
+
|
|
4542
|
+
- **文件**: ${ctx.issue.file}
|
|
4543
|
+
- **行号**: ${ctx.issue.line}
|
|
4544
|
+
- **规则**: ${ctx.issue.ruleId} (来自 ${ctx.issue.specFile})
|
|
4545
|
+
- **问题描述**: ${ctx.issue.reason}
|
|
4546
|
+
${ctx.issue.suggestion ? `- **原建议**: ${ctx.issue.suggestion}` : ""}
|
|
4547
|
+
|
|
4548
|
+
## 当前文件内容
|
|
4549
|
+
|
|
4550
|
+
\`\`\`
|
|
4551
|
+
${linesWithNumbers}
|
|
4552
|
+
\`\`\`
|
|
4553
|
+
|
|
4554
|
+
请判断这个问题是否有效,以及是否已经被修复。`;
|
|
4555
|
+
return {
|
|
4556
|
+
systemPrompt,
|
|
4557
|
+
userPrompt
|
|
4558
|
+
};
|
|
4559
|
+
};
|
|
4560
|
+
|
|
4561
|
+
;// CONCATENATED MODULE: ./src/prompt/index.ts
|
|
4562
|
+
// 统一导出所有提示词
|
|
4563
|
+
// 类型定义
|
|
4564
|
+
// JSON Schemas
|
|
4565
|
+
|
|
4566
|
+
// 代码审查提示词
|
|
4567
|
+
|
|
4568
|
+
// PR 描述生成提示词
|
|
4569
|
+
|
|
4570
|
+
// 删除影响分析提示词
|
|
4571
|
+
|
|
4572
|
+
// 问题验证提示词
|
|
4573
|
+
|
|
4574
|
+
|
|
4575
|
+
;// CONCATENATED MODULE: ./src/system-rules/max-lines-per-file.ts
|
|
4576
|
+
|
|
4577
|
+
const RULE_ID = "system:max-lines-per-file";
|
|
4578
|
+
const SPEC_FILE = "__system__";
|
|
4579
|
+
function checkMaxLinesPerFile(changedFiles, fileContents, rule, round, verbose) {
|
|
4580
|
+
const [maxLine, severity] = rule;
|
|
4581
|
+
const staticIssues = [];
|
|
4582
|
+
const skippedFiles = new Set();
|
|
4583
|
+
if (maxLine <= 0) {
|
|
4584
|
+
return {
|
|
4585
|
+
staticIssues,
|
|
4586
|
+
skippedFiles
|
|
4587
|
+
};
|
|
4588
|
+
}
|
|
4589
|
+
for (const file of changedFiles){
|
|
4590
|
+
if (file.status === "deleted" || !file.filename) continue;
|
|
4591
|
+
const filename = file.filename;
|
|
4592
|
+
const contentLines = fileContents.get(filename);
|
|
4593
|
+
if (!contentLines || contentLines.length <= maxLine) continue;
|
|
4594
|
+
if (shouldLog(verbose, 1)) {
|
|
4595
|
+
console.log(`⚠️ [system-rules/maxLinesPerFile] ${filename}: ${contentLines.length} 行超过限制 ${maxLine} 行,跳过 LLM 审查`);
|
|
4596
|
+
}
|
|
4597
|
+
skippedFiles.add(filename);
|
|
4598
|
+
staticIssues.push({
|
|
4599
|
+
file: filename,
|
|
4600
|
+
line: "1",
|
|
4601
|
+
code: "",
|
|
4602
|
+
ruleId: RULE_ID,
|
|
4603
|
+
specFile: SPEC_FILE,
|
|
4604
|
+
reason: `文件共 ${contentLines.length} 行,超过静态规则限制 ${maxLine} 行,已跳过 LLM 审查。请考虑拆分文件或调大 staticRules.maxLinesPerFile 配置。`,
|
|
4605
|
+
severity,
|
|
4606
|
+
round,
|
|
4607
|
+
date: new Date().toISOString()
|
|
4608
|
+
});
|
|
4609
|
+
}
|
|
4610
|
+
return {
|
|
4611
|
+
staticIssues,
|
|
4612
|
+
skippedFiles
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
;// CONCATENATED MODULE: ./src/system-rules/index.ts
|
|
4617
|
+
|
|
4618
|
+
|
|
4619
|
+
/**
|
|
4620
|
+
* 对变更文件执行所有已启用的静态规则检查。
|
|
4621
|
+
* 返回系统问题列表和需要跳过 LLM 审查的文件集合。
|
|
4622
|
+
*/ function applyStaticRules(changedFiles, fileContents, staticRules, round, verbose) {
|
|
4623
|
+
const staticIssues = [];
|
|
4624
|
+
const skippedFiles = new Set();
|
|
4625
|
+
if (!staticRules) {
|
|
4626
|
+
return {
|
|
4627
|
+
staticIssues,
|
|
4628
|
+
skippedFiles
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
if (staticRules.maxLinesPerFile) {
|
|
4632
|
+
const result = checkMaxLinesPerFile(changedFiles, fileContents, staticRules.maxLinesPerFile, round, verbose);
|
|
4633
|
+
staticIssues.push(...result.staticIssues);
|
|
4634
|
+
result.skippedFiles.forEach((f)=>skippedFiles.add(f));
|
|
4635
|
+
}
|
|
4636
|
+
return {
|
|
4637
|
+
staticIssues,
|
|
4638
|
+
skippedFiles
|
|
4639
|
+
};
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
;// CONCATENATED MODULE: ./src/review-llm.ts
|
|
4643
|
+
|
|
4644
|
+
|
|
4645
|
+
|
|
4646
|
+
|
|
4647
|
+
|
|
4648
|
+
|
|
4649
|
+
|
|
4650
|
+
|
|
3886
4651
|
class ReviewLlmProcessor {
|
|
3887
4652
|
llmProxyService;
|
|
3888
4653
|
reviewSpecService;
|
|
@@ -3924,8 +4689,11 @@ class ReviewLlmProcessor {
|
|
|
3924
4689
|
return false;
|
|
3925
4690
|
}
|
|
3926
4691
|
// 如果有 includes 配置,检查文件名是否匹配 includes 模式
|
|
4692
|
+
// 需先提取纯 glob(去掉 added|/modified| 前缀,过滤 code-* 空串),避免 micromatch 报错
|
|
3927
4693
|
if (spec.includes.length > 0) {
|
|
3928
|
-
|
|
4694
|
+
const globs = extractGlobsFromIncludes(spec.includes);
|
|
4695
|
+
if (globs.length === 0) return true;
|
|
4696
|
+
return micromatch_0.isMatch(filename, globs, {
|
|
3929
4697
|
matchBase: true
|
|
3930
4698
|
});
|
|
3931
4699
|
}
|
|
@@ -3933,57 +4701,52 @@ class ReviewLlmProcessor {
|
|
|
3933
4701
|
return true;
|
|
3934
4702
|
});
|
|
3935
4703
|
}
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
return `你是一个专业的代码审查专家,负责根据团队的编码规范对代码进行严格审查。
|
|
3940
|
-
|
|
3941
|
-
## 审查规范
|
|
3942
|
-
|
|
3943
|
-
${specsSection}
|
|
3944
|
-
|
|
3945
|
-
## 审查要求
|
|
3946
|
-
|
|
3947
|
-
1. **严格遵循规范**:只按照上述审查规范进行审查,不要添加规范之外的要求
|
|
3948
|
-
2. **精准定位问题**:每个问题必须指明具体的行号,行号从文件内容中的 "行号|" 格式获取
|
|
3949
|
-
3. **避免重复报告**:如果提示词中包含"上一次审查结果",请不要重复报告已存在的问题
|
|
3950
|
-
4. **提供可行建议**:对于每个问题,提供具体的修改建议代码
|
|
3951
|
-
|
|
3952
|
-
## 注意事项
|
|
3953
|
-
|
|
3954
|
-
- 变更文件内容已在上下文中提供,无需调用读取工具
|
|
3955
|
-
- 你可以读取项目中的其他文件以了解上下文
|
|
3956
|
-
- 不要调用编辑工具修改文件,你的职责是审查而非修改
|
|
3957
|
-
- 文件内容格式为 "CommitHash 行号| 代码",输出的 line 字段应对应原始行号
|
|
3958
|
-
|
|
3959
|
-
## 输出要求
|
|
3960
|
-
|
|
3961
|
-
- 发现问题时:在 issues 数组中列出所有问题,每个问题包含 file、line、ruleId、specFile、reason、suggestion、severity
|
|
3962
|
-
- 无论是否发现问题:都必须在 summary 中提供该文件的审查总结,简要说明审查结果`;
|
|
3963
|
-
}
|
|
3964
|
-
async buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResult) {
|
|
4704
|
+
async buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResult, whenModifiedCode, verbose, systemRules) {
|
|
4705
|
+
const round = (existingResult?.round ?? 0) + 1;
|
|
4706
|
+
const { staticIssues, skippedFiles } = applyStaticRules(changedFiles, fileContents, systemRules, round, verbose);
|
|
3965
4707
|
const fileDataList = changedFiles.filter((f)=>f.status !== "deleted" && f.filename).map((file)=>{
|
|
3966
4708
|
const filename = file.filename;
|
|
4709
|
+
if (skippedFiles.has(filename)) return null;
|
|
3967
4710
|
const contentLines = fileContents.get(filename);
|
|
3968
4711
|
if (!contentLines) {
|
|
3969
4712
|
return {
|
|
3970
4713
|
filename,
|
|
3971
4714
|
file,
|
|
3972
|
-
|
|
4715
|
+
contentLines: null,
|
|
3973
4716
|
commitsSection: "- 无相关 commits"
|
|
3974
4717
|
};
|
|
3975
4718
|
}
|
|
3976
|
-
const linesWithNumbers = buildLinesWithNumbers(contentLines);
|
|
3977
4719
|
const commitsSection = buildCommitsSection(contentLines, commits);
|
|
3978
4720
|
return {
|
|
3979
4721
|
filename,
|
|
3980
4722
|
file,
|
|
3981
|
-
|
|
4723
|
+
contentLines,
|
|
3982
4724
|
commitsSection
|
|
3983
4725
|
};
|
|
3984
4726
|
});
|
|
3985
|
-
const filePrompts = await Promise.all(fileDataList.map(async ({ filename, file,
|
|
4727
|
+
const filePrompts = await Promise.all(fileDataList.filter((item)=>item !== null).map(async ({ filename, file, contentLines, commitsSection })=>{
|
|
3986
4728
|
const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
|
|
4729
|
+
// 根据文件过滤 specs,只注入与当前文件匹配的规则
|
|
4730
|
+
const fileSpecs = this.filterSpecsForFile(specs, filename);
|
|
4731
|
+
// 从全局 whenModifiedCode 配置中解析代码结构过滤类型
|
|
4732
|
+
const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
|
|
4733
|
+
// 构建带行号的内容:有 code-* 过滤时只输出匹配的代码块范围
|
|
4734
|
+
let linesWithNumbers;
|
|
4735
|
+
if (!contentLines) {
|
|
4736
|
+
linesWithNumbers = "(无法获取内容)";
|
|
4737
|
+
} else if (codeBlockTypes.length > 0) {
|
|
4738
|
+
const visibleRanges = extractCodeBlocks(contentLines, codeBlockTypes);
|
|
4739
|
+
// 如果配置了 whenModifiedCode 但没有匹配的代码块,跳过这个文件
|
|
4740
|
+
if (visibleRanges.length === 0) {
|
|
4741
|
+
if (shouldLog(verbose, 2)) {
|
|
4742
|
+
console.log(`[buildReviewPrompt] ${filename}: 没有匹配的 ${codeBlockTypes.join(", ")} 代码块,跳过审查`);
|
|
4743
|
+
}
|
|
4744
|
+
return null;
|
|
4745
|
+
}
|
|
4746
|
+
linesWithNumbers = buildLinesWithNumbers(contentLines, visibleRanges);
|
|
4747
|
+
} else {
|
|
4748
|
+
linesWithNumbers = buildLinesWithNumbers(contentLines);
|
|
4749
|
+
}
|
|
3987
4750
|
// 获取该文件上一次的审查结果
|
|
3988
4751
|
const existingFileSummary = existingResult?.summary?.find((s)=>s.file === filename);
|
|
3989
4752
|
const existingFileIssues = existingResult?.issues?.filter((i)=>i.file === filename) ?? [];
|
|
@@ -4005,37 +4768,27 @@ ${specsSection}
|
|
|
4005
4768
|
}
|
|
4006
4769
|
previousReviewSection = parts.join("\n");
|
|
4007
4770
|
}
|
|
4008
|
-
const userPrompt = `## ${filename} (${file.status})
|
|
4009
|
-
|
|
4010
|
-
### 文件内容
|
|
4011
|
-
|
|
4012
|
-
\`\`\`
|
|
4013
|
-
${linesWithNumbers}
|
|
4014
|
-
\`\`\`
|
|
4015
|
-
|
|
4016
|
-
### 该文件的相关 Commits
|
|
4017
|
-
|
|
4018
|
-
${commitsSection}
|
|
4019
|
-
|
|
4020
|
-
### 该文件所在的目录树
|
|
4021
|
-
|
|
4022
|
-
${fileDirectoryInfo}
|
|
4023
|
-
|
|
4024
|
-
### 上一次审查结果
|
|
4025
|
-
|
|
4026
|
-
${previousReviewSection}`;
|
|
4027
|
-
// 根据文件过滤 specs,只注入与当前文件匹配的规则
|
|
4028
|
-
const fileSpecs = this.filterSpecsForFile(specs, filename);
|
|
4029
4771
|
const specsSection = this.reviewSpecService.buildSpecsSection(fileSpecs);
|
|
4030
|
-
const systemPrompt =
|
|
4772
|
+
const { systemPrompt, userPrompt } = buildFileReviewPrompt({
|
|
4773
|
+
filename,
|
|
4774
|
+
status: file.status,
|
|
4775
|
+
linesWithNumbers,
|
|
4776
|
+
commitsSection,
|
|
4777
|
+
fileDirectoryInfo,
|
|
4778
|
+
previousReviewSection,
|
|
4779
|
+
specsSection
|
|
4780
|
+
});
|
|
4031
4781
|
return {
|
|
4032
4782
|
filename,
|
|
4033
4783
|
systemPrompt,
|
|
4034
4784
|
userPrompt
|
|
4035
4785
|
};
|
|
4036
4786
|
}));
|
|
4787
|
+
// 过滤掉 null 值(跳过的文件)
|
|
4788
|
+
const validFilePrompts = filePrompts.filter((fp)=>fp !== null);
|
|
4037
4789
|
return {
|
|
4038
|
-
filePrompts
|
|
4790
|
+
filePrompts: validFilePrompts,
|
|
4791
|
+
staticIssues: staticIssues.length > 0 ? staticIssues : undefined
|
|
4039
4792
|
};
|
|
4040
4793
|
}
|
|
4041
4794
|
async runLLMReview(llmMode, reviewPrompt, options = {}) {
|
|
@@ -4230,50 +4983,16 @@ ${previousReviewSection}`;
|
|
|
4230
4983
|
* 使用 AI 根据 commits、变更文件和代码内容总结 PR 实现的功能
|
|
4231
4984
|
* @returns 包含 title 和 description 的对象
|
|
4232
4985
|
*/ async generatePrDescription(commits, changedFiles, llmMode, fileContents, verbose) {
|
|
4233
|
-
const
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
const codeSnippets = [];
|
|
4239
|
-
let totalLength = 0;
|
|
4240
|
-
const maxTotalLength = 8000; // 限制代码总长度
|
|
4241
|
-
for (const [filename, lines] of fileContents){
|
|
4242
|
-
if (totalLength >= maxTotalLength) break;
|
|
4243
|
-
// 只提取有变更的行(commitHash 不是 "-------")
|
|
4244
|
-
const changedLines = lines.map(([hash, code], idx)=>hash !== "-------" ? `${idx + 1}: ${code}` : null).filter(Boolean);
|
|
4245
|
-
if (changedLines.length > 0) {
|
|
4246
|
-
const snippet = `### ${filename}\n\`\`\`\n${changedLines.slice(0, 50).join("\n")}\n\`\`\``;
|
|
4247
|
-
if (totalLength + snippet.length <= maxTotalLength) {
|
|
4248
|
-
codeSnippets.push(snippet);
|
|
4249
|
-
totalLength += snippet.length;
|
|
4250
|
-
}
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
if (codeSnippets.length > 0) {
|
|
4254
|
-
codeChangesSection = `\n\n## 代码变更内容\n${codeSnippets.join("\n\n")}`;
|
|
4255
|
-
}
|
|
4256
|
-
}
|
|
4257
|
-
const prompt = `请根据以下 PR 的 commit 记录、文件变更和代码内容,用简洁的中文总结这个 PR 实现了什么功能。
|
|
4258
|
-
要求:
|
|
4259
|
-
1. 第一行输出 PR 标题,格式必须是: Feat xxx 或 Fix xxx 或 Refactor xxx(根据变更类型选择,整体不超过 50 个字符)
|
|
4260
|
-
2. 空一行后输出详细描述
|
|
4261
|
-
3. 描述应该简明扼要,突出核心功能点
|
|
4262
|
-
4. 使用 Markdown 格式
|
|
4263
|
-
5. 不要逐条列出 commit,而是归纳总结
|
|
4264
|
-
6. 重点分析代码变更的实际功能
|
|
4265
|
-
|
|
4266
|
-
## Commit 记录 (${commits.length} 个)
|
|
4267
|
-
${commitMessages || "无"}
|
|
4268
|
-
|
|
4269
|
-
## 文件变更 (${changedFiles.length} 个文件)
|
|
4270
|
-
${fileChanges || "无"}
|
|
4271
|
-
${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` : ""}${codeChangesSection}`;
|
|
4986
|
+
const { userPrompt } = buildPrDescriptionPrompt({
|
|
4987
|
+
commits,
|
|
4988
|
+
changedFiles,
|
|
4989
|
+
fileContents
|
|
4990
|
+
});
|
|
4272
4991
|
try {
|
|
4273
4992
|
const stream = this.llmProxyService.chatStream([
|
|
4274
4993
|
{
|
|
4275
4994
|
role: "user",
|
|
4276
|
-
content:
|
|
4995
|
+
content: userPrompt
|
|
4277
4996
|
}
|
|
4278
4997
|
], {
|
|
4279
4998
|
adapter: llmMode
|
|
@@ -4304,25 +5023,15 @@ ${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` :
|
|
|
4304
5023
|
/**
|
|
4305
5024
|
* 使用 LLM 生成 PR 标题
|
|
4306
5025
|
*/ async generatePrTitle(commits, changedFiles) {
|
|
4307
|
-
const
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
1. 格式必须是: Feat: xxx 或 Fix: xxx 或 Refactor: xxx
|
|
4312
|
-
2. 根据变更内容选择合适的前缀(新功能用 Feat,修复用 Fix,重构用 Refactor)
|
|
4313
|
-
3. xxx 部分用简短的中文描述(整体不超过 50 个字符)
|
|
4314
|
-
4. 只输出标题,不要加任何解释
|
|
4315
|
-
|
|
4316
|
-
Commit 记录:
|
|
4317
|
-
${commitMessages || "无"}
|
|
4318
|
-
|
|
4319
|
-
文件变更:
|
|
4320
|
-
${fileChanges || "无"}`;
|
|
5026
|
+
const { userPrompt } = buildPrTitlePrompt({
|
|
5027
|
+
commits,
|
|
5028
|
+
changedFiles
|
|
5029
|
+
});
|
|
4321
5030
|
try {
|
|
4322
5031
|
const stream = this.llmProxyService.chatStream([
|
|
4323
5032
|
{
|
|
4324
5033
|
role: "user",
|
|
4325
|
-
content:
|
|
5034
|
+
content: userPrompt
|
|
4326
5035
|
}
|
|
4327
5036
|
], {
|
|
4328
5037
|
adapter: "openai"
|
|
@@ -4386,6 +5095,7 @@ ${fileChanges || "无"}`;
|
|
|
4386
5095
|
|
|
4387
5096
|
|
|
4388
5097
|
|
|
5098
|
+
|
|
4389
5099
|
class ReviewService {
|
|
4390
5100
|
gitProvider;
|
|
4391
5101
|
config;
|
|
@@ -4448,9 +5158,17 @@ class ReviewService {
|
|
|
4448
5158
|
const source = await this.resolveSourceData(context);
|
|
4449
5159
|
if (source.earlyReturn) return source.earlyReturn;
|
|
4450
5160
|
const { prModel, commits, changedFiles, headSha, isDirectFileMode } = source;
|
|
5161
|
+
const effectiveWhenModifiedCode = isDirectFileMode ? undefined : context.whenModifiedCode;
|
|
5162
|
+
if (isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
|
|
5163
|
+
console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
|
|
5164
|
+
}
|
|
4451
5165
|
// 2. 规则匹配
|
|
4452
5166
|
const specs = await this.issueFilter.loadSpecs(specSources, verbose);
|
|
4453
5167
|
const applicableSpecs = this.reviewSpecService.filterApplicableSpecs(specs, changedFiles);
|
|
5168
|
+
if (shouldLog(verbose, 2)) {
|
|
5169
|
+
console.log(`[execute] loadSpecs: loaded ${specs.length} specs from sources: ${JSON.stringify(specSources)}`);
|
|
5170
|
+
console.log(`[execute] filterApplicableSpecs: ${applicableSpecs.length} applicable out of ${specs.length}, changedFiles=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
|
|
5171
|
+
}
|
|
4454
5172
|
if (shouldLog(verbose, 1)) {
|
|
4455
5173
|
console.log(` 适用的规则文件: ${applicableSpecs.length}`);
|
|
4456
5174
|
}
|
|
@@ -4471,7 +5189,7 @@ class ReviewService {
|
|
|
4471
5189
|
if (shouldLog(verbose, 1)) {
|
|
4472
5190
|
console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
|
|
4473
5191
|
}
|
|
4474
|
-
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null);
|
|
5192
|
+
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null, effectiveWhenModifiedCode, verbose, context.systemRules);
|
|
4475
5193
|
const result = await this.runLLMReview(llmMode, reviewPrompt, {
|
|
4476
5194
|
verbose,
|
|
4477
5195
|
concurrency: context.concurrency,
|
|
@@ -4495,6 +5213,16 @@ class ReviewService {
|
|
|
4495
5213
|
isDirectFileMode,
|
|
4496
5214
|
context
|
|
4497
5215
|
});
|
|
5216
|
+
// 静态规则产生的系统问题直接合并,不经过过滤管道
|
|
5217
|
+
if (reviewPrompt.staticIssues?.length) {
|
|
5218
|
+
result.issues = [
|
|
5219
|
+
...reviewPrompt.staticIssues,
|
|
5220
|
+
...result.issues
|
|
5221
|
+
];
|
|
5222
|
+
if (shouldLog(verbose, 1)) {
|
|
5223
|
+
console.log(`⚙️ 追加 ${reviewPrompt.staticIssues.length} 个静态规则系统问题`);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
4498
5226
|
if (shouldLog(verbose, 1)) {
|
|
4499
5227
|
console.log(`📝 最终发现 ${result.issues.length} 个问题`);
|
|
4500
5228
|
}
|
|
@@ -4516,8 +5244,8 @@ class ReviewService {
|
|
|
4516
5244
|
* 包含前置过滤(merge commit、files、commits、includes)。
|
|
4517
5245
|
* 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
|
|
4518
5246
|
*/ async resolveSourceData(context) {
|
|
4519
|
-
const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode,
|
|
4520
|
-
const isDirectFileMode = !!(files && files.length > 0 &&
|
|
5247
|
+
const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode, duplicateWorkflowResolved } = context;
|
|
5248
|
+
const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
|
|
4521
5249
|
let isLocalMode = !!localMode;
|
|
4522
5250
|
let effectiveBaseRef = baseRef;
|
|
4523
5251
|
let effectiveHeadRef = headRef;
|
|
@@ -4576,8 +5304,17 @@ class ReviewService {
|
|
|
4576
5304
|
}
|
|
4577
5305
|
}
|
|
4578
5306
|
}
|
|
4579
|
-
//
|
|
4580
|
-
if (
|
|
5307
|
+
// 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
|
|
5308
|
+
if (isDirectFileMode) {
|
|
5309
|
+
if (shouldLog(verbose, 1)) {
|
|
5310
|
+
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
|
|
5311
|
+
}
|
|
5312
|
+
changedFiles = files.map((f)=>({
|
|
5313
|
+
filename: f,
|
|
5314
|
+
status: "modified"
|
|
5315
|
+
}));
|
|
5316
|
+
isLocalMode = true;
|
|
5317
|
+
} else if (prNumber) {
|
|
4581
5318
|
if (shouldLog(verbose, 1)) {
|
|
4582
5319
|
console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
|
|
4583
5320
|
}
|
|
@@ -4591,9 +5328,9 @@ class ReviewService {
|
|
|
4591
5328
|
console.log(` Changed files: ${changedFiles.length}`);
|
|
4592
5329
|
}
|
|
4593
5330
|
// 检查是否有其他同名 review workflow 正在运行中
|
|
4594
|
-
if (
|
|
4595
|
-
const
|
|
4596
|
-
if (
|
|
5331
|
+
if (duplicateWorkflowResolved !== "off" && ci && prInfo?.head?.sha) {
|
|
5332
|
+
const duplicateResult = await this.checkDuplicateWorkflow(prModel, prInfo.head.sha, duplicateWorkflowResolved, verbose);
|
|
5333
|
+
if (duplicateResult) {
|
|
4597
5334
|
return {
|
|
4598
5335
|
prModel,
|
|
4599
5336
|
commits,
|
|
@@ -4601,20 +5338,12 @@ class ReviewService {
|
|
|
4601
5338
|
headSha: prInfo.head.sha,
|
|
4602
5339
|
isLocalMode,
|
|
4603
5340
|
isDirectFileMode,
|
|
4604
|
-
earlyReturn:
|
|
5341
|
+
earlyReturn: duplicateResult
|
|
4605
5342
|
};
|
|
4606
5343
|
}
|
|
4607
5344
|
}
|
|
4608
5345
|
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
4609
|
-
if (
|
|
4610
|
-
if (shouldLog(verbose, 1)) {
|
|
4611
|
-
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
|
|
4612
|
-
}
|
|
4613
|
-
changedFiles = files.map((f)=>({
|
|
4614
|
-
filename: f,
|
|
4615
|
-
status: "modified"
|
|
4616
|
-
}));
|
|
4617
|
-
} else if (changedFiles.length === 0) {
|
|
5346
|
+
if (changedFiles.length === 0) {
|
|
4618
5347
|
if (shouldLog(verbose, 1)) {
|
|
4619
5348
|
console.log(`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`);
|
|
4620
5349
|
}
|
|
@@ -4677,10 +5406,19 @@ class ReviewService {
|
|
|
4677
5406
|
// 3. 使用 includes 过滤文件和 commits(支持 added|/modified|/deleted| 前缀语法)
|
|
4678
5407
|
if (includes && includes.length > 0) {
|
|
4679
5408
|
const beforeFiles = changedFiles.length;
|
|
5409
|
+
if (shouldLog(verbose, 2)) {
|
|
5410
|
+
console.log(`[resolveSourceData] filterFilesByIncludes: before=${JSON.stringify(changedFiles.map((f)=>({
|
|
5411
|
+
filename: f.filename,
|
|
5412
|
+
status: f.status
|
|
5413
|
+
})))}, includes=${JSON.stringify(includes)}`);
|
|
5414
|
+
}
|
|
4680
5415
|
changedFiles = filterFilesByIncludes(changedFiles, includes);
|
|
4681
5416
|
if (shouldLog(verbose, 1)) {
|
|
4682
5417
|
console.log(` Includes 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
4683
5418
|
}
|
|
5419
|
+
if (shouldLog(verbose, 2)) {
|
|
5420
|
+
console.log(`[resolveSourceData] filterFilesByIncludes: after=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
|
|
5421
|
+
}
|
|
4684
5422
|
const globs = extractGlobsFromIncludes(includes);
|
|
4685
5423
|
const beforeCommits = commits.length;
|
|
4686
5424
|
const filteredCommits = [];
|
|
@@ -5012,7 +5750,8 @@ class ReviewService {
|
|
|
5012
5750
|
}
|
|
5013
5751
|
/**
|
|
5014
5752
|
* 检查是否有其他同名 review workflow 正在运行中
|
|
5015
|
-
|
|
5753
|
+
* 根据 duplicateWorkflowResolved 配置决定是跳过还是删除旧评论
|
|
5754
|
+
*/ async checkDuplicateWorkflow(prModel, headSha, mode, verbose) {
|
|
5016
5755
|
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
5017
5756
|
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
5018
5757
|
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prModel.number;
|
|
@@ -5024,6 +5763,16 @@ class ReviewService {
|
|
|
5024
5763
|
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
5025
5764
|
const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
|
|
5026
5765
|
if (duplicateReviewRuns.length > 0) {
|
|
5766
|
+
if (mode === "delete") {
|
|
5767
|
+
// 删除模式:清理旧的 AI Review 评论和 PR Review
|
|
5768
|
+
if (shouldLog(verbose, 1)) {
|
|
5769
|
+
console.log(`🗑️ 检测到 ${duplicateReviewRuns.length} 个同名 workflow,清理旧的 AI Review 评论...`);
|
|
5770
|
+
}
|
|
5771
|
+
await this.cleanupDuplicateAiReviews(prModel, verbose);
|
|
5772
|
+
// 清理后继续执行当前审查
|
|
5773
|
+
return null;
|
|
5774
|
+
}
|
|
5775
|
+
// 跳过模式(默认)
|
|
5027
5776
|
if (shouldLog(verbose, 1)) {
|
|
5028
5777
|
console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
|
|
5029
5778
|
}
|
|
@@ -5042,6 +5791,50 @@ class ReviewService {
|
|
|
5042
5791
|
}
|
|
5043
5792
|
return null;
|
|
5044
5793
|
}
|
|
5794
|
+
/**
|
|
5795
|
+
* 清理重复的 AI Review 评论(Issue Comments 和 PR Reviews)
|
|
5796
|
+
*/ async cleanupDuplicateAiReviews(prModel, verbose) {
|
|
5797
|
+
try {
|
|
5798
|
+
// 删除 Issue Comments(主评论)
|
|
5799
|
+
const comments = await prModel.getComments();
|
|
5800
|
+
const aiComments = comments.filter((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
5801
|
+
let deletedComments = 0;
|
|
5802
|
+
for (const comment of aiComments){
|
|
5803
|
+
if (comment.id) {
|
|
5804
|
+
try {
|
|
5805
|
+
await prModel.deleteComment(comment.id);
|
|
5806
|
+
deletedComments++;
|
|
5807
|
+
} catch {
|
|
5808
|
+
// 忽略删除失败
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
if (deletedComments > 0 && shouldLog(verbose, 1)) {
|
|
5813
|
+
console.log(` 已删除 ${deletedComments} 个重复的 AI Review 主评论`);
|
|
5814
|
+
}
|
|
5815
|
+
// 删除 PR Reviews(行级评论)
|
|
5816
|
+
const reviews = await prModel.getReviews();
|
|
5817
|
+
const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
5818
|
+
let deletedReviews = 0;
|
|
5819
|
+
for (const review of aiReviews){
|
|
5820
|
+
if (review.id) {
|
|
5821
|
+
try {
|
|
5822
|
+
await prModel.deleteReview(review.id);
|
|
5823
|
+
deletedReviews++;
|
|
5824
|
+
} catch {
|
|
5825
|
+
// 已提交的 review 无法删除,忽略
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
if (deletedReviews > 0 && shouldLog(verbose, 1)) {
|
|
5830
|
+
console.log(` 已删除 ${deletedReviews} 个重复的 AI Review PR Review`);
|
|
5831
|
+
}
|
|
5832
|
+
} catch (error) {
|
|
5833
|
+
if (shouldLog(verbose, 1)) {
|
|
5834
|
+
console.warn(`⚠️ 清理旧评论失败:`, error instanceof Error ? error.message : error);
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5045
5838
|
// --- Delegation methods for backward compatibility with tests ---
|
|
5046
5839
|
async fillIssueAuthors(...args) {
|
|
5047
5840
|
return this.contextBuilder.fillIssueAuthors(...args);
|
|
@@ -5114,31 +5907,9 @@ class ReviewService {
|
|
|
5114
5907
|
|
|
5115
5908
|
;// CONCATENATED MODULE: ./src/issue-verify.service.ts
|
|
5116
5909
|
|
|
5910
|
+
|
|
5117
5911
|
const TRUE = "true";
|
|
5118
5912
|
const FALSE = "false";
|
|
5119
|
-
const VERIFY_SCHEMA = {
|
|
5120
|
-
type: "object",
|
|
5121
|
-
properties: {
|
|
5122
|
-
fixed: {
|
|
5123
|
-
type: "boolean",
|
|
5124
|
-
description: "问题是否已被修复"
|
|
5125
|
-
},
|
|
5126
|
-
valid: {
|
|
5127
|
-
type: "boolean",
|
|
5128
|
-
description: "问题是否有效,有效的条件就是你需要看看代码是否符合规范"
|
|
5129
|
-
},
|
|
5130
|
-
reason: {
|
|
5131
|
-
type: "string",
|
|
5132
|
-
description: "判断依据,说明为什么认为问题已修复或仍存在"
|
|
5133
|
-
}
|
|
5134
|
-
},
|
|
5135
|
-
required: [
|
|
5136
|
-
"fixed",
|
|
5137
|
-
"valid",
|
|
5138
|
-
"reason"
|
|
5139
|
-
],
|
|
5140
|
-
additionalProperties: false
|
|
5141
|
-
};
|
|
5142
5913
|
class IssueVerifyService {
|
|
5143
5914
|
llmProxyService;
|
|
5144
5915
|
reviewSpecService;
|
|
@@ -5211,13 +5982,13 @@ class IssueVerifyService {
|
|
|
5211
5982
|
}
|
|
5212
5983
|
}
|
|
5213
5984
|
});
|
|
5214
|
-
const results = await executor.map(toVerify, async ({ issue, fileContent, ruleInfo })=>this.verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose), ({ issue })=>`${issue.file}:${issue.line}`);
|
|
5985
|
+
const results = await executor.map(toVerify, async ({ issue, fileContent, ruleInfo })=>this.verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose), ({ issue })=>`${issue.file}:${issue.line}:${issue.ruleId}`);
|
|
5215
5986
|
for (const result of results){
|
|
5216
5987
|
if (result.success && result.result) {
|
|
5217
5988
|
verifiedIssues.push(result.result);
|
|
5218
5989
|
} else {
|
|
5219
5990
|
// 失败时保留原始 issue
|
|
5220
|
-
const originalItem = toVerify.find((item)=>`${item.issue.file}:${item.issue.line}` === result.id);
|
|
5991
|
+
const originalItem = toVerify.find((item)=>`${item.issue.file}:${item.issue.line}:${item.issue.ruleId}` === result.id);
|
|
5221
5992
|
if (originalItem) {
|
|
5222
5993
|
verifiedIssues.push(originalItem.issue);
|
|
5223
5994
|
}
|
|
@@ -5233,7 +6004,15 @@ class IssueVerifyService {
|
|
|
5233
6004
|
/**
|
|
5234
6005
|
* 验证单个 issue 是否已修复
|
|
5235
6006
|
*/ async verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose) {
|
|
5236
|
-
const
|
|
6007
|
+
const specsSection = ruleInfo ? this.reviewSpecService.buildSpecsSection([
|
|
6008
|
+
ruleInfo.spec
|
|
6009
|
+
]) : "";
|
|
6010
|
+
const verifyPrompt = buildIssueVerifyPrompt({
|
|
6011
|
+
issue,
|
|
6012
|
+
fileContent,
|
|
6013
|
+
ruleInfo,
|
|
6014
|
+
specsSection
|
|
6015
|
+
});
|
|
5237
6016
|
try {
|
|
5238
6017
|
const stream = this.llmProxyService.chatStream([
|
|
5239
6018
|
{
|
|
@@ -5294,66 +6073,18 @@ class IssueVerifyService {
|
|
|
5294
6073
|
}
|
|
5295
6074
|
}
|
|
5296
6075
|
/**
|
|
6076
|
+
* @deprecated 使用 prompt/issue-verify.ts 中的 buildIssueVerifyPrompt
|
|
5297
6077
|
* 构建验证单个 issue 是否已修复的 prompt
|
|
5298
6078
|
*/ buildVerifyPrompt(issue, fileContent, ruleInfo) {
|
|
5299
|
-
const
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
- valid: 布尔值,true 表示问题有效(代码确实违反了规则),false 表示问题无效(误报)
|
|
5309
|
-
- fixed: 布尔值,true 表示问题已经被修复,false 表示问题仍然存在
|
|
5310
|
-
- reason: 判断依据
|
|
5311
|
-
|
|
5312
|
-
## 判断标准
|
|
5313
|
-
|
|
5314
|
-
### valid 判断
|
|
5315
|
-
- 根据规则 ID 和问题描述,判断代码是否真的违反了该规则
|
|
5316
|
-
- 如果问题描述与实际代码不符,valid 为 false
|
|
5317
|
-
- 如果规则不适用于该代码场景,valid 为 false
|
|
5318
|
-
|
|
5319
|
-
### fixed 判断
|
|
5320
|
-
- 只有当问题所在的代码已被修改,且修改后的代码不再违反规则时,fixed 才为 true
|
|
5321
|
-
- 如果问题所在的代码仍然存在且仍违反规则,fixed 必须为 false
|
|
5322
|
-
- 如果代码行号发生变化但问题本质仍存在,fixed 必须为 false
|
|
5323
|
-
|
|
5324
|
-
## 重要提醒
|
|
5325
|
-
- valid=false 时,fixed 的值无意义(无效问题无需修复)
|
|
5326
|
-
- 请确保 valid 和 fixed 的值与 reason 的描述一致!`;
|
|
5327
|
-
// 构建规则定义部分
|
|
5328
|
-
let ruleSection = "";
|
|
5329
|
-
if (ruleInfo) {
|
|
5330
|
-
ruleSection = this.reviewSpecService.buildSpecsSection([
|
|
5331
|
-
ruleInfo.spec
|
|
5332
|
-
]);
|
|
5333
|
-
}
|
|
5334
|
-
const userPrompt = `## 规则定义
|
|
5335
|
-
|
|
5336
|
-
${ruleSection}
|
|
5337
|
-
|
|
5338
|
-
## 之前发现的问题
|
|
5339
|
-
|
|
5340
|
-
- **文件**: ${issue.file}
|
|
5341
|
-
- **行号**: ${issue.line}
|
|
5342
|
-
- **规则**: ${issue.ruleId} (来自 ${issue.specFile})
|
|
5343
|
-
- **问题描述**: ${issue.reason}
|
|
5344
|
-
${issue.suggestion ? `- **原建议**: ${issue.suggestion}` : ""}
|
|
5345
|
-
|
|
5346
|
-
## 当前文件内容
|
|
5347
|
-
|
|
5348
|
-
\`\`\`
|
|
5349
|
-
${linesWithNumbers}
|
|
5350
|
-
\`\`\`
|
|
5351
|
-
|
|
5352
|
-
请判断这个问题是否有效,以及是否已经被修复。`;
|
|
5353
|
-
return {
|
|
5354
|
-
systemPrompt,
|
|
5355
|
-
userPrompt
|
|
5356
|
-
};
|
|
6079
|
+
const specsSection = ruleInfo ? this.reviewSpecService.buildSpecsSection([
|
|
6080
|
+
ruleInfo.spec
|
|
6081
|
+
]) : "";
|
|
6082
|
+
return buildIssueVerifyPrompt({
|
|
6083
|
+
issue,
|
|
6084
|
+
fileContent,
|
|
6085
|
+
ruleInfo,
|
|
6086
|
+
specsSection
|
|
6087
|
+
});
|
|
5357
6088
|
}
|
|
5358
6089
|
}
|
|
5359
6090
|
|
|
@@ -5362,69 +6093,7 @@ ${linesWithNumbers}
|
|
|
5362
6093
|
|
|
5363
6094
|
|
|
5364
6095
|
|
|
5365
|
-
|
|
5366
|
-
type: "object",
|
|
5367
|
-
properties: {
|
|
5368
|
-
impacts: {
|
|
5369
|
-
type: "array",
|
|
5370
|
-
items: {
|
|
5371
|
-
type: "object",
|
|
5372
|
-
properties: {
|
|
5373
|
-
file: {
|
|
5374
|
-
type: "string",
|
|
5375
|
-
description: "被删除代码所在的文件路径"
|
|
5376
|
-
},
|
|
5377
|
-
deletedCode: {
|
|
5378
|
-
type: "string",
|
|
5379
|
-
description: "被删除的代码片段摘要(前50字符)"
|
|
5380
|
-
},
|
|
5381
|
-
riskLevel: {
|
|
5382
|
-
type: "string",
|
|
5383
|
-
enum: [
|
|
5384
|
-
"high",
|
|
5385
|
-
"medium",
|
|
5386
|
-
"low",
|
|
5387
|
-
"none"
|
|
5388
|
-
],
|
|
5389
|
-
description: "风险等级:high=可能导致功能异常,medium=可能影响部分功能,low=影响较小,none=无影响"
|
|
5390
|
-
},
|
|
5391
|
-
affectedFiles: {
|
|
5392
|
-
type: "array",
|
|
5393
|
-
items: {
|
|
5394
|
-
type: "string"
|
|
5395
|
-
},
|
|
5396
|
-
description: "可能受影响的文件列表"
|
|
5397
|
-
},
|
|
5398
|
-
reason: {
|
|
5399
|
-
type: "string",
|
|
5400
|
-
description: "影响分析的详细说明"
|
|
5401
|
-
},
|
|
5402
|
-
suggestion: {
|
|
5403
|
-
type: "string",
|
|
5404
|
-
description: "建议的处理方式"
|
|
5405
|
-
}
|
|
5406
|
-
},
|
|
5407
|
-
required: [
|
|
5408
|
-
"file",
|
|
5409
|
-
"deletedCode",
|
|
5410
|
-
"riskLevel",
|
|
5411
|
-
"affectedFiles",
|
|
5412
|
-
"reason"
|
|
5413
|
-
],
|
|
5414
|
-
additionalProperties: false
|
|
5415
|
-
}
|
|
5416
|
-
},
|
|
5417
|
-
summary: {
|
|
5418
|
-
type: "string",
|
|
5419
|
-
description: "删除代码影响的整体总结"
|
|
5420
|
-
}
|
|
5421
|
-
},
|
|
5422
|
-
required: [
|
|
5423
|
-
"impacts",
|
|
5424
|
-
"summary"
|
|
5425
|
-
],
|
|
5426
|
-
additionalProperties: false
|
|
5427
|
-
};
|
|
6096
|
+
|
|
5428
6097
|
class DeletionImpactService {
|
|
5429
6098
|
llmProxyService;
|
|
5430
6099
|
gitProvider;
|
|
@@ -5853,43 +6522,10 @@ class DeletionImpactService {
|
|
|
5853
6522
|
* 使用 LLM 分析删除代码的影响
|
|
5854
6523
|
*/ async analyzeWithLLM(deletedBlocks, references, llmMode, verbose) {
|
|
5855
6524
|
const llmJsonPut = new LlmJsonPut(DELETION_IMPACT_SCHEMA);
|
|
5856
|
-
const systemPrompt =
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
## 分析要点
|
|
5862
|
-
1. **功能依赖**: 被删除的代码是否被其他模块调用或依赖
|
|
5863
|
-
2. **接口变更**: 删除是否会导致 API 或接口不兼容
|
|
5864
|
-
3. **副作用**: 删除是否会影响系统的其他行为
|
|
5865
|
-
4. **数据流**: 删除是否会中断数据处理流程
|
|
5866
|
-
|
|
5867
|
-
## 风险等级判断标准
|
|
5868
|
-
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
5869
|
-
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
5870
|
-
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
5871
|
-
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
5872
|
-
|
|
5873
|
-
## 输出要求
|
|
5874
|
-
- 对每个有风险的删除块给出详细分析
|
|
5875
|
-
- 如果删除是安全的,也要说明原因
|
|
5876
|
-
- 提供具体的建议`;
|
|
5877
|
-
const deletedCodeSection = deletedBlocks.map((block, index)=>{
|
|
5878
|
-
const refs = references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
|
5879
|
-
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}
|
|
5880
|
-
|
|
5881
|
-
\`\`\`
|
|
5882
|
-
${block.content}
|
|
5883
|
-
\`\`\`
|
|
5884
|
-
|
|
5885
|
-
可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}
|
|
5886
|
-
`;
|
|
5887
|
-
}).join("\n");
|
|
5888
|
-
const userPrompt = `## 被删除的代码块
|
|
5889
|
-
|
|
5890
|
-
${deletedCodeSection}
|
|
5891
|
-
|
|
5892
|
-
请分析这些删除操作可能带来的影响。`;
|
|
6525
|
+
const { systemPrompt, userPrompt } = buildDeletionImpactPrompt({
|
|
6526
|
+
deletedBlocks,
|
|
6527
|
+
references
|
|
6528
|
+
});
|
|
5893
6529
|
if (shouldLog(verbose, 2)) {
|
|
5894
6530
|
console.log(`\nsystemPrompt:\n----------------\n${systemPrompt}\n----------------`);
|
|
5895
6531
|
console.log(`\nuserPrompt:\n----------------\n${userPrompt}\n----------------`);
|
|
@@ -5945,54 +6581,10 @@ ${deletedCodeSection}
|
|
|
5945
6581
|
* Claude Agent 可以使用工具主动探索代码库,分析更深入
|
|
5946
6582
|
*/ async analyzeWithAgent(analysisMode, deletedBlocks, references, verbose) {
|
|
5947
6583
|
const llmJsonPut = new LlmJsonPut(DELETION_IMPACT_SCHEMA);
|
|
5948
|
-
const systemPrompt =
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
## 你的能力
|
|
5954
|
-
你可以使用以下工具来深入分析代码:
|
|
5955
|
-
- **Read**: 读取文件内容,查看被删除代码的完整上下文
|
|
5956
|
-
- **Grep**: 搜索代码库,查找对被删除代码的引用
|
|
5957
|
-
- **Glob**: 查找匹配模式的文件
|
|
5958
|
-
|
|
5959
|
-
## 分析流程
|
|
5960
|
-
1. 首先阅读被删除代码的上下文,理解其功能
|
|
5961
|
-
2. 使用 Grep 搜索代码库中对这些代码的引用
|
|
5962
|
-
3. 分析引用处的代码,判断删除后的影响
|
|
5963
|
-
4. 给出风险评估和建议
|
|
5964
|
-
|
|
5965
|
-
## 风险等级判断标准
|
|
5966
|
-
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
5967
|
-
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
5968
|
-
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
5969
|
-
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
5970
|
-
|
|
5971
|
-
## 输出要求
|
|
5972
|
-
- 对每个有风险的删除块给出详细分析
|
|
5973
|
-
- 如果删除是安全的,也要说明原因
|
|
5974
|
-
- 提供具体的建议`;
|
|
5975
|
-
const deletedCodeSection = deletedBlocks.map((block, index)=>{
|
|
5976
|
-
const refs = references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
|
5977
|
-
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}
|
|
5978
|
-
|
|
5979
|
-
\`\`\`
|
|
5980
|
-
${block.content}
|
|
5981
|
-
\`\`\`
|
|
5982
|
-
|
|
5983
|
-
可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}
|
|
5984
|
-
`;
|
|
5985
|
-
}).join("\n");
|
|
5986
|
-
const userPrompt = `## 被删除的代码块
|
|
5987
|
-
|
|
5988
|
-
${deletedCodeSection}
|
|
5989
|
-
|
|
5990
|
-
## 补充说明
|
|
5991
|
-
|
|
5992
|
-
请使用你的工具能力深入分析这些删除操作可能带来的影响。
|
|
5993
|
-
- 如果需要查看更多上下文,请读取相关文件
|
|
5994
|
-
- 如果需要确认引用关系,请搜索代码库
|
|
5995
|
-
- 分析完成后,给出结构化的影响评估`;
|
|
6584
|
+
const { systemPrompt, userPrompt } = buildDeletionImpactAgentPrompt({
|
|
6585
|
+
deletedBlocks,
|
|
6586
|
+
references
|
|
6587
|
+
});
|
|
5996
6588
|
if (shouldLog(verbose, 2)) {
|
|
5997
6589
|
console.log(`\n[Agent Mode] systemPrompt:\n----------------\n${systemPrompt}\n----------------`);
|
|
5998
6590
|
console.log(`\n[Agent Mode] userPrompt:\n----------------\n${userPrompt}\n----------------`);
|
|
@@ -6169,6 +6761,7 @@ ${deletedCodeSection}
|
|
|
6169
6761
|
|
|
6170
6762
|
|
|
6171
6763
|
|
|
6764
|
+
|
|
6172
6765
|
/** MCP 工具输入 schema */ const listRulesInputSchema = z.object({});
|
|
6173
6766
|
const getRulesForFileInputSchema = z.object({
|
|
6174
6767
|
filePath: z.string().describe(t("review:mcp.dto.filePath")),
|
|
@@ -6275,7 +6868,9 @@ const tools = [
|
|
|
6275
6868
|
const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{
|
|
6276
6869
|
const includes = rule.includes || spec.includes;
|
|
6277
6870
|
if (includes.length === 0) return true;
|
|
6278
|
-
|
|
6871
|
+
const globs = extractGlobsFromIncludes(includes);
|
|
6872
|
+
if (globs.length === 0) return true;
|
|
6873
|
+
return micromatch.isMatch(filePath, globs, {
|
|
6279
6874
|
matchBase: true
|
|
6280
6875
|
});
|
|
6281
6876
|
}).map((rule)=>({
|