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.
@@ -6,6 +6,7 @@
6
6
  import { stripCommentsForAI, stripNoReviewForAI } from './utils/strip.js';
7
7
  import { logger } from './utils/logger.js';
8
8
  import { SEGMENTATION_CONSTANTS, AI_CONSTANTS } from './utils/constants.js';
9
+ import { t, getLocale, FIELD_LABELS, buildPrompts } from './utils/i18n.js';
9
10
 
10
11
  export class SegmentedAnalyzer {
11
12
  constructor(config = {}) {
@@ -37,11 +38,9 @@ export class SegmentedAnalyzer {
37
38
  */
38
39
  async analyzeFileSegmented(filePath, content, aiClient, customPrompt = '', staticIssues = []) {
39
40
  try {
40
- logger.progress(`开始分段分析文件: ${filePath}`);
41
-
42
41
  // 第一步:智能分段
43
42
  const segments = this.createIntelligentSegments(content, filePath);
44
- logger.info(`文件分为 ${segments.length} 段`);
43
+ logger.progress(t(this.config, 'segment_overall_start', { file: filePath, total: segments.length, concurrency: '', totalNote: '' }));
45
44
 
46
45
  const allIssues = [];
47
46
  const segmentSummaries = []; // 存储每段的摘要
@@ -50,8 +49,8 @@ export class SegmentedAnalyzer {
50
49
  // 第二步:逐段分析
51
50
  for (let i = 0; i < segments.length; i++) {
52
51
  const segment = segments[i];
53
- logger.info(`开始分析第 ${i + 1}/${segments.length} (行 ${segment.startLine}-${segment.endLine})`);
54
- logger.info(` 预估${segment.tokens} tokens, 共${segment.endLine - segment.startLine + 1} 行代码`);
52
+ logger.info(t(this.config, 'segment_start_label', { file: filePath, index: i + 1, total: segments.length, start: segment.startLine, end: segment.endLine }));
53
+ logger.debug(t(this.config, 'segment_size_estimate_dbg', { tokens: segment.tokens, lines: (segment.endLine - segment.startLine + 1) }));
55
54
 
56
55
  try {
57
56
  // 分析当前段
@@ -69,9 +68,9 @@ export class SegmentedAnalyzer {
69
68
  // 收集问题
70
69
  if (segmentResult.issues && segmentResult.issues.length > 0) {
71
70
  allIssues.push(...segmentResult.issues);
72
- logger.success(`第 ${i + 1} 段分析完成,发现 ${segmentResult.issues.length} 个问题`);
71
+ logger.success(t(this.config, 'segment_analysis_done_n_issues', { batch: '', file: filePath, index: i + 1, count: segmentResult.issues.length }));
73
72
  } else {
74
- logger.success(`第 ${i + 1} 段分析完成,未发现问题`);
73
+ logger.success(t(this.config, 'segment_analysis_done_zero', { batch: '', file: filePath, index: i + 1 }));
75
74
  }
76
75
 
77
76
  // 收集摘要(如果启用)
@@ -84,13 +83,14 @@ export class SegmentedAnalyzer {
84
83
 
85
84
  // 更新上下文(保留最近几段的摘要)
86
85
  const recentSummaries = segmentSummaries.slice(-3); // 保留最近3段摘要
87
- previousContext = recentSummaries.map(s =>
88
- `第${s.segmentIndex}段(行${s.lineRange}): ${s.summary}`
89
- ).join('\n');
86
+ const loc = getLocale(this.config);
87
+ previousContext = [t(this.config, 'segment_prev_context_header'),
88
+ ...recentSummaries.map(s => t(this.config, 'segment_prev_context_item', { index: s.segmentIndex, range: s.lineRange, summary: s.summary }))
89
+ ].join('\n');
90
90
  }
91
91
 
92
92
  } catch (error) {
93
- logger.error(`第 ${i + 1} 段分析失败: ${error.message}`);
93
+ logger.error(t(this.config, 'segment_analysis_failed', { index: i + 1, error: error?.message || String(error) }));
94
94
  // 继续分析下一段,不中断整个流程
95
95
  }
96
96
  }
@@ -106,7 +106,7 @@ export class SegmentedAnalyzer {
106
106
  };
107
107
 
108
108
  } catch (error) {
109
- logger.error(`分段分析失败: ${error.message}`);
109
+ logger.error(t(this.config, 'segment_file_failed', { error: error?.message || String(error) }));
110
110
  throw error;
111
111
  }
112
112
  }
@@ -227,13 +227,13 @@ export class SegmentedAnalyzer {
227
227
  */
228
228
  async analyzeSegment(segment, filePath, aiClient, customPrompt, staticIssues, previousContext, currentSegmentNum, totalSegments) {
229
229
  try {
230
- logger.debug(` 准备第 ${currentSegmentNum} 段代码内容...`);
230
+ logger.debug(t(this.config, 'segment_prepare_content_dbg', { index: currentSegmentNum }));
231
231
 
232
232
  // 准备代码内容
233
233
  const prepared = await stripNoReviewForAI(segment.content, filePath);
234
234
  const clean = await stripCommentsForAI(prepared, filePath);
235
235
 
236
- logger.debug(` 构建第 ${currentSegmentNum} 段分析提示词...`);
236
+ logger.debug(t(this.config, 'segment_build_prompt_dbg', { index: currentSegmentNum }));
237
237
 
238
238
  // 构建分段分析提示词
239
239
  const segmentPrompt = this.buildSegmentPrompt(
@@ -267,12 +267,12 @@ export class SegmentedAnalyzer {
267
267
  // 解析AI响应
268
268
  const result = this.parseSegmentResponse(responseContent, filePath, segment);
269
269
 
270
- logger.info(` ${currentSegmentNum} 段响应解析完成,发现 ${result.issues ? result.issues.length : 0} 个问题`);
270
+ logger.info(t(this.config, 'segment_response_parsed_info', { index: currentSegmentNum, count: (result.issues ? result.issues.length : 0) }));
271
271
 
272
272
  return result;
273
273
 
274
274
  } catch (error) {
275
- logger.error(`第 ${currentSegmentNum} 段分析失败: ${error.message}`);
275
+ logger.error(t(this.config, 'segment_analysis_failed', { index: currentSegmentNum, error: error?.message || String(error) }));
276
276
  throw error;
277
277
  }
278
278
  }
@@ -281,61 +281,48 @@ export class SegmentedAnalyzer {
281
281
  * 构建分段分析提示词
282
282
  */
283
283
  buildSegmentPrompt(segment, filePath, cleanContent, customPrompt, staticIssues, previousContext, currentSegmentNum, totalSegments) {
284
- let prompt = `我正在对一个大文件进行分段代码审查。以下是第 ${currentSegmentNum}/${totalSegments} 段的内容:
285
-
286
- **文件路径**: ${filePath}
287
- **当前分段**: 第 ${currentSegmentNum}/${totalSegments} 段
288
- **代码行范围**: ${segment.startLine}-${segment.endLine}`;
289
-
290
- // 添加上下文说明
291
- if (segment.contextInfo.hasPreContext || segment.contextInfo.hasPostContext) {
292
- prompt += `\n**上下文说明**: `;
293
- if (segment.contextInfo.hasPreContext) {
294
- prompt += `前 ${segment.contextInfo.preContextLines} 行为上文上下文`;
295
- }
296
- if (segment.contextInfo.hasPostContext) {
297
- if (segment.contextInfo.hasPreContext) prompt += `,`;
298
- prompt += `后 ${segment.contextInfo.postContextLines} 行为下文上下文`;
299
- }
300
- prompt += `,这些上下文行仅用于帮助理解代码逻辑,请避免对重叠部分重复报告问题。`;
301
- }
284
+ const loc = getLocale(this.config);
285
+ const L = FIELD_LABELS[loc];
286
+ let prompt = t(this.config, 'segment_prompt_template', {
287
+ index: currentSegmentNum,
288
+ total: totalSegments,
289
+ Lfile: L.file,
290
+ Lcontent: L.content,
291
+ file: filePath,
292
+ content: cleanContent
293
+ });
302
294
 
303
- // 添加前面段落的摘要上下文
295
+ // 追加前面段落摘要上下文
304
296
  if (previousContext && this.config.enableSummaryContext) {
305
- prompt += `\n\n**前面段落摘要**:\n${previousContext}`;
297
+ prompt += `\n\n${previousContext}`;
306
298
  }
307
299
 
308
- // 添加代码内容
309
- prompt += `\n\n**代码内容**:\n\`\`\`\n${cleanContent}\n\`\`\``;
310
-
311
- // 添加静态问题提示
300
+ // 添加静态问题提示(仅与当前段相关)
312
301
  if (staticIssues && staticIssues.length > 0) {
313
- const relevantStaticIssues = staticIssues.filter(issue => {
314
- // 简单的行号匹配,实际可以更精确
315
- return issue.line >= segment.startLine && issue.line <= segment.endLine;
316
- });
317
-
302
+ const relevantStaticIssues = staticIssues.filter(issue => issue.line >= segment.startLine && issue.line <= segment.endLine);
318
303
  if (relevantStaticIssues.length > 0) {
319
- prompt += `\n\n**本段相关的静态检测问题**:\n`;
304
+ prompt += `\n\n${t(this.config, 'segment_static_issues_header', { index: currentSegmentNum })}\n`;
320
305
  relevantStaticIssues.forEach((issue, idx) => {
321
- prompt += `${idx + 1}. 第${issue.line}行 (${issue.risk}): ${issue.message}\n`;
306
+ const riskDisp = issue.risk;
307
+ const snippetLabel = L.snippet;
308
+ const suggest = issue.suggestion ? ` ${issue.suggestion}` : '';
309
+ prompt += t(this.config, 'segment_static_issue_line', {
310
+ index: idx + 1,
311
+ risk: riskDisp,
312
+ message: issue.message,
313
+ suggest,
314
+ snippetLabel,
315
+ snippet: issue.snippet || ''
316
+ }) + '\n';
322
317
  });
323
318
  }
324
319
  }
325
320
 
326
- // 添加自定义提示词
321
+ // 追加自定义提示词
327
322
  if (customPrompt) {
328
- prompt += `\n\n**自定义审查要求**:\n${customPrompt}`;
323
+ prompt += `\n\n${customPrompt}`;
329
324
  }
330
325
 
331
- // 添加分段分析要求
332
- prompt += `\n\n**分段分析要求**:
333
- 1. 请仅分析当前分段的代码,不要分析上下文行中的问题
334
- 2. 如果启用摘要功能,请在分析结果后提供一个简短的代码摘要
335
- 3. 严格按照指定格式返回结果
336
- 4. 对于跨段的问题,请在当前段中标注,后续段会参考前面的摘要进行综合判断
337
- 5. 分段上下文限制:不要评估“导入是否被使用”,严格忽略关于“未使用的导入/模块/依赖”的任何提示或建议(包括建议删除未使用的导入)`;
338
-
339
326
  return prompt;
340
327
  }
341
328
 
@@ -343,52 +330,7 @@ export class SegmentedAnalyzer {
343
330
  * 获取分段分析的系统提示词
344
331
  */
345
332
  getSegmentSystemPrompt() {
346
- let systemPrompt = `你是一个专业的代码审查专家,正在对大文件进行分段分析。
347
-
348
- **分段分析特点**:
349
- - 你收到的是文件的一个片段,可能包含上下文行
350
- - 上下文行仅用于理解代码逻辑,不应对其报告问题
351
- - 需要考虑代码的连续性和上下文关系
352
- - 避免对重叠部分重复报告问题
353
-
354
- **输出格式要求**:
355
- 请严格按照以下格式返回分析结果:
356
-
357
- **-----代码分析结果-----**
358
- 文件路径:{文件路径}
359
- 代码片段:{具体的问题代码片段}
360
- 风险等级:{致命/高危/中危/低危/建议}
361
- 风险原因:{详细原因}
362
- 修改建议:{具体建议}
363
-
364
- [如果有多个问题,用空行分隔]`;
365
-
366
- // 如果启用摘要功能,添加摘要要求
367
- if (this.config.enableSummaryContext) {
368
- systemPrompt += `
369
-
370
- **摘要要求**:
371
- 在所有问题分析完成后,请提供一个简短摘要:
372
-
373
- **-----段落摘要-----**
374
- {简要描述这段代码的主要功能、关键逻辑和重要特征,控制在${this.config.maxSummaryLength}字符以内}`;
375
- }
376
-
377
- systemPrompt += `
378
-
379
- **风险等级定义**:
380
- - 致命:可能导致系统崩溃、数据丢失、严重安全漏洞
381
- - 高危:可能导致安全漏洞、数据泄露、业务逻辑错误
382
- - 中危:可能影响系统稳定性、性能问题、用户体验
383
- - 低危:代码质量问题、不符合最佳实践
384
- - 建议:改进建议,提升代码质量`;
385
-
386
- systemPrompt += `
387
-
388
- **分段场景的忽略规则(必须遵守)**:
389
- - 由于其它段可能引用当前段的导入,请不要评估或报告“未使用的导入/模块/依赖”相关问题;即使当前片段内未见引用,也不要建议移除该导入。
390
- - 仅在本段内可以明确识别的实际错误或风险时再输出问题。`;
391
-
333
+ const { systemPrompt } = buildPrompts(this.config);
392
334
  return systemPrompt;
393
335
  }
394
336
 
@@ -400,19 +342,51 @@ export class SegmentedAnalyzer {
400
342
  let summary = '';
401
343
 
402
344
  try {
403
- // 分离问题分析和摘要
404
- const parts = responseContent.split('**-----段落摘要-----**');
405
- const analysisContent = parts[0];
406
- const summaryContent = parts[1];
345
+ // 分离问题分析和摘要(支持中英文摘要标记)
346
+ const summaryMarkerZh = t(this.config, 'segment_summary_marker');
347
+ const summaryMarkerEn = '**-----Segment Summary-----**';
348
+ let analysisContent = responseContent;
349
+ let summaryContent = '';
350
+ if (responseContent.includes(summaryMarkerZh)) {
351
+ const parts = responseContent.split(summaryMarkerZh);
352
+ analysisContent = parts[0];
353
+ summaryContent = parts[1];
354
+ } else if (responseContent.includes(summaryMarkerEn)) {
355
+ const parts = responseContent.split(summaryMarkerEn);
356
+ analysisContent = parts[0];
357
+ summaryContent = parts[1];
358
+ }
407
359
 
408
- // 解析问题
409
- if (analysisContent.includes('**-----代码分析结果-----**')) {
410
- const problemSections = analysisContent.split('**-----代码分析结果-----**').slice(1);
411
-
412
- for (const section of problemSections) {
413
- const issue = this.parseIssueSection(section.trim(), filePath, segment);
414
- if (issue) {
415
- issues.push(issue);
360
+ // 解析问题(支持多种标记:开始/结束 或 单标记 中英文)
361
+ const startEndMarkers = [
362
+ { start: '**-----代码分析结果开始-----**', end: '**-----代码分析结果结束-----**' },
363
+ { start: '**-----Code Analysis Result Start-----**', end: '**-----Code Analysis Result End-----**' },
364
+ ];
365
+ let parsedByStartEnd = false;
366
+ for (const m of startEndMarkers) {
367
+ if (analysisContent.includes(m.start) && analysisContent.includes(m.end)) {
368
+ parsedByStartEnd = true;
369
+ const regex = new RegExp(`${m.start}([\s\S]*?)${m.end}`, 'g');
370
+ const matches = Array.from(analysisContent.matchAll(regex));
371
+ for (const match of matches) {
372
+ const section = match[1].trim();
373
+ const issue = this.parseIssueSection(section, filePath, segment);
374
+ if (issue) issues.push(issue);
375
+ }
376
+ break;
377
+ }
378
+ }
379
+
380
+ if (!parsedByStartEnd) {
381
+ const singleMarkers = ['**-----代码分析结果-----**', '**-----Code Review Result-----**'];
382
+ for (const marker of singleMarkers) {
383
+ if (analysisContent.includes(marker)) {
384
+ const problemSections = analysisContent.split(marker).slice(1);
385
+ for (const section of problemSections) {
386
+ const issue = this.parseIssueSection(section.trim(), filePath, segment);
387
+ if (issue) issues.push(issue);
388
+ }
389
+ break;
416
390
  }
417
391
  }
418
392
  }
@@ -423,7 +397,7 @@ export class SegmentedAnalyzer {
423
397
  }
424
398
 
425
399
  } catch (error) {
426
- logger.warn(`解析AI响应失败: ${error.message}`);
400
+ logger.warn(t(this.config, 'parse_seg_response_failed_warn', { error: error?.message || String(error) }));
427
401
  }
428
402
 
429
403
  return {
@@ -438,6 +412,8 @@ export class SegmentedAnalyzer {
438
412
  parseIssueSection(section, filePath, segment) {
439
413
  try {
440
414
  const lines = section.split('\n').filter(line => line.trim());
415
+ const loc = getLocale(this.config);
416
+ const L = FIELD_LABELS[loc];
441
417
 
442
418
  let issue = {
443
419
  file: filePath,
@@ -447,16 +423,16 @@ export class SegmentedAnalyzer {
447
423
  };
448
424
 
449
425
  for (const line of lines) {
450
- if (line.startsWith('文件路径:')) {
426
+ if (line.startsWith(L.file)) {
451
427
  // 文件路径已知,跳过
452
- } else if (line.startsWith('代码片段:')) {
453
- issue.snippet = line.substring('代码片段:'.length).trim();
454
- } else if (line.startsWith('风险等级:')) {
455
- issue.risk = line.substring('风险等级:'.length).trim();
456
- } else if (line.startsWith('风险原因:')) {
457
- issue.message = line.substring('风险原因:'.length).trim();
458
- } else if (line.startsWith('修改建议:')) {
459
- issue.suggestion = line.substring('修改建议:'.length).trim();
428
+ } else if (line.startsWith(L.snippet)) {
429
+ issue.snippet = line.substring(L.snippet.length).trim();
430
+ } else if (line.startsWith(L.risk)) {
431
+ issue.risk = line.substring(L.risk.length).trim();
432
+ } else if (line.startsWith(L.reason)) {
433
+ issue.message = line.substring(L.reason.length).trim();
434
+ } else if (line.startsWith(L.suggestion)) {
435
+ issue.suggestion = line.substring(L.suggestion.length).trim();
460
436
  }
461
437
  }
462
438
 
@@ -466,7 +442,7 @@ export class SegmentedAnalyzer {
466
442
  }
467
443
 
468
444
  } catch (error) {
469
- logger.warn(`解析问题段落失败: ${error.message}`);
445
+ logger.warn(t(this.config, 'parse_issue_section_failed_warn', { error: error?.message || String(error) }));
470
446
  }
471
447
 
472
448
  return null;
@@ -2,6 +2,7 @@ import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import path from 'path';
4
4
  import { logger } from './logger.js';
5
+ import { t } from './i18n.js';
5
6
 
6
7
  const execAsync = promisify(exec);
7
8
 
@@ -28,7 +29,7 @@ export class GitDiffParser {
28
29
  });
29
30
  return stdout;
30
31
  } catch (error) {
31
- logger.error('获取git diff失败:', error);
32
+ logger.error(t(this.config, 'git_diff_parse_failed', { error: error?.message || String(error) }));
32
33
  return '';
33
34
  }
34
35
  }
@@ -81,7 +82,7 @@ export class GitDiffParser {
81
82
 
82
83
  // 检查文件是否应该被忽略
83
84
  if (!this.isReviewableFile(filePath)) {
84
- logger.debug(`文件被忽略规则跳过: ${filePath}`);
85
+ logger.debug(t(this.config, 'file_skipped_by_ignore_rule_dbg', { file: filePath }));
85
86
  return null;
86
87
  }
87
88
 
@@ -259,7 +260,7 @@ export class GitDiffParser {
259
260
  smartSegments.push(this.finalizeSegment(currentSegment));
260
261
  }
261
262
 
262
- logger.debug(`文件 ${filePath} 智能分段完成: ${sections.length} 个原始段 -> ${smartSegments.length} 个智能段`);
263
+ logger.debug(t(this.config, 'smart_segmentation_done_dbg', { file: filePath, raw: sections.length, smart: smartSegments.length }));
263
264
  return smartSegments;
264
265
  }
265
266
 
@@ -309,7 +310,7 @@ export class GitDiffParser {
309
310
  const reviewLineStrings = reviewItems.map(it => (it.type === '+' ? '+' : ' ') + it.content);
310
311
  const filteredLines = this.filterDisabledLines(reviewLineStrings, addedLineNumbers);
311
312
  if (filteredLines.addedLineNumbers.length === 0) {
312
- logger.debug(`所有新增行都被代码内指令忽略: ${filePath}`);
313
+ logger.debug(t(this.config, 'all_added_lines_ignored_dbg', { file: filePath }));
313
314
  return null;
314
315
  }
315
316
 
@@ -363,7 +364,7 @@ export class GitDiffParser {
363
364
  });
364
365
  return stdout;
365
366
  } catch (error) {
366
- logger.debug(`无法获取暂存区文件内容 ${filePath}:`, error.message);
367
+ logger.debug(t(this.config, 'get_staged_file_failed_dbg', { file: filePath, error: error?.message || String(error) }));
367
368
  return '';
368
369
  }
369
370
  }
@@ -373,11 +374,11 @@ export class GitDiffParser {
373
374
  * @returns {Promise<Array>} 审查数据数组
374
375
  */
375
376
  async getStagedDiffReviewData() {
376
- logger.progress('正在分析暂存区变更...');
377
+ logger.progress(t(this.config, 'analyze_staged_changes_progress'));
377
378
 
378
379
  const diffOutput = await this.getStagedDiff();
379
380
  if (!diffOutput.trim()) {
380
- logger.info('暂存区没有代码变更需要审查');
381
+ logger.info(t(this.config, 'no_changes_skip'));
381
382
  return [];
382
383
  }
383
384
 
@@ -398,7 +399,7 @@ export class GitDiffParser {
398
399
  }
399
400
  }
400
401
 
401
- logger.info(`发现 ${reviewData.length} 个文件有代码变更需要审查`);
402
+ logger.info(t(this.config, 'found_changed_files_n', { count: reviewData.length }));
402
403
  return reviewData;
403
404
  }
404
405