этап 2: переиспользуемый модуль upload (sink) вместо рукописного транспорта

- копирую модуль upload/ из drhider (слои 1-2)
- blueprint: параметр sink (drhider-сессия по умолчанию, сверка — DB+парсинг)
- upload_bp: contracts_upload_sink = _store_and_parse
- routes: регистрирую create_upload_refs_blueprint(cfg, sink=...)
- app.py: корень репо в sys.path (для import upload)
- History: план переиспользования + ревью Соннета
This commit is contained in:
“Naeel”
2026-08-26 08:07:43 +03:00
parent 55bd7a027d
commit db58a433fb
46 changed files with 1986 additions and 27 deletions
@@ -0,0 +1,100 @@
# Переиспользование модуля загрузки 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
- [ ] Этап 2 — sink (доработка blueprint + contracts-sink + регистрация)
- [ ] Удалить рукописный транспорт (после этапа 2)
- [ ] Этап 3 — UI