solver-sdk
Version:
SDK для интеграции с Code Solver Backend API
585 lines • 28.9 kB
JavaScript
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
;