105 lines
7.1 KiB
Markdown
105 lines
7.1 KiB
Markdown
# Переиспользование модуля загрузки drhider в contracts-flask
|
||
|
||
Дата: 2026-08-26
|
||
|
||
## Контекст
|
||
|
||
- Шлюз managed-кластера рвёт тела запросов >~64 КБ, egress не ограничен.
|
||
- Паттерн загрузки: браузер `PUT` файла на ВМ-буфер (WebDAV) → Flask `pull` (egress GET) → обработка.
|
||
- В drhider этот паттерн отлажен и вынесен в переиспользуемый модуль `upload/`
|
||
(слой 1 — выбор файлов/папок/архивов → таблица; слой 2 — закачка PUT→pull; слой 3 — логика приложения, НЕ в модуле).
|
||
- Задача: взять из drhider выбор+загрузку, сшить с логикой сверки contracts-flask,
|
||
**НЕ меняя саму логику сверки** (классификация/группы/сравнение).
|
||
|
||
Текущее состояние contracts-flask: v2.0.11 (рабочая загрузка через ВМ, написана вручную в этой сессии).
|
||
|
||
---
|
||
|
||
## Ревью Соннета (итог)
|
||
|
||
Вердикт: **«с оговорками»** — одна критическая: слои 1+2 нельзя взять AS-IS для
|
||
фронт-части слоя 2. `uploadViaVM.js` шлёт `{session, files:[...]}`, а сверка ожидает
|
||
`{batch_id, contract_id, zip_source, files}`. Несовместимые форматы.
|
||
|
||
### Ключевые находки Соннета
|
||
|
||
1. **Привязка к in-memory сессии** (blueprint.py, 4 точки): `create_session()`,
|
||
`add_file(sid, name, content)`, `get_files(sid) is None`, `file_count(sid)`.
|
||
→ заменить одним `sink(name, content, **ctx)`, где `ctx = {batch_id, contract_id, zip_source}`.
|
||
|
||
2. **Граница «логика сверки» — НЕЛЬЗЯ трогать:**
|
||
| Файл | Функции |
|
||
|---|---|
|
||
| `pipeline_bp.py` | `process_v2`, `classify_batch_route` (SSE + classify) |
|
||
| `services/process.py` | `run_pipeline` |
|
||
| `services/classify.py` | `classify_batch`, `_call_llm_classify`, garbage filters |
|
||
| `services/grouping.py` | `group_documents`, `apply_groups`, `normalize_number` |
|
||
| `db/documents.py` | ВСЕ (контракт данных) |
|
||
| `db/supplements.py` | ВСЕ |
|
||
|
||
3. **Риски:**
|
||
- клиентский ZIP — полная распаковка в память браузера (у сверки PDF ~19 МБ);
|
||
- `allowedExt` разный: drhider `[.pdf,.doc,.docx,.txt,.md]` vs сверка `{pdf,docx,doc,zip}`;
|
||
- `parse.py` уже пытается парсить `.doc` напрямую (`elif ext=="doc": _parse_docx(data)`) — sink должен перехватывать `.doc` ДО `parse_file`;
|
||
- дедуп: name+size (слой 1) vs content-hash (sink) — не ошибка, двойная защита;
|
||
- `session` (drhider) vs `batch_id` (сверка, `crypto.randomUUID()` в state.js);
|
||
- `zip_source` — поле documents, одно на вызов (для UI-группировки).
|
||
|
||
4. **Безопасный порядок (Соннет):** расширить blueprint (sink) → extra-поля → contracts_sink → backend → frontend layer1 → frontend layer2 → удалить `/api/unzip_refs`,`/api/convert_refs`.
|
||
|
||
### Противоречия в плане (Соннет)
|
||
|
||
- «Слои 1+2 КАК ЕСТЬ» неверно для фронт-части слоя 2 (`{session}` vs `{batch_id,...}`).
|
||
- `.doc` в sink неполно описан (порядок «конвертация ДО parse»).
|
||
- `allowedExt` при интеграции не упомянут.
|
||
|
||
---
|
||
|
||
## Моё решение (3 этапа)
|
||
|
||
| Этап | Что | Риск |
|
||
|---|---|---|
|
||
| **1. Транспорт** | скопировать `upload/` в contracts-flask; заменить рукописные `_safe_name` + `_pull_with_retries` на модульные | низкий |
|
||
| **2. Sink** | `create_upload_refs_blueprint(cfg, sink=...)`; sink = `_store_and_parse` + перехват `.doc` → внешний LibreOffice → `parse_file` | средний |
|
||
| **3. UI** | `initUploadTable` (файлы+папки+архивы) | высокий, последним |
|
||
|
||
### Принятые решения
|
||
|
||
- **ZIP → серверный** (оставить `/api/unzip_refs`): у сверки крупные PDF, клиентская распаковка = регресс по памяти (осознанное отступление от «как в хайдере»).
|
||
- **`.doc` → конвертация в sink** через внешний LibreOffice-сервис (`CONVERT_SERVICE_URL`), последовательно (один тяжёлый `.doc` блокирует пакет; параллелить потом).
|
||
- **UI → последним, изолированно** от «загрузка не работает».
|
||
- **Реализация:** я (спецификация + sink), субагент-младшая модель (механика этапа 1), я ревьюю дифф.
|
||
|
||
### Что НЕ переносить из drhider
|
||
|
||
- in-memory сессию (сверка хранит в SQLite `documents` по `batch_id`/`doc_id`);
|
||
- клиентскую распаковку ZIP;
|
||
- обфускацию/слой 3 drhider.
|
||
|
||
---
|
||
|
||
## Находка при чтении транспортных файлов модуля (2026-08-26)
|
||
|
||
Модуль — это **целостный Blueprint**, а не набор drop-in функций:
|
||
|
||
- `safe_name(name)` **сохраняет подпапки** и возвращает `""` при небезопасном имени.
|
||
У сверки `_safe_name` — **basename-only** (rsplit) и возвращает `"file.bin"`. НЕ 1:1.
|
||
- `pull_file(client, url, ...)` требует `httpx.Client` + стриминг (`client.stream`).
|
||
У сверки `_pull_with_retries(url)` — `httpx.get` внутри. Разные сигнатуры.
|
||
- `blueprint.py` жёстко завязан на сессию: `create_session`, `add_file`, `get_files`,
|
||
`file_count`.
|
||
|
||
**Следствие:** «этап 1: заменить 2 функции 1:1» НЕ выполним. Переиспользование идёт
|
||
на уровне **blueprint через sink** (этап 2), с адаптацией формы запроса/ответа.
|
||
|
||
## Статус
|
||
|
||
- [x] Скопировать `upload/` в contracts-flask (commit db58a43)
|
||
- [x] Этап 2 — sink (blueprint + contracts_upload_sink + регистрация), проверено
|
||
- [x] Удалён рукописный `/api/upload_refs` (основной путь теперь через модуль)
|
||
- [x] Этап 3 — UI: выбор папок (webkitdirectory) + рекурсивный клиентский ZIP (commit d2894ad)
|
||
- [ ] Осталось (cleanup, опционально): .doc→.docx в sink, удалить `/api/unzip_refs`/`/api/convert_refs` + `addZipFile`/`convertDoc`
|
||
|
||
Примечание: `/api/unzip_refs` и `/api/convert_refs` пока оставлены как fallback
|
||
(server-side ZIP и .doc), фронт `addZipFile`/`convertDoc` не удалены.
|