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/bin/review.js CHANGED
@@ -6,6 +6,7 @@ import { CodeReviewer } from '../lib/reviewer.js';
6
6
  import { ConfigLoader } from '../lib/config-loader.js';
7
7
  import { logger } from '../lib/utils/logger.js';
8
8
  import { BATCH_CONSTANTS } from '../lib/utils/constants.js';
9
+ import { t, displayRisk } from '../lib/utils/i18n.js';
9
10
 
10
11
  class ReviewCLI {
11
12
  constructor() {
@@ -33,30 +34,31 @@ class ReviewCLI {
33
34
  const args = process.argv.slice(2);
34
35
 
35
36
  try {
36
- logger.progress('代码审查启动中,请等待...');
37
37
  // 加载配置
38
38
  const configLoader = new ConfigLoader(this.projectRoot);
39
39
  const config = await configLoader.loadConfig();
40
+ this.config = config;
41
+ logger.progress(t(config, 'cli_start'));
40
42
 
41
43
  // 调试日志开关(命令行)
42
44
  if (args.includes('--debug')) {
43
45
  logger.debugMode = true;
44
- logger.info('通过命令行参数启用调试日志模式');
46
+ logger.info(t(config, 'debug_enabled'));
45
47
  }
46
48
 
47
49
  // 处理AI相关的命令行参数
48
50
  if (args.includes('--no-ai')) {
49
51
  config.ai = { ...config.ai, enabled: false };
50
- logger.info('通过命令行参数禁用AI分析');
52
+ logger.info(t(config, 'ai_disabled'));
51
53
  } else if (args.includes('--ai')) {
52
54
  config.ai = { ...config.ai, enabled: true };
53
- logger.info('通过命令行参数启用AI分析');
55
+ logger.info(t(config, 'ai_enabled'));
54
56
  }
55
57
 
56
58
  // 处理Git Diff审查相关参数
57
59
  if (args.includes('--diff-only')) {
58
60
  config.ai = { ...config.ai, reviewOnlyChanges: true };
59
- logger.info('通过命令行参数启用Git Diff增量审查模式');
61
+ logger.info(t(config, 'diff_only_enabled'));
60
62
  }
61
63
 
62
64
  const rules = await configLoader.loadRules(config);
@@ -71,33 +73,33 @@ class ReviewCLI {
71
73
  const fileList = args[filesIndex + 1]?.split(',').map(f => f.trim()) || [];
72
74
  result = await reviewer.reviewSpecificFiles(fileList);
73
75
  } else {
74
- logger.info('使用方法:');
75
- logger.info(' npx smart-code-reviewer --staged # 审查暂存区文件');
76
- logger.info(' npx smart-code-reviewer --staged --diff-only # 仅审查暂存区变动内容(git diff)');
77
- logger.info(' npx smart-code-reviewer --files file1,file2 # 审查指定文件');
76
+ logger.info(t(config, 'usage_header'));
77
+ logger.info(t(config, 'usage_staged'));
78
+ logger.info(t(config, 'usage_diffonly'));
79
+ logger.info(t(config, 'usage_files'));
78
80
  process.exit(1);
79
81
  }
80
82
 
81
- this.printResults(result);
83
+ this.printResults(result, config);
82
84
  process.exit(result.blockSubmission ? 1 : 0);
83
85
 
84
86
  } catch (error) {
85
- logger.error('审查执行失败:', error);
87
+ logger.error(t(this.config || process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'review_error', { error: error.message }));
86
88
  process.exit(1);
87
89
  }
88
90
  }
89
91
 
90
- printResults(result) {
92
+ printResults(result, config) {
91
93
  const staticIssues = result.issues.filter(i => i.source === 'static');
92
94
  const aiIssues = result.issues.filter(i => i.source === 'ai');
93
95
  // 本地规则审查结果
94
- logger.info('\n本地规则审查结果');
96
+ logger.info('\n' + t(config, 'local_analysis_header'));
95
97
  const staticByFile = this.groupIssuesByFile(staticIssues);
96
98
  if (staticIssues.length === 0) {
97
- logger.info('');
99
+ logger.info(t(config, 'no_issues'));
98
100
  } else {
99
101
  Object.entries(staticByFile).forEach(([file, issues]) => {
100
- logger.info(`\n文件: ${file}`);
102
+ logger.info('\n' + t(config, 'file_label', { file }));
101
103
  // 根据行号排序,保证位置从上到下
102
104
  const getLineKey = (i) => {
103
105
  const s = Number(i.lineStart);
@@ -110,36 +112,36 @@ class ReviewCLI {
110
112
  };
111
113
  const sorted = [...issues].sort((a, b) => getLineKey(a) - getLineKey(b));
112
114
  sorted.forEach((issue, index) => {
113
- logger.info(`\n问题${index + 1}:`);
114
- const locationLabel = this.formatLocationLabel(issue);
115
+ logger.info('\n' + t(config, 'issue_label', { index: index + 1 }));
116
+ const locationLabel = this.formatLocationLabel(issue, config);
115
117
  if (locationLabel) logger.info(locationLabel);
116
118
  // 美化代码片段输出:去除行号前缀并统一缩进
117
119
  if (issue.snippet && issue.snippet.trim().length > 0) {
118
- logger.info('代码片段:');
120
+ logger.info(t(config, 'snippet_label'));
119
121
  logger.info(this.formatSnippet(issue.snippet));
120
122
  } else {
121
- logger.info('代码片段:(全局性问题)');
123
+ logger.info(t(config, 'snippet_global_label'));
122
124
  }
123
- logger.info(`风险等级:${this.getRiskLevelText(issue.risk)}`);
124
- logger.info(`风险原因:${issue.message}`);
125
- if (issue.suggestion) logger.info(`修改建议:${issue.suggestion}`);
125
+ logger.info(t(config, 'risk_level_label') + displayRisk(issue.risk, config));
126
+ logger.info(t(config, 'risk_reason_label') + issue.message);
127
+ if (issue.suggestion) logger.info(t(config, 'suggestions_label') + issue.suggestion);
126
128
  });
127
129
  });
128
130
  }
129
131
 
130
132
  // AI代码分析结果(若有)
131
133
  if (result.aiRan) {
132
- logger.info('\nAI代码分析结果');
134
+ logger.info('\n' + t(config, 'ai_analysis_header'));
133
135
  // 说明:行号可能不连续是预处理所致(剥离注释/无需审查片段),请忽略行号跳跃
134
- logger.info('提示:片段行号为源文件绝对行号,因清洗注释/无需审查片段可能出现跳跃,请忽略行号不连续。');
136
+ logger.info(t(config, 'tip_line_numbers'));
135
137
  // 去重:按 file+line+message 进行去重,避免重复输出
136
138
  // 打印时不再对 AI 结果做粗略去重(聚合逻辑已在 reviewer.generateResult 中完成),仅分文件展示
137
139
  const aiByFile = this.groupIssuesByFile(aiIssues);
138
140
  if (aiIssues.length === 0) {
139
- logger.info('');
141
+ logger.info(t(config, 'no_issues'));
140
142
  } else {
141
143
  Object.entries(aiByFile).forEach(([file, issues]) => {
142
- logger.info(`\n文件: ${file}`);
144
+ logger.info('\n' + t(config, 'file_label', { file }));
143
145
  // 根据行号排序:起始行号优先,其次单行号;无行号的排后
144
146
  const getLineKey = (i) => {
145
147
  const s = Number(i.lineStart);
@@ -152,19 +154,19 @@ class ReviewCLI {
152
154
  };
153
155
  const sorted = [...issues].sort((a, b) => getLineKey(a) - getLineKey(b));
154
156
  sorted.forEach((issue, index) => {
155
- logger.info(`\n问题${index + 1}:`);
156
- const locationLabel = this.formatLocationLabel(issue);
157
+ logger.info('\n' + t(config, 'issue_label', { index: index + 1 }));
158
+ const locationLabel = this.formatLocationLabel(issue, config);
157
159
  if (locationLabel) logger.info(locationLabel);
158
160
  // 美化代码片段输出:去除行号前缀并统一缩进
159
161
  if (issue.snippet && issue.snippet.trim().length > 0) {
160
- logger.info('代码片段:');
162
+ logger.info(t(config, 'snippet_label'));
161
163
  logger.info(this.formatSnippet(issue.snippet));
162
164
  } else {
163
- logger.info('代码片段:(全局性问题)');
165
+ logger.info(t(config, 'snippet_global_label'));
164
166
  }
165
- logger.info(`风险等级:${this.getRiskLevelText(issue.risk)}`);
166
- logger.info(`风险原因:${issue.message}`);
167
- if (issue.suggestion) logger.info(`修改建议:${issue.suggestion}`);
167
+ logger.info(t(config, 'risk_level_label') + displayRisk(issue.risk, config));
168
+ logger.info(t(config, 'risk_reason_label') + issue.message);
169
+ if (issue.suggestion) logger.info(t(config, 'suggestions_label') + issue.suggestion);
168
170
  });
169
171
  });
170
172
  }
@@ -188,28 +190,21 @@ class ReviewCLI {
188
190
  }, {});
189
191
  }
190
192
 
191
- getRiskLevelText(risk) {
192
- const levels = {
193
- 'critical': '致命',
194
- 'high': '高危',
195
- 'medium': '中危',
196
- 'low': '低危',
197
- 'suggestion': '建议'
198
- };
199
- return levels[risk] || risk;
193
+ getRiskLevelText(risk, config) {
194
+ return displayRisk(risk, config);
200
195
  }
201
196
 
202
197
  // 位置标签:范围为“行号范围:start-end”,单行为“行号:n”
203
- formatLocationLabel(issue) {
198
+ formatLocationLabel(issue, config) {
204
199
  const start = Number(issue.lineStart);
205
200
  const end = Number(issue.lineEnd);
206
201
  const single = Number(issue.line);
207
202
  if (Number.isFinite(start) && Number.isFinite(end) && start > 0 && end >= start) {
208
- if (start === end) return `行号:${start}`;
209
- return `行号范围:${start}-${end}`;
203
+ if (start === end) return t(config, 'line_label', { line: start });
204
+ return t(config, 'line_range_label', { start, end });
210
205
  }
211
206
  if (Number.isFinite(single) && single > 0) {
212
- return `行号:${single}`;
207
+ return t(config, 'line_label', { line: single });
213
208
  }
214
209
  return '';
215
210
  }
@@ -253,4 +248,4 @@ class ReviewCLI {
253
248
 
254
249
  // 运行审查
255
250
  const cli = new ReviewCLI();
256
- cli.run().catch(error => logger.error('CLI运行失败:', error));
251
+ cli.run().catch(error => logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'review_error', { error: error.message })));
package/index.js CHANGED
@@ -2,4 +2,3 @@ export { CodeReviewer } from './lib/reviewer.js';
2
2
  export { AIClient } from './lib/ai-client.js';
3
3
  export { ConfigLoader } from './lib/config-loader.js';
4
4
  export { defaultConfig, defaultRules } from './lib/default-config.js';
5
- // https://ci.das-security.cn/repository/ah_npm
@@ -3,9 +3,9 @@
3
3
  * 管理多个AI客户端实例,支持并发请求处理
4
4
  */
5
5
 
6
- import path from 'path';
7
6
  import { AIClient } from './ai-client.js';
8
7
  import { logger } from './utils/logger.js';
8
+ import { t } from './utils/i18n.js';
9
9
 
10
10
  export class AIClientPool {
11
11
  constructor(config, rules, poolSize = 3, concurrencyLimiter = null) {
@@ -30,10 +30,15 @@ export class AIClientPool {
30
30
  * 初始化客户端池
31
31
  */
32
32
  initializePool() {
33
- logger.debug(`初始化AI客户端池,大小: ${this.poolSize}`);
33
+ logger.debug(t(this.config, 'init_pool_dbg', { size: this.poolSize }));
34
34
 
35
35
  for (let i = 0; i < this.poolSize; i++) {
36
- const client = new AIClient(this.config.ai);
36
+ // reviewDir locale 一并传递,确保池中客户端的国际化与自定义提示词目录正确
37
+ const client = new AIClient({
38
+ ...this.config.ai,
39
+ reviewDir: this.config.reviewDir,
40
+ locale: this.config.locale
41
+ });
37
42
  client.poolId = i;
38
43
  // 注入全局并发限速器,确保批次与分段共享并发资源
39
44
  if (this.concurrencyLimiter) {
@@ -42,7 +47,7 @@ export class AIClientPool {
42
47
  this.clients.push(client);
43
48
  }
44
49
 
45
- logger.debug(`AI客户端池初始化完成,共${this.clients.length}个客户端`);
50
+ logger.debug(t(this.config, 'pool_init_done_dbg', { count: this.clients.length }));
46
51
  }
47
52
 
48
53
  /**
@@ -142,7 +147,7 @@ export class AIClientPool {
142
147
  successCount++;
143
148
  } catch (error) {
144
149
  failureCount++;
145
- logger.error(`批次 ${batch.originalIndex + 1} 处理失败: ${error.message}`);
150
+ logger.error(t(this.config, 'batch_process_failed', { i: batch.originalIndex + 1, error: error.message }));
146
151
  }
147
152
  }
148
153
 
@@ -165,7 +170,7 @@ export class AIClientPool {
165
170
  return [];
166
171
  }
167
172
 
168
- logger.debug(`开始并发处理 ${batches.length} 个批次`);
173
+ logger.debug(t(this.config, 'start_concurrent_dbg', { count: batches.length }));
169
174
 
170
175
  // 设置总请求数用于进度显示
171
176
  this.stats.totalRequests = batches.length;
@@ -198,15 +203,15 @@ export class AIClientPool {
198
203
  }
199
204
  } else {
200
205
  failureCount++;
201
- logger.error(`文件组 ${index + 1} 处理失败: ${result.reason.message}`);
206
+ logger.error(t(this.config, 'concurrent_group_failed', { index: index + 1, error: result.reason?.message || String(result.reason) }));
202
207
  }
203
208
  });
204
209
 
205
- logger.debug(`并发处理完成: 成功 ${successCount}, 失败 ${failureCount}`);
210
+ logger.debug(t(this.config, 'concurrent_done_dbg', { succ: successCount, fail: failureCount }));
206
211
  return { issues: allIssues, totalDurationMs };
207
212
 
208
213
  } catch (error) {
209
- logger.error(`并发处理过程中发生错误: ${error.message}`);
214
+ logger.error(t(this.config, 'concurrent_processing_error', { error: error?.message || String(error) }));
210
215
  throw error;
211
216
  }
212
217
  }
@@ -254,7 +259,13 @@ export class AIClientPool {
254
259
  if (Array.isArray(parsed)) issueCount = parsed.length;
255
260
  else if (parsed && Array.isArray(parsed.issues)) issueCount = parsed.issues.length;
256
261
  } catch (e) {}
257
- logger.success(`批次 ${batchIndex + 1}/${this.stats.totalRequests}(${fileNames})分析完成,发现 ${issueCount} 个问题,耗时 ${(duration/1000).toFixed(1)}秒`);
262
+ logger.success(t(this.config, 'batch_complete', {
263
+ i: batchIndex + 1,
264
+ total: this.stats.totalRequests,
265
+ context: fileNames,
266
+ issues: issueCount,
267
+ secs: (duration/1000).toFixed(1)
268
+ }));
258
269
  if (typeof progressCallback === 'function') {
259
270
  try { progressCallback(batchIndex, batch, 'completed', null); } catch (e) {}
260
271
  }
@@ -268,13 +279,24 @@ export class AIClientPool {
268
279
  const item = batch.items[0];
269
280
  const fullPath = item.originalFilePath || item.filePath;
270
281
  const totalSeg = item.totalChunks || batch.totalSegments || 1;
271
- logger.info(`开始分析第 ${batchIndex + 1}/${this.stats.totalRequests} 批次(分段整体),文件: ${fullPath},共 ${totalSeg} 段`);
282
+ logger.info(t(this.config, 'batch_start_segmented', {
283
+ i: batchIndex + 1,
284
+ total: this.stats.totalRequests,
285
+ path: fullPath,
286
+ segments: totalSeg
287
+ }));
272
288
  } else {
273
289
  // 小文件批次:保持一致的“批次 i/x”格式
274
- logger.info(`开始分析第 ${batchIndex + 1}/${this.stats.totalRequests} 批次,文件: ${fileNames},预估${batch.totalTokens} tokens, 共${batch.items.length}个文件`);
290
+ logger.info(t(this.config, 'batch_start_regular', {
291
+ i: batchIndex + 1,
292
+ total: this.stats.totalRequests,
293
+ files: fileNames,
294
+ tokens: batch.totalTokens,
295
+ count: batch.items.length
296
+ }));
275
297
  }
276
298
  const startTime = Date.now();
277
- logger.debug(`🔍 开始AI分析请求,批次 ${batchIndex + 1},使用客户端: ${client.constructor.name}`);
299
+ logger.debug(t(this.config, 'request_batch_start_dbg', { index: batchIndex + 1, client: client.constructor.name }));
278
300
 
279
301
  const result = await client.analyzeSmartBatch(formattedBatch, batch, requestMeta);
280
302
 
@@ -286,9 +308,21 @@ export class AIClientPool {
286
308
  if (batch.isLargeFileSegment) {
287
309
  const item = batch.items[0];
288
310
  const fullPath = item.originalFilePath || item.filePath;
289
- logger.success(`批次 ${batchIndex + 1}/${this.stats.totalRequests}(${fullPath})分析完成,发现 ${issueCount} 个问题,耗时 ${(duration/1000).toFixed(1)}秒`);
311
+ logger.success(t(this.config, 'batch_complete', {
312
+ i: batchIndex + 1,
313
+ total: this.stats.totalRequests,
314
+ context: fullPath,
315
+ issues: issueCount,
316
+ secs: (duration/1000).toFixed(1)
317
+ }));
290
318
  } else {
291
- logger.success(`批次 ${batchIndex + 1}/${this.stats.totalRequests}(${fileNames})分析完成,发现 ${issueCount} 个问题,耗时 ${(duration/1000).toFixed(1)}秒`);
319
+ logger.success(t(this.config, 'batch_complete', {
320
+ i: batchIndex + 1,
321
+ total: this.stats.totalRequests,
322
+ context: fileNames,
323
+ issues: issueCount,
324
+ secs: (duration/1000).toFixed(1)
325
+ }));
292
326
  }
293
327
  }
294
328
 
@@ -296,19 +330,19 @@ export class AIClientPool {
296
330
  if (batch.isLargeFileSegment) {
297
331
  const item = batch.items[0];
298
332
  const fullPath = item.originalFilePath || item.filePath;
299
- logger.debug(`⚠️ ${fullPath} ${item.chunkIndex + 1}/${item.totalChunks} 段未发现问题,AI响应内容: ${JSON.stringify(result).substring(0, 200)}...`);
333
+ logger.debug(t(this.config, 'seg_chunk_no_issues_dbg', { file: fullPath, chunk: item.chunkIndex + 1, total: item.totalChunks, preview: JSON.stringify(result).substring(0, 200) + '...' }));
300
334
  } else {
301
335
  const fileNamesAbs = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
302
- logger.debug(`⚠️ ${fileNamesAbs} 未发现问题,AI响应内容: ${JSON.stringify(result).substring(0, 200)}...`);
336
+ logger.debug(t(this.config, 'files_no_issues_dbg', { files: fileNamesAbs, preview: JSON.stringify(result).substring(0, 200) + '...' }));
303
337
  }
304
338
  } else {
305
339
  if (batch.isLargeFileSegment) {
306
340
  const item = batch.items[0];
307
341
  const fullPath = item.originalFilePath || item.filePath;
308
- logger.debug(`📋 ${fullPath} 第 ${item.chunkIndex + 1}/${item.totalChunks} 段发现的问题风险等级: ${result.issues.map(i => i.risk || 'unknown').join(', ')}`);
342
+ logger.debug(t(this.config, 'issues_risk_levels_dbg', { file: fullPath, levels: result.issues.map(i => i.risk || 'unknown').join(', ') }));
309
343
  } else {
310
344
  const fileNamesAbs = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
311
- logger.debug(`📋 ${fileNamesAbs} 发现的问题风险等级: ${result.issues.map(i => i.risk || 'unknown').join(', ')}`);
345
+ logger.debug(t(this.config, 'issues_risk_levels_dbg', { file: fileNamesAbs, levels: result.issues.map(i => i.risk || 'unknown').join(', ') }));
312
346
  }
313
347
  }
314
348
 
@@ -317,13 +351,13 @@ export class AIClientPool {
317
351
  if (batch.isLargeFileSegment) {
318
352
  const item = batch.items[0];
319
353
  const fullPath = item.originalFilePath || item.filePath;
320
- logger.debug(`${fullPath} ${item.chunkIndex + 1}/${item.totalChunks} 段发现的问题详情:`);
354
+ logger.debug(t(this.config, 'issues_details_dbg', { file: fullPath }));
321
355
  } else {
322
356
  const fileNamesAbs = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
323
- logger.debug(`${fileNamesAbs} 发现的问题详情:`);
357
+ logger.debug(t(this.config, 'issues_details_dbg', { file: fileNamesAbs }));
324
358
  }
325
359
  result.issues.forEach((issue, idx) => {
326
- logger.debug(` 问题 ${idx + 1}: ${issue.risk} - ${issue.message?.slice(0, 100)}...`);
360
+ logger.debug(t(this.config, 'issue_item_dbg', { index: idx + 1, risk: issue.risk, message: (issue.message || '').slice(0, 100) + '...' }));
327
361
  });
328
362
  }
329
363
 
@@ -343,7 +377,11 @@ export class AIClientPool {
343
377
  this.stats.retryCount++;
344
378
 
345
379
  if (retryCount < maxRetries) {
346
- logger.warn(`批次 ${batchIndex + 1} 处理失败,进行第 ${retryCount + 1} 次重试: ${error.message}`);
380
+ logger.warn(t(this.config, 'batch_retry_warn', {
381
+ i: batchIndex + 1,
382
+ retry: retryCount + 1,
383
+ error: error.message
384
+ }));
347
385
 
348
386
  // 指数退避延迟
349
387
  const delay = Math.pow(2, retryCount) * 1000;
@@ -351,7 +389,7 @@ export class AIClientPool {
351
389
 
352
390
  return this.processBatchWithRetry(batch, batchIndex, progressCallback, retryCount + 1);
353
391
  } else {
354
- logger.error(`批次 ${batchIndex + 1} 重试 ${maxRetries} 次后仍然失败: ${error.message}`);
392
+ logger.error(t(this.config, 'batch_retry_error', { i: batchIndex + 1, max: maxRetries, error: error.message }));
355
393
 
356
394
  if (progressCallback) {
357
395
  progressCallback(batchIndex, batch, 'failed', error);
@@ -405,7 +443,7 @@ export class AIClientPool {
405
443
  * 清理资源
406
444
  */
407
445
  cleanup() {
408
- logger.debug('清理AI客户端池资源');
446
+ logger.debug(t(this.config, 'cleanup_pool_dbg'));
409
447
 
410
448
  // 清理等待队列
411
449
  this.waitingQueue.forEach(resolve => {