smart-review 1.0.1

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.
@@ -0,0 +1,624 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import { logger } from './logger.js';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ /**
9
+ * Git Diff 解析器
10
+ * 用于解析git diff输出,提取新增代码片段,并构建带上下文的审查内容
11
+ */
12
+ export class GitDiffParser {
13
+ constructor(projectRoot, contextMergeLines = 10, config = {}) {
14
+ this.projectRoot = projectRoot;
15
+ this.contextMergeLines = contextMergeLines;
16
+ this.config = config;
17
+ }
18
+
19
+ /**
20
+ * 获取暂存区的git diff内容
21
+ * @returns {Promise<string>} git diff输出
22
+ */
23
+ async getStagedDiff() {
24
+ try {
25
+ const { stdout } = await execAsync('git diff --cached -U10', {
26
+ cwd: this.projectRoot,
27
+ maxBuffer: 50 * 1024 * 1024 // 50MB buffer
28
+ });
29
+ return stdout;
30
+ } catch (error) {
31
+ logger.error('获取git diff失败:', error);
32
+ return '';
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 解析git diff输出,提取文件变更信息
38
+ * @param {string} diffOutput git diff的输出内容
39
+ * @returns {Array} 文件变更信息数组
40
+ */
41
+ parseDiffOutput(diffOutput) {
42
+ if (!diffOutput.trim()) {
43
+ return [];
44
+ }
45
+
46
+ const files = [];
47
+ const fileBlocks = diffOutput.split(/^diff --git /m).filter(block => block.trim());
48
+
49
+ for (const block of fileBlocks) {
50
+ const fileInfo = this.parseFileBlock(block);
51
+ if (fileInfo) {
52
+ files.push(fileInfo);
53
+ }
54
+ }
55
+
56
+ return files;
57
+ }
58
+
59
+ /**
60
+ * 解析单个文件的diff块
61
+ * @param {string} block 单个文件的diff内容
62
+ * @returns {Object|null} 文件变更信息
63
+ */
64
+ parseFileBlock(block) {
65
+ const lines = block.split('\n');
66
+
67
+ // 解析文件路径
68
+ const firstLine = lines[0];
69
+ const pathMatch = firstLine.match(/^a\/(.+) b\/(.+)$/);
70
+ if (!pathMatch) {
71
+ return null;
72
+ }
73
+
74
+ const filePath = pathMatch[2]; // 使用新文件路径
75
+
76
+ // 检查是否为删除文件
77
+ const isDeleted = lines.some(line => line.startsWith('deleted file mode'));
78
+ if (isDeleted) {
79
+ return null; // 跳过删除的文件
80
+ }
81
+
82
+ // 检查文件是否应该被忽略
83
+ if (!this.isReviewableFile(filePath)) {
84
+ logger.debug(`文件被忽略规则跳过: ${filePath}`);
85
+ return null;
86
+ }
87
+
88
+ // 解析hunks(代码块)
89
+ const hunks = this.parseHunks(lines);
90
+ if (hunks.length === 0) {
91
+ return null;
92
+ }
93
+
94
+ return {
95
+ filePath,
96
+ hunks,
97
+ hasChanges: hunks.some(hunk => hunk.addedLines.length > 0)
98
+ };
99
+ }
100
+
101
+ /**
102
+ * 解析diff hunks(代码变更块)
103
+ * @param {Array} lines diff文件的所有行
104
+ * @returns {Array} hunks数组
105
+ */
106
+ parseHunks(lines) {
107
+ const hunks = [];
108
+ let currentHunk = null;
109
+ let lineIndex = 0;
110
+
111
+ for (let i = 0; i < lines.length; i++) {
112
+ const line = lines[i];
113
+
114
+ // 检测hunk头部 (@@开头)
115
+ if (line.startsWith('@@')) {
116
+ if (currentHunk) {
117
+ hunks.push(currentHunk);
118
+ }
119
+
120
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
121
+ if (hunkMatch) {
122
+ currentHunk = {
123
+ oldStart: parseInt(hunkMatch[1]),
124
+ oldCount: parseInt(hunkMatch[2] || '1'),
125
+ newStart: parseInt(hunkMatch[3]),
126
+ newCount: parseInt(hunkMatch[4] || '1'),
127
+ lines: [],
128
+ addedLines: [],
129
+ contextLines: []
130
+ };
131
+ lineIndex = currentHunk.newStart;
132
+ }
133
+ continue;
134
+ }
135
+
136
+ // 处理hunk内容
137
+ if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))) {
138
+ const lineType = line[0];
139
+ const content = line.substring(1);
140
+
141
+ currentHunk.lines.push({
142
+ type: lineType,
143
+ content,
144
+ lineNumber: lineType === '-' ? null : lineIndex
145
+ });
146
+
147
+ if (lineType === '+') {
148
+ // 新增行
149
+ currentHunk.addedLines.push({
150
+ content,
151
+ lineNumber: lineIndex
152
+ });
153
+ lineIndex++;
154
+ } else if (lineType === ' ') {
155
+ // 上下文行
156
+ currentHunk.contextLines.push({
157
+ content,
158
+ lineNumber: lineIndex
159
+ });
160
+ lineIndex++;
161
+ }
162
+ // 删除行(-) 不增加行号
163
+ }
164
+ }
165
+
166
+ // 添加最后一个hunk
167
+ if (currentHunk) {
168
+ hunks.push(currentHunk);
169
+ }
170
+
171
+ return hunks;
172
+ }
173
+
174
+ /**
175
+ * 构建审查内容
176
+ * @param {Object} fileInfo 文件变更信息
177
+ * @returns {Promise<Object>} 构建的审查内容
178
+ */
179
+ async buildReviewContent(fileInfo) {
180
+ if (!fileInfo.hasChanges) {
181
+ return null;
182
+ }
183
+
184
+ const reviewSections = [];
185
+
186
+ for (const hunk of fileInfo.hunks) {
187
+ if (hunk.addedLines.length === 0) {
188
+ continue; // 跳过没有新增内容的hunk
189
+ }
190
+
191
+ const section = await this.buildHunkReviewSection(hunk, fileInfo.filePath);
192
+ if (section) {
193
+ reviewSections.push(section);
194
+ }
195
+ }
196
+
197
+ if (reviewSections.length === 0) {
198
+ return null;
199
+ }
200
+
201
+ // 应用智能分段策略
202
+ const smartSegments = this.applySmartSegmentation(reviewSections, fileInfo.filePath);
203
+
204
+ return {
205
+ filePath: fileInfo.filePath,
206
+ segments: smartSegments,
207
+ totalAddedLines: fileInfo.hunks.reduce((sum, hunk) => sum + hunk.addedLines.length, 0)
208
+ };
209
+ }
210
+
211
+ /**
212
+ * 智能分段策略 - 根据token限制和上下文长度进行合理分段
213
+ * @param {Array} sections 原始代码段
214
+ * @param {string} filePath 文件路径
215
+ * @returns {Array} 智能分段后的代码段
216
+ */
217
+ applySmartSegmentation(sections, filePath) {
218
+ const MAX_TOKENS_PER_SEGMENT = 3000; // 每段最大token数
219
+ const ESTIMATED_CHARS_PER_TOKEN = 4; // 估算每个token的字符数
220
+ const MAX_CHARS_PER_SEGMENT = MAX_TOKENS_PER_SEGMENT * ESTIMATED_CHARS_PER_TOKEN;
221
+
222
+ const smartSegments = [];
223
+ let currentSegment = null;
224
+ let currentSize = 0;
225
+
226
+ for (const section of sections) {
227
+ const sectionSize = section.content.length;
228
+
229
+ // 如果当前段为空,或者添加这个section会超出限制,则开始新段
230
+ if (!currentSegment || (currentSize + sectionSize > MAX_CHARS_PER_SEGMENT)) {
231
+ // 保存当前段(如果存在)
232
+ if (currentSegment) {
233
+ smartSegments.push(this.finalizeSegment(currentSegment));
234
+ }
235
+
236
+ // 开始新段
237
+ currentSegment = {
238
+ startLine: section.startLine,
239
+ endLine: section.endLine,
240
+ content: section.content,
241
+ addedLineNumbers: [...section.addedLineNumbers],
242
+ addedLinesCount: section.addedLinesCount,
243
+ sections: [section]
244
+ };
245
+ currentSize = sectionSize;
246
+ } else {
247
+ // 合并到当前段
248
+ currentSegment.endLine = section.endLine;
249
+ currentSegment.content += '\n\n' + section.content;
250
+ currentSegment.addedLineNumbers.push(...section.addedLineNumbers);
251
+ currentSegment.addedLinesCount += section.addedLinesCount;
252
+ currentSegment.sections.push(section);
253
+ currentSize += sectionSize;
254
+ }
255
+ }
256
+
257
+ // 添加最后一段
258
+ if (currentSegment) {
259
+ smartSegments.push(this.finalizeSegment(currentSegment));
260
+ }
261
+
262
+ logger.debug(`文件 ${filePath} 智能分段完成: ${sections.length} 个原始段 -> ${smartSegments.length} 个智能段`);
263
+ return smartSegments;
264
+ }
265
+
266
+ /**
267
+ * 完善分段信息
268
+ * @param {Object} segment 分段信息
269
+ * @returns {Object} 完善后的分段信息
270
+ */
271
+ finalizeSegment(segment) {
272
+ return {
273
+ startLine: segment.startLine,
274
+ endLine: segment.endLine,
275
+ content: segment.content,
276
+ addedLineNumbers: segment.addedLineNumbers,
277
+ addedLinesCount: segment.addedLinesCount,
278
+ estimatedTokens: Math.ceil(segment.content.length / 4), // 估算token数
279
+ originalSections: segment.sections.length // 原始段数
280
+ };
281
+ }
282
+
283
+ /**
284
+ * 构建单个hunk的审查内容
285
+ * @param {Object} hunk hunk信息
286
+ * @param {string} filePath 文件路径
287
+ * @returns {Promise<Object|null>} 审查内容
288
+ */
289
+ async buildHunkReviewSection(hunk, filePath) {
290
+ // 构建审查项列表,包含类型、内容与新文件的行号
291
+ const reviewItems = [];
292
+ const addedLineNumbers = [];
293
+
294
+ for (const line of hunk.lines) {
295
+ if (line.type === '+') {
296
+ reviewItems.push({ type: '+', content: line.content, lineNumber: line.lineNumber });
297
+ addedLineNumbers.push(line.lineNumber);
298
+ } else if (line.type === ' ') {
299
+ reviewItems.push({ type: ' ', content: line.content, lineNumber: line.lineNumber });
300
+ }
301
+ // 删除行(-) 完全忽略,不发送给AI
302
+ }
303
+
304
+ if (addedLineNumbers.length === 0) {
305
+ return null;
306
+ }
307
+
308
+ // 检查代码内指令忽略(仅影响新增行的保留)
309
+ const reviewLineStrings = reviewItems.map(it => (it.type === '+' ? '+' : ' ') + it.content);
310
+ const filteredLines = this.filterDisabledLines(reviewLineStrings, addedLineNumbers);
311
+ if (filteredLines.addedLineNumbers.length === 0) {
312
+ logger.debug(`所有新增行都被代码内指令忽略: ${filePath}`);
313
+ return null;
314
+ }
315
+
316
+ // 将过滤后的行映射回审查项以恢复精确行号
317
+ const filteredItems = [];
318
+ let cursor = 0;
319
+ for (const fr of filteredLines.reviewLines) {
320
+ for (let j = cursor; j < reviewItems.length; j++) {
321
+ const candidate = (reviewItems[j].type === '+' ? '+' : ' ') + reviewItems[j].content;
322
+ if (candidate === fr) {
323
+ filteredItems.push(reviewItems[j]);
324
+ cursor = j + 1;
325
+ break;
326
+ }
327
+ }
328
+ }
329
+
330
+ // 为每行添加 [行号] 前缀,并剔除空白行
331
+ const numberedLines = [];
332
+ for (const item of filteredItems) {
333
+ const code = item.content ?? '';
334
+ if (code.trim().length === 0) continue;
335
+ const sign = item.type; // '+' 表示新增,' ' 表示上下文
336
+ numberedLines.push(`${sign}[${item.lineNumber}] ${code}`);
337
+ }
338
+ if (numberedLines.length === 0) {
339
+ return null;
340
+ }
341
+
342
+ const content = numberedLines.join('\n');
343
+
344
+ return {
345
+ startLine: Math.min(...filteredLines.addedLineNumbers),
346
+ endLine: Math.max(...filteredLines.addedLineNumbers),
347
+ content,
348
+ addedLineNumbers: filteredLines.addedLineNumbers,
349
+ addedLinesCount: filteredLines.addedLineNumbers.length
350
+ };
351
+ }
352
+
353
+ /**
354
+ * 获取暂存区文件的完整内容(用于上下文参考)
355
+ * @param {string} filePath 文件路径
356
+ * @returns {Promise<string>} 文件内容
357
+ */
358
+ async getStagedFileContent(filePath) {
359
+ try {
360
+ const { stdout } = await execAsync(`git show :"${filePath}"`, {
361
+ cwd: this.projectRoot,
362
+ maxBuffer: 10 * 1024 * 1024
363
+ });
364
+ return stdout;
365
+ } catch (error) {
366
+ logger.debug(`无法获取暂存区文件内容 ${filePath}:`, error.message);
367
+ return '';
368
+ }
369
+ }
370
+
371
+ /**
372
+ * 主要方法:获取暂存区的diff审查数据
373
+ * @returns {Promise<Array>} 审查数据数组
374
+ */
375
+ async getStagedDiffReviewData() {
376
+ logger.progress('正在分析暂存区变更...');
377
+
378
+ const diffOutput = await this.getStagedDiff();
379
+ if (!diffOutput.trim()) {
380
+ logger.info('暂存区没有代码变更需要审查');
381
+ return [];
382
+ }
383
+
384
+ const files = this.parseDiffOutput(diffOutput);
385
+ const reviewData = [];
386
+
387
+ for (const fileInfo of files) {
388
+ const reviewContent = await this.buildReviewContent(fileInfo);
389
+ if (reviewContent) {
390
+ // 获取完整文件内容作为额外上下文
391
+ const fullContent = await this.getStagedFileContent(fileInfo.filePath);
392
+
393
+ reviewData.push({
394
+ ...reviewContent,
395
+ fullContent, // 完整文件内容,用于更好的上下文理解
396
+ isDiffMode: true // 标记这是diff模式
397
+ });
398
+ }
399
+ }
400
+
401
+ logger.info(`发现 ${reviewData.length} 个文件有代码变更需要审查`);
402
+ return reviewData;
403
+ }
404
+
405
+ /**
406
+ * 检查文件是否可以审查(不在忽略列表中)
407
+ * @param {string} filePath 文件路径
408
+ * @returns {boolean} 是否可以审查
409
+ */
410
+ isReviewableFile(filePath) {
411
+ const extensions = this.config.fileExtensions || [];
412
+ const ignoreFiles = this.config.ignoreFiles || [];
413
+
414
+ const ext = path.extname(filePath).toLowerCase();
415
+ const shouldInclude = extensions.includes(ext);
416
+
417
+ if (!shouldInclude || ignoreFiles.length === 0) {
418
+ return shouldInclude;
419
+ }
420
+
421
+ const normalized = filePath.replace(/\\/g, '/');
422
+ const relativePath = path.relative(this.config.projectRoot || this.projectRoot, filePath).replace(/\\/g, '/');
423
+ const basename = path.basename(filePath);
424
+
425
+ // 检查是否应该忽略此文件
426
+ const shouldIgnore = ignoreFiles.some(pattern => {
427
+ const originalPattern = String(pattern);
428
+
429
+ // 1. 精确匹配(支持相对路径、绝对路径、文件名)
430
+ if (originalPattern === normalized || originalPattern === relativePath || originalPattern === basename) {
431
+ return true;
432
+ }
433
+
434
+ // 2. 检查是否为正则表达式(以/开头和结尾,或包含正则特殊字符但不是glob)
435
+ if (this.isRegexPattern(originalPattern)) {
436
+ try {
437
+ const regex = this.createRegexFromPattern(originalPattern);
438
+ const normalizedMatch = regex.test(normalized);
439
+ const relativeMatch = regex.test(relativePath);
440
+ return normalizedMatch || relativeMatch;
441
+ } catch (e) {
442
+ return false;
443
+ }
444
+ }
445
+
446
+ // 3. glob模式匹配(只对glob模式进行路径分隔符转换)
447
+ const patternStr = originalPattern.replace(/\\/g, '/');
448
+ return this.matchPattern(normalized, patternStr) || this.matchPattern(relativePath, patternStr);
449
+ });
450
+
451
+ return !shouldIgnore;
452
+ }
453
+
454
+ /**
455
+ * 匹配glob模式
456
+ * @param {string} filePath 文件路径
457
+ * @param {string} pattern glob模式
458
+ * @returns {boolean} 是否匹配
459
+ */
460
+ matchPattern(filePath, pattern) {
461
+ // 处理glob模式转换为正则表达式
462
+ let regexPattern = pattern
463
+ // 转义正则表达式特殊字符(除了*和?)
464
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&');
465
+
466
+ // 先处理 ** 模式(必须在单个 * 之前处理)
467
+ regexPattern = regexPattern.replace(/\*\*/g, '§DOUBLESTAR§');
468
+
469
+ // 处理单个 * 匹配单个路径段中的任意字符(不包括路径分隔符)
470
+ regexPattern = regexPattern.replace(/\*/g, '[^/]*');
471
+
472
+ // 恢复 ** 为匹配任意路径(包括跨目录)
473
+ regexPattern = regexPattern.replace(/§DOUBLESTAR§/g, '.*');
474
+
475
+ // 处理 ? 匹配单个字符
476
+ regexPattern = regexPattern.replace(/\?/g, '.');
477
+
478
+ // 特殊处理:如果模式以 **/ 开头,允许匹配根目录
479
+ if (pattern.startsWith('**/')) {
480
+ regexPattern = regexPattern.replace(/^\.\*\//, '(.*\/|^)');
481
+ }
482
+
483
+ // 特殊处理:/**/* 模式应该匹配目录下的任何文件
484
+ regexPattern = regexPattern.replace(/\/\.\*\/\[.*?\]\*$/, '(/.*)?');
485
+
486
+ return new RegExp(`^${regexPattern}$`).test(filePath);
487
+ }
488
+
489
+ /**
490
+ * 检查是否为正则表达式模式
491
+ * @param {string} pattern 模式字符串
492
+ * @returns {boolean} 是否为正则表达式
493
+ */
494
+ isRegexPattern(pattern) {
495
+ // 以/开头和结尾的正则表达式格式(优先检查)
496
+ if (pattern.startsWith('/') && pattern.lastIndexOf('/') > 0) {
497
+ return true;
498
+ }
499
+
500
+ // 检查是否包含典型的glob模式(双星号或单独的星号用于路径匹配)
501
+ const globPatterns = /\*\*\/|\*\*$|\/\*\*|^\*\*|\/\*\.|\*\.[a-zA-Z]+$/;
502
+
503
+ // 如果包含明显的glob模式,则不是正则表达式
504
+ if (globPatterns.test(pattern)) {
505
+ return false;
506
+ }
507
+
508
+ // 检查是否看起来像普通的文件路径(包含路径分隔符和文件扩展名)
509
+ if (/^[^()[\]{}^$+|\\*?]+\.[a-zA-Z0-9]+$/.test(pattern) ||
510
+ /^[^()[\]{}^$+|\\*?]*\/[^()[\]{}^$+|\\*?]*\.[a-zA-Z0-9]+$/.test(pattern)) {
511
+ return false;
512
+ }
513
+
514
+ // 包含正则特殊字符的字符串(排除普通的点号)
515
+ const regexChars = /[()[\]{}^$+|\\*?]/;
516
+
517
+ // 如果包含正则表达式特殊字符,则认为是正则表达式
518
+ return regexChars.test(pattern);
519
+ }
520
+
521
+ /**
522
+ * 从模式字符串创建正则表达式
523
+ * @param {string} pattern 模式字符串
524
+ * @returns {RegExp} 正则表达式对象
525
+ */
526
+ createRegexFromPattern(pattern) {
527
+ // 如果是/pattern/flags格式
528
+ if (pattern.startsWith('/') && pattern.length > 1) {
529
+ const lastSlashIndex = pattern.lastIndexOf('/');
530
+ if (lastSlashIndex > 0) {
531
+ const regexBody = pattern.slice(1, lastSlashIndex);
532
+ const flags = pattern.slice(lastSlashIndex + 1);
533
+ return new RegExp(regexBody, flags);
534
+ }
535
+ }
536
+
537
+ // 否则直接作为正则表达式主体
538
+ return new RegExp(pattern);
539
+ }
540
+
541
+ /**
542
+ * 过滤被代码内指令禁用的行
543
+ * @param {Array} reviewLines 审查行数组
544
+ * @param {Array} addedLineNumbers 新增行号数组
545
+ * @returns {Object} 过滤后的结果
546
+ */
547
+ filterDisabledLines(reviewLines, addedLineNumbers) {
548
+ const nextToken = 'review-disable-next-line';
549
+ const startToken = 'review-disable-start';
550
+ const endToken = 'review-disable-end';
551
+
552
+ const filteredReviewLines = [];
553
+ const filteredAddedLineNumbers = [];
554
+
555
+ let blockDisabled = false;
556
+ let nextLineDisabled = false;
557
+ let addedLineIndex = 0; // 跟踪当前处理的新增行索引
558
+
559
+ for (let i = 0; i < reviewLines.length; i++) {
560
+ const line = reviewLines[i];
561
+ const lineContent = line.substring(1); // 移除+或空格前缀
562
+ const isAddedLine = line.startsWith('+');
563
+
564
+ // 检查当前行是否包含禁用指令
565
+ const lowerContent = lineContent.toLowerCase();
566
+
567
+ if (lowerContent.includes(nextToken)) {
568
+ nextLineDisabled = true;
569
+ // 包含禁用指令的行本身不需要被忽略,只是设置下一行忽略标志
570
+ filteredReviewLines.push(line);
571
+ if (isAddedLine) {
572
+ filteredAddedLineNumbers.push(addedLineNumbers[addedLineIndex]);
573
+ addedLineIndex++;
574
+ }
575
+ continue;
576
+ }
577
+
578
+ if (lowerContent.includes(startToken)) {
579
+ blockDisabled = true;
580
+ // 包含start指令的行本身不需要被忽略,只是设置块忽略标志
581
+ filteredReviewLines.push(line);
582
+ if (isAddedLine) {
583
+ filteredAddedLineNumbers.push(addedLineNumbers[addedLineIndex]);
584
+ addedLineIndex++;
585
+ }
586
+ continue;
587
+ }
588
+
589
+ if (lowerContent.includes(endToken)) {
590
+ blockDisabled = false;
591
+ // 包含end指令的行本身不需要被忽略,只是取消块忽略标志
592
+ filteredReviewLines.push(line);
593
+ if (isAddedLine) {
594
+ filteredAddedLineNumbers.push(addedLineNumbers[addedLineIndex]);
595
+ addedLineIndex++;
596
+ }
597
+ continue;
598
+ }
599
+
600
+ // 检查当前行是否应该被忽略
601
+ const shouldSkip = nextLineDisabled || blockDisabled;
602
+
603
+ if (shouldSkip && isAddedLine) {
604
+ addedLineIndex++; // 跳过这个新增行,但仍需要增加索引
605
+ nextLineDisabled = false; // 重置下一行禁用标志
606
+ continue;
607
+ }
608
+
609
+ // 保留该行
610
+ filteredReviewLines.push(line);
611
+ if (isAddedLine) {
612
+ filteredAddedLineNumbers.push(addedLineNumbers[addedLineIndex]);
613
+ addedLineIndex++;
614
+ }
615
+
616
+ nextLineDisabled = false; // 重置下一行禁用标志
617
+ }
618
+
619
+ return {
620
+ reviewLines: filteredReviewLines,
621
+ addedLineNumbers: filteredAddedLineNumbers
622
+ };
623
+ }
624
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 简单的日志工具
3
+ * 区分用户信息输出和调试信息
4
+ */
5
+
6
+ export class Logger {
7
+ constructor(options = {}) {
8
+ this.debugMode = options.debug || process.env.DEBUG_SMART_REVIEW === 'true';
9
+ this.silent = options.silent || false;
10
+ }
11
+
12
+ /**
13
+ * 用户信息输出 - 总是显示
14
+ */
15
+ info(message, ...args) {
16
+ if (!this.silent) {
17
+ console.log(message, ...args);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * 成功信息
23
+ */
24
+ success(message, ...args) {
25
+ if (!this.silent) {
26
+ console.log(`✅ ${message}`, ...args);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * 警告信息
32
+ */
33
+ warn(message, ...args) {
34
+ if (!this.silent) {
35
+ console.log(`⚠️ ${message}`, ...args);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 错误信息
41
+ */
42
+ error(message, ...args) {
43
+ console.error(`❌ ${message}`, ...args);
44
+ }
45
+
46
+ /**
47
+ * 调试信息 - 只在调试模式下显示
48
+ */
49
+ debug(message, ...args) {
50
+ if (this.debugMode) {
51
+ console.log(`🔍 [DEBUG] ${message}`, ...args);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 进度信息
57
+ */
58
+ progress(message, ...args) {
59
+ if (!this.silent) {
60
+ console.log(`🔄 ${message}`, ...args);
61
+ }
62
+ }
63
+ }
64
+
65
+ // 默认实例
66
+ export const logger = new Logger();