160 lines
9.2 KiB
Markdown
160 lines
9.2 KiB
Markdown
# Сессия #5 — SSE-парсинг, интерактивный прогресс, отказ от LLM
|
||
|
||
_Дата: 2026-06-16_
|
||
_Версия: v1.0 → v1.8 (8 повышений)_
|
||
_Коммиты: fa4c7fe → e0c58fc (8 шт.)_
|
||
|
||
---
|
||
|
||
## Контекст
|
||
|
||
Пользователь (Наиль) захотел:
|
||
1. Видеть **интерактивный прогресс** при обработке файлов (имя + таймер в секундах)
|
||
2. После обработки — **сводка**: общее время, байты
|
||
3. **Никакого LLM** пока — только парсинг
|
||
4. Вспомнил что очередь файлов (JS-слой) уже была реализована в коммите `17c4841`
|
||
|
||
---
|
||
|
||
## Шаг 1: SSE-поток для LLM (v1.1, fa4c7fe)
|
||
|
||
**Что сделано:**
|
||
- `_process` в app.py переделан с блокирующего POST на SSE-поток (`text/event-stream`)
|
||
- Маршрут `/process/<cid>` сменён на GET (нужно для `EventSource`)
|
||
- События: `file_start` → `file_done`/`file_error` → `summary`
|
||
- В шаблоне — JavaScript с `EventSource`, живые таймеры (каждые 500мс)
|
||
|
||
**Ошибка:** Назвал деплой «поды в Kubernetes» — пользователь поправил: это **managed service** на pythonk8s.services.ngcloud.ru, деплой = `git push master`.
|
||
|
||
**Ошибка:** Забыл повысить версию → v1.1 отдельным коммитом.
|
||
|
||
---
|
||
|
||
## Шаг 2: Полный отказ от LLM (v1.2, 3cffca7)
|
||
|
||
**Причина:** Пользователь сказал «НЕ НАДО ЛЛМ и на экране тоже».
|
||
|
||
**Что сделано:**
|
||
- `_process` → `_parse` — только парсинг, без `extractor.extract()` и `differ.diff()`
|
||
- `_upload_files` — только сохраняет файлы в БД, без парсинга на этом этапе
|
||
- `POST /` возвращает JSON `{"contract_id": "..."}` вместо редиректа
|
||
- Весь LLM (`extractor`, `differ`, `llm_client`) убран из основного потока
|
||
|
||
**Интерфейс:**
|
||
- Файлы → сразу в таблицу (имя, дата изменения, размер)
|
||
- ZIP — JSZip показывает содержимое в браузере
|
||
- Кнопка **«Парсинг»** (не «Обработать LLM»)
|
||
- Клиент: POST файлов через `fetch`, затем SSE `/parse/<cid>`
|
||
- Прогресс в столбце «Статус»: ⏳ Xс → ✓ X.Xс
|
||
- Итоговая строка: ✓ Готово: N файлов | размер | время
|
||
|
||
---
|
||
|
||
## Шаг 3: ZIP — файлы внутри не парсились (v1.3, 21f8c0b)
|
||
|
||
**Проблема:** ZIP-файл парсился как единое целое (✓ 0.2с), а файлы внутри него (`допник-1.doc`, `спецификация.docx`...) оставались с `—` в статусе.
|
||
|
||
**Причина:** В `_parse` парсер просто сливал все elements из ZIP в один документ. Файлы из JSZip-превью были только в браузере, сервер про них не знал.
|
||
|
||
**Решение:**
|
||
- Сервер сам открывает ZIP (`zipfile.ZipFile`)
|
||
- Для каждого файла внутри: создаёт отдельный `documents` + `supplements`, парсит, шлёт SSE-события
|
||
- JSZip и сервер используют одинаковые имена → `findRowByName` находит строку
|
||
- ZIP-строка показывает `↗ N файлов` вместо галочки
|
||
|
||
---
|
||
|
||
## Шаг 4: Подпапки в ZIP (v1.4, d5858d7)
|
||
|
||
**Проблема:** Если в ZIP есть подпапки, и в разных папках файлы с одинаковыми именами — конфликт.
|
||
|
||
**Решение:** Использовать полный относительный путь (`подпапка/файл.docx`) вместо basename.
|
||
|
||
Изменено в двух местах:
|
||
- **Клиент**: `relativePath` вместо `relativePath.split('/').pop()`
|
||
- **Сервер**: `zname` вместо `zbasename`
|
||
|
||
---
|
||
|
||
## Шаг 5: Только PDF и Word (v1.5, 435f617)
|
||
|
||
**Проблема:** Из ZIP парсились ВСЕ файлы (включая `.txt`, `.xlsx`), давая пустые результаты.
|
||
|
||
**Решение:** Фильтр по MIME-типу:
|
||
- Сервер: `if zmime not in (pdf, docx, doc): continue`
|
||
- Клиент (JSZip): `if ['docx','doc','pdf'].indexOf(ext) === -1: return`
|
||
|
||
---
|
||
|
||
## Архитектура (текущая)
|
||
|
||
```
|
||
app.py:
|
||
POST / → _upload_files() → сохранить файлы в БД, вернуть JSON {contract_id}
|
||
GET / → _index() → render_template("upload.html")
|
||
GET /parse/<cid> → _parse() → SSE-поток парсинга
|
||
|
||
_parse():
|
||
1. Для каждого документа договора:
|
||
- Обычный файл → parser_mod.parse() → сохранить parsed_text
|
||
- ZIP → zipfile.ZipFile() → для каждого PDF/Word внутри:
|
||
создать document + supplement → parser_mod.parse() → сохранить
|
||
2. SSE-события: file_start → file_done/file_error → zip_expanded → summary
|
||
```
|
||
|
||
---
|
||
|
||
## Выученные уроки
|
||
|
||
1. **⛔ Не фантазировать про инфраструктуру.** Managed service ≠ Kubernetes. Деплой = `git push`.
|
||
2. **Версия — всегда.** После каждого изменения кода — bump.
|
||
3. **ZIP требует особой обработки.** Недостаточно просто распарсить — нужно создать документы для каждого файла внутри.
|
||
4. **Имена должны совпадать** между клиентом (JSZip) и сервером (zipfile) — иначе `findRowByName` не находит строку.
|
||
5. **`replace_string_in_file` портит файлы** — для больших правок использовать heredoc (`cat > file`).
|
||
|
||
---
|
||
|
||
## Шаг 6: Таймаут при загрузке (v1.6, 64b2295)
|
||
|
||
**Проблема:** Все файлы отправлялись одним POST-запросом → >30с → Ingress убивал соединение → «NetworkError when attempting to fetch resource». Никакого прогресса загрузки не было видно.
|
||
|
||
**Решение:**
|
||
- `_upload_files` поддерживает `?cid=X` — первый файл создаёт договор, остальные добавляются
|
||
- Клиент: загрузка по одному файлу через XHR с `upload.onprogress`
|
||
- Прогресс: ↑ 0% → ↑ 45% → ↑ 100% → ✓ загружен
|
||
- XHR таймаут: 60 секунд на файл
|
||
- Размеры файлов из ZIP — через `zipEntry._data.uncompressedSize`
|
||
- Размеры выровнены вправо (`.num-cell`)
|
||
|
||
---
|
||
|
||
## Шаг 7: Прогресс для файлов из ZIP не показывался (v1.7, c710a2d)
|
||
|
||
**Проблема:** `findRowByName` имел условие `&& !fileQueue[i].isZipChild`, поэтому
|
||
SSE-события для файлов из ZIP не находили строку в таблице — статус оставался пустым.
|
||
|
||
**Причина:** в v1.6 `findRowByName` использовалась и для upload (realFiles)
|
||
и для parse (все файлы). Фильтр защищал от нахождения zip-child при upload,
|
||
но ломал parse.
|
||
|
||
**Решение:** убрать `!fileQueue[i].isZipChild`. Для upload это безопасно —
|
||
`realFiles` и так фильтрует `!isZipChild` перед вызовом.
|
||
|
||
**БД:** проверена — все 5 файлов из ZIP созданы, 4 распарсены, 1 .doc с пустым parsed_text (нет LibreOffice).
|
||
|
||
---
|
||
|
||
## Шаг 8: Баги после полной проверки (v1.8, e0c58fc)
|
||
|
||
**Баг 1 — `zip_expanded` счётчик:** `zip_names.append(zname)` стоял ДО фильтра PDF/Word,
|
||
поэтому «↗ N файлов» включал .txt, .xlsx и т.д. **Исправлено:** перенос после фильтра.
|
||
|
||
**Баг 2 — повторный `_parse` пересоздаст ZIP-файлы:** проверка `status='parsed'` пропускала
|
||
только распарсенные, но ZIP имеют статус `expanded`. При повторном вызове `_parse`
|
||
ZIP обрабатывался заново, создавая дубликаты. **Исправлено:** `status IN ('parsed','expanded')`.
|
||
|
||
**Другие проверки (без ошибок):**
|
||
- `resetBtn()` — function declaration, hoisting работает, вызов до определения корректен
|
||
- `removeFile` — удаление детей перед родителем, индексы не смещаются (дети всегда после родителя)
|
||
- `fileTable.appendChild(summaryRow)` — повторные запуски создадут несколько строк-итогов (допустимо)
|