310 lines
9.1 KiB
JavaScript
310 lines
9.1 KiB
JavaScript
import { env } from '../config/env.js';
|
|
import {
|
|
getGenerationByUuidForUser,
|
|
getGenerationSteps,
|
|
updateGenerationResult,
|
|
getGenerationMetaByUuid,
|
|
} from '../repositories/scenario.repository.js';
|
|
|
|
/**
|
|
* Валидация формата результата от n8n
|
|
*
|
|
* Ожидаемый формат body (массив файлов):
|
|
* {
|
|
* files: [
|
|
* {
|
|
* contentType: 'image' | 'video',
|
|
* url: string, // ссылка на файл (GET запрос)
|
|
* thumbnailUrl?: string,
|
|
* duration?: number,
|
|
* width?: number,
|
|
* height?: number,
|
|
* size?: number,
|
|
* format?: string
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
function validateResultBody(body) {
|
|
const errors = [];
|
|
|
|
if (!body || typeof body !== 'object') {
|
|
errors.push('Body must be an object');
|
|
return { valid: false, errors };
|
|
}
|
|
|
|
// files: обязательный массив
|
|
if (!body.files) {
|
|
errors.push('files is required');
|
|
} else if (!Array.isArray(body.files)) {
|
|
errors.push('files must be an array');
|
|
} else if (body.files.length === 0) {
|
|
errors.push('files array cannot be empty');
|
|
} else {
|
|
// Валидация каждого файла
|
|
body.files.forEach((file, index) => {
|
|
const prefix = `files[${index}]`;
|
|
|
|
// contentType
|
|
if (!file.contentType) {
|
|
errors.push(`${prefix}.contentType is required`);
|
|
} else if (!['image', 'video'].includes(file.contentType)) {
|
|
errors.push(`${prefix}.contentType must be 'image' or 'video'`);
|
|
}
|
|
|
|
// url: обязателен
|
|
if (!file.url) {
|
|
errors.push(`${prefix}.url is required`);
|
|
} else if (typeof file.url !== 'string' || file.url.trim() === '') {
|
|
errors.push(`${prefix}.url must be a non-empty string`);
|
|
} else {
|
|
try {
|
|
new URL(file.url);
|
|
} catch {
|
|
errors.push(`${prefix}.url must be a valid URL`);
|
|
}
|
|
}
|
|
|
|
// thumbnailUrl: опционален
|
|
if (file.thumbnailUrl !== undefined && file.thumbnailUrl !== null) {
|
|
if (typeof file.thumbnailUrl !== 'string') {
|
|
errors.push(`${prefix}.thumbnailUrl must be a string`);
|
|
} else {
|
|
try {
|
|
new URL(file.thumbnailUrl);
|
|
} catch {
|
|
errors.push(`${prefix}.thumbnailUrl must be a valid URL`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// duration: опционален, только для видео
|
|
if (file.duration !== undefined && file.duration !== null) {
|
|
if (typeof file.duration !== 'number') {
|
|
errors.push(`${prefix}.duration must be a number`);
|
|
} else if (file.duration < 0) {
|
|
errors.push(`${prefix}.duration must be >= 0`);
|
|
}
|
|
}
|
|
|
|
// width/height: опциональны
|
|
if (file.width !== undefined && file.width !== null) {
|
|
if (!Number.isInteger(file.width) || file.width <= 0) {
|
|
errors.push(`${prefix}.width must be a positive integer`);
|
|
}
|
|
}
|
|
|
|
if (file.height !== undefined && file.height !== null) {
|
|
if (!Number.isInteger(file.height) || file.height <= 0) {
|
|
errors.push(`${prefix}.height must be a positive integer`);
|
|
}
|
|
}
|
|
|
|
// size: опционален
|
|
if (file.size !== undefined && file.size !== null) {
|
|
if (typeof file.size !== 'number' || file.size < 0) {
|
|
errors.push(`${prefix}.size must be a non-negative number`);
|
|
}
|
|
}
|
|
|
|
// format: опционален
|
|
if (file.format !== undefined && file.format !== null) {
|
|
if (typeof file.format !== 'string' || file.format.trim() === '') {
|
|
errors.push(`${prefix}.format must be a non-empty string`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Получить результат генерации для пользователя
|
|
* Формат ответа для фронта:
|
|
* {
|
|
* generationUuid: string,
|
|
* scenarioId: string,
|
|
* scenarioName: string,
|
|
* status: 'running' | 'completed' | 'failed' | 'waiting_for_input',
|
|
* files: Array<{
|
|
* id: string,
|
|
* contentType: 'image' | 'video',
|
|
* url: string,
|
|
* thumbnailUrl?: string,
|
|
* duration?: number,
|
|
* width?: number,
|
|
* height?: number,
|
|
* size?: number,
|
|
* format?: string
|
|
* }>,
|
|
* steps: [...],
|
|
* startedAt: date,
|
|
* finishedAt: date
|
|
* }
|
|
*/
|
|
export async function getResultForUser({ userId, generationUuid }) {
|
|
const generation = await getGenerationByUuidForUser(generationUuid, userId);
|
|
|
|
if (!generation) {
|
|
return null;
|
|
}
|
|
|
|
const steps = await getGenerationSteps(generationUuid);
|
|
|
|
// Преобразуем result_payload в формат для фронта
|
|
const files = parseResultPayload(generation.result_payload);
|
|
|
|
return {
|
|
generationUuid: generation.generation_uuid,
|
|
scenarioId: generation.scenario_id,
|
|
scenarioName: generation.scenario_name,
|
|
status: generation.status,
|
|
files,
|
|
steps,
|
|
startedAt: generation.started_at,
|
|
finishedAt: generation.finished_at,
|
|
requestPayload: generation.request_payload,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Сохранить результат в БД
|
|
*/
|
|
export async function saveResultToDB(generationUuid, files) {
|
|
const resultPayload = { files };
|
|
return updateGenerationResult(generationUuid, resultPayload, 'completed');
|
|
}
|
|
|
|
/**
|
|
* Получить метаданные генерации (только scenarioId и scenarioName)
|
|
*/
|
|
export async function getGenerationMeta({ userId, generationUuid }) {
|
|
const generation = await getGenerationMetaByUuid(generationUuid, userId);
|
|
|
|
if (!generation) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
scenarioId: generation.scenario_id,
|
|
scenarioName: generation.scenario_name,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Парсит result_payload в массив файлов
|
|
*/
|
|
function parseResultPayload(resultPayload) {
|
|
if (!resultPayload || !resultPayload.files) {
|
|
return [];
|
|
}
|
|
|
|
return resultPayload.files.map((file, index) => ({
|
|
id: `file-${index}`,
|
|
contentType: file.contentType,
|
|
url: file.url,
|
|
thumbnailUrl: file.thumbnailUrl,
|
|
duration: file.duration,
|
|
width: file.width,
|
|
height: file.height,
|
|
size: file.size,
|
|
format: file.format,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Обработать результат от n8n
|
|
*/
|
|
export async function processResultFromN8n({ generationUuid, userId, scenarioId, resultBody }) {
|
|
// Проверяем, что generation существует и принадлежит пользователю
|
|
const generation = await getGenerationByUuidForUser(generationUuid, userId);
|
|
|
|
if (!generation) {
|
|
const error = new Error(`Generation '${generationUuid}' not found or does not belong to user '${userId}'`);
|
|
error.code = 'GENERATION_NOT_FOUND';
|
|
error.status = 404;
|
|
throw error;
|
|
}
|
|
|
|
// Валидация результата
|
|
const validation = validateResultBody(resultBody);
|
|
if (!validation.valid) {
|
|
const error = new Error(`Invalid result format: ${validation.errors.join(', ')}`);
|
|
error.code = 'INVALID_RESULT_FORMAT';
|
|
error.status = 400;
|
|
error.details = validation.errors;
|
|
throw error;
|
|
}
|
|
|
|
// Сохраняем результат в БД
|
|
const updatedGeneration = await updateGenerationResult(generationUuid, resultBody, 'completed');
|
|
|
|
return {
|
|
generationUuid: updatedGeneration.generation_uuid,
|
|
status: updatedGeneration.status,
|
|
files: parseResultPayload(updatedGeneration.result_payload),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Вызов n8n webhook для получения результата
|
|
* stepData - данные из response_payload шага (taskId, recordId и т.д.)
|
|
*/
|
|
export async function fetchResultFromN8n({ generationUuid, userId, scenarioId, stepData = {} }) {
|
|
// Вызываем webhook без generationUuid в пути (передаём в meta/body)
|
|
const n8nUrl = `${env.N8N_BASE_URL}/webhook/result`;
|
|
|
|
const payload = {
|
|
meta: {
|
|
generationUuid,
|
|
userId,
|
|
scenarioId,
|
|
stepData,
|
|
},
|
|
body: stepData,
|
|
};
|
|
|
|
console.log('[fetchResultFromN8n] Calling:', n8nUrl);
|
|
console.log('[fetchResultFromN8n] Payload:', JSON.stringify(payload, null, 2));
|
|
|
|
try {
|
|
const response = await fetch(n8nUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
console.log('[fetchResultFromN8n] Response status:', response.status, response.statusText);
|
|
|
|
const responseText = await response.text();
|
|
console.log('[fetchResultFromN8n] Response body:', responseText);
|
|
|
|
if (!response.ok) {
|
|
const error = new Error(`n8n webhook failed: ${response.status} ${response.statusText}`);
|
|
error.code = 'N8N_WEBHOOK_ERROR';
|
|
error.status = response.status;
|
|
error.n8nResponse = responseText;
|
|
throw error;
|
|
}
|
|
|
|
const data = JSON.parse(responseText);
|
|
console.log('[fetchResultFromN8n] Parsed data:', data);
|
|
return data;
|
|
} catch (err) {
|
|
console.error('[fetchResultFromN8n] Error:', err.message);
|
|
if (err.code === 'N8N_WEBHOOK_ERROR') {
|
|
throw err;
|
|
}
|
|
const error = new Error(`Failed to call n8n webhook: ${err.message}`);
|
|
error.code = 'N8N_CONNECTION_ERROR';
|
|
error.status = 503;
|
|
throw error;
|
|
}
|
|
}
|