этап 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:
@@ -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
|
||||
Reference in New Issue
Block a user