@spaceflow/review 0.77.0 → 0.78.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 +35 -0
- package/dist/index.js +1060 -481
- 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 +29 -5
- 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 +11 -1
- package/src/review.service.ts +105 -5
- 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
|
@@ -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) {
|
|
@@ -3064,6 +3256,10 @@ class ReviewContextBuilder {
|
|
|
3064
3256
|
}
|
|
3065
3257
|
}
|
|
3066
3258
|
// 合并参数优先级:命令行 > PR 标题 > 配置文件 > 默认值
|
|
3259
|
+
const ctxIncludes = options.includes ?? titleOptions.includes ?? reviewConf.includes;
|
|
3260
|
+
if (shouldLog(options.verbose, 2)) {
|
|
3261
|
+
console.log(`[getContextFromEnv] includes: commandLine=${JSON.stringify(options.includes)}, title=${JSON.stringify(titleOptions.includes)}, config=${JSON.stringify(reviewConf.includes)}, final=${JSON.stringify(ctxIncludes)}`);
|
|
3262
|
+
}
|
|
3067
3263
|
return {
|
|
3068
3264
|
owner,
|
|
3069
3265
|
repo,
|
|
@@ -3074,7 +3270,8 @@ class ReviewContextBuilder {
|
|
|
3074
3270
|
dryRun: options.dryRun || titleOptions.dryRun || false,
|
|
3075
3271
|
ci: options.ci ?? false,
|
|
3076
3272
|
verbose: normalizeVerbose(options.verbose ?? titleOptions.verbose),
|
|
3077
|
-
includes:
|
|
3273
|
+
includes: ctxIncludes,
|
|
3274
|
+
whenModifiedCode: options.whenModifiedCode ?? reviewConf.whenModifiedCode,
|
|
3078
3275
|
llmMode: options.llmMode ?? titleOptions.llmMode ?? reviewConf.llmMode,
|
|
3079
3276
|
files: this.normalizeFilePaths(options.files),
|
|
3080
3277
|
commits: options.commits,
|
|
@@ -3095,8 +3292,9 @@ class ReviewContextBuilder {
|
|
|
3095
3292
|
flush: options.flush ?? false,
|
|
3096
3293
|
eventAction: options.eventAction,
|
|
3097
3294
|
localMode,
|
|
3098
|
-
|
|
3099
|
-
autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false
|
|
3295
|
+
duplicateWorkflowResolved: options.duplicateWorkflowResolved ?? reviewConf.duplicateWorkflowResolved ?? "delete",
|
|
3296
|
+
autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false,
|
|
3297
|
+
systemRules: options.systemRules ?? reviewConf.systemRules
|
|
3100
3298
|
};
|
|
3101
3299
|
}
|
|
3102
3300
|
/**
|
|
@@ -3268,13 +3466,32 @@ class ReviewContextBuilder {
|
|
|
3268
3466
|
// 为每个 issue 填充 author
|
|
3269
3467
|
return issues.map((issue)=>{
|
|
3270
3468
|
if (issue.author) {
|
|
3469
|
+
const shortHash = issue.commit?.slice(0, 7);
|
|
3470
|
+
if (shortHash?.includes("---")) {
|
|
3471
|
+
return {
|
|
3472
|
+
...issue,
|
|
3473
|
+
commit: undefined,
|
|
3474
|
+
valid: "false"
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3271
3477
|
if (shouldLog(verbose, 2)) {
|
|
3272
3478
|
console.log(`[fillIssueAuthors] issue already has author: ${issue.author.login}`);
|
|
3273
3479
|
}
|
|
3274
3480
|
return issue;
|
|
3275
3481
|
}
|
|
3276
3482
|
const shortHash = issue.commit?.slice(0, 7);
|
|
3277
|
-
const
|
|
3483
|
+
const isValidHash = Boolean(shortHash && !shortHash.includes("---"));
|
|
3484
|
+
if (!isValidHash) {
|
|
3485
|
+
if (shouldLog(verbose, 2)) {
|
|
3486
|
+
console.log(`[fillIssueAuthors] issue: file=${issue.file}, commit=${issue.commit} is invalid hash, marking as invalid`);
|
|
3487
|
+
}
|
|
3488
|
+
return {
|
|
3489
|
+
...issue,
|
|
3490
|
+
commit: undefined,
|
|
3491
|
+
valid: "false"
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
const author = commitAuthorMap.get(shortHash);
|
|
3278
3495
|
if (shouldLog(verbose, 2)) {
|
|
3279
3496
|
console.log(`[fillIssueAuthors] issue: file=${issue.file}, commit=${issue.commit}, shortHash=${shortHash}, foundAuthor=${author?.login}, finalAuthor=${(author || defaultAuthor)?.login}`);
|
|
3280
3497
|
}
|
|
@@ -3677,125 +3894,139 @@ class ReviewIssueFilter {
|
|
|
3677
3894
|
}
|
|
3678
3895
|
}
|
|
3679
3896
|
|
|
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
|
-
};
|
|
3897
|
+
;// CONCATENATED MODULE: ./src/utils/review-llm.ts
|
|
3691
3898
|
/**
|
|
3692
|
-
*
|
|
3899
|
+
* 构建带行号的文件内容字符串。
|
|
3693
3900
|
*
|
|
3694
|
-
*
|
|
3695
|
-
*
|
|
3696
|
-
*
|
|
3697
|
-
*/ function
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3901
|
+
* @param contentLines [hash, code] 行数组
|
|
3902
|
+
* @param visibleRanges 可选,指定需要输出的行号区间 [startLine, endLine](不含则输出全文)
|
|
3903
|
+
* 被跳过的连续行用 `...... ..| ignore {start}-{end} code` 占位
|
|
3904
|
+
*/ function buildLinesWithNumbers(contentLines, visibleRanges) {
|
|
3905
|
+
const padWidth = String(contentLines.length).length;
|
|
3906
|
+
if (!visibleRanges || visibleRanges.length === 0) {
|
|
3907
|
+
return contentLines.map(([hash, line], index)=>{
|
|
3908
|
+
const lineNum = index + 1;
|
|
3909
|
+
return `${hash} ${String(lineNum).padStart(padWidth)}| ${line}`;
|
|
3910
|
+
}).join("\n");
|
|
3703
3911
|
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3912
|
+
// 将 ranges 按起始行号排序并合并重叠区间
|
|
3913
|
+
const sorted = [
|
|
3914
|
+
...visibleRanges
|
|
3915
|
+
].sort((a, b)=>a[0] - b[0]);
|
|
3916
|
+
const merged = [];
|
|
3917
|
+
for (const range of sorted){
|
|
3918
|
+
if (merged.length > 0 && range[0] <= merged[merged.length - 1][1] + 1) {
|
|
3919
|
+
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], range[1]);
|
|
3920
|
+
} else {
|
|
3921
|
+
merged.push([
|
|
3922
|
+
...range
|
|
3923
|
+
]);
|
|
3924
|
+
}
|
|
3710
3925
|
}
|
|
3711
|
-
const
|
|
3712
|
-
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
}
|
|
3926
|
+
const output = [];
|
|
3927
|
+
let prevEnd = 0;
|
|
3928
|
+
for (const [start, end] of merged){
|
|
3929
|
+
const clampedStart = Math.max(1, start);
|
|
3930
|
+
const clampedEnd = Math.min(contentLines.length, end);
|
|
3931
|
+
// 被忽略的前缀区间
|
|
3932
|
+
if (clampedStart > prevEnd + 1) {
|
|
3933
|
+
output.push(`....... ignore ${prevEnd + 1}-${clampedStart - 1} line .......`);
|
|
3934
|
+
}
|
|
3935
|
+
// 输出可见行
|
|
3936
|
+
for(let i = clampedStart - 1; i < clampedEnd; i++){
|
|
3937
|
+
const [hash, line] = contentLines[i];
|
|
3938
|
+
const lineNum = i + 1;
|
|
3939
|
+
output.push(`${hash} ${String(lineNum).padStart(padWidth)}| ${line}`);
|
|
3940
|
+
}
|
|
3941
|
+
prevEnd = clampedEnd;
|
|
3720
3942
|
}
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
}
|
|
3943
|
+
// 被忽略的末尾区间
|
|
3944
|
+
if (prevEnd < contentLines.length) {
|
|
3945
|
+
output.push(`....... ignore ${prevEnd + 1}-${contentLines.length} line .......`);
|
|
3946
|
+
}
|
|
3947
|
+
return output.join("\n");
|
|
3725
3948
|
}
|
|
3726
3949
|
/**
|
|
3727
|
-
*
|
|
3950
|
+
* 从 contentLines 中提取新增代码里的指定结构类型的行号范围。
|
|
3728
3951
|
*
|
|
3729
|
-
*
|
|
3730
|
-
* 1.
|
|
3731
|
-
* 2.
|
|
3732
|
-
* 3.
|
|
3952
|
+
* 逻辑:
|
|
3953
|
+
* 1. 只考虑 hash !== "-------" 的新增行
|
|
3954
|
+
* 2. 用各类型的正则匹配结构开头行,再用层级计数找到结尾行
|
|
3955
|
+
* 3. 返回行号范围列表 [startLine, endLine](从 1 计)
|
|
3733
3956
|
*
|
|
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
|
-
|
|
3957
|
+
* @param contentLines 文件的 [hash, code] 行列表
|
|
3958
|
+
* @param types 要提取的结构类型
|
|
3959
|
+
*/ function extractCodeBlocks(contentLines, types) {
|
|
3960
|
+
if (types.length === 0) return [];
|
|
3961
|
+
const ranges = [];
|
|
3962
|
+
// 将所有行的实际代码组成文本(用于层级计数)
|
|
3963
|
+
const fullLines = contentLines.map(([, code])=>code);
|
|
3964
|
+
// 各类型的开头識别正则(匹配行首)
|
|
3965
|
+
const PATTERNS = {
|
|
3966
|
+
function: /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
3967
|
+
class: /^\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/,
|
|
3968
|
+
interface: /^\s*(?:export\s+)?interface\s+\w+/,
|
|
3969
|
+
type: /^\s*(?:export\s+)?type\s+\w+\s*[=<]/,
|
|
3970
|
+
method: /^\s*(?:(?:public|protected|private|static|async|override|readonly|abstract)\s+)*(?!(?:if|for|while|switch|return|const|let|var|throw|new)\b)(\w+)\s*[(<]/
|
|
3971
|
+
};
|
|
3972
|
+
for(let i = 0; i < fullLines.length; i++){
|
|
3973
|
+
const lineNum = i + 1;
|
|
3974
|
+
const isAdded = contentLines[i][0] !== "-------";
|
|
3975
|
+
if (!isAdded) continue;
|
|
3976
|
+
for (const type of types){
|
|
3977
|
+
const pattern = PATTERNS[type];
|
|
3978
|
+
if (!pattern.test(fullLines[i])) continue;
|
|
3979
|
+
// 找到结构开头,用层级计数找封闭括号结尾
|
|
3980
|
+
const endLine = findBlockEnd(fullLines, i);
|
|
3981
|
+
ranges.push([
|
|
3982
|
+
lineNum,
|
|
3983
|
+
endLine
|
|
3984
|
+
]);
|
|
3985
|
+
break; // 同一行只匹配一种类型
|
|
3760
3986
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3987
|
+
}
|
|
3988
|
+
return mergeRanges(ranges);
|
|
3989
|
+
}
|
|
3990
|
+
/**
|
|
3991
|
+
* 从开始行向下层级计数,找到匹配的封闭括号位置(行号从 1 计)。
|
|
3992
|
+
* 如果没有找到匹配括号,返回开始行到文件末尾。
|
|
3993
|
+
*/ function findBlockEnd(lines, startIndex) {
|
|
3994
|
+
let depth = 0;
|
|
3995
|
+
let foundOpen = false;
|
|
3996
|
+
for(let i = startIndex; i < lines.length; i++){
|
|
3997
|
+
for (const ch of lines[i]){
|
|
3998
|
+
if (ch === "{") {
|
|
3999
|
+
depth++;
|
|
4000
|
+
foundOpen = true;
|
|
4001
|
+
} else if (ch === "}") {
|
|
4002
|
+
depth--;
|
|
4003
|
+
if (foundOpen && depth === 0) {
|
|
4004
|
+
return i + 1; // 行号从 1 计
|
|
3779
4005
|
}
|
|
3780
4006
|
}
|
|
3781
4007
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
4008
|
+
}
|
|
4009
|
+
return lines.length; // 没有找到匹配括号,返回文件末尾
|
|
3784
4010
|
}
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
4011
|
+
function mergeRanges(ranges) {
|
|
4012
|
+
if (ranges.length === 0) return [];
|
|
4013
|
+
const sorted = [
|
|
4014
|
+
...ranges
|
|
4015
|
+
].sort((a, b)=>a[0] - b[0]);
|
|
4016
|
+
const merged = [
|
|
4017
|
+
sorted[0]
|
|
4018
|
+
];
|
|
4019
|
+
for(let i = 1; i < sorted.length; i++){
|
|
4020
|
+
const last = merged[merged.length - 1];
|
|
4021
|
+
if (sorted[i][0] <= last[1] + 1) {
|
|
4022
|
+
last[1] = Math.max(last[1], sorted[i][1]);
|
|
4023
|
+
} else {
|
|
4024
|
+
merged.push([
|
|
4025
|
+
...sorted[i]
|
|
4026
|
+
]);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
return merged;
|
|
3799
4030
|
}
|
|
3800
4031
|
function buildCommitsSection(contentLines, commits) {
|
|
3801
4032
|
const fileCommitHashes = new Set();
|
|
@@ -3811,13 +4042,10 @@ function buildCommitsSection(contentLines, commits) {
|
|
|
3811
4042
|
return relatedCommits.length > 0 ? relatedCommits.map((c)=>`- \`${c.sha?.slice(0, 7)}\` ${c.commit?.message?.split("\n")[0]}`).join("\n") : "- 无相关 commits";
|
|
3812
4043
|
}
|
|
3813
4044
|
|
|
3814
|
-
;// CONCATENATED MODULE: ./src/
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
const REVIEW_SCHEMA = {
|
|
4045
|
+
;// CONCATENATED MODULE: ./src/prompt/schemas.ts
|
|
4046
|
+
/**
|
|
4047
|
+
* 代码审查结果 JSON Schema
|
|
4048
|
+
*/ const REVIEW_SCHEMA = {
|
|
3821
4049
|
type: "object",
|
|
3822
4050
|
properties: {
|
|
3823
4051
|
issues: {
|
|
@@ -3883,6 +4111,532 @@ const REVIEW_SCHEMA = {
|
|
|
3883
4111
|
],
|
|
3884
4112
|
additionalProperties: false
|
|
3885
4113
|
};
|
|
4114
|
+
/**
|
|
4115
|
+
* 删除影响分析结果 JSON Schema
|
|
4116
|
+
*/ const DELETION_IMPACT_SCHEMA = {
|
|
4117
|
+
type: "object",
|
|
4118
|
+
properties: {
|
|
4119
|
+
impacts: {
|
|
4120
|
+
type: "array",
|
|
4121
|
+
items: {
|
|
4122
|
+
type: "object",
|
|
4123
|
+
properties: {
|
|
4124
|
+
file: {
|
|
4125
|
+
type: "string",
|
|
4126
|
+
description: "被删除代码所在的文件路径"
|
|
4127
|
+
},
|
|
4128
|
+
deletedCode: {
|
|
4129
|
+
type: "string",
|
|
4130
|
+
description: "被删除的代码片段摘要(前50字符)"
|
|
4131
|
+
},
|
|
4132
|
+
riskLevel: {
|
|
4133
|
+
type: "string",
|
|
4134
|
+
enum: [
|
|
4135
|
+
"high",
|
|
4136
|
+
"medium",
|
|
4137
|
+
"low",
|
|
4138
|
+
"none"
|
|
4139
|
+
],
|
|
4140
|
+
description: "风险等级:high=可能导致功能异常,medium=可能影响部分功能,low=影响较小,none=无影响"
|
|
4141
|
+
},
|
|
4142
|
+
affectedFiles: {
|
|
4143
|
+
type: "array",
|
|
4144
|
+
items: {
|
|
4145
|
+
type: "string"
|
|
4146
|
+
},
|
|
4147
|
+
description: "可能受影响的文件列表"
|
|
4148
|
+
},
|
|
4149
|
+
reason: {
|
|
4150
|
+
type: "string",
|
|
4151
|
+
description: "影响分析的详细说明"
|
|
4152
|
+
},
|
|
4153
|
+
suggestion: {
|
|
4154
|
+
type: "string",
|
|
4155
|
+
description: "建议的处理方式"
|
|
4156
|
+
}
|
|
4157
|
+
},
|
|
4158
|
+
required: [
|
|
4159
|
+
"file",
|
|
4160
|
+
"deletedCode",
|
|
4161
|
+
"riskLevel",
|
|
4162
|
+
"affectedFiles",
|
|
4163
|
+
"reason"
|
|
4164
|
+
],
|
|
4165
|
+
additionalProperties: false
|
|
4166
|
+
}
|
|
4167
|
+
},
|
|
4168
|
+
summary: {
|
|
4169
|
+
type: "string",
|
|
4170
|
+
description: "删除代码影响的整体总结"
|
|
4171
|
+
}
|
|
4172
|
+
},
|
|
4173
|
+
required: [
|
|
4174
|
+
"impacts",
|
|
4175
|
+
"summary"
|
|
4176
|
+
],
|
|
4177
|
+
additionalProperties: false
|
|
4178
|
+
};
|
|
4179
|
+
/**
|
|
4180
|
+
* 问题验证结果 JSON Schema
|
|
4181
|
+
*/ const VERIFY_SCHEMA = {
|
|
4182
|
+
type: "object",
|
|
4183
|
+
properties: {
|
|
4184
|
+
fixed: {
|
|
4185
|
+
type: "boolean",
|
|
4186
|
+
description: "问题是否已被修复"
|
|
4187
|
+
},
|
|
4188
|
+
valid: {
|
|
4189
|
+
type: "boolean",
|
|
4190
|
+
description: "问题是否有效,有效的条件就是你需要看看代码是否符合规范"
|
|
4191
|
+
},
|
|
4192
|
+
reason: {
|
|
4193
|
+
type: "string",
|
|
4194
|
+
description: "判断依据,说明为什么认为问题已修复或仍存在"
|
|
4195
|
+
}
|
|
4196
|
+
},
|
|
4197
|
+
required: [
|
|
4198
|
+
"fixed",
|
|
4199
|
+
"valid",
|
|
4200
|
+
"reason"
|
|
4201
|
+
],
|
|
4202
|
+
additionalProperties: false
|
|
4203
|
+
};
|
|
4204
|
+
|
|
4205
|
+
;// CONCATENATED MODULE: ./src/prompt/types.ts
|
|
4206
|
+
/**
|
|
4207
|
+
* 提示词回调函数类型定义
|
|
4208
|
+
*/ /**
|
|
4209
|
+
* 输入验证错误类
|
|
4210
|
+
*/ class PromptValidationError extends Error {
|
|
4211
|
+
constructor(message){
|
|
4212
|
+
super(message);
|
|
4213
|
+
this.name = "PromptValidationError";
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* 输入验证工具函数
|
|
4218
|
+
*/ function validateRequired(value, fieldName) {
|
|
4219
|
+
if (value === undefined || value === null) {
|
|
4220
|
+
throw new PromptValidationError(`${fieldName} is required but was ${value}`);
|
|
4221
|
+
}
|
|
4222
|
+
return value;
|
|
4223
|
+
}
|
|
4224
|
+
function types_validateNonEmptyString(value, fieldName) {
|
|
4225
|
+
if (value === undefined || value === null || value.trim() === "") {
|
|
4226
|
+
throw new PromptValidationError(`${fieldName} is required and cannot be empty`);
|
|
4227
|
+
}
|
|
4228
|
+
return value;
|
|
4229
|
+
}
|
|
4230
|
+
function validateArray(value, fieldName) {
|
|
4231
|
+
if (!Array.isArray(value)) {
|
|
4232
|
+
throw new PromptValidationError(`${fieldName} must be an array but was ${typeof value}`);
|
|
4233
|
+
}
|
|
4234
|
+
return value;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
;// CONCATENATED MODULE: ./src/prompt/code-review.ts
|
|
4238
|
+
|
|
4239
|
+
/**
|
|
4240
|
+
* 代码审查公共系统提示词基础
|
|
4241
|
+
*/ const CODE_REVIEW_BASE_SYSTEM_PROMPT = `你是一个专业的代码审查专家,负责根据团队的编码规范对代码进行严格审查。
|
|
4242
|
+
|
|
4243
|
+
## 审查要求
|
|
4244
|
+
|
|
4245
|
+
1. **严格遵循规范**:只按照上述审查规范进行审查,不要添加规范之外的要求
|
|
4246
|
+
2. **精准定位问题**:每个问题必须指明具体的行号,行号从文件内容中的 "行号|" 格式获取
|
|
4247
|
+
3. **避免重复报告**:如果提示词中包含"上一次审查结果",请不要重复报告已存在的问题
|
|
4248
|
+
4. **提供可行建议**:对于每个问题,提供具体的修改建议代码
|
|
4249
|
+
|
|
4250
|
+
## 注意事项
|
|
4251
|
+
|
|
4252
|
+
- 变更文件内容已在上下文中提供,无需调用读取工具
|
|
4253
|
+
- 你可以读取项目中的其他文件以了解上下文
|
|
4254
|
+
- 不要调用编辑工具修改文件,你的职责是审查而非修改
|
|
4255
|
+
- 文件内容格式为 "CommitHash 行号| 代码",输出的 line 字段应对应原始行号
|
|
4256
|
+
|
|
4257
|
+
## 输出要求
|
|
4258
|
+
|
|
4259
|
+
- 发现问题时:在 issues 数组中列出所有问题,每个问题包含 file、line、ruleId、specFile、reason、suggestion、severity
|
|
4260
|
+
- 无论是否发现问题:都必须在 summary 中提供该文件的审查总结,简要说明审查结果`;
|
|
4261
|
+
const buildCodeReviewSystemPrompt = (ctx)=>{
|
|
4262
|
+
validateNonEmptyString(ctx.specsSection, "specsSection");
|
|
4263
|
+
return {
|
|
4264
|
+
systemPrompt: `${CODE_REVIEW_BASE_SYSTEM_PROMPT}
|
|
4265
|
+
|
|
4266
|
+
## 审查规范
|
|
4267
|
+
|
|
4268
|
+
${ctx.specsSection}`,
|
|
4269
|
+
userPrompt: ""
|
|
4270
|
+
};
|
|
4271
|
+
};
|
|
4272
|
+
const buildFileReviewPrompt = (ctx)=>{
|
|
4273
|
+
// 验证必需的输入参数
|
|
4274
|
+
types_validateNonEmptyString(ctx.filename, "filename");
|
|
4275
|
+
types_validateNonEmptyString(ctx.status, "status");
|
|
4276
|
+
types_validateNonEmptyString(ctx.linesWithNumbers, "linesWithNumbers");
|
|
4277
|
+
validateRequired(ctx.specsSection, "specsSection");
|
|
4278
|
+
return {
|
|
4279
|
+
systemPrompt: `${CODE_REVIEW_BASE_SYSTEM_PROMPT}
|
|
4280
|
+
|
|
4281
|
+
## 审查规范
|
|
4282
|
+
|
|
4283
|
+
${ctx.specsSection}`,
|
|
4284
|
+
userPrompt: `## ${ctx.filename} (${ctx.status})
|
|
4285
|
+
|
|
4286
|
+
### 文件内容
|
|
4287
|
+
|
|
4288
|
+
\`\`\`
|
|
4289
|
+
${ctx.linesWithNumbers}
|
|
4290
|
+
\`\`\`
|
|
4291
|
+
|
|
4292
|
+
### 该文件的相关 Commits
|
|
4293
|
+
|
|
4294
|
+
${ctx.commitsSection}
|
|
4295
|
+
|
|
4296
|
+
### 该文件所在的目录树
|
|
4297
|
+
|
|
4298
|
+
${ctx.fileDirectoryInfo}
|
|
4299
|
+
|
|
4300
|
+
### 上一次审查结果
|
|
4301
|
+
|
|
4302
|
+
${ctx.previousReviewSection}`
|
|
4303
|
+
};
|
|
4304
|
+
};
|
|
4305
|
+
|
|
4306
|
+
;// CONCATENATED MODULE: ./src/prompt/pr-description.ts
|
|
4307
|
+
|
|
4308
|
+
/**
|
|
4309
|
+
* 内存使用限制常量
|
|
4310
|
+
*/ const MEMORY_LIMITS = {
|
|
4311
|
+
MAX_TOTAL_LENGTH: 8000,
|
|
4312
|
+
MAX_FILES: 30,
|
|
4313
|
+
MAX_SNIPPET_LENGTH: 50,
|
|
4314
|
+
MAX_COMMITS: 10,
|
|
4315
|
+
MAX_FILES_FOR_TITLE: 20
|
|
4316
|
+
};
|
|
4317
|
+
const buildPrDescriptionPrompt = (ctx)=>{
|
|
4318
|
+
// 验证必需的输入参数
|
|
4319
|
+
validateArray(ctx.commits, "commits");
|
|
4320
|
+
validateArray(ctx.changedFiles, "changedFiles");
|
|
4321
|
+
const commitMessages = ctx.commits.map((c)=>`- ${c.sha?.slice(0, 7)}: ${c.commit?.message?.split("\n")[0]}`).join("\n");
|
|
4322
|
+
const fileChanges = ctx.changedFiles.slice(0, MEMORY_LIMITS.MAX_FILES).map((f)=>`- ${f.filename} (${f.status})`).join("\n");
|
|
4323
|
+
// 构建代码变更内容(只包含变更行,优化内存使用)
|
|
4324
|
+
let codeChangesSection = "";
|
|
4325
|
+
if (ctx.fileContents && ctx.fileContents.size > 0) {
|
|
4326
|
+
const codeSnippets = [];
|
|
4327
|
+
let totalLength = 0;
|
|
4328
|
+
// 使用 Map.entries() 进行更高效的迭代
|
|
4329
|
+
for (const [filename, lines] of ctx.fileContents){
|
|
4330
|
+
if (totalLength >= MEMORY_LIMITS.MAX_TOTAL_LENGTH) break;
|
|
4331
|
+
// 只提取有变更的行(commitHash 不是 "-------")
|
|
4332
|
+
const changedLines = lines.map(([hash, code], idx)=>hash !== "-------" ? `${idx + 1}: ${code}` : null).filter(Boolean);
|
|
4333
|
+
if (changedLines.length > 0) {
|
|
4334
|
+
// 限制每个文件的代码行数,避免单个文件占用过多内存
|
|
4335
|
+
const limitedLines = changedLines.slice(0, MEMORY_LIMITS.MAX_SNIPPET_LENGTH);
|
|
4336
|
+
const snippet = `### ${filename}\n\`\`\`\n${limitedLines.join("\n")}\n\`\`\``;
|
|
4337
|
+
// 检查添加此片段是否会超过内存限制
|
|
4338
|
+
if (totalLength + snippet.length <= MEMORY_LIMITS.MAX_TOTAL_LENGTH) {
|
|
4339
|
+
codeSnippets.push(snippet);
|
|
4340
|
+
totalLength += snippet.length;
|
|
4341
|
+
} else {
|
|
4342
|
+
// 如果添加当前片段会超过限制,尝试截断它
|
|
4343
|
+
const remainingLength = MEMORY_LIMITS.MAX_TOTAL_LENGTH - totalLength;
|
|
4344
|
+
if (remainingLength > 100) {
|
|
4345
|
+
// 至少保留 100 字符的片段
|
|
4346
|
+
// snippet 格式为 "### filename\n```\ncode\n```"
|
|
4347
|
+
// 截断时去掉结尾的 ``` 再追加,避免双重代码块
|
|
4348
|
+
const closingTag = "\n```";
|
|
4349
|
+
const contentEnd = snippet.lastIndexOf(closingTag);
|
|
4350
|
+
const truncateAt = Math.max(0, contentEnd > 0 ? Math.min(remainingLength - 20, contentEnd) : remainingLength - 20);
|
|
4351
|
+
const truncatedSnippet = snippet.substring(0, truncateAt) + "\n..." + closingTag;
|
|
4352
|
+
codeSnippets.push(truncatedSnippet);
|
|
4353
|
+
break;
|
|
4354
|
+
}
|
|
4355
|
+
break;
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
if (codeSnippets.length > 0) {
|
|
4360
|
+
codeChangesSection = `\n\n## 代码变更内容\n${codeSnippets.join("\n\n")}`;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
return {
|
|
4364
|
+
systemPrompt: "",
|
|
4365
|
+
userPrompt: `请根据以下 PR 的 commit 记录、文件变更和代码内容,用简洁的中文总结这个 PR 实现了什么功能。
|
|
4366
|
+
要求:
|
|
4367
|
+
1. 第一行输出 PR 标题,格式必须是: Feat xxx 或 Fix xxx 或 Refactor xxx(根据变更类型选择,整体不超过 50 个字符)
|
|
4368
|
+
2. 空一行后输出详细描述
|
|
4369
|
+
3. 描述应该简明扼要,突出核心功能点
|
|
4370
|
+
4. 使用 Markdown 格式
|
|
4371
|
+
5. 不要逐条列出 commit,而是归纳总结
|
|
4372
|
+
6. 重点分析代码变更的实际功能
|
|
4373
|
+
|
|
4374
|
+
## Commit 记录 (${ctx.commits.length} 个)
|
|
4375
|
+
${commitMessages || "无"}
|
|
4376
|
+
|
|
4377
|
+
## 文件变更 (${ctx.changedFiles.length} 个文件)
|
|
4378
|
+
${fileChanges || "无"}${ctx.changedFiles.length > MEMORY_LIMITS.MAX_FILES ? `\n... 等 ${ctx.changedFiles.length - MEMORY_LIMITS.MAX_FILES} 个文件` : ""}${codeChangesSection}`
|
|
4379
|
+
};
|
|
4380
|
+
};
|
|
4381
|
+
const buildPrTitlePrompt = (ctx)=>{
|
|
4382
|
+
// 验证必需的输入参数
|
|
4383
|
+
validateArray(ctx.commits, "commits");
|
|
4384
|
+
validateArray(ctx.changedFiles, "changedFiles");
|
|
4385
|
+
const commitMessages = ctx.commits.slice(0, MEMORY_LIMITS.MAX_COMMITS).map((c)=>c.commit?.message?.split("\n")[0]).filter(Boolean).join("\n");
|
|
4386
|
+
const fileChanges = ctx.changedFiles.slice(0, MEMORY_LIMITS.MAX_FILES_FOR_TITLE).map((f)=>`${f.filename} (${f.status})`).join("\n");
|
|
4387
|
+
return {
|
|
4388
|
+
systemPrompt: "",
|
|
4389
|
+
userPrompt: `请根据以下 commit 记录和文件变更,生成一个简短的 PR 标题。
|
|
4390
|
+
要求:
|
|
4391
|
+
1. 格式必须是: Feat: xxx 或 Fix: xxx 或 Refactor: xxx
|
|
4392
|
+
2. 根据变更内容选择合适的前缀(新功能用 Feat,修复用 Fix,重构用 Refactor)
|
|
4393
|
+
3. xxx 部分用简短的中文描述(整体不超过 50 个字符)
|
|
4394
|
+
4. 只输出标题,不要加任何解释
|
|
4395
|
+
|
|
4396
|
+
Commit 记录:
|
|
4397
|
+
${commitMessages || "无"}
|
|
4398
|
+
|
|
4399
|
+
文件变更:
|
|
4400
|
+
${fileChanges || "无"}`
|
|
4401
|
+
};
|
|
4402
|
+
};
|
|
4403
|
+
|
|
4404
|
+
;// CONCATENATED MODULE: ./src/prompt/deletion-impact.ts
|
|
4405
|
+
|
|
4406
|
+
const DELETION_IMPACT_SYSTEM = `你是一个代码审查专家,专门分析删除代码可能带来的影响。
|
|
4407
|
+
|
|
4408
|
+
## 任务
|
|
4409
|
+
分析以下被删除的代码块,判断删除这些代码是否会影响到其他功能。
|
|
4410
|
+
|
|
4411
|
+
## 分析要点
|
|
4412
|
+
1. **功能依赖**: 被删除的代码是否被其他模块调用或依赖
|
|
4413
|
+
2. **接口变更**: 删除是否会导致 API 或接口不兼容
|
|
4414
|
+
3. **副作用**: 删除是否会影响系统的其他行为
|
|
4415
|
+
4. **数据流**: 删除是否会中断数据处理流程
|
|
4416
|
+
|
|
4417
|
+
## 风险等级判断标准
|
|
4418
|
+
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
4419
|
+
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
4420
|
+
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
4421
|
+
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
4422
|
+
|
|
4423
|
+
## 输出要求
|
|
4424
|
+
- 对每个有风险的删除块给出详细分析
|
|
4425
|
+
- 如果删除是安全的,也要说明原因
|
|
4426
|
+
- 提供具体的建议`;
|
|
4427
|
+
function buildDeletedCodeSection(ctx) {
|
|
4428
|
+
return ctx.deletedBlocks.map((block, index)=>{
|
|
4429
|
+
const refs = ctx.references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
|
4430
|
+
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}\n\n\`\`\`\n${block.content}\n\`\`\`\n\n可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}\n`;
|
|
4431
|
+
}).join("\n");
|
|
4432
|
+
}
|
|
4433
|
+
const buildDeletionImpactPrompt = (ctx)=>{
|
|
4434
|
+
validateArray(ctx.deletedBlocks, "deletedBlocks");
|
|
4435
|
+
validateRequired(ctx.references, "references");
|
|
4436
|
+
return {
|
|
4437
|
+
systemPrompt: DELETION_IMPACT_SYSTEM,
|
|
4438
|
+
userPrompt: `## 被删除的代码块\n\n${buildDeletedCodeSection(ctx)}\n请分析这些删除操作可能带来的影响。`
|
|
4439
|
+
};
|
|
4440
|
+
};
|
|
4441
|
+
/** @deprecated 使用 buildDeletionImpactPrompt */ const buildDeletionImpactSystemPrompt = (ctx)=>buildDeletionImpactPrompt(ctx);
|
|
4442
|
+
/** @deprecated 使用 buildDeletionImpactPrompt */ const buildDeletionImpactUserPrompt = (ctx)=>buildDeletionImpactPrompt(ctx);
|
|
4443
|
+
const DELETION_IMPACT_AGENT_SYSTEM = `你是一个资深代码架构师,擅长分析代码变更的影响范围和潜在风险。
|
|
4444
|
+
|
|
4445
|
+
## 任务
|
|
4446
|
+
深入分析以下被删除的代码块,评估删除操作对代码库的影响。
|
|
4447
|
+
|
|
4448
|
+
## 你的能力
|
|
4449
|
+
你可以使用以下工具来深入分析代码:
|
|
4450
|
+
- **Read**: 读取文件内容,查看被删除代码的完整上下文
|
|
4451
|
+
- **Grep**: 搜索代码库,查找对被删除代码的引用
|
|
4452
|
+
- **Glob**: 查找匹配模式的文件
|
|
4453
|
+
|
|
4454
|
+
## 分析流程
|
|
4455
|
+
1. 首先阅读被删除代码的上下文,理解其功能
|
|
4456
|
+
2. 使用 Grep 搜索代码库中对这些代码的引用
|
|
4457
|
+
3. 分析引用处的代码,判断删除后的影响
|
|
4458
|
+
4. 给出风险评估和建议
|
|
4459
|
+
|
|
4460
|
+
## 风险等级判断标准
|
|
4461
|
+
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
4462
|
+
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
4463
|
+
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
4464
|
+
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
4465
|
+
|
|
4466
|
+
## 输出要求
|
|
4467
|
+
- 对每个有风险的删除块给出详细分析
|
|
4468
|
+
- 如果删除是安全的,也要说明原因
|
|
4469
|
+
- 提供具体的建议`;
|
|
4470
|
+
const buildDeletionImpactAgentPrompt = (ctx)=>{
|
|
4471
|
+
validateArray(ctx.deletedBlocks, "deletedBlocks");
|
|
4472
|
+
validateRequired(ctx.references, "references");
|
|
4473
|
+
return {
|
|
4474
|
+
systemPrompt: DELETION_IMPACT_AGENT_SYSTEM,
|
|
4475
|
+
userPrompt: `## 被删除的代码块\n\n${buildDeletedCodeSection(ctx)}\n## 补充说明\n\n请使用你的工具能力深入分析这些删除操作可能带来的影响。\n- 如果需要查看更多上下文,请读取相关文件\n- 如果需要确认引用关系,请搜索代码库\n- 分析完成后,给出结构化的影响评估`
|
|
4476
|
+
};
|
|
4477
|
+
};
|
|
4478
|
+
/** @deprecated 使用 buildDeletionImpactAgentPrompt */ const buildDeletionImpactAgentSystemPrompt = (ctx)=>buildDeletionImpactAgentPrompt(ctx);
|
|
4479
|
+
/** @deprecated 使用 buildDeletionImpactAgentPrompt */ const buildDeletionImpactAgentUserPrompt = (ctx)=>buildDeletionImpactAgentPrompt(ctx);
|
|
4480
|
+
|
|
4481
|
+
;// CONCATENATED MODULE: ./src/prompt/issue-verify.ts
|
|
4482
|
+
|
|
4483
|
+
/**
|
|
4484
|
+
* 构建问题验证提示词
|
|
4485
|
+
*/ const buildIssueVerifyPrompt = (ctx)=>{
|
|
4486
|
+
// 验证必需的输入参数
|
|
4487
|
+
validateRequired(ctx.issue, "issue");
|
|
4488
|
+
validateArray(ctx.fileContent, "fileContent");
|
|
4489
|
+
const padWidth = String(ctx.fileContent.length).length;
|
|
4490
|
+
const linesWithNumbers = ctx.fileContent.map(([, line], index)=>`${String(index + 1).padStart(padWidth)}| ${line}`).join("\n");
|
|
4491
|
+
const systemPrompt = `你是一个代码审查专家。你的任务是判断之前发现的一个代码问题:
|
|
4492
|
+
1. 是否有效(是否真的违反了规则)
|
|
4493
|
+
2. 是否已经被修复
|
|
4494
|
+
|
|
4495
|
+
请仔细分析当前的代码内容。
|
|
4496
|
+
|
|
4497
|
+
## 输出要求
|
|
4498
|
+
- valid: 布尔值,true 表示问题有效(代码确实违反了规则),false 表示问题无效(误报)
|
|
4499
|
+
- fixed: 布尔值,true 表示问题已经被修复,false 表示问题仍然存在
|
|
4500
|
+
- reason: 判断依据
|
|
4501
|
+
|
|
4502
|
+
## 判断标准
|
|
4503
|
+
|
|
4504
|
+
### valid 判断
|
|
4505
|
+
- 根据规则 ID 和问题描述,判断代码是否真的违反了该规则
|
|
4506
|
+
- 如果问题描述与实际代码不符,valid 为 false
|
|
4507
|
+
- 如果规则不适用于该代码场景,valid 为 false
|
|
4508
|
+
|
|
4509
|
+
### fixed 判断
|
|
4510
|
+
- 只有当问题所在的代码已被修改,且修改后的代码不再违反规则时,fixed 才为 true
|
|
4511
|
+
- 如果问题所在的代码仍然存在且仍违反规则,fixed 必须为 false
|
|
4512
|
+
- 如果代码行号发生变化但问题本质仍存在,fixed 必须为 false
|
|
4513
|
+
|
|
4514
|
+
## 重要提醒
|
|
4515
|
+
- valid=false 时,fixed 的值无意义(无效问题无需修复)
|
|
4516
|
+
- 请确保 valid 和 fixed 的值与 reason 的描述一致!`;
|
|
4517
|
+
// 构建规则定义部分
|
|
4518
|
+
let ruleSection = "";
|
|
4519
|
+
if (ctx.specsSection) {
|
|
4520
|
+
ruleSection = ctx.specsSection;
|
|
4521
|
+
} else if (ctx.ruleInfo) {
|
|
4522
|
+
const { spec, rule } = ctx.ruleInfo;
|
|
4523
|
+
ruleSection = `### ${spec.filename} (${spec.type})\n\n${spec.content.slice(0, 200)}...\n\n#### 规则\n- ${rule.id}: ${rule.title}\n ${rule.description}`;
|
|
4524
|
+
}
|
|
4525
|
+
const userPrompt = `## 规则定义
|
|
4526
|
+
|
|
4527
|
+
${ruleSection}
|
|
4528
|
+
|
|
4529
|
+
## 之前发现的问题
|
|
4530
|
+
|
|
4531
|
+
- **文件**: ${ctx.issue.file}
|
|
4532
|
+
- **行号**: ${ctx.issue.line}
|
|
4533
|
+
- **规则**: ${ctx.issue.ruleId} (来自 ${ctx.issue.specFile})
|
|
4534
|
+
- **问题描述**: ${ctx.issue.reason}
|
|
4535
|
+
${ctx.issue.suggestion ? `- **原建议**: ${ctx.issue.suggestion}` : ""}
|
|
4536
|
+
|
|
4537
|
+
## 当前文件内容
|
|
4538
|
+
|
|
4539
|
+
\`\`\`
|
|
4540
|
+
${linesWithNumbers}
|
|
4541
|
+
\`\`\`
|
|
4542
|
+
|
|
4543
|
+
请判断这个问题是否有效,以及是否已经被修复。`;
|
|
4544
|
+
return {
|
|
4545
|
+
systemPrompt,
|
|
4546
|
+
userPrompt
|
|
4547
|
+
};
|
|
4548
|
+
};
|
|
4549
|
+
|
|
4550
|
+
;// CONCATENATED MODULE: ./src/prompt/index.ts
|
|
4551
|
+
// 统一导出所有提示词
|
|
4552
|
+
// 类型定义
|
|
4553
|
+
// JSON Schemas
|
|
4554
|
+
|
|
4555
|
+
// 代码审查提示词
|
|
4556
|
+
|
|
4557
|
+
// PR 描述生成提示词
|
|
4558
|
+
|
|
4559
|
+
// 删除影响分析提示词
|
|
4560
|
+
|
|
4561
|
+
// 问题验证提示词
|
|
4562
|
+
|
|
4563
|
+
|
|
4564
|
+
;// CONCATENATED MODULE: ./src/system-rules/max-lines-per-file.ts
|
|
4565
|
+
|
|
4566
|
+
const RULE_ID = "system:max-lines-per-file";
|
|
4567
|
+
const SPEC_FILE = "__system__";
|
|
4568
|
+
function checkMaxLinesPerFile(changedFiles, fileContents, rule, round, verbose) {
|
|
4569
|
+
const [maxLine, severity] = rule;
|
|
4570
|
+
const staticIssues = [];
|
|
4571
|
+
const skippedFiles = new Set();
|
|
4572
|
+
if (maxLine <= 0) {
|
|
4573
|
+
return {
|
|
4574
|
+
staticIssues,
|
|
4575
|
+
skippedFiles
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
for (const file of changedFiles){
|
|
4579
|
+
if (file.status === "deleted" || !file.filename) continue;
|
|
4580
|
+
const filename = file.filename;
|
|
4581
|
+
const contentLines = fileContents.get(filename);
|
|
4582
|
+
if (!contentLines || contentLines.length <= maxLine) continue;
|
|
4583
|
+
if (shouldLog(verbose, 1)) {
|
|
4584
|
+
console.log(`⚠️ [system-rules/maxLinesPerFile] ${filename}: ${contentLines.length} 行超过限制 ${maxLine} 行,跳过 LLM 审查`);
|
|
4585
|
+
}
|
|
4586
|
+
skippedFiles.add(filename);
|
|
4587
|
+
staticIssues.push({
|
|
4588
|
+
file: filename,
|
|
4589
|
+
line: "1",
|
|
4590
|
+
code: "",
|
|
4591
|
+
ruleId: RULE_ID,
|
|
4592
|
+
specFile: SPEC_FILE,
|
|
4593
|
+
reason: `文件共 ${contentLines.length} 行,超过静态规则限制 ${maxLine} 行,已跳过 LLM 审查。请考虑拆分文件或调大 staticRules.maxLinesPerFile 配置。`,
|
|
4594
|
+
severity,
|
|
4595
|
+
round,
|
|
4596
|
+
date: new Date().toISOString()
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
return {
|
|
4600
|
+
staticIssues,
|
|
4601
|
+
skippedFiles
|
|
4602
|
+
};
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
;// CONCATENATED MODULE: ./src/system-rules/index.ts
|
|
4606
|
+
|
|
4607
|
+
|
|
4608
|
+
/**
|
|
4609
|
+
* 对变更文件执行所有已启用的静态规则检查。
|
|
4610
|
+
* 返回系统问题列表和需要跳过 LLM 审查的文件集合。
|
|
4611
|
+
*/ function applyStaticRules(changedFiles, fileContents, staticRules, round, verbose) {
|
|
4612
|
+
const staticIssues = [];
|
|
4613
|
+
const skippedFiles = new Set();
|
|
4614
|
+
if (!staticRules) {
|
|
4615
|
+
return {
|
|
4616
|
+
staticIssues,
|
|
4617
|
+
skippedFiles
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4620
|
+
if (staticRules.maxLinesPerFile) {
|
|
4621
|
+
const result = checkMaxLinesPerFile(changedFiles, fileContents, staticRules.maxLinesPerFile, round, verbose);
|
|
4622
|
+
staticIssues.push(...result.staticIssues);
|
|
4623
|
+
result.skippedFiles.forEach((f)=>skippedFiles.add(f));
|
|
4624
|
+
}
|
|
4625
|
+
return {
|
|
4626
|
+
staticIssues,
|
|
4627
|
+
skippedFiles
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
;// CONCATENATED MODULE: ./src/review-llm.ts
|
|
4632
|
+
|
|
4633
|
+
|
|
4634
|
+
|
|
4635
|
+
|
|
4636
|
+
|
|
4637
|
+
|
|
4638
|
+
|
|
4639
|
+
|
|
3886
4640
|
class ReviewLlmProcessor {
|
|
3887
4641
|
llmProxyService;
|
|
3888
4642
|
reviewSpecService;
|
|
@@ -3924,8 +4678,11 @@ class ReviewLlmProcessor {
|
|
|
3924
4678
|
return false;
|
|
3925
4679
|
}
|
|
3926
4680
|
// 如果有 includes 配置,检查文件名是否匹配 includes 模式
|
|
4681
|
+
// 需先提取纯 glob(去掉 added|/modified| 前缀,过滤 code-* 空串),避免 micromatch 报错
|
|
3927
4682
|
if (spec.includes.length > 0) {
|
|
3928
|
-
|
|
4683
|
+
const globs = extractGlobsFromIncludes(spec.includes);
|
|
4684
|
+
if (globs.length === 0) return true;
|
|
4685
|
+
return micromatch_0.isMatch(filename, globs, {
|
|
3929
4686
|
matchBase: true
|
|
3930
4687
|
});
|
|
3931
4688
|
}
|
|
@@ -3933,57 +4690,52 @@ class ReviewLlmProcessor {
|
|
|
3933
4690
|
return true;
|
|
3934
4691
|
});
|
|
3935
4692
|
}
|
|
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) {
|
|
4693
|
+
async buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResult, whenModifiedCode, verbose, systemRules) {
|
|
4694
|
+
const round = (existingResult?.round ?? 0) + 1;
|
|
4695
|
+
const { staticIssues, skippedFiles } = applyStaticRules(changedFiles, fileContents, systemRules, round, verbose);
|
|
3965
4696
|
const fileDataList = changedFiles.filter((f)=>f.status !== "deleted" && f.filename).map((file)=>{
|
|
3966
4697
|
const filename = file.filename;
|
|
4698
|
+
if (skippedFiles.has(filename)) return null;
|
|
3967
4699
|
const contentLines = fileContents.get(filename);
|
|
3968
4700
|
if (!contentLines) {
|
|
3969
4701
|
return {
|
|
3970
4702
|
filename,
|
|
3971
4703
|
file,
|
|
3972
|
-
|
|
4704
|
+
contentLines: null,
|
|
3973
4705
|
commitsSection: "- 无相关 commits"
|
|
3974
4706
|
};
|
|
3975
4707
|
}
|
|
3976
|
-
const linesWithNumbers = buildLinesWithNumbers(contentLines);
|
|
3977
4708
|
const commitsSection = buildCommitsSection(contentLines, commits);
|
|
3978
4709
|
return {
|
|
3979
4710
|
filename,
|
|
3980
4711
|
file,
|
|
3981
|
-
|
|
4712
|
+
contentLines,
|
|
3982
4713
|
commitsSection
|
|
3983
4714
|
};
|
|
3984
4715
|
});
|
|
3985
|
-
const filePrompts = await Promise.all(fileDataList.map(async ({ filename, file,
|
|
4716
|
+
const filePrompts = await Promise.all(fileDataList.filter((item)=>item !== null).map(async ({ filename, file, contentLines, commitsSection })=>{
|
|
3986
4717
|
const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
|
|
4718
|
+
// 根据文件过滤 specs,只注入与当前文件匹配的规则
|
|
4719
|
+
const fileSpecs = this.filterSpecsForFile(specs, filename);
|
|
4720
|
+
// 从全局 whenModifiedCode 配置中解析代码结构过滤类型
|
|
4721
|
+
const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
|
|
4722
|
+
// 构建带行号的内容:有 code-* 过滤时只输出匹配的代码块范围
|
|
4723
|
+
let linesWithNumbers;
|
|
4724
|
+
if (!contentLines) {
|
|
4725
|
+
linesWithNumbers = "(无法获取内容)";
|
|
4726
|
+
} else if (codeBlockTypes.length > 0) {
|
|
4727
|
+
const visibleRanges = extractCodeBlocks(contentLines, codeBlockTypes);
|
|
4728
|
+
// 如果配置了 whenModifiedCode 但没有匹配的代码块,跳过这个文件
|
|
4729
|
+
if (visibleRanges.length === 0) {
|
|
4730
|
+
if (shouldLog(verbose, 2)) {
|
|
4731
|
+
console.log(`[buildReviewPrompt] ${filename}: 没有匹配的 ${codeBlockTypes.join(", ")} 代码块,跳过审查`);
|
|
4732
|
+
}
|
|
4733
|
+
return null;
|
|
4734
|
+
}
|
|
4735
|
+
linesWithNumbers = buildLinesWithNumbers(contentLines, visibleRanges);
|
|
4736
|
+
} else {
|
|
4737
|
+
linesWithNumbers = buildLinesWithNumbers(contentLines);
|
|
4738
|
+
}
|
|
3987
4739
|
// 获取该文件上一次的审查结果
|
|
3988
4740
|
const existingFileSummary = existingResult?.summary?.find((s)=>s.file === filename);
|
|
3989
4741
|
const existingFileIssues = existingResult?.issues?.filter((i)=>i.file === filename) ?? [];
|
|
@@ -4005,37 +4757,27 @@ ${specsSection}
|
|
|
4005
4757
|
}
|
|
4006
4758
|
previousReviewSection = parts.join("\n");
|
|
4007
4759
|
}
|
|
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
4760
|
const specsSection = this.reviewSpecService.buildSpecsSection(fileSpecs);
|
|
4030
|
-
const systemPrompt =
|
|
4761
|
+
const { systemPrompt, userPrompt } = buildFileReviewPrompt({
|
|
4762
|
+
filename,
|
|
4763
|
+
status: file.status,
|
|
4764
|
+
linesWithNumbers,
|
|
4765
|
+
commitsSection,
|
|
4766
|
+
fileDirectoryInfo,
|
|
4767
|
+
previousReviewSection,
|
|
4768
|
+
specsSection
|
|
4769
|
+
});
|
|
4031
4770
|
return {
|
|
4032
4771
|
filename,
|
|
4033
4772
|
systemPrompt,
|
|
4034
4773
|
userPrompt
|
|
4035
4774
|
};
|
|
4036
4775
|
}));
|
|
4776
|
+
// 过滤掉 null 值(跳过的文件)
|
|
4777
|
+
const validFilePrompts = filePrompts.filter((fp)=>fp !== null);
|
|
4037
4778
|
return {
|
|
4038
|
-
filePrompts
|
|
4779
|
+
filePrompts: validFilePrompts,
|
|
4780
|
+
staticIssues: staticIssues.length > 0 ? staticIssues : undefined
|
|
4039
4781
|
};
|
|
4040
4782
|
}
|
|
4041
4783
|
async runLLMReview(llmMode, reviewPrompt, options = {}) {
|
|
@@ -4227,53 +4969,19 @@ ${previousReviewSection}`;
|
|
|
4227
4969
|
});
|
|
4228
4970
|
}
|
|
4229
4971
|
/**
|
|
4230
|
-
* 使用 AI 根据 commits、变更文件和代码内容总结 PR 实现的功能
|
|
4231
|
-
* @returns 包含 title 和 description 的对象
|
|
4232
|
-
*/ 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}`;
|
|
4972
|
+
* 使用 AI 根据 commits、变更文件和代码内容总结 PR 实现的功能
|
|
4973
|
+
* @returns 包含 title 和 description 的对象
|
|
4974
|
+
*/ async generatePrDescription(commits, changedFiles, llmMode, fileContents, verbose) {
|
|
4975
|
+
const { userPrompt } = buildPrDescriptionPrompt({
|
|
4976
|
+
commits,
|
|
4977
|
+
changedFiles,
|
|
4978
|
+
fileContents
|
|
4979
|
+
});
|
|
4272
4980
|
try {
|
|
4273
4981
|
const stream = this.llmProxyService.chatStream([
|
|
4274
4982
|
{
|
|
4275
4983
|
role: "user",
|
|
4276
|
-
content:
|
|
4984
|
+
content: userPrompt
|
|
4277
4985
|
}
|
|
4278
4986
|
], {
|
|
4279
4987
|
adapter: llmMode
|
|
@@ -4304,25 +5012,15 @@ ${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` :
|
|
|
4304
5012
|
/**
|
|
4305
5013
|
* 使用 LLM 生成 PR 标题
|
|
4306
5014
|
*/ 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 || "无"}`;
|
|
5015
|
+
const { userPrompt } = buildPrTitlePrompt({
|
|
5016
|
+
commits,
|
|
5017
|
+
changedFiles
|
|
5018
|
+
});
|
|
4321
5019
|
try {
|
|
4322
5020
|
const stream = this.llmProxyService.chatStream([
|
|
4323
5021
|
{
|
|
4324
5022
|
role: "user",
|
|
4325
|
-
content:
|
|
5023
|
+
content: userPrompt
|
|
4326
5024
|
}
|
|
4327
5025
|
], {
|
|
4328
5026
|
adapter: "openai"
|
|
@@ -4386,6 +5084,7 @@ ${fileChanges || "无"}`;
|
|
|
4386
5084
|
|
|
4387
5085
|
|
|
4388
5086
|
|
|
5087
|
+
|
|
4389
5088
|
class ReviewService {
|
|
4390
5089
|
gitProvider;
|
|
4391
5090
|
config;
|
|
@@ -4451,6 +5150,10 @@ class ReviewService {
|
|
|
4451
5150
|
// 2. 规则匹配
|
|
4452
5151
|
const specs = await this.issueFilter.loadSpecs(specSources, verbose);
|
|
4453
5152
|
const applicableSpecs = this.reviewSpecService.filterApplicableSpecs(specs, changedFiles);
|
|
5153
|
+
if (shouldLog(verbose, 2)) {
|
|
5154
|
+
console.log(`[execute] loadSpecs: loaded ${specs.length} specs from sources: ${JSON.stringify(specSources)}`);
|
|
5155
|
+
console.log(`[execute] filterApplicableSpecs: ${applicableSpecs.length} applicable out of ${specs.length}, changedFiles=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
|
|
5156
|
+
}
|
|
4454
5157
|
if (shouldLog(verbose, 1)) {
|
|
4455
5158
|
console.log(` 适用的规则文件: ${applicableSpecs.length}`);
|
|
4456
5159
|
}
|
|
@@ -4471,7 +5174,7 @@ class ReviewService {
|
|
|
4471
5174
|
if (shouldLog(verbose, 1)) {
|
|
4472
5175
|
console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
|
|
4473
5176
|
}
|
|
4474
|
-
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null);
|
|
5177
|
+
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null, context.whenModifiedCode, verbose, context.systemRules);
|
|
4475
5178
|
const result = await this.runLLMReview(llmMode, reviewPrompt, {
|
|
4476
5179
|
verbose,
|
|
4477
5180
|
concurrency: context.concurrency,
|
|
@@ -4495,6 +5198,16 @@ class ReviewService {
|
|
|
4495
5198
|
isDirectFileMode,
|
|
4496
5199
|
context
|
|
4497
5200
|
});
|
|
5201
|
+
// 静态规则产生的系统问题直接合并,不经过过滤管道
|
|
5202
|
+
if (reviewPrompt.staticIssues?.length) {
|
|
5203
|
+
result.issues = [
|
|
5204
|
+
...reviewPrompt.staticIssues,
|
|
5205
|
+
...result.issues
|
|
5206
|
+
];
|
|
5207
|
+
if (shouldLog(verbose, 1)) {
|
|
5208
|
+
console.log(`⚙️ 追加 ${reviewPrompt.staticIssues.length} 个静态规则系统问题`);
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
4498
5211
|
if (shouldLog(verbose, 1)) {
|
|
4499
5212
|
console.log(`📝 最终发现 ${result.issues.length} 个问题`);
|
|
4500
5213
|
}
|
|
@@ -4516,7 +5229,7 @@ class ReviewService {
|
|
|
4516
5229
|
* 包含前置过滤(merge commit、files、commits、includes)。
|
|
4517
5230
|
* 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
|
|
4518
5231
|
*/ async resolveSourceData(context) {
|
|
4519
|
-
const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode,
|
|
5232
|
+
const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode, duplicateWorkflowResolved } = context;
|
|
4520
5233
|
const isDirectFileMode = !!(files && files.length > 0 && baseRef === headRef);
|
|
4521
5234
|
let isLocalMode = !!localMode;
|
|
4522
5235
|
let effectiveBaseRef = baseRef;
|
|
@@ -4591,9 +5304,9 @@ class ReviewService {
|
|
|
4591
5304
|
console.log(` Changed files: ${changedFiles.length}`);
|
|
4592
5305
|
}
|
|
4593
5306
|
// 检查是否有其他同名 review workflow 正在运行中
|
|
4594
|
-
if (
|
|
4595
|
-
const
|
|
4596
|
-
if (
|
|
5307
|
+
if (duplicateWorkflowResolved !== "off" && ci && prInfo?.head?.sha) {
|
|
5308
|
+
const duplicateResult = await this.checkDuplicateWorkflow(prModel, prInfo.head.sha, duplicateWorkflowResolved, verbose);
|
|
5309
|
+
if (duplicateResult) {
|
|
4597
5310
|
return {
|
|
4598
5311
|
prModel,
|
|
4599
5312
|
commits,
|
|
@@ -4601,7 +5314,7 @@ class ReviewService {
|
|
|
4601
5314
|
headSha: prInfo.head.sha,
|
|
4602
5315
|
isLocalMode,
|
|
4603
5316
|
isDirectFileMode,
|
|
4604
|
-
earlyReturn:
|
|
5317
|
+
earlyReturn: duplicateResult
|
|
4605
5318
|
};
|
|
4606
5319
|
}
|
|
4607
5320
|
}
|
|
@@ -4677,10 +5390,19 @@ class ReviewService {
|
|
|
4677
5390
|
// 3. 使用 includes 过滤文件和 commits(支持 added|/modified|/deleted| 前缀语法)
|
|
4678
5391
|
if (includes && includes.length > 0) {
|
|
4679
5392
|
const beforeFiles = changedFiles.length;
|
|
5393
|
+
if (shouldLog(verbose, 2)) {
|
|
5394
|
+
console.log(`[resolveSourceData] filterFilesByIncludes: before=${JSON.stringify(changedFiles.map((f)=>({
|
|
5395
|
+
filename: f.filename,
|
|
5396
|
+
status: f.status
|
|
5397
|
+
})))}, includes=${JSON.stringify(includes)}`);
|
|
5398
|
+
}
|
|
4680
5399
|
changedFiles = filterFilesByIncludes(changedFiles, includes);
|
|
4681
5400
|
if (shouldLog(verbose, 1)) {
|
|
4682
5401
|
console.log(` Includes 过滤文件: ${beforeFiles} -> ${changedFiles.length} 个文件`);
|
|
4683
5402
|
}
|
|
5403
|
+
if (shouldLog(verbose, 2)) {
|
|
5404
|
+
console.log(`[resolveSourceData] filterFilesByIncludes: after=${JSON.stringify(changedFiles.map((f)=>f.filename))}`);
|
|
5405
|
+
}
|
|
4684
5406
|
const globs = extractGlobsFromIncludes(includes);
|
|
4685
5407
|
const beforeCommits = commits.length;
|
|
4686
5408
|
const filteredCommits = [];
|
|
@@ -5012,7 +5734,8 @@ class ReviewService {
|
|
|
5012
5734
|
}
|
|
5013
5735
|
/**
|
|
5014
5736
|
* 检查是否有其他同名 review workflow 正在运行中
|
|
5015
|
-
|
|
5737
|
+
* 根据 duplicateWorkflowResolved 配置决定是跳过还是删除旧评论
|
|
5738
|
+
*/ async checkDuplicateWorkflow(prModel, headSha, mode, verbose) {
|
|
5016
5739
|
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
5017
5740
|
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
5018
5741
|
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prModel.number;
|
|
@@ -5024,6 +5747,16 @@ class ReviewService {
|
|
|
5024
5747
|
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
5025
5748
|
const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
|
|
5026
5749
|
if (duplicateReviewRuns.length > 0) {
|
|
5750
|
+
if (mode === "delete") {
|
|
5751
|
+
// 删除模式:清理旧的 AI Review 评论和 PR Review
|
|
5752
|
+
if (shouldLog(verbose, 1)) {
|
|
5753
|
+
console.log(`🗑️ 检测到 ${duplicateReviewRuns.length} 个同名 workflow,清理旧的 AI Review 评论...`);
|
|
5754
|
+
}
|
|
5755
|
+
await this.cleanupDuplicateAiReviews(prModel, verbose);
|
|
5756
|
+
// 清理后继续执行当前审查
|
|
5757
|
+
return null;
|
|
5758
|
+
}
|
|
5759
|
+
// 跳过模式(默认)
|
|
5027
5760
|
if (shouldLog(verbose, 1)) {
|
|
5028
5761
|
console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
|
|
5029
5762
|
}
|
|
@@ -5042,6 +5775,50 @@ class ReviewService {
|
|
|
5042
5775
|
}
|
|
5043
5776
|
return null;
|
|
5044
5777
|
}
|
|
5778
|
+
/**
|
|
5779
|
+
* 清理重复的 AI Review 评论(Issue Comments 和 PR Reviews)
|
|
5780
|
+
*/ async cleanupDuplicateAiReviews(prModel, verbose) {
|
|
5781
|
+
try {
|
|
5782
|
+
// 删除 Issue Comments(主评论)
|
|
5783
|
+
const comments = await prModel.getComments();
|
|
5784
|
+
const aiComments = comments.filter((c)=>c.body?.includes(REVIEW_COMMENT_MARKER));
|
|
5785
|
+
let deletedComments = 0;
|
|
5786
|
+
for (const comment of aiComments){
|
|
5787
|
+
if (comment.id) {
|
|
5788
|
+
try {
|
|
5789
|
+
await prModel.deleteComment(comment.id);
|
|
5790
|
+
deletedComments++;
|
|
5791
|
+
} catch {
|
|
5792
|
+
// 忽略删除失败
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
if (deletedComments > 0 && shouldLog(verbose, 1)) {
|
|
5797
|
+
console.log(` 已删除 ${deletedComments} 个重复的 AI Review 主评论`);
|
|
5798
|
+
}
|
|
5799
|
+
// 删除 PR Reviews(行级评论)
|
|
5800
|
+
const reviews = await prModel.getReviews();
|
|
5801
|
+
const aiReviews = reviews.filter((r)=>r.body?.includes(REVIEW_LINE_COMMENTS_MARKER));
|
|
5802
|
+
let deletedReviews = 0;
|
|
5803
|
+
for (const review of aiReviews){
|
|
5804
|
+
if (review.id) {
|
|
5805
|
+
try {
|
|
5806
|
+
await prModel.deleteReview(review.id);
|
|
5807
|
+
deletedReviews++;
|
|
5808
|
+
} catch {
|
|
5809
|
+
// 已提交的 review 无法删除,忽略
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
if (deletedReviews > 0 && shouldLog(verbose, 1)) {
|
|
5814
|
+
console.log(` 已删除 ${deletedReviews} 个重复的 AI Review PR Review`);
|
|
5815
|
+
}
|
|
5816
|
+
} catch (error) {
|
|
5817
|
+
if (shouldLog(verbose, 1)) {
|
|
5818
|
+
console.warn(`⚠️ 清理旧评论失败:`, error instanceof Error ? error.message : error);
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5045
5822
|
// --- Delegation methods for backward compatibility with tests ---
|
|
5046
5823
|
async fillIssueAuthors(...args) {
|
|
5047
5824
|
return this.contextBuilder.fillIssueAuthors(...args);
|
|
@@ -5114,31 +5891,9 @@ class ReviewService {
|
|
|
5114
5891
|
|
|
5115
5892
|
;// CONCATENATED MODULE: ./src/issue-verify.service.ts
|
|
5116
5893
|
|
|
5894
|
+
|
|
5117
5895
|
const TRUE = "true";
|
|
5118
5896
|
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
5897
|
class IssueVerifyService {
|
|
5143
5898
|
llmProxyService;
|
|
5144
5899
|
reviewSpecService;
|
|
@@ -5211,13 +5966,13 @@ class IssueVerifyService {
|
|
|
5211
5966
|
}
|
|
5212
5967
|
}
|
|
5213
5968
|
});
|
|
5214
|
-
const results = await executor.map(toVerify, async ({ issue, fileContent, ruleInfo })=>this.verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose), ({ issue })=>`${issue.file}:${issue.line}`);
|
|
5969
|
+
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
5970
|
for (const result of results){
|
|
5216
5971
|
if (result.success && result.result) {
|
|
5217
5972
|
verifiedIssues.push(result.result);
|
|
5218
5973
|
} else {
|
|
5219
5974
|
// 失败时保留原始 issue
|
|
5220
|
-
const originalItem = toVerify.find((item)=>`${item.issue.file}:${item.issue.line}` === result.id);
|
|
5975
|
+
const originalItem = toVerify.find((item)=>`${item.issue.file}:${item.issue.line}:${item.issue.ruleId}` === result.id);
|
|
5221
5976
|
if (originalItem) {
|
|
5222
5977
|
verifiedIssues.push(originalItem.issue);
|
|
5223
5978
|
}
|
|
@@ -5233,7 +5988,15 @@ class IssueVerifyService {
|
|
|
5233
5988
|
/**
|
|
5234
5989
|
* 验证单个 issue 是否已修复
|
|
5235
5990
|
*/ async verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose) {
|
|
5236
|
-
const
|
|
5991
|
+
const specsSection = ruleInfo ? this.reviewSpecService.buildSpecsSection([
|
|
5992
|
+
ruleInfo.spec
|
|
5993
|
+
]) : "";
|
|
5994
|
+
const verifyPrompt = buildIssueVerifyPrompt({
|
|
5995
|
+
issue,
|
|
5996
|
+
fileContent,
|
|
5997
|
+
ruleInfo,
|
|
5998
|
+
specsSection
|
|
5999
|
+
});
|
|
5237
6000
|
try {
|
|
5238
6001
|
const stream = this.llmProxyService.chatStream([
|
|
5239
6002
|
{
|
|
@@ -5294,66 +6057,18 @@ class IssueVerifyService {
|
|
|
5294
6057
|
}
|
|
5295
6058
|
}
|
|
5296
6059
|
/**
|
|
6060
|
+
* @deprecated 使用 prompt/issue-verify.ts 中的 buildIssueVerifyPrompt
|
|
5297
6061
|
* 构建验证单个 issue 是否已修复的 prompt
|
|
5298
6062
|
*/ 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
|
-
};
|
|
6063
|
+
const specsSection = ruleInfo ? this.reviewSpecService.buildSpecsSection([
|
|
6064
|
+
ruleInfo.spec
|
|
6065
|
+
]) : "";
|
|
6066
|
+
return buildIssueVerifyPrompt({
|
|
6067
|
+
issue,
|
|
6068
|
+
fileContent,
|
|
6069
|
+
ruleInfo,
|
|
6070
|
+
specsSection
|
|
6071
|
+
});
|
|
5357
6072
|
}
|
|
5358
6073
|
}
|
|
5359
6074
|
|
|
@@ -5362,69 +6077,7 @@ ${linesWithNumbers}
|
|
|
5362
6077
|
|
|
5363
6078
|
|
|
5364
6079
|
|
|
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
|
-
};
|
|
6080
|
+
|
|
5428
6081
|
class DeletionImpactService {
|
|
5429
6082
|
llmProxyService;
|
|
5430
6083
|
gitProvider;
|
|
@@ -5853,43 +6506,10 @@ class DeletionImpactService {
|
|
|
5853
6506
|
* 使用 LLM 分析删除代码的影响
|
|
5854
6507
|
*/ async analyzeWithLLM(deletedBlocks, references, llmMode, verbose) {
|
|
5855
6508
|
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
|
-
请分析这些删除操作可能带来的影响。`;
|
|
6509
|
+
const { systemPrompt, userPrompt } = buildDeletionImpactPrompt({
|
|
6510
|
+
deletedBlocks,
|
|
6511
|
+
references
|
|
6512
|
+
});
|
|
5893
6513
|
if (shouldLog(verbose, 2)) {
|
|
5894
6514
|
console.log(`\nsystemPrompt:\n----------------\n${systemPrompt}\n----------------`);
|
|
5895
6515
|
console.log(`\nuserPrompt:\n----------------\n${userPrompt}\n----------------`);
|
|
@@ -5945,54 +6565,10 @@ ${deletedCodeSection}
|
|
|
5945
6565
|
* Claude Agent 可以使用工具主动探索代码库,分析更深入
|
|
5946
6566
|
*/ async analyzeWithAgent(analysisMode, deletedBlocks, references, verbose) {
|
|
5947
6567
|
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
|
-
- 分析完成后,给出结构化的影响评估`;
|
|
6568
|
+
const { systemPrompt, userPrompt } = buildDeletionImpactAgentPrompt({
|
|
6569
|
+
deletedBlocks,
|
|
6570
|
+
references
|
|
6571
|
+
});
|
|
5996
6572
|
if (shouldLog(verbose, 2)) {
|
|
5997
6573
|
console.log(`\n[Agent Mode] systemPrompt:\n----------------\n${systemPrompt}\n----------------`);
|
|
5998
6574
|
console.log(`\n[Agent Mode] userPrompt:\n----------------\n${userPrompt}\n----------------`);
|
|
@@ -6169,6 +6745,7 @@ ${deletedCodeSection}
|
|
|
6169
6745
|
|
|
6170
6746
|
|
|
6171
6747
|
|
|
6748
|
+
|
|
6172
6749
|
/** MCP 工具输入 schema */ const listRulesInputSchema = z.object({});
|
|
6173
6750
|
const getRulesForFileInputSchema = z.object({
|
|
6174
6751
|
filePath: z.string().describe(t("review:mcp.dto.filePath")),
|
|
@@ -6275,7 +6852,9 @@ const tools = [
|
|
|
6275
6852
|
const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{
|
|
6276
6853
|
const includes = rule.includes || spec.includes;
|
|
6277
6854
|
if (includes.length === 0) return true;
|
|
6278
|
-
|
|
6855
|
+
const globs = extractGlobsFromIncludes(includes);
|
|
6856
|
+
if (globs.length === 0) return true;
|
|
6857
|
+
return micromatch.isMatch(filePath, globs, {
|
|
6279
6858
|
matchBase: true
|
|
6280
6859
|
});
|
|
6281
6860
|
}).map((rule)=>({
|