- справлена ошибка 422 при создании инвойса (возврат к целочисленным значениям) - Сделана вся карточка оффера кликабельной для улучшения UX - обавлено предотвращение двойного срабатывания при клике на кнопку
337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
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<string, string> = {
|
||
'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<any> {
|
||
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<string> {
|
||
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<number> {
|
||
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<PendingTask[]> {
|
||
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<GeneratedImage[]> {
|
||
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<string> {
|
||
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<GenerationResult> {
|
||
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;
|