12 KiB
12 KiB
Архитектура 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.