smart-review 1.0.1 → 1.0.2

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/lib/ai-client.js CHANGED
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { prepareForAIWithLineMap } from './utils/strip.js';
5
5
  import { logger } from './utils/logger.js';
6
6
  import { AI_CONSTANTS, HTTP_STATUS } from './utils/constants.js';
7
+ import { buildPrompts, t, getLocale, FIELD_LABELS, displayRisk } from './utils/i18n.js';
7
8
 
8
9
  export class AIClient {
9
10
  static nodeVersionWarned = false; // 静态变量,确保只警告一次
@@ -28,13 +29,13 @@ export class AIClient {
28
29
  const { apiKey, baseURL } = this.config;
29
30
 
30
31
  if (!apiKey) {
31
- throw new Error('未配置AI API密钥');
32
+ throw new Error(t(this.config, 'no_api_key'));
32
33
  }
33
34
 
34
35
  // 环境检测:OpenAI SDK 推荐 Node >=18(内置 fetch)。
35
36
  const nodeMajor = Number(process.versions.node.split('.')[0]);
36
37
  if ((nodeMajor < 18 || typeof fetch === 'undefined') && !AIClient.nodeVersionWarned) {
37
- logger.warn('检测到 Node 版本 < 18 或缺少全局 fetch,可能导致连接异常。建议升级到 Node >=18。');
38
+ logger.warn(t(this.config, 'node_version_warn'));
38
39
  AIClient.nodeVersionWarned = true;
39
40
  }
40
41
 
@@ -51,7 +52,10 @@ export class AIClient {
51
52
  // 如果是大文件分段批次,改走分段整体分析路径,确保行号为绝对源行号
52
53
  if (originalBatch?.isLargeFileSegment) {
53
54
  try {
54
- logger.debug(`检测到分段批次,改用分段整体分析:${originalBatch.segmentedFile}(${originalBatch.totalSegments}段)`);
55
+ logger.debug(t(this.config, 'detected_segmented_batch_dbg', {
56
+ path: originalBatch.segmentedFile,
57
+ segments: originalBatch.totalSegments
58
+ }));
55
59
  } catch (e) {}
56
60
  const result = await this.handleSegmentBatch(originalBatch);
57
61
  return {
@@ -69,16 +73,16 @@ export class AIClient {
69
73
  const cfg = this.config?.ai || {};
70
74
  const includeStaticHints = cfg.includeStaticHints === true;
71
75
  const customPrompts = await this.readCustomPrompts();
76
+ const loc = getLocale(this.config);
77
+ const L = FIELD_LABELS[loc];
78
+ const batchIntro = t(this.config, 'batch_intro');
72
79
  const messages = [
73
80
  { role: 'system', content: this.getSystemPrompt() },
74
- {
75
- role: 'user',
76
- content: `我会发送一个批次的文件进行代码审查。其中可能包含分段文件(大文件被分成多段)。对于分段文件,请在收到所有段后进行整体分析。每个问题用空行分隔,务必包含"文件路径:绝对路径"与代码片段,且禁止任何"第X行/第X-Y行"等行号或行范围描述。`
77
- }
81
+ { role: 'user', content: batchIntro }
78
82
  ];
79
83
 
80
84
  if (customPrompts.length > 0) {
81
- messages.push({ role: 'user', content: `\n[自定义提示词]\n${customPrompts.join('\n\n---\n')}` });
85
+ messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
82
86
  }
83
87
  // 处理每个文件
84
88
  const requestPreviews = [];
@@ -89,7 +93,7 @@ export class AIClient {
89
93
  const contentForAI = attachLineNumbers ? this.addLineNumberPrefixes(clean, lineMap) : clean;
90
94
  messages.push({
91
95
  role: 'user',
92
- content: `文件路径:${file.filePath}\n代码内容:\n\`\`\`\n${contentForAI}\n\`\`\``
96
+ content: `${L.file}${file.filePath}\n${L.content}\n\`\`\`\n${contentForAI}\n\`\`\``
93
97
  });
94
98
  requestPreviews.push({ filePath: file.filePath, contentForAI });
95
99
  }
@@ -99,8 +103,19 @@ export class AIClient {
99
103
  for (const file of batchData.files) {
100
104
  const staticIssues = file.staticIssues || [];
101
105
  if (staticIssues.length === 0) continue;
102
- const lines = staticIssues.map((i, idx) => `${idx + 1}. 片段(${i.risk}):${i.message}${i.suggestion ? `;建议:${i.suggestion}` : ''};代码片段:${i.snippet || ''}`);
103
- hintsParts.push(`[本地规则发现的问题 - ${file.filePath}]\n${lines.join('\n')}`);
106
+ const lines = staticIssues.map((i, idx) => {
107
+ const riskDisp = displayRisk(i.risk || 'suggestion', this.config);
108
+ const suggestPart = i.suggestion ? t(this.config, 'inline_suggestion', { suggestion: i.suggestion }) : '';
109
+ return t(this.config, 'local_rule_hint_line', {
110
+ index: idx + 1,
111
+ risk: riskDisp,
112
+ message: i.message,
113
+ suggest: suggestPart,
114
+ snippet: i.snippet || ''
115
+ });
116
+ });
117
+ const header = t(this.config, 'local_rule_findings_header', { file: file.filePath });
118
+ hintsParts.push(`${header}\n${lines.join('\n')}`);
104
119
  }
105
120
  if (hintsParts.length > 0) {
106
121
  messages.push({ role: 'user', content: hintsParts.join('\n\n') });
@@ -108,10 +123,10 @@ export class AIClient {
108
123
  }
109
124
 
110
125
  // 添加最终指令,确保包含片段并保留行号前缀
111
- const finalInstructionBatch = `请逐文件进行审查,每个问题用空行分隔,必须包含"文件路径:绝对路径"与具体的代码片段。禁止使用文字行号或行范围描述(如“第X行/第X-Y行”);如片段中存在每行的[n]前缀请原样保留。`;
126
+ const finalInstructionBatch = t(this.config, 'final_instruction_batch');
112
127
  messages.push({ role: 'user', content: finalInstructionBatch });
113
128
  // 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
114
- const finalIgnoreRule = `注意:代码可能经过预处理(剥离注释、跳过无需审查片段),因此行号前缀可能不连续。这是正常的,请严格忽略“行号跳跃/行号不连续/被预处理移除”等现象,不要将其视为问题或风险,也不要提出“检查代码完整性/补全缺失代码”类建议。仅针对给定片段中的有效代码提出问题与修改建议。`;
129
+ const finalIgnoreRule = t(this.config, 'ignore_rule');
115
130
  messages.push({ role: 'user', content: finalIgnoreRule });
116
131
 
117
132
  // 使用分段响应处理(携带可读的请求ID,便于日志关联)
@@ -131,10 +146,10 @@ export class AIClient {
131
146
  }
132
147
  };
133
148
  } catch (error) {
134
- logger.error(`AI批量文件分析失败: ${error.message}`);
149
+ logger.error(t(this.config, 'ai_batch_failed', { error: error.message }));
135
150
  // 如果是AI请求失败,应该终止程序而不是继续处理
136
151
  if (error.message.includes('Connection error') || error.message.includes('API') || error.message.includes('请求失败')) {
137
- logger.error('AI服务连接失败,终止分析过程');
152
+ logger.error(t(this.config, 'ai_connection_failed'));
138
153
  process.exit(1);
139
154
  }
140
155
  throw error; // 重新抛出错误,让上层调用者处理
@@ -190,16 +205,24 @@ export class AIClient {
190
205
  const cfg = this.config?.ai || this.config || {};
191
206
  const includeStaticHints = cfg.includeStaticHints === true;
192
207
  const customPrompts = await this.readCustomPrompts();
208
+ const loc = getLocale(this.config);
209
+ const L = FIELD_LABELS[loc];
193
210
 
194
211
  // 并发设置:从配置读取,<=1 则保持串行(兼容顶层/嵌套两种配置形态)
195
212
  const segConcurrency = Math.max(1, Number((this.config?.ai?.concurrency ?? this.config?.concurrency) || 1));
196
213
  const effectiveTotal = Array.isArray(file.chunks) ? file.chunks.length : (file.totalChunks || 1);
197
214
  const availableSlots = this.concurrencyLimiter ? this.concurrencyLimiter.getAvailable() : segConcurrency;
198
215
  const workersHead = Math.max(1, Math.min(availableSlots, effectiveTotal));
199
- const totalNote = (file.totalChunks && file.totalChunks !== effectiveTotal)
200
- ? `(总段数 ${file.totalChunks},当前批次 ${effectiveTotal})`
216
+ const concurrencyNote = workersHead > 1 ? t(this.config, 'segment_concurrency_note', { workers: workersHead }) : '';
217
+ const totalNoteText = (file.totalChunks && file.totalChunks !== effectiveTotal)
218
+ ? t(this.config, 'segment_total_note', { totalChunks: file.totalChunks, effectiveTotal })
201
219
  : '';
202
- logger.progress(`开始逐段分析文件: ${file.filePath},共 ${effectiveTotal} 段${workersHead > 1 ? `(并发 ${workersHead})` : ''}${totalNote}`);
220
+ logger.progress(t(this.config, 'segment_overall_start', {
221
+ file: file.filePath,
222
+ total: effectiveTotal,
223
+ concurrency: concurrencyNote,
224
+ totalNote: totalNoteText
225
+ }));
203
226
 
204
227
  const allIssues = [];
205
228
 
@@ -209,7 +232,7 @@ export class AIClient {
209
232
 
210
233
  // 提前让出事件循环,允许并发协程启动
211
234
  // 注意:真正的“开始分析第X/段”提示将在取得并发许可后输出
212
- logger.debug(`分段待启动:第 ${i + 1}/${effectiveTotal} (行 ${chunk.startLine}-${chunk.endLine})`) ;
235
+ logger.debug(t(this.config, 'segment_wait_start_dbg', { index: i + 1, total: effectiveTotal, start: chunk.startLine, end: chunk.endLine }));
213
236
 
214
237
  // 立即让出事件循环,让其他并发协程尽快启动打印日志
215
238
  await Promise.resolve();
@@ -243,44 +266,25 @@ export class AIClient {
243
266
 
244
267
  // 添加自定义提示词
245
268
  if (customPrompts && customPrompts.length > 0) {
246
- messages.push({ role: 'user', content: `\n[自定义提示词]\n${customPrompts.join('\n\n---\n')}` });
269
+ messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
247
270
  }
248
271
 
249
272
  // 构建分段分析提示
250
273
  const attachLineNumbers = (this.config?.ai?.attachLineNumbersInBatch ?? this.config?.attachLineNumbersInBatch) !== false;
251
274
  const contentForAI = attachLineNumbers ? this.addLineNumberPrefixes(clean, lineMapAbs) : clean;
252
275
 
253
- const segmentPrompt = `请对以下代码段进行完整的代码审查分析。这是一个大文件的第 ${i + 1}/${file.totalChunks} 段:
254
-
255
- 文件路径:${file.filePath}
256
- 代码内容:
257
- \`\`\`
258
- ${contentForAI}
259
- \`\`\`
260
-
261
- 请仔细审查这段代码,查找以下类型的问题:
262
- - 类型安全问题(如使用any类型)
263
- - 安全漏洞
264
- - 性能问题
265
- - 代码质量问题
266
- - 最佳实践违反
267
-
268
- 重要:请立即开始分析,不要只是确认收到。必须按以下格式输出每个发现的问题:
269
-
270
- **-----代码分析结果-----**
271
- 文件路径:${file.filePath}
272
- 代码片段:[具体的问题代码]
273
- 风险等级:[高/中/低]
274
- 风险原因:[问题描述]
275
- 修改建议:[具体的修改建议]
276
-
277
- 如果发现多个问题,每个问题都要用 **-----代码分析结果-----** 开头。
278
- 如果没有发现问题,请回复:
279
-
280
- **-----代码分析结果-----**
281
- 本段代码无明显问题
282
-
283
- 注意:如片段中每行包含形如 [n] 的源行号前缀,请在你的输出的代码片段中原样保留这些前缀,以便后续定位。`;
276
+ const segmentPrompt = t(this.config, 'segment_prompt_template', {
277
+ index: i + 1,
278
+ total: file.totalChunks,
279
+ Lfile: L.file,
280
+ Lcontent: L.content,
281
+ Lsnippet: L.snippet,
282
+ Lrisk: L.risk,
283
+ Lreason: L.reason,
284
+ Lsuggestion: L.suggestion,
285
+ file: file.filePath,
286
+ content: contentForAI
287
+ });
284
288
 
285
289
  messages.push({
286
290
  role: 'user',
@@ -293,44 +297,57 @@ ${contentForAI}
293
297
  issue.line >= chunk.startLine && issue.line <= chunk.endLine
294
298
  );
295
299
  if (segmentStaticIssues.length > 0) {
296
- const lines = segmentStaticIssues.map((si, idx) => `${idx + 1}. 片段(${si.risk}):${si.message}${si.suggestion ? `;建议:${si.suggestion}` : ''};代码片段:${si.snippet || ''}`);
297
- messages.push({ role: 'user', content: `[本地规则发现的问题 - 第${i + 1}段]\n${lines.join('\n')}` });
300
+ const lines = segmentStaticIssues.map((si, idx) => {
301
+ const riskDisp = displayRisk(si.risk || 'suggestion', this.config);
302
+ const suggestPart = si.suggestion ? t(this.config, 'inline_suggestion', { suggestion: si.suggestion }) : '';
303
+ return t(this.config, 'segment_static_issue_line', {
304
+ index: idx + 1,
305
+ risk: riskDisp,
306
+ message: si.message,
307
+ suggest: suggestPart,
308
+ snippetLabel: L.snippet,
309
+ snippet: si.snippet || ''
310
+ });
311
+ });
312
+ messages.push({ role: 'user', content: `${t(this.config, 'segment_static_issues_header', { index: i + 1 })}\n${lines.join('\n')}` });
298
313
  }
299
314
  }
300
315
 
301
316
  // 发送分段分析请求
302
317
  const segReqId = `segment_${path.basename(file.filePath)}_${i + 1}of${file.totalChunks}`;
303
- const startLabel = `开始分析 ${file.filePath} ${i + 1}/${effectiveTotal} 段(行 ${chunk.startLine}-${chunk.endLine})`;
318
+ const startLabel = t(this.config, 'segment_start_label', { file: file.filePath, index: i + 1, total: effectiveTotal, start: chunk.startLine, end: chunk.endLine });
304
319
  const responseContent = await this.handleChunkedResponse(messages, segReqId, { onStart: () => logger.info(startLabel) });
305
320
 
306
321
  // 解析分段响应
307
322
  const segmentResult = this.parseAIResponse(responseContent, file.filePath, {});
308
323
 
309
324
  const batchPrefix = (typeof file.batchIndex === 'number' && typeof file.batchTotal === 'number')
310
- ? `批次 ${file.batchIndex + 1}/${file.batchTotal} `
325
+ ? t(this.config, 'segment_batch_prefix', { index: file.batchIndex + 1, total: file.batchTotal })
311
326
  : '';
312
327
  if (Array.isArray(segmentResult)) {
313
328
  allIssues.push(...segmentResult);
314
- logger.success(`${batchPrefix}(${file.filePath})第 ${i + 1} 段分析完成,发现 ${segmentResult.length} 个问题`);
329
+ logger.success(t(this.config, 'segment_analysis_done_n_issues', { batch: batchPrefix, file: file.filePath, index: i + 1, count: segmentResult.length }));
315
330
  } else if (segmentResult && segmentResult.issues) {
316
331
  allIssues.push(...segmentResult.issues);
317
- logger.success(`${batchPrefix}(${file.filePath})第 ${i + 1} 段分析完成,发现 ${segmentResult.issues.length} 个问题`);
332
+ logger.success(t(this.config, 'segment_analysis_done_n_issues', { batch: batchPrefix, file: file.filePath, index: i + 1, count: segmentResult.issues.length }));
318
333
  } else {
319
- logger.success(`${batchPrefix}(${file.filePath})第 ${i + 1} 段分析完成,发现 0 个问题`);
334
+ logger.success(t(this.config, 'segment_analysis_done_zero', { batch: batchPrefix, file: file.filePath, index: i + 1 }));
320
335
  }
321
336
  } catch (error) {
322
- logger.error(`第 ${i + 1} 段分析失败: ${error.message}`);
337
+ logger.error(t(this.config, 'segment_analysis_failed', { index: i + 1, error: error.message }));
323
338
  }
324
339
  };
325
340
 
326
341
  // 按配置执行并发或串行
327
342
  const total = effectiveTotal;
328
343
  const workers = Math.max(1, Math.min((this.concurrencyLimiter ? this.concurrencyLimiter.getAvailable() : segConcurrency), total));
329
- const schedNote = (file.totalChunks && file.totalChunks !== total)
330
- ? `(总段数 ${file.totalChunks},本批次处理 ${total} 段)`
331
- : '';
344
+
332
345
  // 调度细节降为调试级别,避免扰乱终端主要进度
333
- logger.debug(`分段并发调度:workers=${workers}, total=${total}${schedNote}`);
346
+ logger.debug(t(this.config, 'segment_schedule_dbg', {
347
+ workers,
348
+ total,
349
+ note: (file.totalChunks && file.totalChunks !== total) ? t(this.config, 'segment_total_note', { totalChunks: file.totalChunks, effectiveTotal: total }) : ''
350
+ }));
334
351
  if (workers <= 1) {
335
352
  for (let i = 0; i < total; i++) {
336
353
  // eslint-disable-next-line no-await-in-loop
@@ -340,7 +357,7 @@ ${contentForAI}
340
357
  let cursor = 0;
341
358
  const runWorker = async (workerId) => {
342
359
  // 并发协程启动提示降为调试级别
343
- logger.debug(`启动分段并发协程 #${workerId}`);
360
+ logger.debug(t(this.config, 'segment_worker_start_dbg', { id: workerId }));
344
361
  while (true) {
345
362
  const i = cursor++;
346
363
  if (i >= total) break;
@@ -349,7 +366,7 @@ ${contentForAI}
349
366
  }
350
367
  };
351
368
  await Promise.all(Array.from({ length: workers }, (_, idx) => runWorker(idx + 1)));
352
- logger.debug(`分段并发完成:已处理 ${total}${file.totalChunks && file.totalChunks !== total ? `/${file.totalChunks}` : ''} 段`);
369
+ logger.debug(t(this.config, 'segment_concurrency_done_dbg', { total, extra: (file.totalChunks && file.totalChunks !== total) ? `/${file.totalChunks}` : '' }));
353
370
  }
354
371
 
355
372
  return {
@@ -361,10 +378,10 @@ ${contentForAI}
361
378
  };
362
379
 
363
380
  } catch (error) {
364
- logger.error(`分段文件分析失败: ${error.message}`);
381
+ logger.error(t(this.config, 'segment_file_failed', { error: error.message }));
365
382
  // 如果是AI请求失败,应该终止程序而不是继续处理
366
383
  if (error.message.includes('Connection error') || error.message.includes('API') || error.message.includes('请求失败')) {
367
- logger.error('AI服务连接失败,终止分析过程');
384
+ logger.error(t(this.config, 'ai_connection_failed'));
368
385
  process.exit(1);
369
386
  }
370
387
  throw error; // 重新抛出错误,让上层调用者处理
@@ -383,8 +400,9 @@ ${contentForAI}
383
400
  const includeStaticHints = cfg.includeStaticHints === true;
384
401
  const customPrompts = await this.readCustomPrompts();
385
402
  const staticIssues = options.staticIssues || [];
386
-
387
- logger.debug(`开始Git Diff分析: ${fileData.filePath} (${fileData.totalAddedLines} 行新增代码)`);
403
+ const loc = getLocale(this.config);
404
+ const L = FIELD_LABELS[loc];
405
+ logger.debug(t(this.config, 'ai_diff_start_dbg', { file: fileData.filePath, added: fileData.totalAddedLines }));
388
406
 
389
407
  // 构建diff专用的系统提示词
390
408
  const diffSystemPrompt = this.getDiffSystemPrompt();
@@ -395,67 +413,72 @@ ${contentForAI}
395
413
 
396
414
  // 添加自定义提示词
397
415
  if (customPrompts && customPrompts.length > 0) {
398
- messages.push({ role: 'user', content: `\n[自定义提示词]\n${customPrompts.join('\n\n---\n')}` });
416
+ messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
399
417
  }
400
418
 
401
419
  // 构建diff分析提示
402
- const diffPrompt = `请对以下Git变更进行代码审查。重点关注新增的代码行(+号标记),上下文代码仅供理解,无需审查。
403
-
404
- 文件路径:${fileData.filePath}
405
- 新增代码行数:${fileData.totalAddedLines}
406
- 智能分段数:${fileData.segments.length}
407
-
408
- 变更内容:`;
420
+ const intro = t(this.config, 'diff_intro');
421
+ const diffPrompt = `${intro}\n\n${L.file}${fileData.filePath}\n${t(this.config, 'diff_added_lines_label')}${fileData.totalAddedLines}\n${t(this.config, 'diff_smart_segments_label')}${fileData.segments.length}\n\n${t(this.config, 'diff_changes_label')}`;
409
422
 
410
423
  messages.push({ role: 'user', content: diffPrompt });
411
424
 
412
425
  // 添加每个智能分段
413
426
  for (let i = 0; i < fileData.segments.length; i++) {
414
427
  const segment = fileData.segments[i];
415
- const segmentPrompt = `
416
- [智能分段 ${i + 1}/${fileData.segments.length}] (行范围: ${segment.startLine}-${segment.endLine}, 新增${segment.addedLinesCount}行, 约${segment.estimatedTokens} tokens)
417
- \`\`\`diff
418
- ${segment.content}
419
- \`\`\``;
428
+ const segTitle = t(this.config, 'diff_segment_title', { index: i + 1, total: fileData.segments.length });
429
+ const segMeta = t(this.config, 'diff_segment_meta', {
430
+ start: segment.startLine,
431
+ end: segment.endLine,
432
+ added: segment.addedLinesCount,
433
+ tokens: segment.estimatedTokens
434
+ });
435
+ const segmentPrompt = `\n[${segTitle}] (${segMeta})\n\`\`\`diff\n${segment.content}\n\`\`\``;
420
436
 
421
437
  messages.push({ role: 'user', content: segmentPrompt });
422
438
  // 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
423
- messages.push({ role: 'user', content: `重要:该分段可能经过预处理,行号可能不连续。请严格忽略“行号跳跃/行号不连续/被预处理移除”等现象,不要将其视为问题或风险,也不要提出“检查代码完整性/补全缺失代码”类建议。` });
439
+ messages.push({ role: 'user', content: t(this.config, 'ignore_rule') });
424
440
  }
425
441
 
426
442
  // 添加静态分析提示(如果有)
427
443
  if (includeStaticHints && staticIssues.length > 0) {
428
- const hintLines = staticIssues.map((issue, idx) =>
429
- `${idx + 1}. 风险等级:${issue.risk},问题:${issue.message}${issue.suggestion ? `,建议:${issue.suggestion}` : ''},代码片段:${issue.snippet || ''}`
430
- );
431
- const hintsPrompt = `\n[本地规则发现的问题]\n${hintLines.join('\n')}`;
444
+ const hintLines = staticIssues.map((issue, idx) => {
445
+ const riskDisp = displayRisk(issue.risk || 'suggestion', this.config);
446
+ const suggestPart = issue.suggestion ? t(this.config, 'inline_suggestion', { suggestion: issue.suggestion }) : '';
447
+ return t(this.config, 'local_rule_hint_line', {
448
+ index: idx + 1,
449
+ risk: riskDisp,
450
+ message: issue.message,
451
+ suggest: suggestPart,
452
+ snippet: issue.snippet || ''
453
+ });
454
+ });
455
+ const hintsPrompt = `\n[${t(this.config, 'local_rule_findings')}]\n${hintLines.join('\n')}`;
432
456
  messages.push({ role: 'user', content: hintsPrompt });
433
457
  }
434
458
 
435
459
  // 添加最终指令
436
- const finalInstruction = `
437
- 请仅对标记为"+"的新增代码行进行审查,忽略删除行(-)和上下文行。每个问题用空行分隔,必须包含"文件路径:${fileData.filePath}"和具体的代码片段。禁止使用文字行号或行范围描述(如“第X行/第X-Y行”);如片段中存在每行的[行号]前缀请原样保留。`;
460
+ const finalInstruction = t(this.config, 'diff_final_instruction', { file: fileData.filePath });
438
461
 
439
462
  messages.push({ role: 'user', content: finalInstruction });
440
463
  // 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
441
- messages.push({ role: 'user', content: `注意:变更内容可能经过预处理(剥离注释、跳过无需审查片段),新增片段中的源行号可能不连续。这是正常的,请严格忽略“行号跳跃/行号不连续/被预处理移除”等现象,不要将其视为问题或风险,也不要提出“检查代码完整性/补全缺失代码”类建议。仅针对给定片段中的有效新增代码提出问题与修改建议。` });
464
+ messages.push({ role: 'user', content: t(this.config, 'ignore_rule') });
442
465
 
443
466
  // 记录请求信息
444
- logger.debug(`发送Git Diff AI请求 - 模型: ${this.config.model ?? 'gpt-3.5-turbo'}, 消息数: ${messages.length}`);
467
+ logger.debug(t(this.config, 'ai_diff_send_dbg', { model: this.config.model ?? 'gpt-3.5-turbo', messages: messages.length }));
445
468
 
446
469
  // 发送请求并处理响应
447
470
  const diffReqId = `diff_${path.basename(fileData.filePath)}`;
448
471
  const responseContent = await this.handleChunkedResponse(messages, diffReqId);
449
472
  const issues = this.parseAIResponse(responseContent, fileData.filePath);
450
473
 
451
- logger.debug(`Git Diff分析完成: ${fileData.filePath},发现 ${issues.length} 个问题`);
474
+ logger.debug(t(this.config, 'ai_diff_done_dbg', { file: fileData.filePath, issues: issues.length }));
452
475
 
453
476
  return issues || [];
454
477
 
455
478
  } catch (error) {
456
- logger.error(`Git Diff文件分析失败: ${error.message}`);
479
+ logger.error(t(this.config, 'ai_diff_failed', { path: fileData.filePath, error: error.message }));
457
480
  if (error.message.includes('Connection error') || error.message.includes('API') || error.message.includes('请求失败')) {
458
- logger.error('AI服务连接失败,终止分析过程');
481
+ logger.error(t(this.config, 'ai_connection_failed'));
459
482
  process.exit(1);
460
483
  }
461
484
  throw error;
@@ -468,17 +491,20 @@ ${segment.content}
468
491
  const cfg = this.config?.ai || {};
469
492
  const includeStaticHints = cfg.includeStaticHints === true;
470
493
  const customPrompts = await this.readCustomPrompts();
494
+ const loc = getLocale(this.config);
495
+ const L = FIELD_LABELS[loc];
496
+ const { systemPrompt } = buildPrompts(this.config);
471
497
 
472
498
  const messages = [
473
- { role: 'system', content: this.getSystemPrompt() },
499
+ { role: 'system', content: systemPrompt },
474
500
  {
475
501
  role: 'user',
476
- content: `我会一次性发送多个文件的完整代码,请逐文件进行审查并返回结果。每个问题用空行分隔,务必包含"文件路径:绝对路径"与代码片段,且禁止任何"第X行/第X-Y行"等行号或行范围描述。`
502
+ content: t(this.config, 'batch_files_intro')
477
503
  }
478
504
  ];
479
505
 
480
506
  if (customPrompts.length > 0) {
481
- messages.push({ role: 'user', content: `\n[自定义提示词]\n${customPrompts.join('\n\n---\n')}` });
507
+ messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
482
508
  }
483
509
 
484
510
  // 逐文件添加内容
@@ -488,9 +514,10 @@ ${segment.content}
488
514
  const { clean, lineMap } = await prepareForAIWithLineMap(content, filePath);
489
515
  const attachLineNumbers = this.config?.ai?.attachLineNumbersInBatch !== false;
490
516
  const contentForAI = attachLineNumbers ? this.addLineNumberPrefixes(clean, lineMap) : clean;
517
+ const failedText = failedStatic ? t(this.config, 'file_failed_static_suffix') : '';
491
518
  messages.push({
492
519
  role: 'user',
493
- content: `文件路径:${filePath}${failedStatic ? '(本地审查未通过)' : ''}\n代码内容:\n\`\`\`\n${contentForAI}\n\`\`\``
520
+ content: `${L.file}${filePath}${failedText}\n${L.content}\n\`\`\`\n${contentForAI}\n\`\`\``
494
521
  });
495
522
  requestPreviews.push({ filePath, contentForAI });
496
523
  }
@@ -502,8 +529,19 @@ ${segment.content}
502
529
  if (e.failedStatic !== true) continue; // 仅针对本地未通过的文件汇总静态提示
503
530
  const staticIssues = e.staticIssues || [];
504
531
  if (staticIssues.length === 0) continue;
505
- const lines = staticIssues.map((i, idx) => `${idx + 1}. 片段(${i.risk}):${i.message}${i.suggestion ? `;建议:${i.suggestion}` : ''};代码片段:${i.snippet || ''}`);
506
- hintsParts.push(`[本地规则发现的问题 - ${e.filePath}]\n${lines.join('\n')}`);
532
+ const lines = staticIssues.map((i, idx) => {
533
+ const riskDisp = displayRisk(i.risk || 'suggestion', this.config);
534
+ const suggestPart = i.suggestion ? t(this.config, 'inline_suggestion', { suggestion: i.suggestion }) : '';
535
+ return t(this.config, 'local_rule_hint_line', {
536
+ index: idx + 1,
537
+ risk: riskDisp,
538
+ message: i.message,
539
+ suggest: suggestPart,
540
+ snippet: i.snippet || ''
541
+ });
542
+ });
543
+ const header = t(this.config, 'local_rule_findings_header', { file: e.filePath });
544
+ hintsParts.push(`${header}\n${lines.join('\n')}`);
507
545
  }
508
546
  if (hintsParts.length > 0) {
509
547
  messages.push({ role: 'user', content: hintsParts.join('\n\n') });
@@ -511,8 +549,10 @@ ${segment.content}
511
549
  }
512
550
 
513
551
  // 添加最终指令,确保包含片段并保留行号前缀
514
- const finalInstructionBatch = `请逐文件进行审查,每个问题用空行分隔,必须包含"文件路径:绝对路径"与具体的代码片段。禁止使用文字行号或行范围描述(如“第X行/第X-Y行”);如片段中存在每行的[n]前缀请原样保留。`;
552
+ const finalInstructionBatch = t(this.config, 'final_instruction_batch');
515
553
  messages.push({ role: 'user', content: finalInstructionBatch });
554
+ // 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
555
+ messages.push({ role: 'user', content: t(this.config, 'ignore_rule') });
516
556
  const useDynamic = (this.config?.ai?.dynamicMaxTokens !== false);
517
557
  const contextWindow = (() => {
518
558
  const cw = Number(this.config.contextWindow ?? AI_CONSTANTS.DEFAULT_CONTEXT_WINDOW);
@@ -546,7 +586,7 @@ ${segment.content}
546
586
  // 批量响应:不传单个 filePath,让解析函数从AI返回的“文件名称:”中读取
547
587
  return this.parseAIResponse(response.choices[0].message.content, undefined, {});
548
588
  } catch (error) {
549
- logger.error(`AI批量文件分析失败: ${error.message}`);
589
+ logger.error(t(this.config, 'ai_batch_failed', { error: error.message }));
550
590
  return [];
551
591
  }
552
592
  }
@@ -587,7 +627,7 @@ ${segment.content}
587
627
  throw error;
588
628
  }
589
629
  const delay = baseDelay * Math.pow(2, attempt - 1);
590
- logger.warn(`AI请求失败,重试(${attempt}/${retries}),等待 ${delay}ms: ${error.message}`);
630
+ logger.warn(t(this.config, 'ai_retry_warn', { attempt, retries, delay, error: error?.message || String(error) }));
591
631
  await new Promise((r) => setTimeout(r, delay));
592
632
  } finally {
593
633
  if (release) {
@@ -603,127 +643,10 @@ ${segment.content}
603
643
  this.cacheStats.hits++;
604
644
  return this.systemPromptCache;
605
645
  }
606
-
607
646
  this.cacheStats.misses++;
608
- this.systemPromptCache = `你是一个专业的代码审查与优化专家。请以通用的方式对收到的代码进行全面审查,识别不合理实现与潜在问题,并提出切实可行的修复或优化建议。
609
-
610
- **分段响应协议:**
611
- 如果你的回答内容过长,可能超出单次响应的token限制,请按以下格式进行分段:
612
-
613
- 1. 在每段结尾添加分段标记:
614
- - 未完成:[CHUNK_CONTINUE]
615
- - 已完成:[CHUNK_END]
616
- - 分段索引:[CHUNK_X/Y](X为当前段,Y为总段数)
617
-
618
- 2. 示例格式:
619
- \`\`\`
620
- 这是第一段分析内容...
621
- [CHUNK_1/3]
622
- [CHUNK_CONTINUE]
623
- \`\`\`
624
-
625
- 3. 当收到"继续"时,请继续输出下一段内容。
626
-
627
- 4. 最后一段必须以 [CHUNK_END] 结尾。
628
-
629
- **代码分析输出格式**
630
- 请严格按照以下格式返回分析结果,每个问题必须使用开始/结束标记包裹,并以空行分隔:
631
-
632
- **-----代码分析结果开始-----**
633
- 文件路径:{文件路径}
634
- 代码片段:{具体的给定审查代码的原始片段内容,不允许丢失任何字符,仅当问题属于全局性或架构性问题时才可留空}
635
- 风险等级:{致命/高危/中危/低危/建议}
636
- 风险原因:{详细原因(必须包含证据:代码片段)}
637
- 修改建议:{具体建议(允许多行)}
638
- **-----代码分析结果结束-----**
639
-
640
- 若输入中包含"[本地规则发现的问题]",请顺便判断本地规则是否是误报,如是误报返回对应规则,并在"风险原因"中说明误报理由;若不是误报可忽略该对应本地规则。
641
-
642
- 风险等级及范围定义:
643
-
644
- **致命级别**:可能导致系统崩溃、数据丢失、严重安全漏洞
645
- - 前端:未捕获的异常导致页面崩溃、XSS攻击漏洞、敏感信息泄露到控制台、无限递归导致浏览器卡死、计时器未清理等
646
- - 后端:空指针异常、数据库连接泄露、SQL注入、未授权的数据访问、死循环导致服务器宕机等
647
- - 引擎/核心:内存泄漏、缓冲区溢出、竞态条件、资源未释放、关键算法错误等
648
-
649
- **高危级别**:可能导致安全漏洞、数据泄露、业务逻辑错误
650
- - 前端:CSRF攻击风险、本地存储敏感数据、不安全的API调用、用户输入未验证等
651
- - 后端:密码硬编码、权限验证缺失、敏感数据未加密、API接口未鉴权、文件上传漏洞等
652
- - 引擎/核心:加密算法使用错误、随机数生成不安全、配置文件暴露、日志记录敏感信息等
653
-
654
- **中危级别**:可能影响系统稳定性、性能问题、用户体验
655
- - 前端:组件重复渲染、大量DOM操作、未优化的网络请求、内存占用过高、响应式设计问题等
656
- - 后端:数据库查询未优化、缓存策略不当、线程池配置不合理、API响应时间过长等
657
- - 引擎/核心:算法复杂度过高、资源使用效率低、并发处理能力不足、错误处理机制不完善等
658
-
659
- **低危级别**:代码质量问题、不符合最佳实践、可维护性问题
660
- - 前端:组件职责不清、状态管理混乱、样式代码重复、TypeScript类型定义不准确等
661
- - 后端:代码重复、函数过长、类设计不合理、异常处理不规范、日志记录不完整等
662
- - 引擎/核心:模块耦合度高、接口设计不清晰、命名规范不统一、注释缺失等
663
-
664
- **建议级别**:改进建议,不影响功能,提升代码质量
665
- - 前端:组件拆分优化、Hook使用优化、CSS样式优化、代码格式化、变量命名改进、未使用变量清理等
666
- - 后端:方法重构、参数验证完善、返回值优化、代码注释补充、单元测试覆盖等
667
- - 引擎/核心:性能监控添加、配置参数调优、文档完善、代码结构优化、版本兼容性等
668
-
669
- **代码片段格式要求:**
670
- 1. 必须提供具体的原始代码片段内容,直接返回纯文本代码,不要使用markdown代码块格式(如\`\`\`typescript等)
671
- 2. 仅在以下全局性或架构性问题时才可留空:
672
- - 整体项目架构设计问题
673
- - 跨多个文件的设计模式问题
674
- - 全局配置或依赖管理问题
675
- - 整体代码组织结构问题
676
- 3. 对于具体的代码问题(如函数、变量、语句等),必须提供相关的代码片段作为证据
677
- 4. 提供的原始内容,不允许以任何形式导致字符丢失,必须保证提供的内容准确性
678
- 5. 若输入中包含每行的\`[n]\`行号前缀(例如\`[123]\`),请原样保留这些前缀作为片段的一部分,不要自行生成、修改或移除行号
679
-
680
- 禁止使用文字描述的行号或行范围(如“第X行/第X-Y行”);允许片段中的\`[n]\`前缀,它是输入的一部分
681
-
682
- **行号说明与误报避免:**
683
- 1. 我可能会对代码进行预处理(剥离注释、跳过标记为“无需审查”的片段等),因此片段中的\`[n]\`源行号可能出现不连续(例如从278直接跳到294)。这是正常现象,不代表代码缺失或结构问题。
684
- 2. 严禁将“行号不连续/行号跳跃/注释行号不连续”等现象判定为问题或风险原因;遇到此类情况请直接忽略,不要输出相应的风险或建议。
685
-
686
- **不在审查范围(必须忽略):**
687
- 1. 预处理造成的元信息差异:行号不连续/跳跃、注释移除、被标记为“无需审查”的片段剥离。
688
- 2. 展示层的占位或截断标记(如“中间省略”)。
689
- 3. 请不要在任何输出字段(风险原因、建议或片段)中提及“行号跳跃”、“行号不连续”、“被预处理移除”等措辞;若遇到此类情况,直接忽略,不要生成问题。
690
-
691
- **重点分析领域:**
692
-
693
- 1. **安全问题**:
694
- - 输入验证缺失、SQL注入、XSS攻击、CSRF漏洞等
695
- - 敏感信息硬编码(密码、API密钥、令牌等)
696
- - 权限验证缺失、未授权访问、数据泄露风险等
697
- - 不安全的加密算法、随机数生成、文件操作等
698
-
699
- 2. **性能问题**:
700
- - 算法复杂度过高、嵌套循环、递归深度等
701
- - 内存泄漏、资源未释放、缓存策略不当等
702
- - 数据库查询未优化、N+1查询问题等
703
- - 前端组件重复渲染、大量DOM操作、网络请求优化等
704
-
705
- 3. **代码质量问题**:
706
- - 函数过长、类职责不清、代码重复等
707
- - 复杂的条件判断、深层嵌套、魔法数字等
708
- - 变量命名不规范、类型定义不准确等
709
- - 异常处理不完善、错误信息不明确等
710
-
711
- 4. **架构与设计问题**:
712
- - 模块耦合度过高、依赖关系混乱等
713
- - 设计模式使用不当、接口设计不合理等
714
- - 配置管理问题、环境变量使用等
715
- - 单元测试覆盖率、可测试性设计等
716
-
717
- **代码片段与行号约束**:
718
- - 如输入片段存在每行的\`[行号]\`前缀,请原样保留,它是输入的一部分
719
- - 禁止使用文字描述的行号或行范围(如“第X行/第X-Y行”);必须以片段呈现问题
720
-
721
- **行号说明与误报避免(必须遵守)**:
722
- - 由于预处理(剥离注释、跳过无需审查片段等),片段中的\`[n]\`源行号可能不连续。这是正常的,务必严格忽略“行号不连续/行号跳跃/被预处理移除”等现象,不得将其视为问题或风险原因,不得提出“检查代码完整性/补全缺失代码”等建议。
723
- - 输出中禁止出现相关措辞(如“行号跳跃”、“行号不连续”、“被预处理移除”)。如遇此类情形请直接忽略,不要生成任何问题。
724
- `;
725
-
726
- return this.systemPromptCache;
647
+ const { systemPrompt } = buildPrompts(this.config);
648
+ this.systemPromptCache = systemPrompt;
649
+ return this.systemPromptCache;
727
650
  }
728
651
 
729
652
  /**
@@ -731,74 +654,15 @@ ${segment.content}
731
654
  * @returns {string} diff专用系统提示词
732
655
  */
733
656
  getDiffSystemPrompt() {
734
- return `你是一个专业的代码审查与优化专家,专门负责Git变更的增量代码审查。
735
-
736
- **Git Diff增量审查说明:**
737
- 1. 你将收到Git diff格式的代码变更,包含新增行(+)、删除行(-)和上下文行
738
- 2. **重要:仅对新增行(+号标记)进行审查,删除行(-)和上下文行仅供理解代码逻辑,无需审查**
739
- 3. 上下文行的作用是帮助你理解新增代码的执行环境和逻辑关系
740
- 4. 专注于新增代码可能引入的问题,而不是整个文件的问题
741
-
742
- **代码分析输出格式**
743
- 请严格按照以下格式返回分析结果,每个问题必须使用开始/结束标记包裹,并以空行分隔:
744
-
745
- **-----Git Diff代码分析结果开始-----**
746
- 文件路径:{文件路径}
747
- 代码片段:{具体的新增代码片段(+号标记的内容),不包含+号前缀;如片段中存在每行的[行号]前缀,请原样保留}
748
- 风险等级:{致命/高危/中危/低危/建议}
749
- 风险原因:{详细原因,重点说明新增代码可能引入的问题}
750
- 修改建议:{针对新增代码的具体修改建议(允许多行)}
751
- **-----Git Diff代码分析结果结束-----**
752
-
753
- **风险等级定义(专注于新增代码):**
754
-
755
- **致命级别**:新增代码可能导致系统崩溃、数据丢失、严重安全漏洞
756
- - 新增的未捕获异常、空指针引用、无限循环
757
- - 新增的SQL注入、XSS攻击漏洞、敏感信息泄露
758
- - 新增的内存泄漏、资源未释放、死锁风险
759
-
760
- **高危级别**:新增代码可能导致安全漏洞、数据泄露、业务逻辑错误
761
- - 新增的权限验证缺失、未授权访问
762
- - 新增的密码硬编码、敏感数据未加密
763
- - 新增的不安全API调用、输入验证缺失
764
-
765
- **中危级别**:新增代码可能影响系统稳定性、性能问题
766
- - 新增的性能瓶颈、算法复杂度过高
767
- - 新增的数据库查询未优化、缓存策略不当
768
- - 新增的组件重复渲染、内存占用过高
769
-
770
- **低危级别**:新增代码的质量问题、不符合最佳实践
771
- - 新增的代码重复、函数过长、命名不规范
772
- - 新增的异常处理不规范、日志记录不完整
773
- - 新增的类型定义不准确、注释缺失
774
-
775
- **建议级别**:新增代码的改进建议,提升代码质量
776
- - 新增代码的格式化、变量命名改进
777
- - 新增代码的重构建议、性能优化
778
- - 新增代码的单元测试覆盖、文档完善
779
-
780
- **重点关注新增代码的问题:**
781
- 1. **安全风险**:新增代码是否引入安全漏洞
782
- 2. **性能影响**:新增代码是否影响系统性能
783
- 3. **逻辑错误**:新增代码是否存在业务逻辑错误
784
- 4. **兼容性**:新增代码是否与现有代码兼容
785
- 5. **可维护性**:新增代码是否易于维护和扩展
786
-
787
- **代码片段要求:**
788
- - 只提供新增的代码片段(去除+号前缀,同时保留每行的\`[行号]\`前缀)
789
- - 不要包含上下文行或删除行
790
- - 确保代码片段的准确性,不允许字符丢失
791
- - 禁止使用文字描述的行号或行范围;允许片段中的\`[n]\`前缀,它是输入的一部分
792
-
793
- **行号说明与误报避免:**
794
- - 由于仅审查新增行以及上游可能进行的预处理(如剥离注释、跳过无需审查片段),片段中的\`[n]\`源行号可能出现不连续。这是正常的,请不要将“行号不连续/行号跳跃”等现象视为问题或风险原因,务必忽略。
795
-
796
- **不在审查范围(必须忽略):**
797
- - 预处理引起的行号不连续/跳跃、注释或无需审查内容的移除。
798
- - 展示预览的占位或截断标记(如“中间省略”)。
799
- - 输出中禁止出现“行号跳跃”、“行号不连续”、“被预处理移除”等措辞;如遇此类情形请忽略,不要生成任何问题。
800
-
801
- 记住:你的任务是审查新增的代码变更,帮助开发者在代码合并前发现和修复潜在问题。`;
657
+ // 使用 i18n 生成并缓存 Diff 专用系统提示词
658
+ if (this.diffSystemPromptCache) {
659
+ this.cacheStats.hits++;
660
+ return this.diffSystemPromptCache;
661
+ }
662
+ this.cacheStats.misses++;
663
+ const { diffSystemPrompt } = buildPrompts(this.config);
664
+ this.diffSystemPromptCache = diffSystemPrompt;
665
+ return this.diffSystemPromptCache;
802
666
  }
803
667
 
804
668
  getCacheStats() {
@@ -844,7 +708,7 @@ ${segment.content}
844
708
  prompts.push(content.trim());
845
709
  }
846
710
  } catch (e) {
847
- logger.warn(`读取AI提示文件失败 ${filePath}:`, e.message);
711
+ logger.warn(t(this.config, 'read_ai_prompt_file_failed', { file: filePath, error: e?.message || String(e) }));
848
712
  }
849
713
  }
850
714
  }
@@ -853,7 +717,7 @@ ${segment.content}
853
717
  this.promptCache.set(cacheKey, prompts);
854
718
  return prompts;
855
719
  } catch (error) {
856
- logger.warn('读取自定义AI提示词失败:', error.message);
720
+ logger.warn(t(this.config, 'read_custom_prompts_failed', { error: error?.message || String(error) }));
857
721
  return [];
858
722
  }
859
723
  }
