"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, };