@spaceflow/review 0.77.0 → 0.79.0

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