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:
97
docs/architecture/system-design-baseline.md
Normal file
97
docs/architecture/system-design-baseline.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# System Design Baseline
|
||||
|
||||
**Вердикт:** READY
|
||||
**Артефакт:** `docs/architecture/system-design-baseline.md`
|
||||
**Статус:** 1.0, подтверждён по рабочей копии `main@1c36a21` (рабочее дерево чистое), дата наблюдения 2026-08-26.
|
||||
**Язык:** русский (проект и владелец русскоязычные).
|
||||
|
||||
Этот документ — источник истины по **архитектуро-формирующим требованиям и ограничениям** проекта «Кадр». Он не описывает решение (см. `docs/architecture/target-design.md`). Каждый материал параметр имеет владельца, источник, дату и триггер пересмотра.
|
||||
|
||||
---
|
||||
|
||||
## 1. Идентичность и бизнес-контекст
|
||||
|
||||
| Параметр | Значение | Источник |
|
||||
|---|---|---|
|
||||
| Продукт | «Кадр» — локальный AI-фоторедактор: редактирование фотографии по текстовому промпту (Codex CLI) + 4× апскейл (ComfyUI RealESRGAN) | `README.md` |
|
||||
| Владелец/оператор | Один человек («владелец»), доступ только ему | `README.md` разд. «Авторизация» |
|
||||
| Развёртывание | Один контейнер Docker на хосте Portainer (`kadr-photo-editor`, порт 3000), LAN | `deploy/docker-compose.yml`, `README.md` |
|
||||
| Окружение | Node.js 20+, Express 4, Codex CLI (OpenAI), ComfyUI `http://192.168.31.240:8188`, прокси `http://192.168.31.240:4067` для исходящего к OpenAI | `package.json`, `server.js`, `deploy/docker-compose.yml` |
|
||||
| Читатели документа | Владелец; ИИ-агент, реализующий целевую архитектуру | — |
|
||||
|
||||
Критические сценарии (джорни):
|
||||
1. Владелец загружает фото → видит превью → редактирует промптом (1–3 мин, фон) → результат в истории.
|
||||
2. Владелец увеличивает разрешение версии (несколько секунд) → результат в истории.
|
||||
3. Владелец возвращается к любой версии истории (дерево), продолжает правки с неё.
|
||||
4. Владелец удаляет версию каскадно (с потомками).
|
||||
5. Владелец получает подсказки промптов (из истории / от Codex).
|
||||
|
||||
Горизонт решений: **один пользователь, LAN, один хост, один контейнер**, горизонт планирования 6–12 месяцев. Выход за эти рамки — триггер пересмотра (см. реестр).
|
||||
|
||||
---
|
||||
|
||||
## 2. Область и не-цели
|
||||
|
||||
**В области:** редактирование по промпту, апскейл 4×, история версий с деревом и каскадным удалением, авторизация одного владельца, референс с областью, подсказки промптов, мультизагрузка, деплой Docker/Portainer, CI на Gitea Actions.
|
||||
|
||||
**Вне области (не-цели):** многопользовательность; публичный интернет-доступ; мобильные приложения; пакетная обработка; генерация изображений «с нуля»; онлайн-редактор с кистями/слоями; платёжные интеграции; облачный хостинг. Всё перечисленное — кандидаты на отдельное решение, не для текущего горизонта.
|
||||
|
||||
---
|
||||
|
||||
## 3. Драйверы и ограничения (реестр)
|
||||
|
||||
Легенда: **Крит.** — DRIVER / SUPPORTING / INFORMATIONAL; **Статус** — CONFIRMED (из кода/документов), ASSUMED, UNKNOWN.
|
||||
|
||||
| # | Тема | Параметр | Крит. | Статус | Значение/мера | Источник и владелец | Триггер пересмотра |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| D1 | Бизнес | Один владелец, закрытый доступ | DRIVER | CONFIRMED | Ровно один логин (`APP_USER`/`APP_PASSWORD`), cookie-сессия `kadr_session`, 7 суток, sliding | `server.js:119-218`, `README.md` | Появление второго пользователя |
|
||||
| D2 | Бизнес | Редактирование по промпту — ядро продукта | DRIVER | CONFIRMED | Обработка 60–82 с на 512×512, 1–3 мин на больших; узкое место — вызов image-модели | `README.md` разд. «Тест скорости» | Смена движка редактирования |
|
||||
| D3 | Бизнес | История версий — непрерывная ветка правок | DRIVER | CONFIRMED | Дерево по `parentId`, до 50 версий, каскадное удаление, переживает перезапуск | `server.js:820-900, 1765-1826`, `README.md` | Рост лимита истории |
|
||||
| D4 | Demand | Нагрузка | SUPPORTING | CONFIRMED | Один пользователь, 1 редактирование + 1 апскейл одновременно; опрос статуса каждые 3 с; лимит аплоада 25 МБ, промпт ≤ 1000 симв. | `server.js:24, 1876`, `README.md` | Параллельные правки разных фотографий |
|
||||
| D5 | Demand | Объём данных | SUPPORTING | CONFIRMED | ≤ 50 версий × ≤ 25 МБ исходник; реальные файлы 1–6 МБ | `history/history.json`, `server.js:34` | Лимит 50 перестаёт устраивать |
|
||||
| D6 | Качество | Время ответа API | SUPPORTING | CONFIRMED | `POST /api/edit`, `/api/upscale`, `/api/suggest` отвечают мгновенно (< 1 с) с `jobId`; тяжёлая работа фоном | `README.md` стр. 25-29 | — |
|
||||
| D7 | Качество | Завершение редактирования | DRIVER | CONFIRMED | Codex: таймаут 420 с; ComfyUI: таймаут 300 с; сбои отображаются в UI, задание помечается `error` | `server.js:22, 29, 465-541, 1489-1534` | — |
|
||||
| D8 | Данные | Переживание перезапуска | DRIVER | CONFIRMED | История переживает перезапуск; **сессии и задания — нет** (память, `sessions`/`jobs` Map); `.codex-jobs` очищается при старте | `server.js:141-153, 2259`, `README.md` стр. 98 | Цель перепроектирования: переживание заданий |
|
||||
| D9 | Данные | Согласованность истории | DRIVER | CONFIRMED | Один писатель (`historyWriteChain`), полная перезапись `history.json` при каждом изменении, файлы `history/<uuid>.<ext>` | `server.js:830-868` | Переход на SQLite при > 500 версий |
|
||||
| D10 | Безопасность | Аутентификация | DRIVER | CONFIRMED | Сравнение пароля — `constantTimeEqual` по **открытому** значению env-переменной; без хеширования, без rate-limit на вход | `server.js:155-159, 1641` | Редизайн: хешировать (scrypt) |
|
||||
| D11 | Безопасность | Уязвимости | SUPPORTING | CONFIRMED | Только UUID в путях (`JOB_ID_PATTERN`); MIME+расширение проверяются, но без проверки сигнатуры файла; SameSite=Lax; без Secure (HTTP LAN); без Origin-проверки | `server.js:234-267, 2116` | Появление внешнего доступа |
|
||||
| D12 | Операции | Деплой | SUPPORTING | CONFIRMED | Образ `node:22-bookworm-slim` (glibc обязателен для Codex); том `kadr-history`; секреты `APP_USER`/`APP_PASSWORD` из Gitea; Codex через прокси `:4067` | `Dockerfile`, `deploy/docker-compose.yml`, `.gitea/workflows/*.yml` | Смена хостинга |
|
||||
| D13 | Операции | Наблюдаемость | SUPPORTING | CONFIRMED | Только лог в stdout (уровни debug/info/warn/error), без requestId, без метрик | `server.js:50-79`, `README.md` разд. «Логирование» | — |
|
||||
| D14 | Экономика | Стоимость | INFORMATIONAL | CONFIRMED | Стоимость = токены Codex (ChatGPT API), редактирование ~15–52k токенов/операция | `README.md` разд. «Тест скорости» | Смена тарифа |
|
||||
| D15 | Эволюция | Скорость редактирования | SUPPORTING | CONFIRMED | Время растёт с разрешением исходника (1200×800 медленнее 512×512); замерено: `CODEX_REASONING_EFFORT=low` даёт ~17% | `README.md` разд. «Тест скорости» | Внедрение предварительного даунскейла |
|
||||
| D16 | Ограничение | Внешние сервисы | DRIVER | CONFIRMED | ComfyUI API (upload/workflow/history/view), модель `sdxl_turbo.safetensors`, upscale `RealESRGAN_x4plus.safetensors`; Codex CLI `codex exec` с флагами `-i`, `-o last_message.txt`, `--ephemeral`, bypass sandbox | `server.js:15-17, 397-411`, `README.md` | Обновление ComfyUI/Codex |
|
||||
| D17 | Ограничение | Исходящий egress | DRIVER | CONFIRMED | Прямой выход к OpenAI блокируется (403 cf-ray HEL); обязателен прокси `http://192.168.31.240:4067`, `NO_PROXY` для внутренних адресов | `deploy/docker-compose.yml:20-27`, `README.md` п.5 разд. Portainer | Смена сети/провайдера |
|
||||
|
||||
---
|
||||
|
||||
## 4. Ключевые сценарии (source/stimulus/environment/artifact/response/measure)
|
||||
|
||||
1. **Редактирование с сохранением в историю.** Источник: владелец. Стимул: `POST /api/edit` с фото/`sourceId` + промпт. Окружение: один контейнер, Codex доступен. Артефакт: сервис `/api/edit`. Реакция: ответ < 1 с с `jobId`; ≤ 420 с — новая версия `kind=edit` в истории; мера: версия видна в `GET /api/history` и по `job.status=done`.
|
||||
2. **Апскейл.** Стимул: `POST /api/upscale`. Реакция: ≤ 300 с — версия `kind=upscale` (PNG), исходник масштабируется до ≤ 2048 px до подачи в ComfyUI. Мера: `job.status=done`.
|
||||
3. **Переживание перезапуска (цель).** Стимул: перезапуск контейнера во время `running`-задания. Реакция (целевая): задание помечается прерванным с понятной ошибкой; очередь-задания и сессии переживают рестарт; история не теряется. Мера: после рестарта `GET /api/jobs/:id/status` возвращает `interrupted`/`error`, а не 404; сессия владельца жива.
|
||||
4. **Безопасный вход.** Стимул: 10 неудачных попыток логина за 15 мин с одного IP. Реакция: блокировка с `Retry-After`; пароль не хранится в открытом виде. Мера: rate-limit срабатывает, в логах нет паролей.
|
||||
|
||||
---
|
||||
|
||||
## 5. Допущения и неизвестные
|
||||
|
||||
- **ASSUMED:** ComfyUI и Codex доступны из сети владельца (рабочее состояние подтверждается историей и `.bench/`).
|
||||
- **ASSUMED:** целевой читатель (ИИ-агент) реализует сервер на Node.js/Express и фронтенд без сборки (vanilla ES-модули) — см. решения в target-design.md.
|
||||
- **UNKNOWN:** точные объёмы будущего использования (нет метрик) — не влияет: горизонтальный запас огромен для одного пользователя.
|
||||
- **UNKNOWN:** SLA внешних сервисов (Codex/ComfyUI) — обрабатываются таймаутами и понятными ошибками.
|
||||
|
||||
## 6. Триггеры пересмотра baseline
|
||||
|
||||
- Второй пользователь или внешний доступ (D1, D11).
|
||||
- Лимит истории 50 или объём > 500 версий (D3, D5, D9).
|
||||
- Параллельные правки разных фотографий (D4).
|
||||
- Смена движка редактирования (D2, D16).
|
||||
- Смена хостинга/сети (D12, D17).
|
||||
- Переход на SQLite или другое хранилище (D9).
|
||||
|
||||
---
|
||||
|
||||
# System Design Baseline
|
||||
|
||||
**Checklist: 5/5 complete**<br>
|
||||
**Incomplete: None** — все пункты чек-листа (объём и назначение; реестр доказательств; драйверы; написание; валидация) выполнены; изменены только документы `docs/architecture/*.md`, код не трогался.
|
||||
676
docs/architecture/target-design.md
Normal file
676
docs/architecture/target-design.md
Normal file
@@ -0,0 +1,676 @@
|
||||
# Системный дизайн: «Кадр» v2 — целевая архитектура
|
||||
|
||||
**Вердикт:** READY
|
||||
**Артефакт:** `docs/architecture/target-design.md`
|
||||
**Статус:** 1.0, готов к планированию реализации
|
||||
**Язык:** русский; идентификаторы кода — английские.
|
||||
|
||||
Этот документ — полный проект перепроектирования «Кадр» (локальный AI-фоторедактор). Он написан так, чтобы **человек** мог понять решение, а **ИИ-агент** — реализовать его без дополнительных уточнений: здесь зафиксированы границы, контракты, структура модулей, данные, поведение при сбоях, решения с альтернативами, порядок реализации и критерии приёмки каждой фазы.
|
||||
|
||||
Сопутствующие артефакты: `docs/architecture/system-design-baseline.md` (драйверы и ограничения, реестр D1–D17), `README.md` (текущая документация).
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и цель перепроектирования
|
||||
|
||||
«Кадр» — веб-приложение одного владельца в LAN: редактирование фотографий текстовым промптом (Codex CLI) и 4× апскейл (ComfyUI), с деревом версий, референсом и областью, подсказками промптов. Развёрнуто одним контейнером Docker на Portainer.
|
||||
|
||||
Текущая реализация — рабочий прототип: один файл `server.js` на ~2290 строк и `public/app.js` на ~1523 строки. Она выполняет все функции, но имеет структурные дефекты (см. §2), которые делают её хрупкой, трудной для доработки и для безопасного изменения ИИ-агентом.
|
||||
|
||||
**Цель перепроектирования:** превратить прототип в **модульный монолит** с чёткими границами, устойчивый к перезапуску, безопасный, тестируемый, с теми же внешними зависимостями (Codex CLI, ComfyUI) и той же моделью развёртывания. Никаких новых сервисов, микросервисов и внешних хранилищ.
|
||||
|
||||
**Не-цели:** многопользовательность, интернет-доступ, смена стека фронтенда на сборку (React/Vue + bundler), SQLite, генеративные функции «с нуля». Обоснование — в §8 «Решения».
|
||||
|
||||
---
|
||||
|
||||
## 2. Текущее состояние: что ломается и что сохраняем
|
||||
|
||||
Оценка основана на коде (`server.js`, `public/app.js`, `history/history.json`, `Dockerfile`, `deploy/docker-compose.yml`, `.gitea/workflows/*.yml`).
|
||||
|
||||
### 2.1 Дефекты, которые устраняет v2
|
||||
|
||||
| # | Проблема | Где | Последствие | Решение в v2 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | Монолит в одном файле: auth, история, задания, Codex, ComfyUI, парсеры изображений, роуты | `server.js` (2288 стр.) | Тяжело менять, тестировать, навигировать | Модульная структура `src/` (§4) |
|
||||
| P2 | Задания в памяти: рестарт убивает `running` и теряет `queued`; `.codex-jobs` стирается при старте | `server.js:141-153, 2259` | Пропадает редактирование на 1–3 мин при рестарте | Персистентный job-store + журнал (§6.2), восстановление при старте |
|
||||
| P3 | Глобальный single-flight `codexEditInFlight` → 429, а не очередь; suggest делит флаг с edit | `server.js:91, 1880, 2031` | Нельзя поставить в очередь две правки; UX «дождитесь завершения» | Очередь с персистентностью и кониурентностью по группам (§6.1) |
|
||||
| P4 | Сессии в памяти; пароль сравнивается с открытым env-значением; нет rate-limit на вход | `server.js:141, 155-159, 1641` | Разлогин при рестарте; открытый пароль в сравнении; перебор пароля | scrypt-хеш в памяти + persistent session store + rate-limit (§7) |
|
||||
| P5 | Валидация аплоада только по MIME/расширению | `server.js:244-267` | Поддельный MIME проходит | Проверка сигнатуры (magic bytes) (§7.3) |
|
||||
| P6 | `history.json` переписывается целиком; запись без tmp+rename (частичная запись при крахе) | `server.js:830-868` | Риск порчи манифеста при обрыве | Атомарная запись (tmp+rename+fsync) в history-store (§5.1) |
|
||||
| P7 | URL версии жёстко `/image.jpg` независимо от реального расширения | `server.js:895` | Ложный URL для PNG | URL формируется из фактического `ext` (§5.1) |
|
||||
| P8 | Фронтенд: один файл, глобальные переменные, опрос каждые 3 с | `public/app.js` (1523 стр.) | Трудно поддерживать; задержка обновлений до 3 с | ES-модули (§9) + SSE с фолбэком на опрос (§6.4) |
|
||||
| P9 | Нет тестов, нет CI-проверок; `package.json` только `start` | `package.json` | Регрессии незаметны | `node:test` + supertest + Gitea workflow `test` (§10) |
|
||||
| P10 | Магические константы и env разбросаны по файлу | `server.js:15-47` | Ошибки конфигурации | Единый `config.js` с валидацией (§4.3) |
|
||||
| P11 | Нет requestId/корреляции в логах; нет метрик | `server.js:50-79` | Трудно разбирать инциденты | requestId + структурированные логи + счётчики (§11) |
|
||||
| P12 | Нет предварительного даунскейла для Codex (замер: 1200×800 медленнее 512×512) | `README.md` разд. «Тест скорости» | Дорогие и медленные правки больших фото | Даунскейл `sharp` до `EDIT_MAX_DIMENSION` (§6.5) |
|
||||
|
||||
### 2.2 Что сохраняем без изменений (проверено, работает)
|
||||
|
||||
- Внешние зависимости: Codex CLI (`codex exec`, флаги `-i/-o/--ephemeral/--skip-git-repo-check/--dangerously-bypass-approvals-and-sandbox`), ComfyUI REST (`/object_info`, `/upload/image`, `/prompt`, `/history/{id}`, `/view`), модели `sdxl_turbo.safetensors`, `RealESRGAN_x4plus.safetensors`.
|
||||
- Модель развёртывания: Docker (`node:22-bookworm-slim`), Portainer stack, Gitea Actions build/deploy, прокси `:4067` для egress к OpenAI, тома.
|
||||
- HTTP-контракт публичных эндпоинтов `/api/health`, `/api/login`, `/api/me`, `/api/logout` и семантика основных бизнес-эндпоинтов (см. §7.1 — изменения только аддитивные).
|
||||
- Формат версии истории (поля `id, kind, label, prompt, parentId, referenceId, createdAt, url, width, height, ext`) — обратная совместимость с существующим `history/history.json` и файлами.
|
||||
- UI/UX: русский интерфейс, дерево истории, референс с областью, подсказки, «до/после».
|
||||
- Парсинг размеров PNG/JPEG/WebP из заголовков (без библиотек) — переносится в модуль `images/dimensions.js` как есть.
|
||||
|
||||
---
|
||||
|
||||
## 3. Оценка нагрузки (до выбора компонентов)
|
||||
|
||||
Расчёт для одного владельца в LAN. Никакие компоненты не выбираются «на вырост»; механизмы масштабирования перечислены как отложенные (§8, триггеры).
|
||||
|
||||
| Величина | Значение | Обоснование |
|
||||
|---|---|---|
|
||||
| Пользователей | 1 | D1 |
|
||||
| Правок в сутки | 0–20 (пик) | Оценка по `history.json` (12 версий за 2 дня активного тестирования) |
|
||||
| Апскейлов в сутки | 0–20 | Аналогично |
|
||||
| Параллельность | 1 edit/suggest + ≤ 2 upscale | Текущее узкое место — Codex (один поток); ComfyUI справляется с 1–2 воркфлоу |
|
||||
| Размер загрузки | ≤ 25 МБ, типично 1–6 МБ | `server.js:24`, фактические файлы |
|
||||
| История | ≤ 50 версий, манифест ~10 КБ | `server.js:34` |
|
||||
| Частота опроса статуса | 3 с (фронт) → 0 с при SSE | `README.md` |
|
||||
| Очередь | ≤ 5 заданий в очереди | Лимит для защиты от «накликивания»; дешевле, чем бесконечная очередь |
|
||||
|
||||
**Формула для оценки времени:** `t_edit ≈ 60–90 с × (maxDim/512)^1.3` (эмпирика из README: 512² ≈ 60–82 с, 1200×800 заметно дольше). Даунскейл до 1024 px сокращает среднее время до ~1–1.5 мин на типичных фото 4000×3000.
|
||||
|
||||
**Первый вероятный бутылочный горлышек:** одиночный поток Codex (один `codex exec` на машине). Отложенный механизм: кониурентность > 1 для Codex при появлении второго ключа/аккаунта — не требуется сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 4. Целевая структура: модульный монолит
|
||||
|
||||
### 4.1 Дерево сервера
|
||||
|
||||
```
|
||||
server.js # Тонкий bootstrap-загрузчик: `require("./src/server")` (совместимость с Docker CMD и `npm start`)
|
||||
src/
|
||||
server.js # Реальная точка входа: loadConfig → createApp → listen (см. §13, Фаза 0)
|
||||
config.js # Единый источник конфигурации (env → объект, валидация)
|
||||
logger.js # Структурированный лог (JSON-lines), уровни, requestId
|
||||
errors.js # Иерархия ошибок: AppError(status, message, details)
|
||||
http/
|
||||
app.js # createApp(deps) -> express app (DI для тестов)
|
||||
middleware/
|
||||
request-context.js # requestId, тайминги, лог запросов
|
||||
auth.js # requireAuth (cookie-сессия, sliding TTL)
|
||||
origin-check.js # CSRF-защита: Origin против Host на state-changing запросах
|
||||
rate-limit.js # Окно попыток входа на IP
|
||||
error-handler.js # Единый формат ошибок { ok:false, error, details, requestId }
|
||||
routes/
|
||||
auth.routes.js # POST /api/login, GET /api/me, POST /api/logout
|
||||
health.routes.js # GET /api/health
|
||||
history.routes.js # GET /api/history, POST /api/history/import, DELETE /api/history/:id, GET /api/history/:id/image.{ext}
|
||||
preview.routes.js # POST /api/preview
|
||||
edit.routes.js # POST /api/edit
|
||||
upscale.routes.js # POST /api/upscale
|
||||
suggest.routes.js # GET /api/suggest-prompts, POST /api/suggest
|
||||
jobs.routes.js # GET /api/jobs, GET /api/jobs/:id/status, GET /api/jobs/:id/output.jpg
|
||||
events.routes.js # GET /api/events (SSE)
|
||||
result.routes.js # GET /api/result (прокси к ComfyUI /view)
|
||||
core/
|
||||
auth/
|
||||
password.js # scrypt hash/verify (timing-safe)
|
||||
session-store.js # Персистентный store сессий (JSON-файл, атомарная запись, TTL-sweep)
|
||||
history/
|
||||
history-store.js # Манифест + файлы изображений; атомарные операции
|
||||
history-service.js # Бизнес-операции: импорт, каскадное удаление, прунинг, чтение файла
|
||||
jobs/
|
||||
job-store.js # Персистентный журнал заданий (JSONL) + индекс в памяти
|
||||
queue.js # FIFO-очередь с кониурентностью по группам, восстановлением
|
||||
job-service.js # Создание задания, статусы, эмиссия событий
|
||||
engines/
|
||||
engine.js # Контракт движка + реестр (тип -> фабрика)
|
||||
codex-engine.js # Редактирование и подсказки через Codex CLI
|
||||
comfy-upscale-engine.js # Апскейл через ComfyUI
|
||||
comfy/
|
||||
comfy-client.js # REST-клиент: upload, submit, poll history, download view
|
||||
comfy-runtime.js # Кэш /object_info (30 с), выбор моделей, requireNodes
|
||||
workflows.js # buildUpscaleWorkflow (перенос), будущие воркфлоу
|
||||
codex/
|
||||
cli.js # Обёртка spawn `codex exec`: запуск, ожидание output.jpg/last_message.txt, таймаут
|
||||
prompts.js # buildEditPrompt, buildSuggestPrompt, parseSuggestions, scaleRegion
|
||||
images/
|
||||
dimensions.js # getImageDimensions (PNG/JPEG/WebP) — перенос без изменений
|
||||
heic.js # convertHeicToJpeg (перенос)
|
||||
resize.js # Даунскейл через sharp (только для edit-пути)
|
||||
data/ # Runtime-данные (gitignored, Docker volume /app/data)
|
||||
history.json # Манифест версий
|
||||
sessions.json # Сессии (если SESSION_PERSIST=true)
|
||||
jobs.jsonl # Журнал заданий
|
||||
test/ # node:test + supertest
|
||||
config.test.js logger.test.js images.test.js
|
||||
history-store.test.js job-queue.test.js prompts.test.js
|
||||
api.test.js
|
||||
```
|
||||
|
||||
### 4.2 Правила зависимостей (обязательны для реализации)
|
||||
|
||||
1. **Слои:** `routes → core/* → (engines | comfy | codex | images)`. Роуты не содержат бизнес-логики — только парсинг запроса и вызов сервиса.
|
||||
2. `core` не импортирует `express` (кроме `http/app.js` и middleware). Движки и сервисы получают зависимости через конструктор/DI.
|
||||
3. Никто не читает `process.env` напрямую, кроме `config.js`. Все константы — из `config`.
|
||||
4. `engines/*` не знают про HTTP. Возвращают `EngineResult` (см. §6.3).
|
||||
5. `queue.js` не знает про движки — получает фабрику по типу задания из реестра.
|
||||
6. Файловая система — только через выделенные модули-хранилища (`history-store`, `session-store`, `job-store`, `codex/cli`); случайные `fs.*` в роутах запрещены.
|
||||
|
||||
### 4.3 Конфигурация (`src/config.js`)
|
||||
|
||||
Функция `loadConfig(env = process.env)` → замороженный объект; бросает `ConfigError` со списком проблем при старте. Все значения — с дефолтами (совместимы с текущими):
|
||||
|
||||
| Ключ env | Тип | Дефолт | Комментарий |
|
||||
|---|---|---|---|
|
||||
| `PORT` | number | 3000 | Единый порт (убрать каскад 8080/8090? — оставить как `PORT_CANDIDATES` массив `[3000, 8080, 8090]` для совместимости, управляется `PORT` при задании) |
|
||||
| `COMFY_URL` | url | `http://192.168.31.240:8188` | Валидируется как URL |
|
||||
| `APP_USER` | string | `admin` | |
|
||||
| `APP_PASSWORD` | string | генерируется при старте (баннер в лог, как сейчас) | Никогда не логируется |
|
||||
| `LOG_LEVEL` | debug/info/warn/error | info | |
|
||||
| `DATA_DIR` | path | `./data` | Хранилища сессий и заданий |
|
||||
| `HISTORY_LIMIT` | number | 50 | |
|
||||
| `MAX_UPLOAD_BYTES` | number | 25 МБ | |
|
||||
| `EDIT_MAX_DIMENSION` | number | 1024 | Даунскейл перед Codex; `0` = отключить |
|
||||
| `UPSCALE_MAX_DIMENSION` | number | 2048 | Существующий предел |
|
||||
| `CODEX_CLI_PATH` | string | авто-поиск (`resolveCodexExe` переносится) | |
|
||||
| `CODEX_MODEL`, `CODEX_REASONING_EFFORT` | string | из `~/.codex/config.toml` | Передаются в `codex exec` как временный config или аргументы (как сегодня) |
|
||||
| `CODEX_EDIT_TIMEOUT_MS` | number | 420000 | |
|
||||
| `JOB_TIMEOUT_MS` | number | 300000 | Таймаут ComfyUI |
|
||||
| `REQUEST_TIMEOUT_MS` | number | 15000 | |
|
||||
| `SESSION_TTL_MS` | number | 7 суток | |
|
||||
| `SESSION_PERSIST` | bool | true | Переживание входа через рестарт |
|
||||
| `LOGIN_RATE_LIMIT` | object | `{ windowMs: 15*60*1000, max: 5 }` | |
|
||||
| `CODEX_CONCURRENCY` | number | 1 | Потоков Codex |
|
||||
| `UPSCALE_CONCURRENCY` | number | 2 | Потоков ComfyUI-апскейла |
|
||||
| `MAX_QUEUED_JOBS` | number | 5 | Предел очереди на тип |
|
||||
| `SSE_ENABLED` | bool | true | |
|
||||
| `TRUST_PROXY` | bool | false | Для rate-limit по IP за прокси |
|
||||
|
||||
При старте в лог пишется **редактируемый** снимок конфигурации (без `APP_PASSWORD`, без путей к токенам).
|
||||
|
||||
---
|
||||
|
||||
## 5. Данные и хранилища
|
||||
|
||||
### 5.1 История версий (`core/history`)
|
||||
|
||||
Формат манифеста — **без изменений** (обратная совместимость с текущим `history/history.json`): массив объектов
|
||||
|
||||
```js
|
||||
{
|
||||
id: string, // uuid v4
|
||||
kind: 'original'|'edit'|'upscale',
|
||||
label: string,
|
||||
prompt: string|null,
|
||||
parentId: string|null,
|
||||
referenceId: string|null,
|
||||
createdAt: ISO string,
|
||||
url: `/api/history/${id}/image.${ext}`, // P7: фактическое расширение
|
||||
width: number|null,
|
||||
height: number|null,
|
||||
ext: 'jpg'|'png'|'webp'
|
||||
}
|
||||
```
|
||||
|
||||
Файлы изображений: `data/history/<id>.<ext>` — **важно**: каталог переносится из `history/` в `data/history/` (том `kadr-data` вместо `kadr-history`; миграция описана в §12). Пока не мигрирован — `HISTORY_DIR` конфигурируется и по умолчанию остаётся `./history`; v2 пишет в `DATA_DIR/history`. Выбираем: **`HISTORY_DIR = path.join(DATA_DIR, 'history')`**, миграция при старте: если существует старый `./history/history.json`, а нового нет — перенести файлы (см. §12).
|
||||
|
||||
Интерфейс `history-store`:
|
||||
|
||||
```js
|
||||
class HistoryStore {
|
||||
constructor({ dir, limit, logger })
|
||||
load(): Version[] // чтение манифеста (пусто при отсутствии)
|
||||
async append(version): Promise<void> // атомарно: tmp+rename+fsync; прунинг > limit (удаление файлов вытесненных)
|
||||
async removeCascade(id): Promise<{ removed: Version[] }> // BFS по parentId, удаление файлов
|
||||
get(id): Version | null
|
||||
filePath(version): string
|
||||
}
|
||||
```
|
||||
|
||||
Все мутации сериализуются цепочкой (как текущий `historyWriteChain`) — **сохраняем** этот паттерн, вынося его в store. Атомарность: писать в `<manifest>.tmp`, `fsync`, `rename`, `fsync` директории.
|
||||
|
||||
### 5.2 Сессии (`core/auth/session-store`)
|
||||
|
||||
`SessionStore` — Map + персистентный JSON-файл `data/sessions.json`:
|
||||
|
||||
```js
|
||||
class SessionStore {
|
||||
constructor({ file, ttlMs, persist, logger })
|
||||
create(user): token
|
||||
get(token): { user, expiresAt } | null // sliding: обновляет expiresAt
|
||||
delete(token): void
|
||||
sweep(): void // периодическая чистка TTL
|
||||
_persist(): void // атомарная запись (debounce 500 мс)
|
||||
}
|
||||
```
|
||||
|
||||
Токен — 32 байта hex (как сейчас). В файле хранятся хеши токенов (`sha256(token)`), чтобы кража файла не давала прямых сессий. При `SESSION_PERSIST=false` — чисто in-memory (поведение v1).
|
||||
|
||||
### 5.3 Задания (`core/jobs/job-store`)
|
||||
|
||||
Персистентность: **append-only JSONL-журнал** `data/jobs.jsonl` + индекс в памяти. Каждая строка — событие перехода:
|
||||
|
||||
```jsonl
|
||||
{"seq":1,"jobId":"...","at":"2026-08-26T06:00:00.000Z","event":"created","job":{...полный объект...}}
|
||||
{"seq":2,"jobId":"...","at":"...","event":"status","status":"running","startedAt":"..."}
|
||||
{"seq":3,"jobId":"...","at":"...","event":"status","status":"done","result":{...},"finishedAt":"..."}
|
||||
```
|
||||
|
||||
- При старте: журнал читается, для каждого `jobId` восстанавливается последнее состояние (события применяются по порядку). Схемы строк журнала фиксируются JSDoc-типами (`JobCreatedEvent`, `JobStatusEvent`) в `core/jobs/job-store.js`, чтобы валидация журнала при старте была тривиальной (невалидная строка → warn + пропуск).
|
||||
- `status=running` на момент старта → `interrupted` (см. §6.1 восстановление).
|
||||
- Периодическая компактизация: каждые 6 ч журнал переписывается, оставляя только задания моложе 1 ч после `finishedAt` (терминальные задания живут в индексе 1 ч, как сейчас `server.js:146-153`).
|
||||
- Формат задания:
|
||||
|
||||
```js
|
||||
{
|
||||
id: string, // uuid
|
||||
type: 'edit'|'upscale'|'suggest',
|
||||
status: 'queued'|'running'|'done'|'error'|'interrupted',
|
||||
createdAt: ISO, startedAt: ISO|null, finishedAt: ISO|null,
|
||||
payload: object, // зависит от типа (§7.1)
|
||||
result: object|null, // { version?, summary?, ... }
|
||||
error: string|null, details: any|null,
|
||||
attempts: number, // для восстановления
|
||||
engine: 'codex'|'comfy-upscale' // производная от type
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Что не храним
|
||||
|
||||
- Исходные файлы заданий в `.codex-jobs` **не персистим как данные** — это рабочий каталог движка; очистка: при старте удалять каталоги старше 24 ч (а не все, как сейчас).
|
||||
|
||||
---
|
||||
|
||||
## 6. Очередь, жизненный цикл заданий, восстановление, события
|
||||
|
||||
### 6.1 Очередь (`core/jobs/queue.js`)
|
||||
|
||||
FIFO по `createdAt`; кониурентность по группам движков:
|
||||
|
||||
| Группа | Типы заданий | Кониурентность | Обоснование |
|
||||
|---|---|---|---|
|
||||
| `codex` | edit, suggest | `CODEX_CONCURRENCY` (1) | Один Codex-процесс на машине; узкое место — image-модель |
|
||||
| `comfy-upscale` | upscale | `UPSCALE_CONCURRENCY` (2) | ComfyUI выдерживает 1–2 воркфлоу; апскейлы быстрые |
|
||||
|
||||
Интерфейс:
|
||||
|
||||
```js
|
||||
class Queue {
|
||||
constructor({ concurrencyByGroup, jobStore, engineRegistry, logger, events })
|
||||
async enqueue(job): Promise<{ job, queuePosition }> // 429, если MAX_QUEUED_JOBS достигнут
|
||||
start(): void // запуск воркеров (при старте сервера)
|
||||
stop(): void // graceful shutdown: дождаться running, queued остаются
|
||||
status(jobId): Job | null
|
||||
listActive(): Job[] // для UI-бейджа и SSE-снапшота
|
||||
}
|
||||
```
|
||||
|
||||
Жизненный цикл (state machine):
|
||||
|
||||
```
|
||||
queued ──worker взял──▶ running ──успех──▶ done
|
||||
│
|
||||
├──ошибка──▶ error
|
||||
└──рестарт──▶ interrupted (attempts+1; если attempts < 1 — снова queued, иначе error «прервано перезапуском»)
|
||||
```
|
||||
|
||||
**Политика восстановления после рестарта (решение):**
|
||||
- `queued` → остаётся в очереди, выполняется после старта (входные файлы целы, движок ещё не запускался).
|
||||
- `running` → `interrupted`; **автоматически НЕ перезапускаем** задания Codex (риск двойного списания токенов): пользователь видит ошибку «Задание прервано перезапуском сервера. Запустите заново.» с кнопкой повтора в UI. Исключение: `upscale` — безопасно перезапустить (идемпотентно), `attempts < 1` → requeue.
|
||||
- Входные файлы движка: каталог `.codex-jobs/<jobId>/` сохраняется до завершения задания; при `interrupted` кодекс-задания файлы не нужны (повтор не авто), поэтому каталог можно чистить сразу.
|
||||
|
||||
### 6.2 Контракт движка (`core/engines/engine.js`)
|
||||
|
||||
```js
|
||||
/**
|
||||
* @typedef {Object} EngineContext
|
||||
* @property {Config} config
|
||||
* @property {Logger} logger
|
||||
* @property {HistoryService} history
|
||||
* @property {ComfyClient} comfy
|
||||
* @property {CodexCli} codex
|
||||
*
|
||||
* @typedef {Object} EngineResult
|
||||
* @property {boolean} ok
|
||||
* @property {Version} [version] // созданная версия (edit/upscale)
|
||||
* @property {object} [extra] // summary, promptId, model и т.п. → в job.result
|
||||
* @property {string} [error] // сообщение для пользователя
|
||||
* @property {any} [details] // технические детали (в лог)
|
||||
*
|
||||
* Реестр: engineRegistry.register('edit', (ctx) => ({ run(job) {...} }))
|
||||
*/
|
||||
```
|
||||
|
||||
Типы движков: `codex-engine` (edit, suggest — два метода одного адаптера), `comfy-upscale-engine` (upscale).
|
||||
|
||||
### 6.3 Кодекс-движок (`core/engines/codex-engine.js` + `core/codex/*`)
|
||||
|
||||
`codex/cli.js` — перенос `runCodexEdit`/`runCodexSuggest` (spawn, ожидание `output.jpg`/`last_message.txt`, таймаут `CODEX_EDIT_TIMEOUT_MS`, обработка `exit/error`), с одним изменением: функция принимает `{ jobDir, inputNames, prompt, timeoutMs }` и возвращает `{ exitCode, timedOut, outputPath, lastMessage }`.
|
||||
|
||||
`codex/prompts.js` — перенос `buildCodexPrompt`/`buildSuggestPrompt`/`parseSuggestions` + **новая** функция:
|
||||
|
||||
```js
|
||||
// Масштабирование координат области референса после даунскейла референса.
|
||||
// region — {x,y,w,h} в пикселях ОРИГИНАЛА референса; factor = targetDim / max(origW, origH)
|
||||
function scaleRegion(region, factor): { x,y,w,h } // Math.round каждого, минимум 1px
|
||||
```
|
||||
|
||||
`codex-engine.js` (edit):
|
||||
1. По `job.payload` собрать вход: `input.<ext>` из файла задания или из версии истории (`history-service` выдаёт buffer).
|
||||
2. Даунскейл: если `EDIT_MAX_DIMENSION > 0` и `max(w,h) > EDIT_MAX_DIMENSION` → `images/resize.js` (`sharp`), входной buffer уменьшается, `scaledSize` сохраняется. Если есть референс с областью — референс тоже даунскейлится тем же фактором, `region = scaleRegion(region, factor)`; промпт получает размер **даунскейленного** изображения.
|
||||
3. Промпт: `buildEditPrompt(...)` (см. §6.5).
|
||||
4. Запуск `codex exec`, ожидание `output.jpg`.
|
||||
5. Создание версии `kind=edit`, `parentId=sourceId`, `referenceId`, размеры = даунскейленные (если был даунскейл, в `job.result.extra` пишем `downscaledFrom: {width,height}`).
|
||||
6. `EngineResult { ok, version, extra }`.
|
||||
|
||||
`suggest`: без даунскейла (текстовый вывод), перенос как есть.
|
||||
|
||||
### 6.4 Апскейл-движок (`core/engines/comfy-upscale-engine.js` + `core/comfy/*`)
|
||||
|
||||
Перенос без изменения логики: `uploadToComfy` → `buildUpscaleWorkflow` → `submitWorkflow` → `waitForResult` → `downloadComfyImage` → версия `kind=upscale`. Компоненты: `comfy-client.js` (HTTP-слой с таймаутами и AppError-обёрткой), `comfy-runtime.js` (кэш `/object_info` 30 с, `chooseUpscaleModel`, `requireNodes`), `workflows.js` (построитель графа). `GET /api/result` (прокси к `/view`) остаётся в роуте, используя `comfy-client`.
|
||||
|
||||
### 6.5 Промпт редактирования (обновлённый `buildEditPrompt`)
|
||||
|
||||
Структура совпадает с текущей (`server.js:355-395`), плюс одна строка при даунскейле:
|
||||
|
||||
```
|
||||
Перед тобой фотография input.jpg. Примени к ней следующее редактирование: «{prompt}».
|
||||
[Если референс:] Также дано вспомогательное изображение reference.jpg — тот же сюжет... Координаты области: x=..., y=..., ширина=..., высота=... (в пикселях). Примени правку именно в области...
|
||||
[Если даунскейл:] Исходное фото уменьшено для скорости обработки.
|
||||
Сохрани отредактированное фото в файл output.jpg в текущей директории.
|
||||
Не перерисовывай фото с нуля... Сохрани исходный размер {W}×{H}. // W,H — размер фактически поданного файла
|
||||
Файл output.jpg должен быть валидным JPEG. По завершении кратко опиши, что сделал.
|
||||
```
|
||||
|
||||
### 6.6 События и SSE (`core/jobs/events` + `routes/events.routes.js`)
|
||||
|
||||
`job-service` эмитит события через простой `EventEmitter`:
|
||||
|
||||
```js
|
||||
events.emit('job:updated', { jobId, status, type, payload?, error?, queuePosition? })
|
||||
events.emit('job:removed', { jobId })
|
||||
```
|
||||
|
||||
`GET /api/events` (SSE, авторизованный):
|
||||
- Заголовки: `Content-Type: text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`, `X-Accel-Buffering: no`.
|
||||
- При подключении — снапшот активных заданий (`event: snapshot`), далее `event: job` на каждое обновление; heartbeat `: ping` каждые 15 с.
|
||||
- При `close` — отписка; таймаут простоя 10 мин → закрытие.
|
||||
- Формат данных: `data: {"jobId":"...","status":"running","type":"edit"}` — только разрешённые поля, без внутренних объектов.
|
||||
|
||||
Фронтенд: `EventSource('/api/events')`; при ошибке/`onerror` — фолбэк на текущий опрос `GET /api/jobs/:id/status` каждые 3 с (поведение v1). Это делает SSE аддитивным и безопасным.
|
||||
|
||||
---
|
||||
|
||||
## 7. HTTP-контракт (целевой)
|
||||
|
||||
### 7.1 Эндпоинты
|
||||
|
||||
Все эндпоинты, кроме публичных, требуют сессию (`requireAuth`). Единый формат ошибок:
|
||||
|
||||
```json
|
||||
{ "ok": false, "error": "Текст для пользователя", "details": {}, "requestId": "req_..." }
|
||||
```
|
||||
|
||||
| Метод и путь | Публичный | Тело/параметры | Ответ (успех) | Изменения vs v1 |
|
||||
|---|---|---|---|---|
|
||||
| `GET /api/health` | да | — | `{ ok, comfyAvailable, message, selectedUpscaleModel, upscaleModels, checkpoint, checkpointAvailable, nodes, missingNodes, version }` | + `version` |
|
||||
| `POST /api/login` | да | `{ username, password }` | `{ ok, user }` + cookie | + rate-limit; сравнение по scrypt-хешу |
|
||||
| `GET /api/me` | да | — | `{ ok, user }` | как есть |
|
||||
| `POST /api/logout` | да | — | `{ ok }` | как есть |
|
||||
| `GET /api/history` | нет | — | `{ ok, versions: Version[] }` | как есть |
|
||||
| `POST /api/history/import` | нет | multipart `photo` | `{ ok, version }` | + magic-byte проверка |
|
||||
| `DELETE /api/history/:id` | нет | — | `{ ok, versions }` | как есть (каскад) |
|
||||
| `GET /api/history/:id/image.{ext}` | нет | — | изображение | URL с фактическим ext (P7); старый `/image.jpg` оставить алиасом 302 → совместимость с сохранёнными ссылками |
|
||||
| `POST /api/preview` | нет | multipart `photo` | изображение | как есть |
|
||||
| `GET /api/suggest-prompts` | нет | — | `{ ok, prompts }` | как есть |
|
||||
| `POST /api/suggest` | нет | multipart `photo` (или `sourceId`) | `{ ok, jobId, status:'queued' }` | очередь вместо 429 |
|
||||
| `POST /api/edit` | нет | multipart: `photo`\|`sourceId`, `prompt`, `reference`, `referenceId`, `region` | `{ ok, jobId, status:'queued', queuePosition }` | очередь вместо 429; даунскейл |
|
||||
| `POST /api/upscale` | нет | multipart `photo`\|`sourceId` | `{ ok, jobId, status:'queued', queuePosition }` | очередь |
|
||||
| `GET /api/jobs` | нет | — | `{ ok, jobs: JobSummary[] }` | **новый** (активные + последние терминальные) |
|
||||
| `GET /api/jobs/:id/status` | нет | — | `{ ok, status, type, queuePosition?, payload?, error?, details?, requestId }` | + `queuePosition` |
|
||||
| `GET /api/jobs/:id/output.jpg` | нет | — | JPEG | как есть |
|
||||
| `GET /api/result` | нет | `filename, subfolder, type=output` | изображение (прокси ComfyUI) | как есть |
|
||||
| `GET /api/events` | нет | — | SSE-поток | **новый** |
|
||||
|
||||
`JobSummary = { id, type, status, createdAt, startedAt?, finishedAt?, queuePosition? }` — без payload/result (для бейджа очереди).
|
||||
|
||||
### 7.2 Payload'ы заданий
|
||||
|
||||
```js
|
||||
edit: { sourceId: string|null, inputBuffer?: Buffer, inputMimetype?: string,
|
||||
prompt: string, referenceId?: string|null, region?: {x,y,w,h}|null }
|
||||
upscale: { sourceId: string|null, inputBuffer?: Buffer, inputMimetype?: string }
|
||||
suggest: { sourceId: string|null, inputBuffer?: Buffer, inputMimetype?: string }
|
||||
```
|
||||
(`sourceId` предпочтителен; если его нет — в payload кладётся buffer загруженного файла; buffer НЕ пишется в журнал — только в рабочий каталог движка.)
|
||||
|
||||
`result`:
|
||||
```js
|
||||
edit: { version: Version, summary?: string, extra?: { downscaledFrom?: {width,height}, exitCode?, timedOut? } }
|
||||
upscale: { version: Version, extra?: { promptId, model, scaledSize? } }
|
||||
suggest: { suggestions: string[] }
|
||||
```
|
||||
|
||||
### 7.3 Валидация входов
|
||||
|
||||
- **Загрузка файлов:** multer (лимиты как сейчас); `fileFilter` — по MIME **и** по сигнатуре: `sniffImageType(buffer)` определяет `jpeg|png|webp|heic` по первым байтам (JPEG `FF D8 FF`, PNG-сигнатура, `RIFF....WEBP`, HEIC `ftyp` с брендом `heic/heix/hevc/heif`). Несовпадение → 400.
|
||||
- **Промпт:** trim, 1–1000 символов.
|
||||
- **`region`:** как сейчас (`parseRegion`), дополнительно: целые ≥ 0; не больше размеров референса.
|
||||
- **UUID:** `JOB_ID_PATTERN` остаётся для всех `:id` параметров.
|
||||
- **Origin-проверка (CSRF):** middleware для не-GET `/api/*`: если заголовок `Origin` присутствует и не равен `Host` (с учётом схемы) → 403. Плюс `SameSite=Lax` (уже есть). Для LAN-инструмента этого достаточно (прокси не используется, кроме `/api/result` к ComfyUI).
|
||||
|
||||
---
|
||||
|
||||
## 8. Решения и альтернативы
|
||||
|
||||
| # | Решение | Выбрано | Альтернативы (рассмотрены) | Почему | Триггер пересмотра |
|
||||
|---|---|---|---|---|---|
|
||||
| R1 | Гранулярность | Модульный монолит | Микросервисы; один файл | Один владелец, один хост; микросервисы = жизненный цикл без выгоды | Второй хост/сервис, командная разработка |
|
||||
| R2 | Хранилище истории | JSON-манифест + файлы, атомарная запись | SQLite (better-sqlite3) | ≤ 50 версий; совместимость с существующими данными; ноль нативных зависимостей | > 500 версий, конкурентные писатели, сложные запросы дерева |
|
||||
| R3 | Хранилище заданий | JSONL-журнал + индекс в памяти | SQLite; in-memory (v1) | Транзиентные данные (1 ч); простота восстановления; нет нативных зависимостей | Нужна аналитика по истории заданий |
|
||||
| R4 | Сессии | Персистентный файл (хеши токенов), TTL | In-memory (v1) | «Переживает рестарт» — явная ценность владельца; дёшево | Мультипользователь, внешний доступ |
|
||||
| R5 | Пароль | scrypt (встроенный `crypto`) | bcrypt/argon2 (native); открытый compare (v1) | Ноль зависимостей, timing-safe, NIST-приемлемо | Требования комплаенса |
|
||||
| R6 | Даунскейл перед Codex | `sharp` | `jimp` (чистый JS); без даунскейла (v1) | Замеры README показывают выигрыш; sharp — стандарт, есть prebuilds для bookworm-slim | Проблемы сборки native → заменить на jimp |
|
||||
| R7 | Обновления статуса | SSE + фолбэк на polling | Только polling (v1); WebSocket | SSE — нулевые зависимости, работает через любой прокси; фолбэк сохраняет v1-поведение | Нужны двусторонние каналы (отмена заданий) |
|
||||
| R8 | Фронтенд | Vanilla ES-модули, без сборки | React/Vue + Vite | Ноль build-шага в Docker/CI; текущий HTML/CSS сохраняется; сложность UI невелика | Рост сложности UI (много интерактивных панелей) |
|
||||
| R9 | Очередь | FIFO + кониурентность по группам | Только single-flight (v1); полноценный worker pool | Покрывает реальный сценарий «поставить правку в очередь» без оверкилла | Параллельные правки |
|
||||
| R10 | Восстановление заданий | `queued` выполняются; `running` → `interrupted` (upscale — requeue 1 раз) | Автоперезапуск всего | Избегаем двойного списания токенов Codex; честный UX | Появление идемпотентных движков |
|
||||
| R11 | Тесты | `node:test` + supertest | Jest/Vitest | Встроено в Node 20, ноль зависимостей | Миграция на TS |
|
||||
| R12 | TypeScript | Нет (JS + JSDoc) | TS с сборкой | Текущий стек без сборки; JSDoc даёт контракты для ИИ-агента | Кодовой базы > ~10k строк |
|
||||
|
||||
---
|
||||
|
||||
## 9. Фронтенд (target)
|
||||
|
||||
### 9.1 Структура
|
||||
|
||||
```
|
||||
public/
|
||||
index.html // почти без изменений; подключение: <script type="module" src="/js/main.js"></script>
|
||||
style.css // без изменений (v1); допускается мелкая правка под новые блоки
|
||||
js/
|
||||
main.js // bootstrap: чтение DOM-элементов, инициализация модулей, подписки
|
||||
state.js // единое состояние { currentVersionId, historyVersions, collapsed, reference, busy, activeJobs }
|
||||
api.js // apiFetch(url, opts): JSON-ошибки, 401 → auth.show(), requestId
|
||||
auth.js // showAuthOverlay/hide, checkAuth, submitLogin, logout
|
||||
health.js // опрос /api/health (как сейчас, 30 с)
|
||||
upload.js // drop-zone, мультизагрузка (последовательный импорт, прогресс)
|
||||
reference.js // выбор референса (файл/история), канвас, область (перенос из app.js:1072-1290)
|
||||
suggest.js // chips из истории, «Спросить Codex», подписка на job (перенос)
|
||||
jobs.js // EventSource + фолбэк-поллинг; renderJobSuccess, ошибки
|
||||
history-tree.js // рендер дерева, свёртка групп, выбор версии, удаление (перенос из app.js:532-852)
|
||||
ui.js // мелкие утилиты: formatFileSize, versionFileName, toast
|
||||
```
|
||||
|
||||
### 9.2 Ключевые изменения поведения
|
||||
|
||||
1. **Обновления статуса:** `jobs.js` держит один `EventSource`; на `event: job` обновляет бейдж задания и завершает текущий job-цикл мгновенно (без 3-секундной задержки). При `onerror` (сеть/прокси) — фолбэк: поллинг `GET /api/jobs` каждые 3 с.
|
||||
2. **Очередь:** если ответ `POST /api/edit` = `{ status:'queued', queuePosition }` — UI показывает «В очереди, позиция N» и слушает событие до `running`/`done` (текущий код `runJob` предполагает только `running` — доработать).
|
||||
3. **Ошибка `interrupted`:** после рестарта сервера задание приходит как `interrupted` → показать сообщение и предложить повторно запустить (кнопка повторяет `POST /api/edit` с теми же параметрами, которые UI хранит в памяти сессии).
|
||||
4. **Скачивание:** `download-current` и `download-link` используют `version.url` (теперь с фактическим ext).
|
||||
|
||||
### 9.3 Состояние
|
||||
|
||||
`state.js` — простой объект + `subscribe(fn)`; модули не трогают DOM друг друга напрямую. Это минимально достаточное улучшение без библиотек.
|
||||
|
||||
---
|
||||
|
||||
## 10. Тестирование и CI
|
||||
|
||||
### 10.1 Модульные (node:test, `test/*.test.js`)
|
||||
|
||||
| Файл | Что проверяет |
|
||||
|---|---|
|
||||
| `config.test.js` | дефолты, валидация, ошибки при неверных значениях, редактирование лог-снимка |
|
||||
| `logger.test.js` | уровни, формат JSON-lines, requestId-корреляция, отсутствие пароля |
|
||||
| `images.test.js` | `getImageDimensions` (PNG/JPEG/WebP фикстуры), `sniffImageType` (magic bytes, включая HEIC `ftyp`), `scaleRegion` (округление, factor=1) |
|
||||
| `history-store.test.js` | append + прунинг + удаление файлов; атомарность (нет tmp-файлов после успеха); каскадное удаление; переживание перезаписи |
|
||||
| `job-queue.test.js` | FIFO-порядок, кониурентность по группам (fake-движок с задержками), `MAX_QUEUED_JOBS` → 429, восстановление из журнала (queued остаётся, running → interrupted, upscale requeue) |
|
||||
| `prompts.test.js` | сборка промпта (с референсом/областью/даунскейлом), `parseSuggestions` |
|
||||
| `api.test.js` (supertest) | login (успех/ошибка/rate-limit), auth-защита, import/история, edit/upscale/suggest с **stub-движками** (журнал), SSE-снапшот, ошибки `{ok:false,error,requestId}` |
|
||||
|
||||
Все тесты работают с временным `DATA_DIR` (`fs.mkdtemp`) — реальные `data/` и `history/` не трогаются.
|
||||
|
||||
### 10.2 Приёмочные проверки каждой фазы — в §13.
|
||||
|
||||
### 10.3 CI (Gitea Actions)
|
||||
|
||||
Новый workflow `.gitea/workflows/test.yml` (workflow_dispatch + push на `main`):
|
||||
|
||||
```yaml
|
||||
name: test
|
||||
on: { workflow_dispatch: {}, push: { branches: [main] } }
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20, cache: npm }
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
```
|
||||
|
||||
`package.json`: добавить `"test": "node --test test/"` и devDependencies `supertest`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Наблюдаемость
|
||||
|
||||
- **Логи:** JSON-lines, поля `{ ts, level, msg, requestId, jobId?, versionId?, type?, durationMs? }`. `requestId` генерируется в `request-context.js` (`req_` + uuid8) и прокидывается в контекст задания при его создании (журналируем `requestId` в job-событиях).
|
||||
- **Метрики (лёгкие, in-memory):** счётчики `jobs.created{type}`, `jobs.done{type}`, `jobs.error{type}`, гистограмма `job.duration{type}` (бакеты 5/15/30/60/120/300/600 с), `history.size`. Экспонируются в `GET /api/health` (поле `metrics`) — отдельный `/api/metrics` не нужен для одного владельца.
|
||||
- **Аудит:** отдельный уровень `audit` для входа/выхода/удаления версий (в stdout).
|
||||
- **Health:** как сейчас (ComfyUI `/object_info`), плюс проверка, что очередь не заблокирована (если `running` дольше 10 мин — поле `warning`).
|
||||
|
||||
---
|
||||
|
||||
## 12. Миграция и совместимость
|
||||
|
||||
1. **Данные истории:** при первом старте v2, если `./history/history.json` существует, а `data/history/history.json` — нет: скопировать каталог `history/*` → `data/history/`, оставить `./history` нетронутым (не удалять), залогировать миграцию. Обратная совместимость формата версий — да (поля совпадают).
|
||||
2. **Старый URL изображений:** `GET /api/history/:id/image.jpg` остаётся как redirect 302 на `/api/history/:id/image.<ext>` — сохранённые вкладки/ссылки не ломаются.
|
||||
3. **Docker/deploy:**
|
||||
- `Dockerfile`: `COPY server.js ./`, `COPY src ./src`, `COPY public ./public`; `VOLUME ["/app/history", "/app/data"]`.
|
||||
- `deploy/docker-compose.yml`: заменить `kadr-history:/app/history` на `kadr-data:/app/data` (или добавить `kadr-data` рядом — при первом старте сработает миграция из старого тома только если смонтирован и старый; **рекомендация:** смонтировать оба тома на переходный период: `kadr-history:/app/history:ro`? Нет — проще: оставить `kadr-history:/app/history` и добавить `kadr-data:/app/data`; v2 пишет в `data/history`, миграция читает `./history`).
|
||||
- `.gitignore`: добавить `data/` (уже есть `history/`).
|
||||
4. **README:** обновить разделы «Запуск», «Авторизация» (rate-limit, scrypt), «Логирование» (requestId), добавить раздел «Архитектура» со ссылкой на эти документы и список env-переменных.
|
||||
|
||||
---
|
||||
|
||||
## 13. План реализации для ИИ-агента (по фазам)
|
||||
|
||||
Порядок фаз сохраняет работоспособность сервиса на каждом шаге (каждая фаза заканчивается зелёными проверками и рабочей страницей). Реализатор должен следовать правилам зависимостей §4.2 и контрактам §5–§7.
|
||||
|
||||
### Фаза 0 — Каркас (без изменения поведения)
|
||||
- Создать `src/config.js`, `src/logger.js`, `src/errors.js`; перенести существующие константы и `log()` из `server.js`.
|
||||
- Создать `src/http/app.js` (`createApp(deps)`) и `src/server.js` (bootstrap: `loadConfig → createApp → listen`). Корневой `server.js` становится тонким загрузчиком (`require("./src/server")`) — Docker CMD и `npm start` не меняются.
|
||||
- Перенести все роуты из `server.js` в `src/http/routes/*` **без изменения логики** (механический перенос, временно оставив в одном месте `sessions/jobs` Map).
|
||||
- `package.json`: `"start": "node server.js"`, `"start:dev": "node --watch src/server.js"`.
|
||||
- **Приёмка:** `npm start` поднимает сервер; все сценарии README работают как раньше (ручная проверка); `npm test` (пустой набор) зелёный.
|
||||
|
||||
### Фаза 1 — Хранилища данных
|
||||
- `core/history/history-store.js` + `history-service.js`: атомарная запись, прунинг, каскадное удаление (перенос из `server.js:820-900, 1765-1826`), URL с фактическим ext + алиас `/image.jpg` (302).
|
||||
- `core/auth/password.js` + `session-store.js`: scrypt-хеш, персистентные сессии с хешами токенов.
|
||||
- Миграция `history/` → `data/history/` (§12.1).
|
||||
- Тесты: `history-store.test.js` (модульный), `api.test.js` (импорт/удаление/история).
|
||||
- **Приёмка:** `npm test` зелёный; загрузка/удаление версий работает; перезапуск сервера сохраняет историю и вход (с `SESSION_PERSIST=true`).
|
||||
|
||||
### Фаза 2 — Очередь и движки
|
||||
- `core/jobs/job-store.js`, `queue.js`, `job-service.js`; контракт `engines/engine.js`.
|
||||
- Перенос Codex-логики в `core/codex/cli.js` + `prompts.js` + `core/engines/codex-engine.js`; ComfyUI-логики в `core/comfy/*` + `comfy-upscale-engine.js`.
|
||||
- Роуты edit/upscale/suggest переключаются на `job-service.enqueue` (вместо `codexEditInFlight`); `GET /api/jobs`, `queuePosition`, восстановление при старте.
|
||||
- Тесты: `job-queue.test.js` (fake-движки), `prompts.test.js`, обновить `api.test.js` (stub-движки).
|
||||
- **Приёмка:** две правки подряд встают в очередь; рестарт во время `running` → `interrupted` с понятной ошибкой в UI; `queued` переживает рестарт; апскейл работает.
|
||||
|
||||
### Фаза 3 — Безопасность и валидация
|
||||
- `rate-limit.js` (логин), `origin-check.js`, `sniffImageType` в фильтре аплоада, валидация `region`.
|
||||
- Тесты: login rate-limit, Origin-проверка, поддельный MIME отклоняется.
|
||||
- **Приёмка:** 6 неудачных входов за 15 мин блокируют; curl с чужим Origin на `POST` → 403; файл с переименованным расширением отклоняется.
|
||||
|
||||
### Фаза 4 — Даунскейл и SSE
|
||||
- `images/resize.js` (sharp), даунскейл в `codex-engine` (edit), `scaleRegion`.
|
||||
- `events.routes.js` (SSE) + эмиттер в `job-service`; фронтенд: `js/jobs.js` (EventSource + фолбэк).
|
||||
- Тесты: `images.test.js` (scaleRegion), SSE-снапшот в `api.test.js`.
|
||||
- **Приёмка:** правка фото 4000×3000 уходит в Codex как ≤ 1024 px (проверка лога), результат корректен; статусы приходят мгновенно без поллинга (DevTools Network); при отключённом SSE работает фолбэк.
|
||||
|
||||
### Фаза 5 — Фронтенд-модули
|
||||
- Разбить `public/app.js` на `js/*.js` (ES-модули) по §9.1 без изменения вёрстки/стилей; перенести логику 1-в-1, заменить глобальные переменные на `state.js`.
|
||||
- `index.html`: `<script type="module" src="/js/main.js">`.
|
||||
- **Приёмка:** все сценарии README работают; `npm start` отдаёт модули без build-шага; консоль браузера чистая.
|
||||
|
||||
### Фаза 6 — CI, наблюдаемость, документация
|
||||
- `.gitea/workflows/test.yml`; метрики в `/api/health`; requestId в логах и ошибках; обновление README и Dockerfile/deploy (том `data`).
|
||||
- **Приёмка:** Gitea Actions `test` зелёный; `docker compose up -d --build` собирается; после рестарта контейнера данные и входы на месте.
|
||||
|
||||
---
|
||||
|
||||
## 14. Валидация целевой архитектуры (приёмочные доказательства)
|
||||
|
||||
| Требование | Доказательство |
|
||||
|---|---|
|
||||
| Поведение v1 сохранено | Фаза 0–1 ручной прогон всех README-сценариев; `api.test.js` покрывает контракты |
|
||||
| Переживание рестарта | Тест `job-queue.test.js` (восстановление из журнала) + ручной: рестарт во время edit |
|
||||
| Безопасность | Тесты rate-limit/Origin/magic-bytes; в логах нет паролей и токенов |
|
||||
| Скорость | Замер времени edit до/после даунскейла (лог `durationMs`, сравнение с README-таблицей) |
|
||||
| Наблюдаемость | Лог с requestId по всем API; `GET /api/health` содержит metrics |
|
||||
| Реализуемость | Каждая фаза имеет автотесты и ручную приёмку; контракты в §5–§7 однозначны |
|
||||
|
||||
---
|
||||
|
||||
## 15. Открытые вопросы и остаточные риски
|
||||
|
||||
Вопросы, которые **могут** изменить планирование (на текущий момент решены дефолтами):
|
||||
|
||||
1. **Резкость даунскейла для Codex.** Дефолт 1024 px; если владелец заметит потерю деталей на продуктах (этикетки, текст), поднять до 1536 или отключить (`EDIT_MAX_DIMENSION=0`). Обратимо на уровне env.
|
||||
2. **Судьба старого тома `kadr-history`.** После подтверждённой миграции владелец может удалить том вручную; v2 не удаляет его сам.
|
||||
3. **Кониурентность апскейлов = 2.** Если ComfyUI (2× AMD Radeon Pro VII) начнёт ставить в очередь — снизить до 1; env-переменная уже предусмотрена.
|
||||
|
||||
Риски и их снижение:
|
||||
- **sharp в node:22-bookworm-slim:** prebuilt binaries распространяются для linux-x64 glibc — ожидаемо работает; фолбэк `jimp` (R6).
|
||||
- **Изменения API Codex/ComfyUI:** клиенты изолированы в `core/codex` и `core/comfy`; изменения затрагивают один модуль + тесты.
|
||||
- **SSE через прокси:** в LAN прокси нет (кроме egress-прокси для OpenAI, который не стоит на пути браузера); фолбэк-поллинг покрывает остальные случаи.
|
||||
- **Рост `data/history` при даунскейле:** файлы исходников хранятся как есть; лимит 50 версий действует (D3).
|
||||
|
||||
---
|
||||
|
||||
## 16. Реестр решений — сводка для быстрой навигации
|
||||
|
||||
| Решение | Секция |
|
||||
|---|---|
|
||||
| Модульный монолит, правила зависимостей | §4.1–4.2 |
|
||||
| Единый config с валидацией | §4.3 |
|
||||
| JSON-манифест истории + атомарная запись | §5.1 |
|
||||
| Персистентные сессии (хеши токенов) | §5.2 |
|
||||
| JSONL-журнал заданий + восстановление | §5.3, §6.1 |
|
||||
| Очередь FIFO, кониурентность по группам, MAX_QUEUED_JOBS | §6.1 |
|
||||
| Контракт движка EngineResult | §6.2 |
|
||||
| Даунскейл sharp + scaleRegion | §6.5, Фаза 4 |
|
||||
| SSE + фолбэк-поллинг | §6.6 |
|
||||
| API: аддитивные изменения, requestId | §7 |
|
||||
| scrypt, rate-limit, Origin-check, magic bytes | §7.3 |
|
||||
| Vanilla ES-модули без сборки | §9 |
|
||||
| node:test + supertest + Gitea test workflow | §10 |
|
||||
| requestId-логи, лёгкие метрики | §11 |
|
||||
| Миграция data/ + алиас /image.jpg | §12 |
|
||||
| План по фазам с приёмкой | §13 |
|
||||
|
||||
---
|
||||
|
||||
# System Design Proposal
|
||||
|
||||
**Checklist: 6/6 complete**<br>
|
||||
**Incomplete: None** — обрамление (§1), оценка (§3), домены/данные/контракты (§4–§7), HLD/LLD (§4–§6, §9–§11), решения (§8), написание/валидация (§13–§15) выполнены; изменены только `docs/architecture/*.md`, код не менялся.
|
||||
Reference in New Issue
Block a user