Files
contracts/History/flask-vm-architecture-2026-07-14.md
T

12 KiB
Raw Blame History

Архитектура contracts-flask — срез 2026-07-14

Общая схема

Браузер (index.html)
  │
  │  JS-модули (6 файлов, грузятся как статика Flask)
  │  XHR POST /upload, /convert-doc, /unzip-upload
  │  EventSource GET /process-v2 (SSE)
  │  fetch /api/* (классификация, группы, документы, промпты)
  ▼
Flask (site/app.py) — ~90 строк, ТОЛЬКО ПРОКСИ
  │  GET  /             → render_template("index.html")
  │  POST /chat         → прокси на ВМ
  │  GET  /api/prompts  → прокси на ВМ
  │  Весь HTML/JS/CSS   → статика /static/*
  ▼
ВМ (deploy/convert_server.py) — ВСЯ бизнес-логика
  │  POST /upload              → compare/upload.py
  │  POST /convert-doc         → convert_doc.py (libreoffice)
  │  POST /unzip-upload        → compare/unzip.py
  │  GET  /process-v2          → compare/process.py (SSE)
  │  POST /api/classify-batch  → classify_worker.py
  │  GET  /api/groups          → db/groups
  │  GET  /api/documents/:id   → db/documents
  │  GET  /api/supplements     → db/supplements
  │  POST /api/sync            → db/sync
  │  POST /api/apply-groups    → db/supplements
  │  GET  /api/spec-current    → db/spec_current
  │  GET  /api/batch-progress  → db/classify-progress
  │  GET  /api/prompts         → db/prompts
  │  GET  /api/prompts/list    → db/prompts
  │  POST /api/prompts/save    → db/prompts
  │  POST /api/prompts/activate → db/prompts
  │  POST /api/cleanup         → db/cleanup
  ▼
PostgreSQL (таблицы: documents, supplements, spec_current, prompts, contracts)

JS-фронтенд — 6 модулей

Все лежат в contracts-flask/deploy/, грузятся в HTML в этом порядке:

<script src="/static/state.js"></script>
<script src="/static/app_utils.js"></script>
<script src="/static/files.js"></script>
<script src="/static/groups.js"></script>
<script src="/static/compare.js"></script>
<script src="/static/app.js"></script>

1. state.js — Центральное состояние

var state = {
  files: [],           // [{ name, size, lastModified, doc_id, uploaded, parsed, parseInfo, supp_id, supp_type, status, zip_source }]
  contractId: null,    // ID контракта (приходит с бэкенда после первого upload)
  batchId: crypto.randomUUID(),  // Уникальный ID сессии
  groups: null,        // [{ contract_number, counterparty, documents[], compare }]
  _activeCompare: { es: null, timer: null },  // Одно активное SSE-сравнение
  ui: {
    steps: { upload: '○', classify: '○', groups: '○', compare: '○' }
  }
};

ПАТТЕРН: action → mutate state → render(state). DOM — проекция state, никогда не читаем из DOM.

2. app_utils.js — Утилиты

Функция Назначение
formatSize(bytes) Байты → "1.5 MB"
formatDate(ts) Timestamp → российская дата+время
escHtml(s) Экранирование HTML
removeFile(i) Удалить файл из state.files → render → syncDB
moveUp(i) / moveDown(i) Переместить файл (не используется)
openAbout() / closeAbout() Модальное окно "О сервисе"

3. files.js — Загрузка и таблица файлов

Функция Назначение
statusToHTML(st) Чистая: структура → HTML-статус
renderFiles(state) Рендер таблицы файлов (группировка по zip_source)
syncDB() Синхронизация БД с текущим составом
toggleClassifyDetail(i) Раскрыть результат классификации
uploadFile(file, onProgress, zipSource) XHR-загрузка одного файла
refreshSupps() Обновить supp_id/supp_type
reconcileSelection(files, newFiles) Чистая: согласование состава
applyParseResult(entry, parsed, elapsed) Чистая: применить результат парсинга
addZipFile(file) Обработка ZIP (unzip → for each → addRegularFile)
addRegularFile(file, zipSource) Обработка обычного файла (upload → parse → status)
finalizeUpload() Завершение цикла загрузки
onFilesSelected(newFiles) Оркестратор загрузки

Критичные детали uploadFile:

  • .doc → конвертация в .docx через CONVERT_URL перед загрузкой
  • XHR с timeout = 180000 (3 минуты для больших PDF)
  • onProgress(pct) — callback с процентом
  • Дедупликация по ключу (zip_source, name)
  • При совпадении имён: confirm() — перезаписать/пропустить

4. groups.js — Карточки групп

Функция Назначение
loadGroupsAction() fetch /api/groups → state.groups → renderGroups
renderGroups(state) Пересобрать ВСЕ карточки в #diffBody
renderGroupCard(g, gi) HTML карточки необработанной группы
renderGroupCardDone(g, gi) HTML карточки обработанной группы
renderUnresolvedCard(g) HTML карточки нераспознанных

5. compare.js — SSE-сравнение

Функция Назначение
applyCompareEvent(sections, event) Чистая: SSE-событие → мутация sections
renderCompareSectionHeader(sec) Чистая: заголовок секции
renderCompareOpsTable(ops) Чистая: таблица ADD/UPDATE/DELETE
renderCompareSectionBody(sec) Чистая: тело секции
startCompareSSE(url, container, statusEl, callbacks) Унифицированный жизненный цикл SSE

Критичные детали SSE:

  • EventSource → события: extract_start, llm_done, applied, extract_error, apply_error
  • Только ОДНО сравнение одновременно (state._activeCompare)
  • Все кнопки блокируются на время сравнения
  • Таймер обновляет statusEl каждые 200мс

6. app.js — Оркестратор

Функция Назначение
render(state) Главная функция рендеринга → renderFiles + renderStepper
renderStepper(state) Рендер степпера из state.ui.steps
stepDone(id) / stepActive(id) Мутация шагов
resetStepper(fromId) Сброс шагов
showClassifyBtn() Создать/показать кнопку классификации
runClassify() Классификация (sync/async + poll каждые 2с)
runCompareForGroup(gi, btn) Сравнение группы (apply-groups → SSE)
llmBtn handler Общее сравнение через SSE

HTML-структура (site/templates/index.html)

<!-- Верхняя панель: логотип + заголовок + степпер + кнопка "О сервисе" -->
<div class="topbar">...</div>

<!-- Контент -->
<div class="content">
  <!-- Карточка 1: Загрузка -->
  <div class="card">
    <input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple>
    <table><tbody id="fileTable"></tbody></table>
    <button id="llmBtn">Общее сравнение</button>
    <!-- Промпты LLM -->
  </div>

  <!-- Карточка 2: Классификация и группы (скрыта до classify) -->
  <div class="card" id="diffCard" style="display:none;">
    <div id="diffBody"></div>
  </div>

  <!-- Карточка 3: Результаты сравнения (скрыта до compare) -->
  <div class="card" id="compareCard" style="display:none;">
    <div id="compareBody"></div>
  </div>

  <!-- Карточка 4: Чат с LLM (скрыта) -->
  <div class="card" id="chatCard" style="display:none;">...</div>
</div>

<!-- Модалки: инфо о файле, промпты, о сервисе -->

Flask (site/app.py)

# Тонкий прокси, ~90 строк
@app.route("/")               render_template("index.html")
@app.route("/chat")           прокси на ВМ (LLM API)
@app.route("/api/prompts")    прокси на ВМ
@app.route("/api/prompts/list")   прокси на ВМ
@app.route("/api/prompts/save")   прокси на ВМ
@app.route("/api/prompts/activate")  прокси на ВМ
@app.route("/architect")      render_template("architect.html")

VM_API = "https://contracts.kube5s.ru"
LLM_URL = "https://api.aillm.ru/v1/chat/completions"

ВМ (deploy/convert_server.py)

Python HTTP-сервер (не Flask!) на http.server + ThreadingMixIn. Все эндпоинты перечислены в общей схеме выше.

Критичные модули ВМ:

  • deploy/db/ — PostgreSQL (connection.py, documents.py, supplements.py, spec_current.py, prompts.py)
  • deploy/compare/ — upload.py, unzip.py, process.py (SSE)
  • deploy/convert_doc.py — libreoffice .doc → .docx (stdin → stdout)
  • deploy/classify_worker.py — асинхронная классификация
  • deploy/llm_prompt.py — построение промптов

Pipeline (степпер)

○ Загрузка ──→ ⏳ Загрузка ──→ ✓ Загрузка
                                  └─→ ⏳ Классификация ──→ ✓ Классификация
                                                              └─→ ⏳ Группировка ──→ ✓ Группировка
                                                                                        └─→ ⏳ Сравнение ──→ ✓ Сравнение

Сброс: при добавлении новых файлов → resetStepper('stepUpload') сбрасывает всё начиная с загрузки.

Что нужно перенести с ВМ на Flask

Все API-эндпоинты, которые сейчас на deploy/convert_server.py, должны стать Flask-роутами в site/app.py:

Эндпоинт Метод Назначение
/upload POST Загрузка файла → парсинг → БД
/convert-doc POST .doc → .docx (libreoffice)
/unzip-upload POST Распаковка ZIP
/process-v2 GET SSE сравнения
/api/classify-batch POST Классификация батча
/api/batch-progress GET Прогресс классификации
/api/groups GET Группы по batch_id
/api/documents/:id GET Инфо о документе
/api/supplements GET Список supplement'ов
/api/sync POST Синхронизация БД
/api/apply-groups POST Применить группы
/api/spec-current GET Текущая спецификация
/api/cleanup POST Очистка старых записей

Плюс перенос БД-логики из deploy/db/ во Flask-приложение и конвертера convert_doc.py.