Files
photo-editor/src/server.js
Coder 401969cfdb
Some checks failed
test / test (push) Failing after 24s
v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Реализация целевого дизайна (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/
2026-08-27 12:52:27 +07:00

223 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const http = require("http");
const path = require("path");
const fs = require("fs");
const { EventEmitter } = require("events");
const { loadConfig } = require("./config");
const { createLogger } = require("./logger");
const { hashPasswordSync } = require("./core/auth/password");
const { SessionStore } = require("./core/auth/session-store");
const { HistoryStore } = require("./core/history/history-store");
const { HistoryService, migrateLegacyHistory } = require("./core/history/history-service");
const { JobStore } = require("./core/jobs/job-store");
const { Queue } = require("./core/jobs/queue");
const { JobService } = require("./core/jobs/job-service");
const { ComfyClient } = require("./core/comfy/comfy-client");
const { ComfyRuntime } = require("./core/comfy/comfy-runtime");
const { CodexCli } = require("./core/codex/cli");
const { EngineRegistry } = require("./core/engines/engine");
const { createCodexEditEngine, createCodexSuggestEngine } = require("./core/engines/codex-engine");
const { createComfyUpscaleEngine } = require("./core/engines/comfy-upscale-engine");
const { createApp } = require("./http/app");
function printPasswordBanner(user, password) {
const line = "─".repeat(58);
console.log(`\n┌${line}┐`);
console.log(`│ Кадр: Пароль для входа (сгенерирован автоматически) │`);
console.log(`│ Логин : ${user.padEnd(48)}│`);
console.log(`│ Пароль: ${password.padEnd(48)}│`);
console.log(`│ Задайте APP_PASSWORD в окружении для фиксации. │`);
console.log(`└${line}┘\n`);
}
function listenOnPort(app, port) {
return new Promise((resolve, reject) => {
const server = http.createServer(app);
server.listen(port, () => resolve(server));
server.on("error", (error) => {
server.close();
reject(error);
});
});
}
async function startServer() {
const config = loadConfig(process.env);
const logger = createLogger({ level: config.LOG_LEVEL });
logger.info("Запуск веб-приложения «Кадр» v2...", config.getSanitized());
if (config.isGeneratedPassword) {
printPasswordBanner(config.APP_USER, config.APP_PASSWORD);
}
const passwordHash = hashPasswordSync(config.APP_PASSWORD);
// Ensure directories exist
fs.mkdirSync(config.DATA_DIR, { recursive: true });
fs.mkdirSync(config.HISTORY_DIR, { recursive: true });
fs.mkdirSync(path.join(config.DATA_DIR, ".codex-jobs"), { recursive: true });
// Migrate legacy history if present
migrateLegacyHistory(path.resolve("./history"), config.HISTORY_DIR, logger);
// Core stores and services
const historyStore = new HistoryStore({
dir: config.HISTORY_DIR,
limit: config.HISTORY_LIMIT,
logger,
});
const historyService = new HistoryService({ store: historyStore, logger });
const sessionStore = new SessionStore({
file: path.join(config.DATA_DIR, "sessions.json"),
ttlMs: config.SESSION_TTL_MS,
persist: config.SESSION_PERSIST,
logger,
});
const jobStore = new JobStore({
file: path.join(config.DATA_DIR, "jobs.jsonl"),
logger,
});
const comfyClient = new ComfyClient({
comfyUrl: config.COMFY_URL,
requestTimeoutMs: config.REQUEST_TIMEOUT_MS,
logger,
});
const comfyRuntime = new ComfyRuntime({
client: comfyClient,
requiredNodes: config.REQUIRED_NODES,
checkpoint: config.CHECKPOINT,
logger,
});
const codexCli = new CodexCli({
codexExe: config.CODEX_CLI_PATH,
defaultTimeoutMs: config.CODEX_EDIT_TIMEOUT_MS,
logger,
});
const events = new EventEmitter();
events.setMaxListeners(100);
// Engine Registry
const engineRegistry = new EngineRegistry();
const engineContext = {
config,
logger,
history: historyService,
comfyClient,
comfyRuntime,
codexCli,
};
engineRegistry.register("edit", (ctx) => createCodexEditEngine(ctx));
engineRegistry.register("suggest", (ctx) => createCodexSuggestEngine(ctx));
engineRegistry.register("upscale", (ctx) => createComfyUpscaleEngine(ctx));
// Queue and Job Service
const queue = new Queue({
concurrencyByGroup: {
codex: config.CODEX_CONCURRENCY,
"comfy-upscale": config.UPSCALE_CONCURRENCY,
},
maxQueuedJobs: config.MAX_QUEUED_JOBS,
jobStore,
engineRegistry,
engineContext,
events,
logger,
});
const jobService = new JobService({
store: jobStore,
queue,
dataDir: config.DATA_DIR,
logger,
});
// Start background queue workers
queue.start();
// Create Express App
const app = createApp({
config,
logger,
sessionStore,
passwordHash,
historyService,
jobService,
comfyClient,
comfyRuntime,
events,
});
// Bind to port
let server = null;
let boundPort = null;
for (const candidate of config.PORT_CANDIDATES) {
try {
server = await listenOnPort(app, candidate);
boundPort = candidate;
break;
} catch (err) {
if (err.code === "EADDRINUSE") {
logger.warn(`Порт ${candidate} занят, пробуем следующий...`);
continue;
}
throw err;
}
}
if (!server) {
throw new Error(
`Не удалось запустить сервер: все порты [${config.PORT_CANDIDATES.join(", ")}] заняты.`
);
}
logger.info(`Сервер «Кадр» слушает http://localhost:${boundPort}`, {
port: boundPort,
dataDir: config.DATA_DIR,
historyDir: config.HISTORY_DIR,
});
// Graceful shutdown
const shutdown = async (signal) => {
logger.info(`Получен сигнал ${signal}, корректное завершение работы...`);
try {
await queue.stop();
sessionStore.close();
jobStore.close();
server.close(() => {
logger.info("Сервер остановлен.");
process.exit(0);
});
} catch (err) {
logger.error("Ошибка при остановке сервера", { error: err.message });
process.exit(1);
}
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
return { app, server, boundPort, deps: engineContext };
}
if (require.main === module) {
startServer().catch((err) => {
console.error("Критическая ошибка при старте сервера:", err);
process.exit(1);
});
}
module.exports = {
startServer,
};