UNPKG

solver-sdk

Version:

SDK for WorkAI API - AI-powered code analysis with WorkCoins billing system

362 lines 18.2 kB
"use strict"; /** * 🔄 Delta-Chunking Manager * Менеджер для координации delta-chunking операций */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DeltaChunkingManager = void 0; /** * Менеджер для координации всех delta-chunking операций */ class DeltaChunkingManager { constructor(deltaApi, utils, options, logger) { this.deltaApi = deltaApi; this.utils = utils; this.options = options; this.logger = logger; } /** * Проверка активности delta-chunking */ get isEnabled() { return !!this.deltaApi && !!this.options.deltaChunking?.enabled; } /** * 📤 Синхронизация готовых зашифрованных чанков * * ✅ ПРАВИЛЬНАЯ АРХИТЕКТУРА: * Client Extension передает готовые чанки и rootHash * Backend SDK отправляет их на сервер через HTTP API * * @param projectId ID проекта * @param encryptedChunks Готовые зашифрованные чанки от клиента * @param rootHash Готовый clientRootHash от клиента */ async syncEncryptedChunks(projectId, encryptedChunks, rootHash, options = {}) { this.ensureEnabled(); const startTime = Date.now(); try { this.logger.log(`📤 Начинаю синхронизацию ${encryptedChunks.length} готовых чанков для проекта ${projectId}`); // 📁 Подсчитываем количество уникальных файлов const uniqueFiles = new Set(encryptedChunks .map((chunk) => chunk.obfuscatedPath) .filter((path) => !!path)); const totalFiles = uniqueFiles.size; this.logger.log(`📁 Обнаружено ${totalFiles} уникальных файлов в ${encryptedChunks.length} чанках`); // 1. Инициализация синхронизации const initResult = await this.deltaApi.initSync(projectId, { clientRootHash: rootHash, metadata: { totalFiles, // 📁 Передаем количество файлов для user-friendly прогресса expectedChunks: encryptedChunks.length, } }); // ✅ ИСПРАВЛЕНО: Проверяем нужна ли синхронизация if (!initResult.needsSync) { this.logger.log(`✅ Синхронизация не требуется - проект актуален (hash: ${initResult.lastSyncHash})`); return { success: true, projectId, processedChunks: 0, totalChunks: encryptedChunks.length, duration: Date.now() - startTime, sessionId: undefined, details: { newFiles: 0, changedFiles: 0, deletedFiles: 0 } }; } if (!initResult.sessionId) { throw new Error('Backend error: sessionId missing when sync is needed'); } this.logger.log(`🔄 Сессия инициализирована: ${initResult.sessionId}`); // 2. Отправка готовых зашифрованных чанков батчами const batchSize = options.batchSize || 50; let processedTotal = 0; const errors = []; for (let i = 0; i < encryptedChunks.length; i += batchSize) { const batch = encryptedChunks.slice(i, i + batchSize); const batchNumber = Math.floor(i / batchSize) + 1; const totalBatches = Math.ceil(encryptedChunks.length / batchSize); try { this.logger.log(`📦 Отправка батча ${batchNumber}/${totalBatches} (${batch.length} чанков)`); const batchResult = await this.deltaApi.uploadChunkBatch(projectId, { batchIndex: Math.floor(i / batchSize), totalBatches: totalBatches, chunks: batch }); processedTotal += batchResult.processed; if (batchResult.errors && batchResult.errors.length > 0) { errors.push(...batchResult.errors); } // ✅ Задержка перед следующим батчем для предотвращения Rate Limit const delayMs = options.batchDelayMs ?? 0; if (delayMs > 0 && i + batchSize < encryptedChunks.length) { this.logger.log(`⏱️ Задержка ${delayMs}ms перед батчем ${batchNumber + 1}...`); await new Promise(resolve => setTimeout(resolve, delayMs)); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`❌ Ошибка отправки батча ${batchNumber}: ${errorMessage}`); errors.push(`Batch ${batchNumber}: ${errorMessage}`); } } // 3. Очистка удаленных файлов (если включена) if (options.enableCleanup !== false && options.activeFiles) { this.logger.log(`🧹 Начинаю очистку удаленных файлов`); // ✅ Логируем deletedFiles для диагностики if (options.deletedFiles && options.deletedFiles.length > 0) { this.logger.log(`🗑️ Удаленные файлы от клиента: ${options.deletedFiles.join(', ')}`); } try { // ✅ Передаем deletedFiles в cleanup API const cleanupResult = await this.deltaApi.cleanupFiles(projectId, options.activeFiles, options.deletedFiles // ← НОВОЕ: передаем deletedFiles ); if (cleanupResult.success) { this.logger.log(`✅ Очистка завершена: удалено ${cleanupResult.deletedFromQdrant} векторов из Qdrant, ` + `${cleanupResult.deletedFromPostgres} чанков из PostgreSQL за ${cleanupResult.cleanupTime}ms`); } else { this.logger.warn(`⚠️ Очистка выполнена частично`); } } catch (cleanupError) { // Cleanup не должен ломать основной процесс const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError); this.logger.warn(`⚠️ Ошибка очистки (нескритично): ${cleanupErrorMessage}`); } } else if (options.enableCleanup !== false) { this.logger.warn(`⚠️ Cleanup пропущен - не переданы activeFiles. Передайте activeFiles в options для автоматической очистки`); } // 4. Финализация await this.deltaApi.finalizeSync(projectId); const duration = Date.now() - startTime; this.logger.log(`✅ Синхронизация завершена за ${duration}ms: ${processedTotal}/${encryptedChunks.length} чанков`); return { success: errors.length === 0, projectId, processedChunks: processedTotal, totalChunks: encryptedChunks.length, duration, sessionId: initResult.sessionId, error: errors.length > 0 ? errors.join('; ') : undefined, details: { newFiles: 0, changedFiles: 0, deletedFiles: 0 } }; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`❌ Ошибка синхронизации: ${errorMessage}`); return { success: false, projectId, processedChunks: 0, totalChunks: encryptedChunks.length, duration, error: errorMessage, details: { newFiles: 0, changedFiles: 0, deletedFiles: 0 } }; } } /** * 📊 Получение статуса синхронизации */ async getSyncStatus(projectId) { this.ensureEnabled(); return await this.deltaApi.getSyncStatus(projectId); } /** * ❌ Отмена активной синхронизации */ async cancelSync(projectId) { this.ensureEnabled(); const result = await this.deltaApi.cancelSync(projectId); return result.success; } /** * 🧹 Очистка удаленных файлов из векторной базы */ async cleanupDeletedFiles(projectId, currentFiles) { try { this.logger.log(`🧹 Начинаю очистку удаленных файлов для проекта ${projectId}`); // Подготавливаем список активных файлов const activeFiles = currentFiles.map(file => ({ filePath: file.path, fileHash: this.utils.calculateFileHash(file.content), lastModified: file.lastModified || Date.now() })); this.logger.log(`📋 Отправляю список из ${activeFiles.length} активных файлов для очистки`); // Вызываем API очистки const result = await this.deltaApi.cleanupFiles(projectId, activeFiles); if (result.success) { this.logger.log(`✅ Очистка завершена: удалено ${result.deletedFromQdrant} векторов из Qdrant, ` + `${result.deletedFromPostgres} чанков из PostgreSQL за ${result.cleanupTime}ms`); } else { this.logger.warn(`⚠️ Очистка выполнена частично`); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`❌ Ошибка очистки удаленных файлов: ${errorMessage}`); return { success: false, deletedFromQdrant: 0, deletedFromPostgres: 0, activeFiles: currentFiles.length, cleanupTime: 0 }; } } /** * 🧹 Публичный метод для прямой очистки удаленных файлов * @param projectId ID проекта * @param activeFiles Список активных файлов * @param deletedFiles ОПЦИОНАЛЬНО: Явно удаленные файлы (пути) */ async performCleanup(projectId, activeFiles, deletedFiles) { this.ensureEnabled(); return await this.deltaApi.cleanupFiles(projectId, activeFiles, deletedFiles); } /** * 🗑️ Инвалидировать chunks измененного файла * * Удаляет устаревшие chunks из Qdrant и инвалидирует RAG cache. * После инвалидации отправьте новые chunks через syncEncryptedChunks(). * * @param projectId - UUID проекта * @param options - Опции инвалидации (filePath обязателен) * @returns Результат операции с количеством инвалидированных chunks * * @example * ```typescript * // Инвалидировать файл после изменения * const result = await sdk.deltaManager.invalidateFile(projectId, { * filePath: 'src/app.tsx', * reason: 'file_changed' * }); * * if (result.success) { * console.log(`Invalidated ${result.chunksInvalidated} chunks`); * } * * // Теперь отправить новые chunks * await sdk.deltaManager.syncEncryptedChunks(projectId, newChunks, rootHash); * ``` */ async invalidateFile(projectId, options) { this.ensureEnabled(); if (!options.filePath || !options.filePath.trim()) { throw new Error('filePath is required for file invalidation'); } this.logger.log(`🗑️ Инвалидирую файл: ${options.filePath} (причина: ${options.reason || 'file_changed'})`); try { const result = await this.deltaApi.invalidateFile(projectId, options); if (result.success) { this.logger.log(`✅ Файл инвалидирован: ${result.chunksInvalidated} chunks удалено, ` + `cache инвалидирован: ${result.cacheInvalidated}`); } else { this.logger.warn(`⚠️ Инвалидация файла выполнена с предупреждениями`); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`❌ Ошибка инвалидации файла: ${errorMessage}`); // Graceful degradation: не бросаем ошибку, backend System Prompt recovery подхватит return { success: false, chunksInvalidated: 0, cacheInvalidated: false, filePath: options.filePath }; } } /** * 🗑️ Batch инвалидация файлов (для предотвращения rate limiting) * * Эффективная инвалидация множества файлов за один API запрос. * Используйте этот метод вместо множественных вызовов invalidateFile() * для предотвращения превышения rate limit сервера (30 req/sec). * * @param projectId - UUID проекта * @param files - Массив файлов для инвалидации * @returns Результат batch операции с общей статистикой * * @example * ```typescript * // Инвалидировать несколько файлов одним запросом * const result = await sdk.deltaManager.invalidateFiles(projectId, [ * { filePath: 'src/app.tsx', reason: 'file_changed' }, * { filePath: 'src/utils.ts', reason: 'file_saved' }, * { filePath: 'src/config.json', reason: 'file_changed' } * ]); * * console.log(`Invalidated ${result.totalChunksInvalidated} chunks from ${result.totalFiles} files`); * * // Теперь отправить новые chunks * await sdk.deltaManager.syncEncryptedChunks(projectId, newChunks, rootHash); * ``` */ async invalidateFiles(projectId, files) { this.ensureEnabled(); if (!files || files.length === 0) { throw new Error('files array is required and must not be empty'); } this.logger.log(`🗑️ Batch инвалидация: ${files.length} файлов`); try { const result = await this.deltaApi.invalidateFiles(projectId, files); if (result.success) { this.logger.log(`✅ Batch инвалидация завершена: ${result.totalFiles} файлов, ` + `${result.totalChunksInvalidated} chunks удалено, ` + `${result.totalCacheInvalidated} cache записей инвалидировано`); } else { this.logger.warn(`⚠️ Batch инвалидация выполнена с предупреждениями`); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`❌ Ошибка batch инвалидации: ${errorMessage}`); // Graceful degradation return { success: false, totalFiles: files.length, totalChunksInvalidated: 0, totalCacheInvalidated: 0, results: files.map(f => ({ success: false, chunksInvalidated: 0, cacheInvalidated: false, filePath: f.filePath, error: errorMessage })) }; } } /** * Проверка что delta-chunking включен */ ensureEnabled() { if (!this.isEnabled) { throw new Error('Delta-chunking не включен. Установите deltaChunking.enabled = true в конфигурации SDK'); } } } exports.DeltaChunkingManager = DeltaChunkingManager; //# sourceMappingURL=delta-chunking-manager.js.map