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:
222
src/server.js
Normal file
222
src/server.js
Normal file
@@ -0,0 +1,222 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user