v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s
Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
This commit is contained in:
149
src/http/routes/edit.routes.js
Normal file
149
src/http/routes/edit.routes.js
Normal file
@@ -0,0 +1,149 @@
|
||||
"use strict";
|
||||
|
||||
const express = require("express");
|
||||
const { parseRegion } = require("../../core/codex/prompts");
|
||||
const { isHeic, convertHeicToJpeg } = require("../../core/images/heic");
|
||||
const { sniffImageType } = require("../../core/images/sniff");
|
||||
const { BadRequestError } = require("../../errors");
|
||||
const { JOB_ID_PATTERN } = require("./history.routes");
|
||||
|
||||
/**
|
||||
* @param {Object} deps
|
||||
* @param {import('../../core/jobs/job-service').JobService} deps.jobService
|
||||
* @param {import('../../core/history/history-service').HistoryService} deps.historyService
|
||||
* @param {import('multer').Multer} deps.upload
|
||||
* @param {import('../../logger').Logger} deps.logger
|
||||
*/
|
||||
function createEditRoutes({ jobService, historyService, upload, logger }) {
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
"/edit",
|
||||
upload.fields([
|
||||
{ name: "photo", maxCount: 1 },
|
||||
{ name: "reference", maxCount: 1 },
|
||||
]),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const photoFile = req.files?.photo?.[0];
|
||||
const referenceFile = req.files?.reference?.[0];
|
||||
|
||||
let sourceId = req.body?.sourceId ? String(req.body.sourceId).trim() : null;
|
||||
if (sourceId) {
|
||||
if (!JOB_ID_PATTERN.test(sourceId)) {
|
||||
throw new BadRequestError("Некорректный идентификатор исходной версии (sourceId).");
|
||||
}
|
||||
const version = historyService.getVersion(sourceId);
|
||||
if (!version) {
|
||||
throw new BadRequestError("Исходная версия не найдена в истории.");
|
||||
}
|
||||
} else if (!photoFile || !photoFile.buffer) {
|
||||
throw new BadRequestError("Выберите фотографию или укажите sourceId из истории.");
|
||||
}
|
||||
|
||||
const prompt = String(req.body?.prompt || "").trim();
|
||||
if (!prompt) {
|
||||
throw new BadRequestError("Введите промпт для редактирования фотографии.");
|
||||
}
|
||||
if (prompt.length > 1000) {
|
||||
throw new BadRequestError("Промпт не должен превышать 1000 символов.");
|
||||
}
|
||||
|
||||
let referenceId = req.body?.referenceId ? String(req.body.referenceId).trim() : null;
|
||||
if (referenceId) {
|
||||
if (!JOB_ID_PATTERN.test(referenceId)) {
|
||||
throw new BadRequestError("Некорректный идентификатор референса (referenceId).");
|
||||
}
|
||||
const refVersion = historyService.getVersion(referenceId);
|
||||
if (!refVersion) {
|
||||
throw new BadRequestError("Референс не найден в истории.");
|
||||
}
|
||||
}
|
||||
|
||||
const region = parseRegion(req.body?.region);
|
||||
if (region && !referenceFile && !referenceId) {
|
||||
throw new BadRequestError("Область указана без изображения референса.");
|
||||
}
|
||||
|
||||
// Process photo file buffer if uploaded
|
||||
let inputBuffer = photoFile ? photoFile.buffer : undefined;
|
||||
let inputMimetype = photoFile ? photoFile.mimetype : undefined;
|
||||
let inputOriginalname = photoFile ? photoFile.originalname : undefined;
|
||||
|
||||
if (photoFile) {
|
||||
if (isHeic(photoFile) || sniffImageType(inputBuffer) === "heic") {
|
||||
inputBuffer = await convertHeicToJpeg(inputBuffer);
|
||||
inputMimetype = "image/jpeg";
|
||||
} else {
|
||||
const sniffed = sniffImageType(inputBuffer);
|
||||
if (!sniffed) {
|
||||
throw new BadRequestError(
|
||||
"Загруженная фотография не является поддерживаемым изображением."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process reference file buffer if uploaded
|
||||
let referenceBuffer = referenceFile ? referenceFile.buffer : undefined;
|
||||
let referenceMimetype = referenceFile ? referenceFile.mimetype : undefined;
|
||||
let referenceOriginalname = referenceFile ? referenceFile.originalname : undefined;
|
||||
|
||||
if (referenceFile) {
|
||||
if (isHeic(referenceFile) || sniffImageType(referenceBuffer) === "heic") {
|
||||
referenceBuffer = await convertHeicToJpeg(referenceBuffer);
|
||||
referenceMimetype = "image/jpeg";
|
||||
} else {
|
||||
const sniffed = sniffImageType(referenceBuffer);
|
||||
if (!sniffed) {
|
||||
throw new BadRequestError(
|
||||
"Загруженный референс не является поддерживаемым изображением."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
sourceId,
|
||||
inputBuffer,
|
||||
inputMimetype,
|
||||
inputOriginalname,
|
||||
prompt,
|
||||
referenceId,
|
||||
referenceBuffer,
|
||||
referenceMimetype,
|
||||
referenceOriginalname,
|
||||
region,
|
||||
};
|
||||
|
||||
const result = await jobService.createJob({
|
||||
type: "edit",
|
||||
payload,
|
||||
requestId: req.id,
|
||||
});
|
||||
|
||||
logger.info("Постановка задания edit в очередь", {
|
||||
jobId: result.jobId,
|
||||
sourceId,
|
||||
prompt: prompt.slice(0, 80),
|
||||
queuePosition: result.queuePosition,
|
||||
});
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
jobId: result.jobId,
|
||||
status: result.status,
|
||||
queuePosition: result.queuePosition,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createEditRoutes,
|
||||
};
|
||||
Reference in New Issue
Block a user