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,434 @@
1
+ /**
2
+ * AI客户端池管理器
3
+ * 管理多个AI客户端实例,支持并发请求处理
4
+ */
5
+
6
+ import path from 'path';
7
+ import { AIClient } from './ai-client.js';
8
+ import { logger } from './utils/logger.js';
9
+
10
+ export class AIClientPool {
11
+ constructor(config, rules, poolSize = 3, concurrencyLimiter = null) {
12
+ this.config = config;
13
+ this.rules = rules;
14
+ this.poolSize = poolSize;
15
+ this.concurrencyLimiter = concurrencyLimiter;
16
+ this.clients = [];
17
+ this.busyClients = new Set();
18
+ this.waitingQueue = [];
19
+ this.stats = {
20
+ totalRequests: 0,
21
+ completedRequests: 0,
22
+ failedRequests: 0,
23
+ retryCount: 0
24
+ };
25
+
26
+ this.initializePool();
27
+ }
28
+
29
+ /**
30
+ * 初始化客户端池
31
+ */
32
+ initializePool() {
33
+ logger.debug(`初始化AI客户端池,大小: ${this.poolSize}`);
34
+
35
+ for (let i = 0; i < this.poolSize; i++) {
36
+ const client = new AIClient(this.config.ai);
37
+ client.poolId = i;
38
+ // 注入全局并发限速器,确保批次与分段共享并发资源
39
+ if (this.concurrencyLimiter) {
40
+ client.concurrencyLimiter = this.concurrencyLimiter;
41
+ }
42
+ this.clients.push(client);
43
+ }
44
+
45
+ logger.debug(`AI客户端池初始化完成,共${this.clients.length}个客户端`);
46
+ }
47
+
48
+ /**
49
+ * 获取可用的客户端
50
+ * @returns {Promise<AIClient>} 可用的AI客户端
51
+ */
52
+ async getAvailableClient() {
53
+ // 查找空闲的客户端
54
+ const availableClient = this.clients.find(client => !this.busyClients.has(client));
55
+
56
+ if (availableClient) {
57
+ this.busyClients.add(availableClient);
58
+ return availableClient;
59
+ }
60
+
61
+ // 如果没有可用客户端,等待
62
+ return new Promise((resolve) => {
63
+ this.waitingQueue.push(resolve);
64
+ });
65
+ }
66
+
67
+ /**
68
+ * 释放客户端
69
+ * @param {AIClient} client - 要释放的客户端
70
+ */
71
+ releaseClient(client) {
72
+ this.busyClients.delete(client);
73
+
74
+ // 如果有等待的请求,分配给它们
75
+ if (this.waitingQueue.length > 0) {
76
+ const resolve = this.waitingQueue.shift();
77
+ this.busyClients.add(client);
78
+ resolve(client);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 将批次按文件分组
84
+ * @param {Array} batches - 批次数组
85
+ * @returns {Array} 文件组数组,每组包含同一文件的所有分段批次
86
+ */
87
+ groupBatchesByFile(batches) {
88
+ const fileGroups = new Map();
89
+
90
+ batches.forEach((batch, originalIndex) => {
91
+ let fileKey;
92
+
93
+ if (batch.isLargeFileSegment && batch.segmentedFile) {
94
+ // 大文件分段:使用文件路径作为分组键
95
+ fileKey = batch.segmentedFile;
96
+ } else {
97
+ // 小文件批次:每个批次独立成组
98
+ fileKey = `batch_${originalIndex}`;
99
+ }
100
+
101
+ if (!fileGroups.has(fileKey)) {
102
+ fileGroups.set(fileKey, []);
103
+ }
104
+
105
+ // 保存原始索引用于进度显示
106
+ batch.originalIndex = originalIndex;
107
+ fileGroups.get(fileKey).push(batch);
108
+ });
109
+
110
+ // 对每个文件组内的分段按currentSegment排序
111
+ fileGroups.forEach(group => {
112
+ if (group.length > 1 && group[0].isLargeFileSegment) {
113
+ group.sort((a, b) => (a.currentSegment || 0) - (b.currentSegment || 0));
114
+ }
115
+ });
116
+
117
+ return Array.from(fileGroups.values());
118
+ }
119
+
120
+ /**
121
+ * 串行处理文件组内的所有批次
122
+ * @param {Array} group - 文件组(包含同一文件的所有分段批次)
123
+ * @param {Function} progressCallback - 进度回调函数
124
+ * @returns {Promise<Object>} 处理结果
125
+ */
126
+ async processFileGroupSerially(group, progressCallback = null) {
127
+ const allIssues = [];
128
+ let successCount = 0;
129
+ let failureCount = 0;
130
+ let totalDurationMs = 0;
131
+
132
+ // 串行处理组内的每个批次
133
+ for (const batch of group) {
134
+ try {
135
+ const result = await this.processBatchWithRetry(batch, batch.originalIndex, progressCallback);
136
+ // 新的返回格式 { issues: [...], durationMs }
137
+ const issues = Array.isArray(result) ? result : (result.issues || []);
138
+ allIssues.push(...issues);
139
+ if (result && typeof result.durationMs === 'number') {
140
+ totalDurationMs += result.durationMs;
141
+ }
142
+ successCount++;
143
+ } catch (error) {
144
+ failureCount++;
145
+ logger.error(`批次 ${batch.originalIndex + 1} 处理失败: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ return {
150
+ issues: allIssues,
151
+ successCount,
152
+ failureCount,
153
+ totalDurationMs
154
+ };
155
+ }
156
+
157
+ /**
158
+ * 并发执行多个批次
159
+ * @param {Array} batches - 批次数组
160
+ * @param {Function} progressCallback - 进度回调函数
161
+ * @returns {Promise<Array>} 所有批次的结果
162
+ */
163
+ async executeConcurrentBatches(batches, progressCallback = null) {
164
+ if (!batches || batches.length === 0) {
165
+ return [];
166
+ }
167
+
168
+ logger.debug(`开始并发处理 ${batches.length} 个批次`);
169
+
170
+ // 设置总请求数用于进度显示
171
+ this.stats.totalRequests = batches.length;
172
+
173
+ // 将批次按文件分组:同一文件的分段需要串行处理,不同文件可以并发
174
+ const fileGroups = this.groupBatchesByFile(batches);
175
+
176
+ // 创建文件组处理任务(文件组之间并发,组内分段串行)
177
+ const fileGroupTasks = fileGroups.map(group => {
178
+ return this.processFileGroupSerially(group, progressCallback);
179
+ });
180
+
181
+ try {
182
+ // 使用Promise.allSettled来处理所有文件组,即使某些失败也继续
183
+ const results = await Promise.allSettled(fileGroupTasks);
184
+
185
+ // 收集成功的结果
186
+ const allIssues = [];
187
+ let successCount = 0;
188
+ let failureCount = 0;
189
+ let totalDurationMs = 0;
190
+
191
+ results.forEach((result, index) => {
192
+ if (result.status === 'fulfilled') {
193
+ allIssues.push(...result.value.issues);
194
+ successCount += result.value.successCount;
195
+ failureCount += result.value.failureCount;
196
+ if (typeof result.value.totalDurationMs === 'number') {
197
+ totalDurationMs += result.value.totalDurationMs;
198
+ }
199
+ } else {
200
+ failureCount++;
201
+ logger.error(`文件组 ${index + 1} 处理失败: ${result.reason.message}`);
202
+ }
203
+ });
204
+
205
+ logger.debug(`并发处理完成: 成功 ${successCount}, 失败 ${failureCount}`);
206
+ return { issues: allIssues, totalDurationMs };
207
+
208
+ } catch (error) {
209
+ logger.error(`并发处理过程中发生错误: ${error.message}`);
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 带重试机制的批次处理
216
+ * @param {Object} batch - 批次对象
217
+ * @param {number} batchIndex - 批次索引
218
+ * @param {Function} progressCallback - 进度回调
219
+ * @param {number} retryCount - 重试次数
220
+ * @returns {Promise<Array>} 处理结果
221
+ */
222
+ async processBatchWithRetry(batch, batchIndex, progressCallback = null, retryCount = 0) {
223
+ const maxRetries = 3;
224
+
225
+ try {
226
+ // 获取可用客户端
227
+ const client = await this.getAvailableClient();
228
+
229
+ try {
230
+ // 在批次对象上记录索引与总批次数,便于下游日志展示
231
+ batch.batchIndex = batchIndex;
232
+ batch.totalRequests = this.stats.totalRequests;
233
+
234
+ // 格式化批次数据
235
+ const formattedBatch = this.formatBatchForAI(batch);
236
+
237
+ // 获取批次文件信息用于日志(统一使用绝对路径)
238
+ const fileNames = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
239
+
240
+ // 构造元数据:在释放并发许可前触发“批次完成”和进度回调,保证日志顺序
241
+ let earlySuccessLogged = false;
242
+ let requestMeta = null;
243
+ if (!batch.isLargeFileSegment) {
244
+ requestMeta = {
245
+ onSuccess: (res) => {
246
+ if (earlySuccessLogged) return;
247
+ earlySuccessLogged = true;
248
+ // 计算问题数量与耗时
249
+ const duration = Date.now() - startTime;
250
+ let issueCount = 0;
251
+ try {
252
+ const content = res?.choices?.[0]?.message?.content ?? '';
253
+ const parsed = client.parseAIResponse(content, undefined, {});
254
+ if (Array.isArray(parsed)) issueCount = parsed.length;
255
+ else if (parsed && Array.isArray(parsed.issues)) issueCount = parsed.issues.length;
256
+ } catch (e) {}
257
+ logger.success(`批次 ${batchIndex + 1}/${this.stats.totalRequests}(${fileNames})分析完成,发现 ${issueCount} 个问题,耗时 ${(duration/1000).toFixed(1)}秒`);
258
+ if (typeof progressCallback === 'function') {
259
+ try { progressCallback(batchIndex, batch, 'completed', null); } catch (e) {}
260
+ }
261
+ }
262
+ };
263
+ }
264
+
265
+ // 执行AI分析
266
+ if (batch.isLargeFileSegment) {
267
+ // 大文件分段批次:仅展示总段数,避免与后续逐段日志重复
268
+ const item = batch.items[0];
269
+ const fullPath = item.originalFilePath || item.filePath;
270
+ const totalSeg = item.totalChunks || batch.totalSegments || 1;
271
+ logger.info(`开始分析第 ${batchIndex + 1}/${this.stats.totalRequests} 批次(分段整体),文件: ${fullPath},共 ${totalSeg} 段`);
272
+ } else {
273
+ // 小文件批次:保持一致的“批次 i/x”格式
274
+ logger.info(`开始分析第 ${batchIndex + 1}/${this.stats.totalRequests} 批次,文件: ${fileNames},预估${batch.totalTokens} tokens, 共${batch.items.length}个文件`);
275
+ }
276
+ const startTime = Date.now();
277
+ logger.debug(`🔍 开始AI分析请求,批次 ${batchIndex + 1},使用客户端: ${client.constructor.name}`);
278
+
279
+ const result = await client.analyzeSmartBatch(formattedBatch, batch, requestMeta);
280
+
281
+ const duration = Date.now() - startTime;
282
+ const issueCount = result.issues?.length || 0;
283
+
284
+ // 如果尚未提前输出,则统一输出批次完成日志
285
+ if (!earlySuccessLogged) {
286
+ if (batch.isLargeFileSegment) {
287
+ const item = batch.items[0];
288
+ const fullPath = item.originalFilePath || item.filePath;
289
+ logger.success(`批次 ${batchIndex + 1}/${this.stats.totalRequests}(${fullPath})分析完成,发现 ${issueCount} 个问题,耗时 ${(duration/1000).toFixed(1)}秒`);
290
+ } else {
291
+ logger.success(`批次 ${batchIndex + 1}/${this.stats.totalRequests}(${fileNames})分析完成,发现 ${issueCount} 个问题,耗时 ${(duration/1000).toFixed(1)}秒`);
292
+ }
293
+ }
294
+
295
+ if (issueCount === 0) {
296
+ if (batch.isLargeFileSegment) {
297
+ const item = batch.items[0];
298
+ const fullPath = item.originalFilePath || item.filePath;
299
+ logger.debug(`⚠️ ${fullPath} 第 ${item.chunkIndex + 1}/${item.totalChunks} 段未发现问题,AI响应内容: ${JSON.stringify(result).substring(0, 200)}...`);
300
+ } else {
301
+ const fileNamesAbs = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
302
+ logger.debug(`⚠️ ${fileNamesAbs} 未发现问题,AI响应内容: ${JSON.stringify(result).substring(0, 200)}...`);
303
+ }
304
+ } else {
305
+ if (batch.isLargeFileSegment) {
306
+ const item = batch.items[0];
307
+ const fullPath = item.originalFilePath || item.filePath;
308
+ logger.debug(`📋 ${fullPath} 第 ${item.chunkIndex + 1}/${item.totalChunks} 段发现的问题风险等级: ${result.issues.map(i => i.risk || 'unknown').join(', ')}`);
309
+ } else {
310
+ const fileNamesAbs = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
311
+ logger.debug(`📋 ${fileNamesAbs} 发现的问题风险等级: ${result.issues.map(i => i.risk || 'unknown').join(', ')}`);
312
+ }
313
+ }
314
+
315
+ // 如果有问题,输出详细信息用于调试
316
+ if (issueCount > 0 && result.issues) {
317
+ if (batch.isLargeFileSegment) {
318
+ const item = batch.items[0];
319
+ const fullPath = item.originalFilePath || item.filePath;
320
+ logger.debug(`${fullPath} 第 ${item.chunkIndex + 1}/${item.totalChunks} 段发现的问题详情:`);
321
+ } else {
322
+ const fileNamesAbs = batch.items.map(item => (item.originalFilePath || item.filePath)).join(', ');
323
+ logger.debug(`${fileNamesAbs} 发现的问题详情:`);
324
+ }
325
+ result.issues.forEach((issue, idx) => {
326
+ logger.debug(` 问题 ${idx + 1}: ${issue.risk} - ${issue.message?.slice(0, 100)}...`);
327
+ });
328
+ }
329
+
330
+ // 更新进度
331
+ this.stats.completedRequests++;
332
+ if (progressCallback && !earlySuccessLogged) {
333
+ progressCallback(batchIndex, batch, 'completed', null);
334
+ }
335
+ return { issues: result.issues || result || [], durationMs: duration };
336
+
337
+ } finally {
338
+ // 确保释放客户端
339
+ this.releaseClient(client);
340
+ }
341
+
342
+ } catch (error) {
343
+ this.stats.retryCount++;
344
+
345
+ if (retryCount < maxRetries) {
346
+ logger.warn(`批次 ${batchIndex + 1} 处理失败,进行第 ${retryCount + 1} 次重试: ${error.message}`);
347
+
348
+ // 指数退避延迟
349
+ const delay = Math.pow(2, retryCount) * 1000;
350
+ await new Promise(resolve => setTimeout(resolve, delay));
351
+
352
+ return this.processBatchWithRetry(batch, batchIndex, progressCallback, retryCount + 1);
353
+ } else {
354
+ logger.error(`批次 ${batchIndex + 1} 重试 ${maxRetries} 次后仍然失败: ${error.message}`);
355
+
356
+ if (progressCallback) {
357
+ progressCallback(batchIndex, batch, 'failed', error);
358
+ }
359
+
360
+ throw error;
361
+ }
362
+ }
363
+ }
364
+
365
+ /**
366
+ * 格式化批次数据供AI分析
367
+ * @param {Object} batch - 批次对象
368
+ * @returns {Object} 格式化的批次数据
369
+ */
370
+ formatBatchForAI(batch) {
371
+ return {
372
+ files: batch.items.map(item => ({
373
+ filePath: item.filePath,
374
+ content: item.content,
375
+ staticIssues: item.staticIssues || [],
376
+ isChunk: item.isChunk || false,
377
+ chunkIndex: item.chunkIndex || 0,
378
+ totalChunks: item.totalChunks || 1,
379
+ startLine: item.startLine || 1,
380
+ endLine: item.endLine || 1
381
+ })),
382
+ totalTokens: batch.totalTokens,
383
+ batchIndex: batch.batchIndex,
384
+ isLargeFileSegment: batch.isLargeFileSegment || false,
385
+ segmentedFile: batch.segmentedFile || null,
386
+ totalSegments: batch.totalSegments || 1
387
+ };
388
+ }
389
+
390
+ /**
391
+ * 获取池状态统计
392
+ * @returns {Object} 统计信息
393
+ */
394
+ getPoolStats() {
395
+ return {
396
+ poolSize: this.poolSize,
397
+ busyClients: this.busyClients.size,
398
+ availableClients: this.clients.length - this.busyClients.size,
399
+ waitingRequests: this.waitingQueue.length,
400
+ stats: { ...this.stats }
401
+ };
402
+ }
403
+
404
+ /**
405
+ * 清理资源
406
+ */
407
+ cleanup() {
408
+ logger.debug('清理AI客户端池资源');
409
+
410
+ // 清理等待队列
411
+ this.waitingQueue.forEach(resolve => {
412
+ resolve(null); // 返回null表示池已关闭
413
+ });
414
+ this.waitingQueue = [];
415
+
416
+ // 清理客户端
417
+ this.clients.forEach(client => {
418
+ if (client.cleanup && typeof client.cleanup === 'function') {
419
+ client.cleanup();
420
+ }
421
+ });
422
+
423
+ this.busyClients.clear();
424
+ this.clients = [];
425
+ }
426
+
427
+ /**
428
+ * 获取客户端(兼容方法)
429
+ * @returns {Promise<AIClient>} 可用的AI客户端
430
+ */
431
+ async getClient() {
432
+ return this.getAvailableClient();
433
+ }
434
+ }