import { GenerationResponse, ApiError as ApiErrorType, GeneratedImage, PendingTask, GenerationResult } from '../types/api'; import { baseWorkflow } from '../constants/baseWorkflow'; import { prompts } from '../assets/prompts'; import translateService from './translateService'; import { getCurrentUserId, isTelegramWebAppAvailable } from '../constants/user'; import { trackStickerGeneration, trackRejectedPrompt } from './analyticsService'; const API_BASE_URL = 'https://stickerserver.gymnasticstuff.uk'; /** * Нормализует URL изображения, заменяя IP-адрес на домен, если необходимо. * Если URL уже содержит домен, оставляет его без изменений. * @param url URL изображения * @returns Нормализованный URL */ export const normalizeImageUrl = (url: string): string => { if (!url) return url; // Если URL уже содержит домен, оставляем его без изменений if (url.includes('stickerserver.gymnasticstuff.uk')) { return url; } // Проверяем, содержит ли URL IP-адрес (простая проверка на наличие цифр и точек) const ipRegex = /\d+\.\d+\.\d+\.\d+/; if (ipRegex.test(url)) { // Заменяем IP-адрес на домен // Извлекаем путь из URL (все, что после IP-адреса и порта, если есть) const pathMatch = url.match(/https?:\/\/\d+\.\d+\.\d+\.\d+(:\d+)?(\/.*)/); if (pathMatch && pathMatch[2]) { return `${API_BASE_URL}${pathMatch[2]}`; } } return url; }; // Маппинг стилей к тегам для определения, какой тег использовать для каждого стиля const styleToTagMap: Record = { 'chibi': 'image_generation', 'emotions': 'chibi', // Пока используем chibi для всех стилей 'realism': 'chibi' }; class GenerationError extends Error { constructor(public details: ApiErrorType['detail']) { super('API Error'); this.name = 'GenerationError'; } } // Временное решение для работы с балансом токенов // Используется только если не удалось получить баланс с сервера let mockBalance = 50; // Начальное значение из MOCK_USER const apiService = { // Метод для получения информации о пользователе async getUserInfo(userId: number = getCurrentUserId()): Promise { try { const response = await fetch(`${API_BASE_URL}/users/${userId}`, { method: 'GET', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error('Failed to fetch user info'); } return await response.json(); } catch (error) { console.error('Error fetching user info:', error); throw error; } }, // Метод для создания ссылки на инвойс async createInvoiceLink(userId: number, starsAmount: number, tokens: number): Promise { try { const response = await fetch(`${API_BASE_URL}/create-invoice-link?user_id=${userId}&stars_amount=${starsAmount}&tokens=${tokens}`, { method: 'POST', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error('Failed to create invoice link'); } // Парсим ответ как JSON и извлекаем поле invoice_link const data = await response.json(); if (!data.invoice_link) { throw new Error('Invalid response format: missing invoice_link field'); } return data.invoice_link; } catch (error) { console.error('Error creating invoice link:', error); throw error; } }, // Метод для получения текущего баланса async getBalance(userId: number = getCurrentUserId()): Promise { try { const userInfo = await this.getUserInfo(userId); return userInfo.balance || 0; } catch (error) { console.error('Error getting balance:', error); // В случае ошибки возвращаем mockBalance для обратной совместимости return mockBalance; } }, // Получение списка задач пользователя в статусе PENDING async getUserPendingTasks(userId = getCurrentUserId()): Promise { try { const response = await fetch(`${API_BASE_URL}/user_pending_tasks/${userId}`, { method: 'GET', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error('Failed to fetch pending tasks'); } return await response.json(); } catch (error) { console.error('Error fetching pending tasks:', error); return []; } }, // Получение текущей позиции задачи в очереди async getTaskPosition(taskId: string): Promise<{task_id: number, status: string, queue_position: number | null}> { try { const response = await fetch(`${API_BASE_URL}/task_position/${taskId}`, { method: 'GET', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error('Failed to fetch task position'); } return await response.json(); } catch (error) { console.error('Error fetching task position:', error); throw error; } }, // Расчет примерного времени ожидания в секундах calculateEstimatedWaitTime(queuePosition: number): number { // Среднее время генерации 13-15 секунд const averageGenerationTime = 14; return queuePosition * averageGenerationTime; }, async getGeneratedImages(userId = getCurrentUserId()): Promise { try { const response = await fetch(`${API_BASE_URL}/images_links/${userId}`, { method: 'GET', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error('Failed to fetch images'); } // Получаем массив массивов [file_id, created_at] const rawData = await response.json() as [string, string][]; // Преобразуем в массив объектов GeneratedImage const images = rawData.map((item, index) => { const [fileId, createdAt] = item; return { id: index + 1, link: fileId, prompt_id: '', status: 'COMPLETED', created_at: createdAt, sticker_set_id: null, url: `${API_BASE_URL}/stickers/proxy/sticker/${encodeURIComponent(fileId)}` }; }); // Сортируем изображения от новых к старым по дате создания return images.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); } catch (error) { console.error('Error fetching images:', error); throw new Error('Failed to fetch images'); } }, // Удаление изображения по file_id async deleteImage(fileId: string): Promise { try { const response = await fetch(`${API_BASE_URL}/delete-by-link?link=${encodeURIComponent(fileId)}`, { method: 'DELETE', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error('Failed to delete image'); } return await response.json(); } catch (error) { console.error('Error deleting image:', error); throw new Error('Failed to delete image'); } }, async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string): Promise { try { // Создаем копию базового воркфлоу const workflow = JSON.parse(JSON.stringify(baseWorkflow)); // Вставляем изображение в base64 формате в узел 563 workflow['563'].inputs.image = imageData; // Переменная для хранения использованного промпта let usedPrompt = ''; let translationFailed = false; // Если указан пользовательский промпт и выбрана кнопка "Свой промпт" if (userPrompt && promptId === 'customPrompt') { console.log('Переводим пользовательский промпт:', userPrompt); // Используем новый метод перевода через LLM const translationResult = await translateService.translateWithLLM(userPrompt); if (translationResult.success) { // Успешный перевод console.log('Переведенный промпт:', translationResult.text); workflow['316'].inputs.prompt_1 = translationResult.text; usedPrompt = translationResult.text; } else { // Перевод не удался после всех попыток console.error('Не удалось перевести промпт:', translationResult.text); translationFailed = true; // Отслеживаем событие неудавшейся генерации trackRejectedPrompt(userPrompt); // Не продолжаем генерацию, возвращаем ошибку с сообщением return { translationFailed: true, usedPrompt: 'Недопустимый промпт', // Сообщение для пользователя errorDetails: translationResult.text // Детали ошибки для отладки }; } } // Иначе используем предустановленный промпт (они уже переведены) else if (promptId && prompts[promptId]) { workflow['316'].inputs.prompt_1 = prompts[promptId]; usedPrompt = prompts[promptId]; console.log('Используем предустановленный промпт:', prompts[promptId]); } // Если был сбой перевода, не продолжаем генерацию if (translationFailed) { return { translationFailed: true }; } // Создаем строку JSON для workflow ПОСЛЕ того, как все изменения внесены const workflowJson = JSON.stringify(workflow); // Определяем тег на основе выбранного стиля const tag = styleToTagMap[style || 'chibi'] || 'chibi'; console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`); // Создаем тело запроса как строку JSON вручную const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`; // Сохраняем JSON для отладки только при локальной разработке if (!isTelegramWebAppAvailable()) { const blob = new Blob([requestBodyJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'generation_request.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // Отправляем запрос const response = await fetch(`${API_BASE_URL}/generate_image`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: requestBodyJson }); if (!response.ok) { const errorData: ApiErrorType = await response.json(); throw new GenerationError(errorData.detail); } const result = await response.json() as GenerationResponse; // Отслеживаем событие генерации стикера trackStickerGeneration(usedPrompt); // Возвращаем результат и использованный промпт return { result, usedPrompt, translationFailed: false }; } catch (error) { if (error instanceof GenerationError) { throw error; } throw new Error('Failed to generate image'); } } }; export default apiService;