# Переиспользование модуля загрузки 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` не удалены.