@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/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
- return specs.filter((spec)=>spec.extensions.some((ext)=>changedExtensions.has(ext)));
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 matches = micromatch_0.isMatch(issue.file, includes, {
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
- return issueFile && micromatch_0.isMatch(issueFile, scoped.includes, {
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-utils.ts
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
- const matchedIssue = this._result.issues.find((issue)=>issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position));
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
- skipDuplicateWorkflow: z.boolean().default(false).optional(),
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: options.includes ?? titleOptions.includes ?? reviewConf.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
- skipDuplicateWorkflow: options.skipDuplicateWorkflow ?? reviewConf.skipDuplicateWorkflow ?? false,
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 author = shortHash && !shortHash.includes("---") ? commitAuthorMap.get(shortHash) : undefined;
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-includes-filter.ts
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
- * 解析单条 include 模式,拆分 status 前缀和 glob。
3899
+ * 构建带行号的文件内容字符串。
3693
3900
  *
3694
- * 只有当 `|` 前面的部分是已知 status 关键字时才视为前缀,否则当作普通 glob 处理(容错),
3695
- * 这样可以避免误解析 extglob 语法中含 `|` 的模式(如 `+(*.ts|*.js)`)。
3696
- * 排除模式(以 `!` 开头)始终作为普通 glob 处理。
3697
- */ function parseIncludePattern(pattern) {
3698
- if (pattern.startsWith("!")) {
3699
- return {
3700
- status: undefined,
3701
- glob: pattern
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
- const separatorIndex = pattern.indexOf("|");
3705
- if (separatorIndex === -1) {
3706
- return {
3707
- status: undefined,
3708
- glob: pattern
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 prefix = pattern.slice(0, separatorIndex).trim().toLowerCase();
3712
- const glob = pattern.slice(separatorIndex + 1).trim();
3713
- const status = STATUS_ALIAS[prefix];
3714
- if (!status) {
3715
- // 前缀无法识别(如 extglob 中的 `|`),当作普通 glob 处理
3716
- return {
3717
- status: undefined,
3718
- glob: pattern
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
- return {
3722
- status,
3723
- glob
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
- * 根据 includes 模式列表过滤文件,支持 `status|glob` 前缀语法。
3950
+ * contentLines 中提取新增代码里的指定结构类型的行号范围。
3728
3951
  *
3729
- * 算法:
3730
- * 1. includes 拆分为:排除模式(`!`)、无前缀正向 glob、有 status 前缀 glob
3731
- * 2. 每个文件先检查是否命中任意正向条件(无前缀 glob 或匹配 status 的前缀 glob)
3732
- * 3. 最后用排除模式做全局过滤(排除模式始终优先)
3952
+ * 逻辑:
3953
+ * 1. 只考虑 hash !== "-------" 的新增行
3954
+ * 2. 用各类型的正则匹配结构开头行,再用层级计数找到结尾行
3955
+ * 3. 返回行号范围列表 [startLine, endLine](从 1 计)
3733
3956
  *
3734
- * @param files 待过滤的文件列表
3735
- * @param includes include 模式列表,支持 `added|*.ts`、`modified|*.ts`、`deleted|*.ts` 前缀
3736
- * @returns 匹配的文件列表
3737
- */ function filterFilesByIncludes(files, includes) {
3738
- if (!includes || includes.length === 0) return files;
3739
- const parsed = includes.map(parseIncludePattern);
3740
- // 排除模式(以 ! 开头),用于最终全局过滤
3741
- const negativeGlobs = parsed.filter((p)=>p.status === undefined && p.glob.startsWith("!")).map((p)=>p.glob.slice(1)); // 去掉 ! 前缀,用 micromatch.not 处理
3742
- // 无前缀的正向 globs
3743
- const plainGlobs = parsed.filter((p)=>p.status === undefined && !p.glob.startsWith("!")).map((p)=>p.glob);
3744
- // 有 status 前缀的 patterns
3745
- const statusPatterns = parsed.filter((p)=>p.status !== undefined);
3746
- return files.filter((file)=>{
3747
- const filename = file.filename ?? "";
3748
- if (!filename) return false;
3749
- // 最终排除:命中排除模式的文件直接过滤掉
3750
- if (negativeGlobs.length > 0 && micromatch_0.isMatch(filename, negativeGlobs, {
3751
- matchBase: true
3752
- })) {
3753
- return false;
3754
- }
3755
- // 正向匹配:无前缀 glob
3756
- if (plainGlobs.length > 0 && micromatch_0.isMatch(filename, plainGlobs, {
3757
- matchBase: true
3758
- })) {
3759
- return true;
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
- // 正向匹配:有 status 前缀的 glob,按文件实际 status 过滤
3762
- // glob 可以带 ! 前缀表示在该 status 范围内排除,如 added|!**/*.spec.ts
3763
- if (statusPatterns.length > 0) {
3764
- const fileStatus = STATUS_ALIAS[file.status?.toLowerCase() ?? ""] ?? "modified";
3765
- // status 分组,每组内正向 glob + 排除 glob 合并后批量匹配
3766
- const matchingStatusGlobs = statusPatterns.filter(({ status })=>status === fileStatus).map(({ glob })=>glob);
3767
- if (matchingStatusGlobs.length > 0) {
3768
- // 有正向 glob 才有意义,纯排除 glob 组合 micromatch 会视为全匹配再排除
3769
- const positiveGlobs = matchingStatusGlobs.filter((g)=>!g.startsWith("!"));
3770
- const negativeStatusGlobs = matchingStatusGlobs.filter((g)=>g.startsWith("!")).map((g)=>g.slice(1));
3771
- if (positiveGlobs.length > 0) {
3772
- const matchesPositive = micromatch_0.isMatch(filename, positiveGlobs, {
3773
- matchBase: true
3774
- });
3775
- const matchesNegative = negativeStatusGlobs.length > 0 && micromatch_0.isMatch(filename, negativeStatusGlobs, {
3776
- matchBase: true
3777
- });
3778
- if (matchesPositive && !matchesNegative) return true;
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
- return false;
3783
- });
4008
+ }
4009
+ return lines.length; // 没有找到匹配括号,返回文件末尾
3784
4010
  }
3785
- /**
3786
- * includes 模式列表中提取纯 glob(用于 commit 过滤,commit 没有 status 概念)。
3787
- * status 前缀的模式会去掉前缀,仅保留 glob 部分。
3788
- */ function extractGlobsFromIncludes(includes) {
3789
- return includes.map((p)=>parseIncludePattern(p).glob);
3790
- }
3791
-
3792
- ;// CONCATENATED MODULE: ./src/utils/review-llm.ts
3793
- function buildLinesWithNumbers(contentLines) {
3794
- const padWidth = String(contentLines.length).length;
3795
- return contentLines.map(([hash, line], index)=>{
3796
- const lineNum = index + 1;
3797
- return `${hash} ${String(lineNum).padStart(padWidth)}| ${line}`;
3798
- }).join("\n");
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/review-llm.ts
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
- return micromatch_0.isMatch(filename, spec.includes, {
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
- * 构建 systemPrompt
3938
- */ buildSystemPrompt(specsSection) {
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
- linesWithNumbers: "(无法获取内容)",
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
- linesWithNumbers,
4712
+ contentLines,
3982
4713
  commitsSection
3983
4714
  };
3984
4715
  });
3985
- const filePrompts = await Promise.all(fileDataList.map(async ({ filename, file, linesWithNumbers, commitsSection })=>{
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 = this.buildSystemPrompt(specsSection);
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 commitMessages = commits.map((c)=>`- ${c.sha?.slice(0, 7)}: ${c.commit?.message?.split("\n")[0]}`).join("\n");
4234
- const fileChanges = changedFiles.slice(0, 30).map((f)=>`- ${f.filename} (${f.status})`).join("\n");
4235
- // 构建代码变更内容(只包含变更行,限制总长度)
4236
- let codeChangesSection = "";
4237
- if (fileContents && fileContents.size > 0) {
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: prompt
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 commitMessages = commits.slice(0, 10).map((c)=>c.commit?.message?.split("\n")[0]).filter(Boolean).join("\n");
4308
- const fileChanges = changedFiles.slice(0, 20).map((f)=>`${f.filename} (${f.status})`).join("\n");
4309
- const prompt = `请根据以下 commit 记录和文件变更,生成一个简短的 PR 标题。
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: prompt
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, skipDuplicateWorkflow } = context;
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 (skipDuplicateWorkflow && ci && prInfo?.head?.sha) {
4595
- const skipResult = await this.checkDuplicateWorkflow(prModel, prInfo.head.sha, verbose);
4596
- if (skipResult) {
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: skipResult
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
- */ async checkDuplicateWorkflow(prModel, headSha, verbose) {
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 verifyPrompt = this.buildVerifyPrompt(issue, fileContent, ruleInfo);
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 padWidth = String(fileContent.length).length;
5300
- const linesWithNumbers = fileContent.map(([, line], index)=>`${String(index + 1).padStart(padWidth)}| ${line}`).join("\n");
5301
- const systemPrompt = `你是一个代码审查专家。你的任务是判断之前发现的一个代码问题:
5302
- 1. 是否有效(是否真的违反了规则)
5303
- 2. 是否已经被修复
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
- const DELETION_IMPACT_SCHEMA = {
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
- return micromatch.isMatch(filePath, includes, {
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)=>({