Files
contracts-flask/History/2026-08-26-upload-module-reuse-plan.md
T
2026-08-26 11:40:03 +03:00

105 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Переиспользование модуля загрузки 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` не удалены.