Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ddd3bfbd0 |
@@ -0,0 +1,236 @@
|
||||
# Архитектура 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 в этом порядке:
|
||||
|
||||
```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` — Центральное состояние
|
||||
|
||||
```javascript
|
||||
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)
|
||||
|
||||
```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)
|
||||
|
||||
```python
|
||||
# Тонкий прокси, ~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`.
|
||||
Reference in New Issue
Block a user