@@ -869,7 +733,9 @@ ${segment.content}
869
733
  // 优先:按开始/结束标记提取块
870
734
  const markerRegexes = [
871
735
  /\*\*-----代码分析结果开始-----\*\*([\s\S]*?)\*\*-----代码分析结果结束-----\*\*/g,
872
- /\*\*-----Git Diff代码分析结果开始-----\*\*([\s\S]*?)\*\*-----Git Diff代码分析结果结束-----\*\*/g
736
+ /\*\*-----Git Diff代码分析结果开始-----\*\*([\s\S]*?)\*\*-----Git Diff代码分析结果结束-----\*\*/g,
737
+ /\*\*-----Code Analysis Result Start-----\*\*([\s\S]*?)\*\*-----Code Analysis Result End-----\*\*/g,
738
+ /\*\*-----Git Diff Code Analysis Result Start-----\*\*([\s\S]*?)\*\*-----Git Diff Code Analysis Result End-----\*\*/g
873
739
  ];
874
740
  let hasStandardFormat = false;
875
741
  for (const re of markerRegexes) {
@@ -888,7 +754,12 @@ ${segment.content}
888
754
  if (!hasStandardFormat) {
889
755
  let blocks = response.split('\n\n');
890
756
  for (const block of blocks) {
891
- if (block.includes('**-----代码分析结果-----**') || block.includes('**-----Git Diff代码分析结果-----**')) {
757
+ if (
758
+ block.includes('**-----代码分析结果-----**') ||
759
+ block.includes('**-----Git Diff代码分析结果-----**') ||
760
+ block.includes('**-----Code Analysis Result-----**') ||
761
+ block.includes('**-----Git Diff Code Analysis Result-----**')
762
+ ) {
892
763
  hasStandardFormat = true;
893
764
  const issue = this.parseIssueBlock(block, filePath, context);
894
765
  if (issue) issues.push(issue);
@@ -913,9 +784,9 @@ ${segment.content}
913
784
  return context.staticIssues.map((i) => ({
914
785
  file: filePath,
915
786
  source: 'ai',
916
- risk: i.risk || 'suggestion',
917
- message: `基于本地规则:${i.message}`,
918
- suggestion: i.suggestion || '请根据本地规则进行复核与修复,并补充测试与监控以验证风险。',
787
+ risk: this.normalizeRiskLevel(i.risk || 'suggestion'),
788
+ message: t(this.config, 'fallback_static_reason', { message: i.message }),
789
+ suggestion: i.suggestion || t(this.config, 'fallback_static_suggestion'),
919
790
  snippet: i.snippet || ''
920
791
  }));
921
792
  }
@@ -931,7 +802,7 @@ ${segment.content}
931
802
 
932
803
  for (const line of lines) {
933
804
  // 检查是否是新问题的开始(问题1:, 问题2: 等)
934
- if (/^问题\d+[::]/.test(line.trim())) {
805
+ if (/^问题\d+[::]/.test(line.trim()) || /^Issue\s*\d+[::]?/i.test(line.trim())) {
935
806
  // 如果当前块有内容,保存它
936
807
  if (currentBlock.length > 0) {
937
808
  blocks.push(currentBlock.join('\n'));
@@ -952,162 +823,132 @@ ${segment.content}
952
823
  parseIssueBlock(block, filePath, context = {}) {
953
824
  const lines = block.split('\n');
954
825
  const issue = { source: 'ai' };
955
-
956
- // 在批量模式下,需要从AI返回的内容中匹配文件路径
826
+
827
+ const LCN = FIELD_LABELS['zh-CN'];
828
+ const LEN = FIELD_LABELS['en-US'];
829
+ const FILE_LABELS = [LCN.file, LEN.file];
830
+ const RISK_LABELS = [LCN.risk, LEN.risk];
831
+ const REASON_LABELS = [LCN.reason, LEN.reason];
832
+ const SUGGEST_LABELS = [LCN.suggestion, LEN.suggestion];
833
+ const SNIPPET_LABELS = [LCN.snippet, LEN.snippet];
834
+
835
+ const startsWithAny = (text, labels) => {
836
+ const tline = String(text || '').trim();
837
+ return labels.some(l => tline.startsWith(l));
838
+ };
839
+ const extractAfterAny = (text, labels) => {
840
+ const tline = String(text || '').trim();
841
+ for (const l of labels) {
842
+ if (tline.startsWith(l)) return tline.slice(l.length).trim();
843
+ }
844
+ return null;
845
+ };
846
+
957
847
  if (!filePath && context.fileList && context.fileList.length > 0) {
958
- // 从block中查找文件路径信息
959
- const filePathLine = lines.find(line => line.startsWith('文件路径:'));
848
+ const filePathLine = lines.find(line => startsWithAny(line, FILE_LABELS));
960
849
  if (filePathLine) {
961
- const aiPath = filePathLine.replace('文件路径:', '').trim();
962
- // 查找匹配的文件路径
963
- const matchedPath = context.fileList.find(path => {
964
- const fileName = path.split(/[/\\]/).pop();
965
- const aiFileName = aiPath.split(/[/\\]/).pop();
966
- return fileName === aiFileName || path.includes(aiPath.replace(/^[A-Z]?:?\\?/, ''));
850
+ const aiPath = extractAfterAny(filePathLine, FILE_LABELS) || '';
851
+ const matchedPath = context.fileList.find(p => {
852
+ const fileName = p.split(/[\\/]/).pop();
853
+ const aiFileName = aiPath.split(/[\\/]/).pop();
854
+ return fileName === aiFileName || p.includes(aiPath.replace(/^[A-Z]?:?\\?/, ''));
967
855
  });
968
856
  if (matchedPath) {
969
857
  issue.file = matchedPath;
970
858
  }
971
859
  }
972
860
  } else {
973
- // 单文件模式,直接使用传入的filePath
974
861
  issue.file = filePath;
975
862
  }
976
-
863
+
977
864
  let isInCodeBlock = false;
978
865
  let codeLines = [];
979
- let collectPlainSnippet = false; // 处理“代码片段:”后紧随的非围栏代码
980
- let collectSuggestion = false; // 处理“修改建议:”的多行文本
866
+ let collectPlainSnippet = false;
867
+ let collectSuggestion = false;
981
868
  let suggestionLines = [];
982
869
 
983
870
  const isFieldLine = (text) => {
984
- const t = String(text || '').trim();
985
- if (!t) return false;
986
- if (t.startsWith('文件路径:')) return true;
987
- if (t.startsWith('风险等级:')) return true;
988
- if (t.startsWith('风险原因:')) return true;
989
- if (t.startsWith('修改建议:')) return true;
990
- if (t.startsWith('代码片段:')) return true; // 新片段开始也视为字段边界
991
- if (/^\*\*-----/.test(t)) return true; // 块分隔符
992
- if (/^问题\d+[::]/.test(t)) return true; // 问题编号
871
+ const tline = String(text || '').trim();
872
+ if (!tline) return false;
873
+ if (startsWithAny(tline, FILE_LABELS)) return true;
874
+ if (startsWithAny(tline, RISK_LABELS)) return true;
875
+ if (startsWithAny(tline, REASON_LABELS)) return true;
876
+ if (startsWithAny(tline, SUGGEST_LABELS)) return true;
877
+ if (startsWithAny(tline, SNIPPET_LABELS)) return true;
878
+ if (/^\*\*-----/.test(tline)) return true;
879
+ if (/^问题\d+[::]/.test(tline) || /^Issue\s*\d+[::]?/i.test(tline)) return true;
993
880
  return false;
994
881
  };
995
-
882
+
996
883
  for (let i = 0; i < lines.length; i++) {
997
884
  const line = lines[i];
998
-
999
- // 若正在收集非围栏代码片段,优先处理
885
+
1000
886
  if (collectPlainSnippet) {
1001
887
  if (isFieldLine(line)) {
1002
- // 到达下一个字段,结束收集
1003
888
  issue.snippet = (issue.snippet && issue.snippet.length > 0)
1004
889
  ? issue.snippet
1005
890
  : codeLines.join('\n').trim();
1006
891
  collectPlainSnippet = false;
1007
- // 继续让本行按字段规则处理
1008
892
  } else {
1009
893
  codeLines.push(line);
1010
894
  continue;
1011
895
  }
1012
896
  }
1013
897
 
1014
- // 若正在收集修改建议的多行文本
1015
898
  if (collectSuggestion) {
1016
899
  if (isFieldLine(line)) {
1017
900
  issue.suggestion = suggestionLines.join('\n').trim();
1018
901
  collectSuggestion = false;
1019
902
  suggestionLines = [];
1020
- // 继续处理当前字段
1021
903
  } else {
1022
904
  suggestionLines.push(line);
1023
905
  continue;
1024
906
  }
1025
907
  }
1026
908
 
1027
- // 处理标准格式的字段
1028
- if (line.startsWith('文件路径:')) {
1029
- // 跳过AI返回的文件路径,我们直接使用本地已知的完整路径
1030
- } else if (line.startsWith('风险等级:')) {
1031
- const level = line.replace('风险等级:', '').trim();
909
+ if (startsWithAny(line, FILE_LABELS)) {
910
+ // ignore; handled above
911
+ } else if (startsWithAny(line, RISK_LABELS)) {
912
+ const level = extractAfterAny(line, RISK_LABELS) || '';
1032
913
  issue.risk = this.mapRiskLevel(level);
1033
- } else if (line.startsWith('风险原因:')) {
1034
- issue.message = line.replace('风险原因:', '').trim();
1035
- } else if (line.startsWith('修改建议:')) {
1036
- const rest = line.replace('修改建议:', '').trim();
914
+ } else if (startsWithAny(line, REASON_LABELS)) {
915
+ issue.message = extractAfterAny(line, REASON_LABELS) || '';
916
+ } else if (startsWithAny(line, SUGGEST_LABELS)) {
917
+ const rest = extractAfterAny(line, SUGGEST_LABELS) || '';
1037
918
  collectSuggestion = true;
1038
919
  suggestionLines = rest ? [rest] : [];
1039
- } else if (line.startsWith('代码片段:')) {
1040
- const snippetContent = line.replace('代码片段:', '').trim();
1041
- // 如果代码片段行后面直接有内容,使用该内容
1042
- if (snippetContent && !snippetContent.startsWith('```')) {
1043
- // 改为启动“非围栏片段”模式,收集后续多行直到下一个字段
1044
- collectPlainSnippet = true;
1045
- codeLines = [snippetContent];
1046
- } else {
1047
- // 检查下一行是否是代码块开始
1048
- if (i + 1 < lines.length && lines[i + 1].trim().startsWith('```')) {
1049
- isInCodeBlock = true;
1050
- i++; // 跳过 ``` 行
1051
- codeLines = [];
1052
- } else {
1053
- // 启动非围栏代码片段收集,直到遇到下一个字段
1054
- collectPlainSnippet = true;
1055
- codeLines = [];
1056
- }
1057
- }
1058
- }
1059
- // 处理实际AI响应格式的字段(没有冒号前缀)
1060
- else if (line.trim().startsWith('代码片段:')) {
1061
- const snippetContent = line.replace(/^.*代码片段:/, '').trim();
920
+ } else if (startsWithAny(line, SNIPPET_LABELS)) {
921
+ const snippetContent = extractAfterAny(line, SNIPPET_LABELS) || '';
1062
922
  if (snippetContent && !snippetContent.startsWith('```')) {
1063
- // 改为启动“非围栏片段”模式,收集后续多行直到下一个字段
1064
923
  collectPlainSnippet = true;
1065
924
  codeLines = [snippetContent];
1066
925
  } else {
1067
- // 检查下一行是否是代码块开始
1068
926
  if (i + 1 < lines.length && lines[i + 1].trim().startsWith('```')) {
1069
927
  isInCodeBlock = true;
1070
- i++; // 跳过 ``` 行
928
+ i++;
1071
929
  codeLines = [];
1072
930
  } else {
1073
- // 启动非围栏代码片段收集,直到遇到下一个字段
1074
931
  collectPlainSnippet = true;
1075
932
  codeLines = [];
1076
933
  }
1077
934
  }
1078
- } else if (line.trim().startsWith('风险等级:')) {
1079
- const level = line.replace(/^.*风险等级:/, '').trim();
1080
- issue.risk = this.mapRiskLevel(level);
1081
- } else if (line.trim().startsWith('风险原因:')) {
1082
- issue.message = line.replace(/^.*风险原因:/, '').trim();
1083
- } else if (line.trim().startsWith('修改建议:')) {
1084
- const rest = line.replace(/^.*修改建议:/, '').trim();
1085
- collectSuggestion = true;
1086
- suggestionLines = rest ? [rest] : [];
1087
935
  } else if (isInCodeBlock) {
1088
- // 处理代码块内容
1089
936
  if (line.trim() === '```') {
1090
- // 代码块结束
1091
937
  isInCodeBlock = false;
1092
938
  issue.snippet = codeLines.join('\n').trim();
1093
939
  } else {
1094
- // 收集代码行
1095
940
  codeLines.push(line);
1096
941
  }
1097
942
  }
1098
943
  }
1099
944
 
1100
- // 若到末尾仍在收集非围栏片段,收尾
1101
945
  if (collectPlainSnippet && (!issue.snippet || issue.snippet.length === 0)) {
1102
946
  issue.snippet = codeLines.join('\n').trim();
1103
947
  }
1104
-
1105
- // 若到末尾仍在收集多行修改建议,收尾
1106
948
  if (collectSuggestion && (!issue.suggestion || issue.suggestion.length === 0)) {
1107
949
  issue.suggestion = suggestionLines.join('\n').trim();
1108
950
  }
1109
951
 
1110
- // 回退:如果未找到“代码片段:”字段但块内存在代码块,则提取第一个代码块内容作为片段
1111
952
  if (!issue.snippet) {
1112
953
  const fenceMatch = block.match(/```([\s\S]*?)```/);
1113
954
  if (fenceMatch && fenceMatch[1]) {
@@ -1115,12 +956,10 @@ ${segment.content}
1115
956
  }
1116
957
  }
1117
958
 
1118
- // 片段规范化:移除“中间省略”占位行,裁剪到合理长度,并避免跨越不相邻的行号簇
1119
959
  if (issue.snippet && typeof issue.snippet === 'string') {
1120
960
  issue.snippet = this.normalizeSnippet(issue.snippet);
1121
961
  }
1122
962
 
1123
- // 从代码片段中提取行号范围(支持每行的`[n]`前缀;兼容可选的'+'或空格前缀)
1124
963
  if (issue.snippet && typeof issue.snippet === 'string') {
1125
964
  let startNum = null;
1126
965
  let endNum = null;
@@ -1141,22 +980,11 @@ ${segment.content}
1141
980
  issue.line = issue.lineStart; // 兼容旧字段
1142
981
  }
1143
982
  }
1144
-
1145
- // 必须有原因才认为是有效问题
983
+
1146
984
  if (issue.message) {
1147
- // 确保文件路径不为空,如果AI没有返回文件名称,使用传入的filePath
1148
- if (!issue.file) {
1149
- issue.file = filePath;
1150
- }
1151
-
1152
- // 确保所有问题都有风险等级,如果没有则设为默认值
1153
- if (!issue.risk) {
1154
- issue.risk = 'suggestion'; // 默认为建议等级
1155
- }
1156
-
1157
- // 规范化风险等级:确保在有效范围内
985
+ if (!issue.file) issue.file = filePath;
986
+ if (!issue.risk) issue.risk = 'suggestion';
1158
987
  issue.risk = this.normalizeRiskLevel(issue.risk);
1159
-
1160
988
  return issue;
1161
989
  }
1162
990
 
@@ -1220,15 +1048,30 @@ ${segment.content}
1220
1048
  }
1221
1049
 
1222
1050
  mapRiskLevel(chineseLevel) {
1051
+ const raw = String(chineseLevel || '').trim().toLowerCase();
1223
1052
  const mapping = {
1224
1053
  '致命': 'critical',
1225
1054
  '高危': 'high',
1226
1055
  '中危': 'medium',
1227
1056
  '低危': 'low',
1228
- '建议': 'suggestion'
1057
+ '建议': 'suggestion',
1058
+ 'critical': 'critical',
1059
+ 'high': 'high',
1060
+ 'medium': 'medium',
1061
+ 'low': 'low',
1062
+ 'suggestion': 'suggestion',
1063
+ 'fatal': 'critical',
1064
+ 'blocker': 'critical',
1065
+ 'severe': 'high',
1066
+ 'major': 'high',
1067
+ 'moderate': 'medium',
1068
+ 'minor': 'low',
1069
+ 'info': 'suggestion',
1070
+ 'tip': 'suggestion',
1071
+ 'advice': 'suggestion',
1072
+ 'recommendation': 'suggestion'
1229
1073
  };
1230
-
1231
- return mapping[chineseLevel] || 'suggestion';
1074
+ return mapping[raw] || 'suggestion';
1232
1075
  }
1233
1076
 
1234
1077
  normalizeRiskLevel(riskLevel) {
@@ -1302,19 +1145,23 @@ ${segment.content}
1302
1145
 
1303
1146
  try {
1304
1147
  // 记录请求信息
1305
- logger.debug(`发送分段AI请求 - 模型: ${this.config.model ?? 'gpt-3.5-turbo'}, 消息数: ${collector.messages.length}`);
1148
+ logger.debug(t(this.config, 'chunk_req_info_dbg', {
1149
+ model: this.config.model ?? 'gpt-3.5-turbo',
1150
+ count: collector.messages.length
1151
+ }));
1306
1152
  // 输出请求消息的预览(限制每条消息长度)
1307
- try {
1308
- const preview = collector.messages.map((m, idx) => {
1309
- const text = String(m.content ?? '');
1310
- const maxLen = 1500;
1311
- const cut = text.length > maxLen ? `${text.slice(0, maxLen)}\n...省略 ${text.length - maxLen} 字符` : text;
1312
- return `#${idx + 1} [${m.role}]\n${cut}`;
1313
- }).join('\n---\n');
1314
- logger.debug(`AI请求消息预览:\n${preview}`);
1315
- } catch (e) {
1316
- logger.debug(`AI请求消息预览失败: ${e.message}`);
1317
- }
1153
+ try {
1154
+ const preview = collector.messages.map((m, idx) => {
1155
+ const text = String(m.content ?? '');
1156
+ const maxLen = 1500;
1157
+ const truncatedSuffix = t(this.config, 'preview_truncated_suffix', { count: text.length - maxLen });
1158
+ const cut = text.length > maxLen ? `${text.slice(0, maxLen)}\n${truncatedSuffix}` : text;
1159
+ return `#${idx + 1} [${m.role}]\n${cut}`;
1160
+ }).join('\n---\n');
1161
+ logger.debug(t(this.config, 'chunk_req_preview_dbg', { preview }));
1162
+ } catch (e) {
1163
+ logger.debug(t(this.config, 'chunk_req_preview_fail_dbg', { error: e.message }));
1164
+ }
1318
1165
 
1319
1166
  // 发送请求
1320
1167
  const isFirstCall = this.chunkedResponseCollector.get(requestId).chunks.length === 0;
@@ -1326,7 +1173,7 @@ ${segment.content}
1326
1173
  }, isFirstCall ? meta : null);
1327
1174
 
1328
1175
  const content = response.choices[0].message.content;
1329
- logger.debug(`AI响应: ${content.length} 字符`);
1176
+ logger.debug(t(this.config, 'ai_response_len_dbg', { len: content.length }));
1330
1177
  // 检查是否是分段响应
1331
1178
  if (this.isChunkedResponse(content)) {
1332
1179
  const chunkInfo = this.parseChunkInfo(content);
@@ -1349,9 +1196,9 @@ ${segment.content}
1349
1196
  collector.messages.push({ role: 'assistant', content: content });
1350
1197
  collector.messages.push({
1351
1198
  role: 'user',
1352
- content: '继续'
1199
+ content: t(this.config, 'chunk_continue_prompt')
1353
1200
  });
1354
- logger.debug('分段响应未完成,发送"继续"请求下一段');
1201
+ logger.debug(t(this.config, 'chunk_continue_needed_dbg'));
1355
1202
 
1356
1203
  // 递归获取下一段
1357
1204
  return await this.handleChunkedResponse(null, requestId, null);