UNPKG

solver-sdk

Version:

SDK для интеграции с Code Solver Backend API

585 lines 28.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProjectsApi = exports.ApiEndpoints = void 0; const code_solver_websocket_client_js_1 = require("../utils/code-solver-websocket-client.js"); /** * Константы для API путей */ var ApiEndpoints; (function (ApiEndpoints) { ApiEndpoints["PROJECTS"] = "/api/v1/projects"; ApiEndpoints["PROJECT"] = "/api/v1/projects/:id"; ApiEndpoints["PROJECT_INDEXING_STATUS"] = "/api/v1/projects/:id/indexing_status"; ApiEndpoints["PROJECT_CANCEL_INDEXING"] = "/api/v1/projects/:id/cancel_indexing"; ApiEndpoints["PROJECT_CLEAR_ERROR"] = "/api/v1/projects/:id/clear_error"; })(ApiEndpoints || (exports.ApiEndpoints = ApiEndpoints = {})); /** * Замещает параметры в пути API * @param endpoint Шаблон пути API * @param params Параметры для замещения * @returns Путь API с замещенными параметрами */ function formatEndpoint(endpoint, params) { let result = endpoint; for (const [key, value] of Object.entries(params)) { result = result.replace(`:${key}`, value); } return result; } /** * API для работы с проектами */ class ProjectsApi { /** * Создает новый экземпляр API для работы с проектами * @param {HttpClient} httpClient HTTP клиент */ constructor(httpClient) { /** WebSocket клиент */ this.wsClient = null; /** Родительский SDK */ this.parentSdk = null; this.httpClient = httpClient; } /** * Проверяет валидность идентификатора проекта * @param {string} projectId ID проекта * @throws {Error} Если ID проекта не валиден */ validateProjectId(projectId) { if (projectId === undefined || projectId === null) { throw new Error('Project ID is required'); } if (typeof projectId !== 'string') { throw new Error('Project ID must be a string'); } if (projectId.trim() === '') { throw new Error('Project ID cannot be empty'); } } /** * Получает список всех проектов * @returns {Promise<Project[]>} Список проектов */ async getAllProjects() { return this.httpClient.get('/api/v1/projects'); } /** * Получает проект по ID * @param {string} projectId ID проекта * @returns {Promise<Project>} Проект */ async getProject(projectId) { this.validateProjectId(projectId); return this.httpClient.get('/api/v1/projects/' + projectId); } /** * Создает новый проект * @param {string} name Название проекта * @param {string} path Путь к проекту * @param {ProjectOptions} [options] Дополнительные опции проекта * @returns {Promise<Project>} Созданный проект */ async createProject(name, path, options) { return this.httpClient.post('/api/v1/projects', { name, path, ...options }); } /** * Обновляет проект * @param {string} projectId ID проекта * @param {Partial<ProjectUpdateData>} data Данные для обновления * @returns {Promise<Project>} Обновленный проект */ async updateProject(projectId, data) { this.validateProjectId(projectId); return this.httpClient.put('/api/v1/projects/' + projectId, data); } /** * Удаляет проект * @param {string} projectId ID проекта * @returns {Promise<void>} */ async deleteProject(projectId) { this.validateProjectId(projectId); return this.httpClient.delete('/api/v1/projects/' + projectId); } /** * Запускает индексацию проекта * @param {string} projectId ID проекта * @param {Object} [options] Опции индексации * @param {string} [options.indexingMode] Режим индексации: 'full', 'incremental', 'auto' * @param {boolean} [options.forceFull] Принудительная полная индексация * @param {string[]} [options.includePatterns] Паттерны для включения файлов * @param {string[]} [options.excludePatterns] Паттерны для исключения файлов * @returns {Promise<IndexingResponse>} Информация о начатой индексации */ async indexProject(projectId, options) { this.validateProjectId(projectId); // Преобразуем indexingMode в forceFull для совместимости с бэкендом let requestBody = {}; if (options) { if (options.indexingMode === 'full') { requestBody.forceFull = true; } if (options.forceFull) { requestBody.forceFull = true; } if (options.includePatterns) { requestBody.includePatterns = options.includePatterns; } if (options.excludePatterns) { requestBody.excludePatterns = options.excludePatterns; } } return this.httpClient.post('/api/v1/projects/' + projectId + '/index', requestBody); } /** * Получает статус индексации проекта * @param {string} projectId Идентификатор проекта * @returns {Promise<any>} Статус индексации проекта */ async getIndexingStatus(projectId) { this.validateProjectId(projectId); return this.httpClient.get(formatEndpoint(ApiEndpoints.PROJECT_INDEXING_STATUS, { id: projectId })); } /** * Отменяет индексацию проекта * @param {string} projectId Идентификатор проекта * @returns {Promise<boolean>} Результат отмены индексации */ async cancelIndexing(projectId) { if (!projectId) { throw new Error('Project ID is required'); } return this.httpClient.post(formatEndpoint(ApiEndpoints.PROJECT_CANCEL_INDEXING, { id: projectId })) .then(response => { return true; }) .catch(error => { throw new Error(`Failed to cancel indexing: ${error.message}`); }); } /** * Очищает ошибку индексации проекта * @param {string} projectId Идентификатор проекта * @returns {Promise<boolean>} Результат очистки ошибки */ async clearIndexingError(projectId) { this.validateProjectId(projectId); return this.httpClient.post(formatEndpoint(ApiEndpoints.PROJECT_CLEAR_ERROR, { id: projectId })) .then(() => true) .catch(error => { throw new Error(`Failed to clear indexing error: ${error.message}`); }); } /** * Обновляет индекс конкретного файла в проекте * @param {string} projectId ID проекта * @param {string} filePath Путь к файлу (относительно корня проекта) * @param {Object} options Опции обновления индекса * @param {string} [options.content] Содержимое файла (если не указано, будет прочитано с диска) * @param {boolean} [options.force=false] Принудительная переиндексация, даже если файл не изменился * @param {string} [options.language] Язык файла (если не указан, будет определен автоматически) * @param {boolean} [options.updateDependencies=false] Обновлять зависимости после индексации файла * @returns {Promise<FileIndexResponse>} Информация об обновленном индексе файла */ async updateFileIndex(projectId, filePath, options = {}) { this.validateProjectId(projectId); if (!filePath) { throw new Error('Путь к файлу не может быть пустым'); } // Кодируем путь для URL, чтобы избежать проблем со специальными символами const encodedFilePath = encodeURIComponent(filePath); return this.httpClient.post('/api/v1/projects/' + projectId + '/files/' + encodedFilePath + '/index', options); } /** * Подключается к WebSocket для событий индексации * @returns {Promise<boolean>} Результат подключения */ async connectWebSocket() { try { if (!this.parentSdk || typeof this.parentSdk.getWebSocketClient !== 'function') { throw new Error('Родительский SDK не настроен или не поддерживает WebSocket'); } this.wsClient = this.parentSdk.getWebSocketClient(); // Получаем apiKey из родительского SDK const apiKey = this.parentSdk.options.apiKey; // Подключаемся к пространству имен индексации с параметром authToken await this.wsClient.connectToIndexing(); // Проверяем состояние подключения const isConnected = this.wsClient.isConnected('indexing'); if (!isConnected) { console.warn('[ProjectsApi] Не удалось подключиться к WebSocket'); } return isConnected; } catch (error) { console.error('[ProjectsApi] Ошибка при подключении к WebSocket:', error.message); return false; } } /** * Отключается от WebSocket для событий индексации * @returns {Promise<void>} */ async disconnectWebSocket() { if (this.wsClient) { await this.wsClient.disconnect('indexing'); } } /** * Проверяет, подключен ли WebSocket * @returns {boolean} Состояние подключения */ isWebSocketConnected() { return this.wsClient ? this.wsClient.isConnected('indexing') : false; } /** * Устанавливает родительский SDK * @param sdk Родительский SDK */ setParent(sdk) { this.parentSdk = sdk; } /** * Подписывается на событие через WebSocket * @param event Название события * @param callback Функция обратного вызова */ on(event, callback) { if (!this.wsClient) { console.warn('[ProjectsApi] WebSocket не подключен'); return; } this.wsClient.on(event, callback); } /** * Отправляет событие через WebSocket * @param event Название события * @param data Данные для отправки */ emitSocketEvent(event, data) { if (!this.wsClient) { console.warn('[ProjectsApi] WebSocket не подключен'); return; } this.wsClient.send(code_solver_websocket_client_js_1.WebSocketNamespace.INDEXING, event, data); } /** * Отправляет событие через WebSocket с ожиданием ответа * @param event Имя события * @param data Данные для отправки * @param timeout Таймаут ожидания ответа * @returns {Promise<any>} Ответ от сервера */ async sendSocketEventWithResponse(event, data, timeout = 5000) { if (!this.wsClient) { throw new Error('[ProjectsApi] WebSocket не подключен'); } // Реализуем собственный механизм emitWithAck, если wsClient не поддерживает его return new Promise((resolve, reject) => { try { // Создаем уникальный ID для запроса const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; // Имя события для ответа const responseEvent = `${event}_response`; // Обработчик для получения ответа const responseHandler = (response) => { if (response && response.requestId === requestId) { // Очищаем таймер и удаляем обработчик clearTimeout(timeoutId); if (this.wsClient) { this.wsClient.off(responseEvent, responseHandler); } // Разрешаем промис с ответом resolve(response.data || response); } }; // Устанавливаем таймаут const timeoutId = setTimeout(() => { if (this.wsClient) { this.wsClient.off(responseEvent, responseHandler); } reject(new Error(`Таймаут ожидания ответа на событие ${event}`)); }, timeout); // Регистрируем обработчик this.wsClient.on(responseEvent, responseHandler); // Отправляем событие this.wsClient.send(code_solver_websocket_client_js_1.WebSocketNamespace.INDEXING, event, { ...data, requestId }); } catch (error) { reject(error); } }); } /** * Остановить индексацию проекта через WebSocket * @param projectId ID проекта * @returns {Promise<boolean>} Результат операции */ async stopIndexing(projectId) { this.validateProjectId(projectId); try { if (!this.wsClient || !this.isWebSocketConnected()) { // Если WebSocket не доступен, используем REST API return this.cancelIndexing(projectId); } const result = await this.sendSocketEventWithResponse('STOP_INDEXING', { projectId }, 5000); return result && result.success === true; } catch (error) { console.error('[ProjectsApi] Ошибка при остановке индексации:', error.message); // Пробуем через REST API как запасной вариант return this.cancelIndexing(projectId); } } /** * Кэширует соответствие пути и ID проекта * @param path Путь к проекту * @param projectId ID проекта * @private */ cacheProjectId(path, projectId) { try { const normalizedPath = path.replace(/\\/g, '/').replace(/\/$/, ''); if (typeof localStorage !== 'undefined') { // Браузерное окружение const cachedProjects = JSON.parse(localStorage.getItem('solverSdkProjectCache') || '{}'); cachedProjects[normalizedPath] = projectId; localStorage.setItem('solverSdkProjectCache', JSON.stringify(cachedProjects)); } else if (typeof process !== 'undefined') { // Node.js окружение (для VS Code/Cursor) // TODO: реализовать хранение кэша в Node.js // В простейшем случае можно использовать in-memory хранилище if (!global.solverSdkProjectCache) { global.solverSdkProjectCache = {}; } global.solverSdkProjectCache[normalizedPath] = projectId; } } catch (error) { console.warn(`[ProjectsApi] Не удалось кэшировать ID проекта: ${error instanceof Error ? error.message : String(error)}`); } } /** * Получает ID проекта из кэша * @param path Путь к проекту * @returns ID проекта или null, если не найдено * @private */ getCachedProjectId(path) { try { const normalizedPath = path.replace(/\\/g, '/').replace(/\/$/, ''); if (typeof localStorage !== 'undefined') { // Браузерное окружение const cachedProjects = JSON.parse(localStorage.getItem('solverSdkProjectCache') || '{}'); return cachedProjects[normalizedPath] || null; } else if (typeof process !== 'undefined' && global.solverSdkProjectCache) { // Node.js окружение return global.solverSdkProjectCache[normalizedPath] || null; } return null; } catch (error) { console.warn(`[ProjectsApi] Не удалось получить ID проекта из кэша: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Получает или создает проект по пути * @param path Путь к проекту * @param name Имя проекта (опционально, если не указано - будет сгенерировано из пути) * @returns Данные проекта */ async getOrCreateProject(path, name) { if (!path) { throw new Error('Путь к проекту не может быть пустым'); } const normalizedPath = path.replace(/\\/g, '/').replace(/\/$/, ''); // Сначала проверяем кэш const cachedProjectId = this.getCachedProjectId(normalizedPath); if (cachedProjectId) { try { const project = await this.getProject(cachedProjectId); return project; } catch (error) { // Если ошибка - продолжаем с API запросом console.warn(`[ProjectsApi] Не удалось получить проект из кэша: ${error instanceof Error ? error.message : String(error)}`); } } // Вызываем API для получения/создания проекта const project = await this.httpClient.post('/api/v1/projects/find-or-create', { path: normalizedPath, name: name || normalizedPath.split('/').pop() || 'Unnamed Project' }); // Кэшируем результат this.cacheProjectId(normalizedPath, project.id); return project; } /** * Индексация проекта по пути * @param path Путь к проекту * @param options Опции индексации * @returns Информация об индексации */ async indexProjectByPath(path, options = {}) { if (!path) { throw new Error('Путь к проекту не может быть пустым'); } const normalizedPath = path.replace(/\\/g, '/').replace(/\/$/, ''); try { // Вызываем API для индексации проекта по пути return await this.httpClient.post('/api/v1/projects/index-by-path', { path: normalizedPath, name: options.name, forceFull: options.forceFull, excludePatterns: options.excludePatterns }); } catch (error) { // Улучшаем сообщение об ошибке const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Не удалось выполнить индексацию проекта по пути: ${errorMessage}`); } } /** * Создание и индексация проекта в одной операции * @param path Путь к проекту * @param options Опции создания и индексации * @param {string} [options.name] Имя проекта (если не указано, будет получено из пути) * @param {boolean} [options.forceFull=false] Принудительная полная индексация * @param {string} [options.indexingMode] Режим индексации: 'full', 'incremental', 'auto' * @param {string[]} [options.includePatterns] Паттерны для включения файлов * @param {string[]} [options.excludePatterns] Паттерны для исключения файлов * @returns Информация о созданном проекте и начатой индексации */ async createAndIndexProject(path, options = {}) { try { // Преобразуем indexingMode в forceFull для совместимости с бэкендом let requestOptions = { name: options.name, excludePatterns: options.excludePatterns }; if (options.indexingMode === 'full' || options.forceFull) { requestOptions.forceFull = true; } if (options.includePatterns) { requestOptions.includePatterns = options.includePatterns; } // Используем API для индексации по пути, который также создает проект если его нет const result = await this.indexProjectByPath(path, requestOptions); // Проверяем, что результат содержит нужные поля if (!result || !result.projectId) { throw new Error('Сервер вернул некорректный ответ без идентификатора проекта'); } return { projectId: result.projectId, indexingStatus: result.status || 'indexing' }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Не удалось создать и проиндексировать проект: ${errorMessage}`); } } /** * Проверяет существование проекта и создает резервную копию при необходимости * @param projectId ID проекта для проверки * @param options Опции для создания резервной копии * @returns Существующий проект или созданную резервную копию */ async ensureProjectExists(projectId, options = {}) { try { this.validateProjectId(projectId); // Пытаемся получить проект по ID try { const project = await this.getProject(projectId); return project; } catch (error) { // Проверяем, нужно ли создавать резервную копию if (!options.createBackup) { // Если резервная копия не требуется, пробрасываем ошибку const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Проект с ID ${projectId} не найден: ${errorMessage}`); } // Проверяем наличие пути для резервной копии if (!options.backupPath) { throw new Error('Для создания резервной копии необходимо указать путь (backupPath)'); } const backupName = options.backupName || `Резервная копия для ${projectId}`; // Создаем новый проект как резервную копию console.log(`[ProjectsApi] Создание резервной копии проекта ${projectId} с именем "${backupName}" по пути ${options.backupPath}`); return await this.createProject(backupName, options.backupPath); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Ошибка при проверке/создании проекта ${projectId}: ${errorMessage}`); } } /** * Получает или создает проект по пути с расширенными опциями * @param path Путь к проекту * @param options Дополнительные опции * @returns Найденный или созданный проект */ async getOrCreateProjectByPath(path, options = {}) { if (!path) { throw new Error('Путь к проекту не может быть пустым'); } const normalizedPath = path.replace(/\\/g, '/').replace(/\/$/, ''); try { // Сначала проверяем кэш const cachedProjectId = this.getCachedProjectId(normalizedPath); if (cachedProjectId && options.preferExisting !== false) { try { const project = await this.getProject(cachedProjectId); return project; } catch (error) { // Если ошибка - продолжаем с API запросом console.warn(`[ProjectsApi] Не удалось получить проект из кэша: ${error instanceof Error ? error.message : String(error)}`); } } // Если проект не найден в кэше или не смогли его получить, создаем новый через API find-or-create const project = await this.httpClient.post('/api/v1/projects/find-or-create', { path: normalizedPath, name: options.name || normalizedPath.split('/').pop() || 'Unnamed Project' }); // Кэшируем полученный проект if (project && project.id) { this.cacheProjectId(normalizedPath, project.id); } return project; } catch (error) { throw new Error(`Не удалось получить или создать проект: ${error.message}`); } } /** * Запускает индексацию проекта (псевдоним для indexProject) * @param {string} projectId ID проекта * @param {Object} [options] Опции индексации * @param {string} [options.indexingMode] Режим индексации: 'full', 'incremental', 'auto' * @param {boolean} [options.forceFull] Принудительная полная индексация * @param {string[]} [options.includePatterns] Паттерны для включения файлов * @param {string[]} [options.excludePatterns] Паттерны для исключения файлов * @returns {Promise<IndexingResponse>} Информация о начатой индексации */ async startIndexing(projectId, options) { // Это псевдоним для indexProject для соответствия с документацией return this.indexProject(projectId, options); } } exports.ProjectsApi = ProjectsApi; //# sourceMappingURL=projects-api.js.map