v1.0.177: History — объединение history+History, 5 подпапок
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
Read [](file:///home/naeel/nubes/contracts/contractor/apply_events.cfm)
|
||||
|
||||
Read [](file:///home/naeel/nubes/contracts/contractor/db.cfc#1-1), lines 1 to 150
|
||||
|
||||
Read [](file:///home/naeel/nubes/contracts/contractor/deploy/llm_prompt.py)
|
||||
|
||||
Read [](file:///home/naeel/nubes/contracts/contractor/deploy/convert_server.py#1-1), lines 1 to 150
|
||||
|
||||
Read [](file:///home/naeel/nubes/contracts/contractor/deploy/convert_server.py#150-150), lines 150 to 250
|
||||
|
||||
Проанализировал все файлы. Вот подробный аудит.
|
||||
|
||||
---
|
||||
|
||||
## Аудит Event Sourcing — сверка договоров
|
||||
|
||||
---
|
||||
|
||||
### Находки Gemini — подтверждение/опровержение
|
||||
|
||||
**1. Race condition `MAX(seq)` — ✅ ПОДТВЕРЖДЕНО, критично**
|
||||
|
||||
apply_events.cfm: `SELECT COALESCE(MAX(seq), 0)` → `seq++` в цикле — это read-modify-write без блокировки. Два параллельных вызова (разные supplements) получат одинаковый `MAX(seq)` и сгенерируют одинаковые seq. В DDL есть `UNIQUE(contract_id, seq)` — транзакция упадёт с ошибкой дублирующегося ключа.
|
||||
|
||||
**Рекомендация:** Заменить на `nextval` через sequence (`CREATE SEQUENCE spec_events_seq`) или PostgreSQL `INSERT ... RETURNING` с `seq = (SELECT COALESCE(MAX(seq),0)+1 FROM spec_events WHERE contract_id=... FOR UPDATE)` — с `FOR UPDATE` для пессимистической блокировки.
|
||||
|
||||
---
|
||||
|
||||
**2. `full_replace` с пустым `ops` стирает спецификацию — ✅ ПОДТВЕРЖДЕНО, критично**
|
||||
|
||||
apply_events.cfm: Если LLM вернул `mode=full_replace` с пустым `ops[]` (например, не распознал таблицу), код сначала DELETE всех строк из `spec_current`, потом цикл по ops не выполняется. Спецификация обнулена без восстановления.
|
||||
|
||||
**Рекомендация:** Добавить guard: если `mode == full_replace` и `len(ops) == 0` — отклонить с ошибкой, не трогать `spec_current`.
|
||||
|
||||
---
|
||||
|
||||
**3. LLM возвращает строку вместо числа → `cf_sql_float` падает — ✅ ПОДТВЕРЖДЕНО**
|
||||
|
||||
apply_events.cfm: Переменные `pr`, `qt`, `sm` берутся напрямую из `op.new_row`. Промпт в llm_prompt.py говорит «ЧИСЛА, не строки», но LLM может вернуть `"1 000,00"` или `"null"` как строку. `cf_sql_float` с нечисловой строкой кидает исключение внутри транзакции — вся транзакция откатывается.
|
||||
|
||||
**Рекомендация:** Перед передачей в cfqueryparam привести к числу через `val()` или попытаться `javacast("double", pr)` с catch.
|
||||
|
||||
---
|
||||
|
||||
**4. N+1 SELECT в цикле UPDATE — ✅ ПОДТВЕРЖДЕНО**
|
||||
|
||||
apply_events.cfm: Для каждого UPDATE-op выполняется отдельный `SELECT price, qty, sum, date_start FROM spec_current WHERE name_hash=?`. Если ДС обновляет 50 строк — 50 SELECT-запросов внутри одной транзакции.
|
||||
|
||||
**Рекомендация:** Перед циклом собрать все `target_hash` UPDATE-ops, сделать один `SELECT ... WHERE name_hash = ANY(ARRAY[...])`, сложить результат в struct по hash.
|
||||
|
||||
---
|
||||
|
||||
**5. Построчный INSERT в `full_replace` — ✅ ПОДТВЕРЖДЕНО**
|
||||
|
||||
apply_events.cfm: В `full_replace`-блоке цикл по `curRows` выполняет по одному `INSERT INTO spec_events` на каждую строку. После — снова цикл по `ops`, каждый ADD — ещё 2 INSERT. Для 100-строчной спецификации: 100+100×2 = 300 отдельных INSERT внутри одной транзакции.
|
||||
|
||||
**Рекомендация:** Использовать `INSERT INTO spec_events ... SELECT unnest(...)` или сформировать multi-row VALUES через ColdFusion loop перед запросом.
|
||||
|
||||
---
|
||||
|
||||
**6. `name_hash` зависит от LLM-форматирования — ✅ ПОДТВЕРЖДЕНО, фундаментальная проблема**
|
||||
|
||||
apply_events.cfm: `md5(lower(trim(name)) || coalesce(date_start, ''))` вычисляется на стороне PostgreSQL из данных, которые LLM только что вернул. В llm_prompt.py: в `_build_diff` LLM видит `[hash: {hash}]` и должен вернуть этот же hash в `target_hash`. Но LLM может сократить название, изменить регистр или пробелы → `md5` будет другим → UPDATE-op не найдёт строку → `cfthrow "UPDATE target not found"`.
|
||||
|
||||
**Рекомендация:** Это структурная проблема. Варианты:
|
||||
- Передавать LLM только `hash` как непрозрачный идентификатор (уже так и делается), но добавить fuzzy-matching на стороне apply: если `target_hash` не найден — искать по близкому имени и создавать UNRESOLVED вместо throw.
|
||||
- Добавить отдельный endpoint для ручного разрешения UNRESOLVED.
|
||||
|
||||
---
|
||||
|
||||
**7. `last_event_id` UUID → `cf_sql_varchar` — ✅ ПОДТВЕРЖДЕНО**
|
||||
|
||||
apply_events.cfm: `<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">` для колонки `last_event_id UUID`. Lucee отправит строку, PostgreSQL неявно приведёт к UUID — это работает, но неправильно. При включённом строгом режиме или нестандартном JDBC-драйвере может дать ошибку. Стоит использовать `cfsqltype="cf_sql_char"` с `maxlength="36"` или более явный тип.
|
||||
|
||||
---
|
||||
|
||||
### Упущенные проблемы
|
||||
|
||||
**8. SQL-инъекция в `_lucee_query` — КРИТИЧНО**
|
||||
|
||||
convert_server.py: Метод `_lucee_query` принимает готовую SQL-строку, которую формируют через f-string интерполяцию. Например:
|
||||
```python
|
||||
f"... WHERE s.contract_id='{cid}' ..."
|
||||
```
|
||||
`cid` приходит из GET-параметра `?contract_id=...` без какой-либо санитизации. Если атакующий передаст `cid = "'; DROP TABLE spec_current; --"` — запрос выполнится.
|
||||
|
||||
**Рекомендация:** Передавать `contract_id` как параметр через `params` API Lucee, а не конкатенацией в строку. Либо хотя бы проверять `cid` по regex UUID: `re.fullmatch(r'[0-9a-f-]{36}', cid)`.
|
||||
|
||||
---
|
||||
|
||||
**9. `elements_json` double-decode — баг надёжности**
|
||||
|
||||
convert_server.py:
|
||||
```python
|
||||
ej = docs[0]["elements_json"]
|
||||
if isinstance(ej, dict) and "Value" in ej:
|
||||
ej = ej["Value"]
|
||||
elements = json.loads(ej)
|
||||
```
|
||||
Если Lucee вернул `elements_json` уже как распарсенный dict (без `"Value"` обёртки), то `json.loads(dict)` упадёт с `TypeError`. Условие `isinstance(ej, dict)` обрабатывает только Lucee-обёртку, но если `ej` уже список — код падает.
|
||||
|
||||
**Рекомендация:** `if isinstance(ej, (dict, list)): elements = ej if isinstance(ej, list) else json.loads(ej["Value"])`.
|
||||
|
||||
---
|
||||
|
||||
**10. `full_replace` DELETE не в правильном порядке в аудит-логе**
|
||||
|
||||
apply_events.cfm: DELETE-события для old строк записываются с `seq++` начиная с 0, затем ADD-события из `ops` продолжают seq. Это корректно. НО: если replay этих событий — сначала DELETE всего, потом ADD новых. Проблема: `UNIQUE(contract_id, seq)` не гарантирует порядок применения при replay, если seq не строго монотонный из-за прошлых partial-операций. Нет механизма replay/восстановления из событий.
|
||||
|
||||
**Рекомендация:** Либо добавить флаг `is_replayable` в архитектурный документ (явно задокументировать что replay не поддерживается), либо добавить `applied_at TIMESTAMPTZ` и `parent_seq` для правильного replay.
|
||||
|
||||
---
|
||||
|
||||
**11. LLM-ключ в открытом виде в коде**
|
||||
|
||||
convert_server.py: `LLM_KEY = "sk-ucI5YvOticoOQ9Kuj5K9mQ"` — hardcoded в файле. При любом git push ключ попадёт в историю.
|
||||
|
||||
**Рекомендация:** Перенести в переменную окружения `os.environ.get("LLM_KEY")`.
|
||||
|
||||
---
|
||||
|
||||
**12. Нет идемпотентности — повторный вызов дублирует данные**
|
||||
|
||||
Нет проверки что `supplement_id` уже обработан. Если `/process-v2` вызвать дважды для одного контракта — все operations применятся повторно: ADD-строки задублируются (или упадут на `UNIQUE name_hash`), seq увеличится.
|
||||
|
||||
**Рекомендация:** Добавить `status` колонку в `supplements` (`processed` / `pending`) и проверять в начале цикла. Либо проверять `SELECT COUNT(*) FROM spec_events WHERE supplement_id = ?` перед обработкой.
|
||||
|
||||
---
|
||||
|
||||
### Итоговая таблица приоритетов
|
||||
|
||||
| # | Проблема | Критичность |
|
||||
|---|----------|-------------|
|
||||
| 8 | SQL-инъекция в `_lucee_query` | 🔴 Критично |
|
||||
| 1 | Race condition MAX(seq) | 🔴 Критично |
|
||||
| 2 | full_replace + empty ops → потеря данных | 🔴 Критично |
|
||||
| 12 | Нет идемпотентности | 🟠 Высокая |
|
||||
| 6 | name_hash нестабилен | 🟠 Высокая |
|
||||
| 3 | LLM строка вместо числа → crash | 🟠 Высокая |
|
||||
| 11 | LLM-ключ в коде | 🟡 Средняя |
|
||||
| 9 | elements_json double-decode | 🟡 Средняя |
|
||||
| 4 | N+1 SELECT в UPDATE | 🟡 Средняя |
|
||||
| 5 | Построчный INSERT в full_replace | 🟢 Низкая |
|
||||
| 7 | UUID как cf_sql_varchar | 🟢 Низкая |
|
||||
| 10 | Нет механизма replay | 🟢 Низкая |
|
||||
@@ -0,0 +1,43 @@
|
||||
# Анализ уязвимостей Event Sourcing — Gemini Web
|
||||
|
||||
**Дата:** 2026-06-21
|
||||
|
||||
## 🚨 Критические уязвимости
|
||||
|
||||
### 1. Race Condition в `seq`
|
||||
`apply_events.cfm` — `MAX(seq)` + `seq++` в цикле. При параллельных запросах — одинаковые seq.
|
||||
**Фикс:** `INSERT ... SELECT COALESCE(MAX(seq),0)+1` атомарно.
|
||||
|
||||
### 2. Опустошение контракта при full_replace
|
||||
`apply_events.cfm` — DELETE из spec_current ДО проверки что ops не пуст. LLM упала → спецификация стёрта.
|
||||
**Фикс:** валидация ops.length перед удалением.
|
||||
|
||||
### 3. Падение типизации price/qty
|
||||
`apply_events.cfm` — LLM может вернуть строку вместо числа. `cf_sql_float` падает.
|
||||
**Фикс:** `isNumeric()` перед float.
|
||||
|
||||
### 4. N+1 запросов в UPDATE
|
||||
`apply_events.cfm` — для каждого UPDATE отдельный SELECT. 200 позиций = 200 запросов.
|
||||
**Фикс:** один SELECT всех строк до цикла.
|
||||
|
||||
### 5. Построчный INSERT full_replace
|
||||
`apply_events.cfm` — цикл INSERT вместо массового.
|
||||
**Фикс:** `INSERT INTO ... SELECT`.
|
||||
|
||||
### 6. Хэш зависит от LLM-форматирования
|
||||
`apply_events.cfm` — `md5(lower(trim(name)) || date_start)`. LLM написала «Услуги» vs «Услуга» → разный хэш → ADD/DELETE вместо UPDATE.
|
||||
**Фикс:** LLM сама возвращает target_hash.
|
||||
|
||||
### 7. Несовместимость типов last_event_id
|
||||
`apply_events.cfm` — UUID пишется как `cf_sql_varchar`.
|
||||
**Фикс:** правильный UUID-тип.
|
||||
|
||||
## Файлы для анализа
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `contractor/apply_events.cfm` | Транзакционное применение ops |
|
||||
| `contractor/db.cfc` | Схема таблиц spec_events, spec_current |
|
||||
| `contractor/deploy/llm_prompt.py` | Промпты LLM (ВМ) |
|
||||
| `contractor/deploy/convert_server.py` | SSE + вызов LLM + apply_events (ВМ) |
|
||||
| `contractor/index.cfm` | UI, EventSource, раздел сравнения |
|
||||
@@ -0,0 +1,62 @@
|
||||
# Мой анализ ответов Opus (раунды 1 и 2)
|
||||
|
||||
**Дата:** 2026-06-23
|
||||
|
||||
---
|
||||
|
||||
## Раунд 1: Архитектурные вопросы
|
||||
|
||||
### Что взяли в работу (v1.0.108)
|
||||
- Advisory lock для seq ✅
|
||||
- NFKC-нормализация name_hash ✅
|
||||
- ID-суррогаты r1..rN в промпте вместо хэшей ✅
|
||||
- Обогащение ops именами из spec_current ✅
|
||||
|
||||
### Что отложили
|
||||
- Параллельные вызовы LLM — семантически нельзя (допники кумулятивны)
|
||||
- Таблица-счётчик для seq — advisory lock проще
|
||||
- Эмбеддинги — NFKC достаточно
|
||||
- Мульти-юзеры — преждевременно
|
||||
|
||||
---
|
||||
|
||||
## Раунд 2: Видение и неизведанное
|
||||
|
||||
### Что можно сделать прямо сейчас (v1.0.109)
|
||||
|
||||
| # | Что | Почему |
|
||||
|---|-----|--------|
|
||||
| 1 | `source_document_id` FK в spec_events | Костяк provenance. Связь интерпретации с исходным документом |
|
||||
| 2 | `source_quote` в каждой op от LLM | Точная цитата из документа. Юрист видит источник |
|
||||
| 3 | `confidence` 0.0-1.0 в каждой op | Self-reported моделью. Ниже 0.7 → перепроверить |
|
||||
| 4 | `prompt_version` в spec_events | Какая версия промпта дала этот результат |
|
||||
| 5 | `raw_llm_response` JSONB | Полный ответ модели для аудита |
|
||||
|
||||
### Что отложить на потом
|
||||
|
||||
| # | Что | Когда |
|
||||
|---|-----|-------|
|
||||
| 1 | Temporal UI (ползунок по датам) | После provenance, когда продукт показываем |
|
||||
| 2 | Human-correction как событие | После базового UI |
|
||||
| 3 | Confidence-gating (N прогонов → консенсус) | Когда точность станет критичной |
|
||||
| 4 | Prompt CI (тесты для промптов) | Когда будет >1 версии промпта |
|
||||
| 5 | Двухслойный лог (факты vs интерпретация) | v2.0 |
|
||||
| 6 | Эмбеддинги (pgvector) | Только при доказанных промахах NFKC |
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные принципы (выработанные)
|
||||
|
||||
1. **Сначала данные, потом UI.** source_document_id + source_quote дают основу. Красивый UI — потом.
|
||||
2. **Минимальными средствами.** Одна FK-колонка вместо двухслойного лога. Self-confidence вместо сложного consensus.
|
||||
3. **Домен решает.** Облачный провайдер + ЦОД — названия услуг копируются дословно. NFKC достаточно.
|
||||
4. **Бесплатная LLM меняет экономику.** Можно делать N прогонов, не считая токены. Но лучше умно (только low-confidence).
|
||||
|
||||
---
|
||||
|
||||
## Открытые вопросы к Opus (будущие раунды)
|
||||
|
||||
1. **Prompt engineering для source_quote:** как попросить LLM вернуть точную цитату? Нужен пример промпта.
|
||||
2. **Confidence calibration:** насколько self-reported confidence от gpt-oss-120b коррелирует с реальной точностью? Стоит ли калибровать?
|
||||
3. **Temporal UI реализация:** как технически строить «спецификацию на дату» из spec_events? Реплей с фильтром по seq?
|
||||
4. **Масштабирование LLM:** когда 120B станет узким горлышком, какие варианты? Fine-tune? Distill? Переход на большую модель?
|
||||
@@ -0,0 +1,83 @@
|
||||
# Opus 4.8 — Раунд 2: Практические ответы для реального проекта
|
||||
|
||||
**Дата:** 2026-06-23
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Одна идея с максимальным преимуществом
|
||||
|
||||
**Provenance с кликабельной трассировкой до исходного текста.**
|
||||
|
||||
Юрист подписывается под результатом. «Добавилась услуга X за 15000» без источника — бесполезно. «Добавилась услуга X ← допник №3, п.2.4, вот абзац» → проверка за 2 секунды → реальная экономия.
|
||||
|
||||
**Механика:** каждая op содержит `source_quote` (точная цитата из документа). Клик по строке → показать исходный абзац в документе.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Self-consistency vs logprobs
|
||||
|
||||
**N=1 + self-reported confidence. Не logprobs.**
|
||||
|
||||
Причины:
|
||||
- logprobs на JSON шумные — не отражают уверенность в смысле
|
||||
- Модель сама возвращает `confidence: 0.0-1.0` — ноль инфраструктуры
|
||||
- Self-consistency 3 прогона НЕ утраивает время: прогоны одного файла независимы → параллельно ~80с, не 240с
|
||||
|
||||
**Оптимальный гибрид:** N=1 по умолчанию. Для ops с `confidence < 0.7` → выборочный повторный прогон. Низко-confidence → в очередь на ручную проверку.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: Минимальный двухслойный лог
|
||||
|
||||
**Слой 1 уже есть — таблица `documents`** (original_bytes + elements_json = сырые факты).
|
||||
|
||||
Минимальное: добавить в `spec_events` колонку **`source_document_id`** (FK → documents).
|
||||
|
||||
Не нужно `event_type`, не нужно `parent_event_id`. Одна FK-колонка даёт:
|
||||
- Двухслойное разделение
|
||||
- Provenance-костяк
|
||||
- Возможность ре-интерпретации новыми моделями
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: Эмбеддинги vs NFKC
|
||||
|
||||
**NFKC покрывает ~90-95%. Эмбеддинги преждевременны.**
|
||||
|
||||
NFKC ловит типографику (тире, пробелы, кавычки). Эмбеддинги нужны только для семантического перефраза — но в домене облачного провайдера допники копируют названия дословно. Плюс после подачи LLM `spec_current` с готовыми ID проблема матчинга почти исчезает.
|
||||
|
||||
**Рекомендация:** NFKC достаточно. pgvector добавлять только при доказанных промахах.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Temporal UI
|
||||
|
||||
**Шаговый таймлайн:** `Договор → Допник1 → Допник2 → Допник3`.
|
||||
|
||||
Клик на узел = спецификация на этот момент.
|
||||
|
||||
**Две ручки выбора (from/to):** diff-таблица между версиями.
|
||||
|
||||
**Визуализация:** 🟢 добавлено / 🔴 удалено / 🟡 изменено с inline old→new.
|
||||
|
||||
**Каждая строка кликабельна** → provenance (исходный абзац).
|
||||
|
||||
**Два режима:** «Итог» (последняя vs база) и «Пошагово» (дельта каждого допника).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: Provenance MVP — колонки
|
||||
|
||||
Три колонки в `spec_events`:
|
||||
|
||||
1. **`source_document_id`** FK → documents — самая важная. Костяк всего.
|
||||
2. **`prompt_version`** TEXT — какая версия промпта.
|
||||
3. **`raw_llm_response`** JSONB — полный ответ модели.
|
||||
|
||||
Поле `source_quote` — внутри op (в raw_llm_response), не отдельная колонка.
|
||||
|
||||
---
|
||||
|
||||
## Сквозная логика
|
||||
|
||||
`source_document_id` + `source_quote` от LLM закрывают provenance, двухслойность и трассировку одновременно. Самый дешёвый и конкурентный ход. Temporal UI — следующий слой поверх той же истории.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Ответ Opus 4.8 на вопросы по архитектуре
|
||||
|
||||
**Дата:** 2026-06-23
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Параллельные вызовы LLM
|
||||
|
||||
**Ключевой вопрос — допники кумулятивны или независимы.**
|
||||
|
||||
- Если допник №3 может менять услугу, добавленную допником №2 — цепочка семантически последовательна, распараллелить нельзя.
|
||||
- Если каждый допник трогает свой непересекающийся набор услуг — параллелить можно.
|
||||
|
||||
**Рекомендация:** оставить последовательным. Реальный выигрыш:
|
||||
- Фаза 1 (параллельно) — textify + подготовка контекста (CPU/IO, дёшево)
|
||||
- Фаза 2 (последовательно) — LLM+apply по цепочке
|
||||
- Или: более быстрая модель / меньше токенов / стриминг прогресса
|
||||
|
||||
При параллельном apply_events seq поедет в гонку — ещё одна причина не параллелить.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Атомарный seq без гонки
|
||||
|
||||
**Рекомендация (по предпочтению):**
|
||||
1. Таблица-счётчик `contract_seq(contract_id PK, last_seq)` + `UPDATE ... SET last_seq=last_seq+1 WHERE contract_id=? RETURNING last_seq`. Атомарно, без DDL, без гонок.
|
||||
2. Advisory lock: `SELECT pg_advisory_xact_lock(contract_id)` в начале транзакции, затем MAX+1. Минимальная правка.
|
||||
|
||||
`INSERT ... SELECT MAX+1` не атомарен. `CREATE SEQUENCE` на контракт — DDL-мусор.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: Стабильность name_hash
|
||||
|
||||
**Рекомендация (комбинация):**
|
||||
- В промпт подавать spec_current с готовыми идентификаторами (hash). LLM для UPDATE/DELETE выбирает из готового списка, не сочиняет имя.
|
||||
- Хэш считать только на сервере из нормализованного имени: Unicode NFKC → схлопнуть пробелы → lower → trim.
|
||||
- Fuzzy (`similarity()`) только как fallback-страховка, не основной механизм.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: Имена в UPDATE/DELETE в UI
|
||||
|
||||
**Рекомендация: вариант 2 — обогащать в Python перед SSE.**
|
||||
- spec_current — источник истины, имя для любого target_hash известно.
|
||||
- LLM вообще не должна возвращать имена для UPDATE/DELETE — только идентификатор.
|
||||
- Вариант 1 (LLM возвращает имена) — лишние токены + риск рассинхрона.
|
||||
- Вариант 3 (JS) — лишние round-trip'ы.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Мульти-юзеры и рост данных
|
||||
|
||||
1. `prompts` одна таблица + `user_id TEXT` — норм для 10-10000 юзеров. Индекс `(user_id)`. Партиционирование преждевременно.
|
||||
2. `spec_events` 10K строк — мизер для PG. Партиционирование ближе к 10M+ строк.
|
||||
3. `BYTEA` 30MB — спокойно в PostgreSQL (TOAST). Вынести в S3 когда объём начнёт раздувать бэкапы.
|
||||
|
||||
---
|
||||
|
||||
## Сквозная связь
|
||||
|
||||
В3 и В4 решаются одним сдвигом — давать LLM spec_current с готовыми ID и считать/резолвить всё на сервере. Это убирает и нестабильные хэши, и пустые ячейки в UI.
|
||||
|
||||
**Уточнение Opus:** допники у вас кумулятивные (могут менять то, что добавили предыдущие) или независимые?
|
||||
|
||||
---
|
||||
|
||||
## Уточнения (23.06.2026)
|
||||
|
||||
### 1. Advisory lock в CFML
|
||||
|
||||
**Да, безопасен.** `pg_advisory_xact_lock` — транзакционный: снимается автоматически при завершении транзакции (и commit, и rollback).
|
||||
|
||||
Условия в Lucee:
|
||||
- Лок и INSERT в одном `<cftransaction>` — одно соединение, одна транзакция
|
||||
- `pg_advisory_xact_lock` требует быть внутри транзакции — `<cftransaction>` гарантирует
|
||||
|
||||
Ключ: `pg_advisory_xact_lock(hashtext(contract_id))`. Коллизии hashtext безвредны — два контракта просто сериализуются.
|
||||
|
||||
### 2. NFKC-нормализация — в PostgreSQL
|
||||
|
||||
Делать в PostgreSQL, в том же выражении что считает хэш. Python не дублировать.
|
||||
|
||||
NFKC НЕ объединяет разные тире. Нужна связка в одном выражении:
|
||||
```sql
|
||||
lower(trim(regexp_replace(translate(normalize(name, NFKC), '–—‑−«»""', '------""'), '\s+', ' ', 'g')))
|
||||
```
|
||||
Всё одним выражением — для INSERT-хэша и для lookup.
|
||||
|
||||
### 3. Формат spec_current в промпте с готовыми ID
|
||||
|
||||
Давать LLM компактный список с коротким суррогатным id (`r1, r2...`):
|
||||
|
||||
```json
|
||||
{"current_spec": [
|
||||
{"id": "r1", "name": "Аренда стойко-места...", "price": 15000, "qty": 2, "sum": 30000, "date_start": "2025-01-01"}
|
||||
]}
|
||||
```
|
||||
|
||||
Инструкция в промпте:
|
||||
- UPDATE: `{"op":"UPDATE","target_id":"r1","new_values":{...}}`
|
||||
- DELETE: `{"op":"DELETE","target_id":"r2"}`
|
||||
- ADD: `{"op":"ADD","new_row":{...}}` (без id)
|
||||
|
||||
LLM не видит хэши — только выбирает из готового набора. Сервер маппит `id → name_hash`. Устойчивее, меньше токенов, нет шанса опечатки в md5.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Opus 4.8 Analysis — 2026-06-24 — Auto-classification
|
||||
|
||||
## Главный вывод
|
||||
Текущая модель — «один договор на сессию загрузки». upload.py: первый файл → contracts + supplement initial, остальные → additional к тому же. Связь документ→договор ТОЛЬКО через supplements. Авто-классификация — обратная задача: N файлов → M договоров. Это архитектурное изменение, не промпт.
|
||||
|
||||
## Q1: Поля в documents или staging?
|
||||
**Поля в documents.** + batch_id (привязка к сессии загрузки) + parent_number (отдельно от own_number). 7 колонок:
|
||||
doc_type, own_number, parent_number, doc_date (TEXT, не DATE), counterparty, classify_status, batch_id.
|
||||
|
||||
## Q2: classify в upload или отдельно?
|
||||
**Отдельно.** Upload быстро (0.5с), классификация async. ThreadPoolExecutor(4-8) внутри /classify-batch. Статусная модель: classify_status='pending' → 'classified'/'failed'.
|
||||
|
||||
## Q3: header или весь документ?
|
||||
**Умная выжимка.** header ~1500 симв + regex-хиты по маркерам (договор, №, от, соглашение) из всего документа + даты. Итого ~3000 симв на вход LLM.
|
||||
|
||||
## Q4: parent_contract_number — LLM или regex?
|
||||
**Двухпроходный гибрид.** Проход 1: LLM извлекает строки per-doc. Проход 2: Python нормализует (regexp uppercase+буквы/цифры) и матчит supplements→contracts по parent_number. LLM не делает fuzzy-match.
|
||||
|
||||
## Q5: загрузка 2000 файлов
|
||||
ZIP через /unzip-upload (переделать: store+parse серверно). Прогресс — polling /api/documents?batch=X, не SSE.
|
||||
|
||||
## Q6: группировка — фронт или бэк?
|
||||
**Бэкенд.** Нормализация требует Python. GET /api/groups?batch=X возвращает готовые группы. apply-groups создаёт contracts+supplements.
|
||||
|
||||
## Q7: MVP
|
||||
6 шагов с новыми файлами, ZIP не нужен. Multi-upload уже работает.
|
||||
1. Миграция БД (7 колонок)
|
||||
2. Слой данных (db/documents.py + seed classify prompt)
|
||||
3. services/classify.py (выжимка + LLM + ThreadPoolExecutor)
|
||||
4. services/grouping.py (normalize + group + apply)
|
||||
5. Эндпоинты (/classify-batch, /api/groups, /apply-groups)
|
||||
6. UI (загрузка → classify → polling → карточки групп → per-group Сравнить)
|
||||
@@ -0,0 +1,18 @@
|
||||
# Opus Analysis — 2026-06-24 — Почему 5/6 и нет договора
|
||||
|
||||
## Три бага
|
||||
|
||||
### 1. Договор failed классификацию (корневая причина)
|
||||
Самый большой документ (3000 символов выжимки) + max_tokens=500 → LLM обрезает JSON → _safe_json_parse падает → classify_status='failed'.
|
||||
Именно он — тот 1 из 6, который не прошёл.
|
||||
|
||||
### 2. Мёртвый код в grouping.py (failed не видны)
|
||||
```python
|
||||
unmatched += [d for d in classified if d.get("classify_status") != "classified"]
|
||||
```
|
||||
Итерация по `classified` (уже отфильтрованному), условие всегда ложно.
|
||||
Должно быть: `...for d in docs...`
|
||||
|
||||
### 3. normalize_number ломает сопоставление
|
||||
`"03700_1"` → `"037001"`, `"03700"` → `"03700"`. Не совпадают.
|
||||
Допники никогда не матчатся к базовому договору из-за суффикса _N.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Запрос для Opus — План: группировка, прогресс, промежуточные результаты
|
||||
|
||||
## Что сказал заказчик (дословно)
|
||||
|
||||
> Не распознано (8 файлов)
|
||||
> [supplement] допник-1-XXX002-01200_3.docx ... — нет базового договора №01219_3
|
||||
> ...
|
||||
> зачем нам базовый договор? Вся информация есть в допниках и спеках
|
||||
|
||||
> Было бы хорошо пояснить или показать процесс, что в каком порядке происходит. Так видно только текущую операцию
|
||||
|
||||
> наверно я захочу иметь возможность посмотреть любые промежуточные результаты
|
||||
|
||||
## Что нужно
|
||||
|
||||
Заказчик хочет три улучшения (без фанатизма, главное — устойчивость и понятный UI):
|
||||
|
||||
### #1 — Группировка без базовых договоров
|
||||
|
||||
**Проблема:** сейчас `group_documents()` требует contract-файл как якорь группы. Без него допники/спеки попадают в unresolved с текстом «нет базового договора №X». Заказчик: «зачем нам базовый договор? Вся информация есть в допниках и спеках».
|
||||
|
||||
**Нужно:** группировать документы по `parent_number` / `own_number`, даже если contract-файл отсутствует в загрузке. Создавать «виртуальную» группу без contract-файла.
|
||||
|
||||
**Вопросы:**
|
||||
1. Алгоритм: что приоритетнее — `parent_number` от допника или `own_number` от спеки? Если оба ссылаются на один нормализованный номер — это одна группа?
|
||||
2. Если два допника с одним `parent_number`, но разными `counterparty` — одна группа или разные?
|
||||
3. Как назвать группу без contract-файла: `"№01300_2 — ЗАО XXX003"` из данных классификации? Достаточно?
|
||||
4. Минимальный diff в `group_documents()` — чтобы не сломать текущую логику с contract-файлами?
|
||||
|
||||
### #2 — Прогресс пайплайна
|
||||
|
||||
**Проблема:** юзер видит только статус текущей операции. Непонятно что уже сделано, что предстоит.
|
||||
|
||||
**Нужно:** визуальная шкала этапов с иконками статуса.
|
||||
|
||||
**Вопросы:**
|
||||
1. Достаточно 4 этапов: Загрузка → Классификация → Группировка → Сравнение? (Парсинг — подэтап загрузки, не показывать отдельно)
|
||||
2. Где разместить: в топбаре (всегда видно, не скроллится) или в карточке с результатами?
|
||||
3. При переклассификации после удаления/добавления файла — сбрасывать всю шкалу или только затрагиваемые этапы?
|
||||
|
||||
### #3 — Промежуточные результаты
|
||||
|
||||
**Проблема:** юзер хочет видеть что LLM вернула на каждом шаге: сырой ответ, как определился номер/тип/дата.
|
||||
|
||||
**Нужно:** раскрывающийся блок с деталями для каждого файла.
|
||||
|
||||
**Вопросы:**
|
||||
1. Что хранить: сырой ответ LLM + распарсенный JSON? Достаточно двух новых полей в `documents`?
|
||||
2. Где показывать: раскрывающийся блок под строкой файла в таблице? Или модалка при клике на статус?
|
||||
3. Нужно ли для сравнения (process-v2 SSE) или только для классификации?
|
||||
|
||||
### #4 — Общие ограничения
|
||||
|
||||
Что из трёх самое трудозатратное и что можно упростить без потери юзабилити?
|
||||
|
||||
---
|
||||
|
||||
## Релевантные файлы (читать)
|
||||
|
||||
### Группировка (#1)
|
||||
- `contractor/deploy/services/grouping.py` — `group_documents()`, `normalize_number()`, `apply_groups()`
|
||||
- `contractor/deploy/db/documents.py` — поля `doc_type`, `own_number`, `parent_number`, `counterparty`, `classify_status`
|
||||
- `contractor/deploy/db/contracts.py` — `insert()`, поля `number`, `client`
|
||||
- `contractor/deploy/db/supplements.py` — `insert()`, `list_by_contract()`
|
||||
|
||||
### Прогресс-бар (#2) + Промежуточные результаты (#3)
|
||||
- `contractor/index.cfm` — HTML-оболочка, топбар (`.topbar`, `position: sticky`), карточки, модалки
|
||||
- `contractor/deploy/app.js` — весь фронтенд: загрузка, `runClassify()`, `loadGroups()`, `runCompareForGroup()`, `showClassifyBtn()`, рендеринг таблицы и групп
|
||||
- `contractor/deploy/app_utils.js` — утилиты: `removeFile()`, `renderTable()`
|
||||
- `contractor/deploy/convert_server.py` — роутер (`do_GET`, `do_POST`, `do_DELETE`), SSE (`_handle_process_v2`), эндпоинты `/api/classify-batch`, `/api/groups`, `/api/batch-progress`, `/api/sync`
|
||||
|
||||
### Общий контекст
|
||||
- `contractor/deploy/services/classify.py` — `classify_batch()`, `_smart_extract()`, `_call_llm_classify()`, `_safe_json_parse()`
|
||||
- `contractor/deploy/services/process.py` — `run_pipeline()` (SSE для сравнения: extract/diff)
|
||||
- `contractor/deploy/services/grouping.py` — `group_documents()`, `apply_groups()`
|
||||
- `contractor/deploy/llm_prompt.py` — `build_prompt()`, `build_classify_prompt()`
|
||||
- `contractor/deploy/db/connection.py` — `query()`, `execute()`, `execute_returning()`
|
||||
|
||||
---
|
||||
|
||||
## Игнорировать (не относится к делу)
|
||||
|
||||
- `contracts-app/` — старый Python-бэкенд (Flask), не используется
|
||||
- `contracts-vm/` — старые конфиги ВМ
|
||||
- `DOC/`, `FILES/` — документация, заметки
|
||||
- `dogovora/` — тестовые файлы договоров
|
||||
- `history/`, `History/` — старые сессионные заметки (кроме этого файла)
|
||||
- `contractor/*.cfm` кроме `index.cfm` — старый Lucee-код, не используется
|
||||
- `contractor/deploy/nginx-contracts.conf` — конфиг nginx
|
||||
- `contractor/deploy/convert_doc.py` — конвертер .doc → .docx
|
||||
- `contractor/deploy/sync.sh` — скрипт деплоя
|
||||
- `contractor/upload.cfm`, `contractor/process.cfm`, `contractor/parser.cfm` и т.д. — старый Lucee-код
|
||||
|
||||
---
|
||||
|
||||
## Архитектура (кратко)
|
||||
|
||||
- **Фронт:** `index.cfm` (Lucee, только HTML-оболочка) → грузит `app.js` + `app_utils.js` с ВМ (`https://contracts.kube5s.ru/static/`)
|
||||
- **Бэкенд:** Python 3.12 `http.server` + `ThreadingMixIn` на ВМ (5.172.178.213), порт 8766, systemd-сервис `contracts`
|
||||
- **Все endpoint'ы:** в `convert_server.py` (один файл-роутер, ~400 строк)
|
||||
- **БД:** PostgreSQL 16, прямой доступ через `psycopg2`, connection pool
|
||||
- **Таблицы:** `documents`, `supplements`, `contracts`, `spec_events`, `spec_current`, `prompts`
|
||||
- **LLM:** gpt-oss-120b через `api.aillm.ru`, httpx с http2, `temperature=0.1`, `max_tokens=8000`
|
||||
@@ -0,0 +1,33 @@
|
||||
# Ответ Opus на бриф — моё мнение
|
||||
|
||||
## Что Opus сделал хорошо
|
||||
|
||||
1. **Сверил бриф с кодом** — подтвердил что группировка матчит по `normalize_number`, что прогресс-данные уже есть в `/api/batch-progress`, что сырой LLM-ответ не хранится. Без этого был риск писать план «в воздух».
|
||||
|
||||
2. **Нашёл дубликат `_handle_cleanup`** — я знал про это но не зафиксировал. Opus заметил сам. Побочная находка, полезно.
|
||||
|
||||
3. **Риск baseline для виртуальных групп** — ключевое. Если первый документ виртуальной группы — допник, а не базовый договор, то `run_pipeline()` может построить baseline из допника, а не из полной спецификации. Opus прав: это надо проверить перед релизом #1.
|
||||
|
||||
4. **Приоритеты** — правильно оценил: #3 тяжелее всего (миграция БД + 3 слоя), #2 легче всего (только UI). Совпадает с моей оценкой.
|
||||
|
||||
## Где Opus ошибся или недоработал
|
||||
|
||||
1. **Схема БД** — написал «схема `documents` создаётся на ВМ, в репозитории её нет». Это правда, но миграция через ALTER TABLE на лету — хрупко. Лучше через `seed_defaults()` или отдельный `ensure_schema()`, как уже сделано для prompts. Opus не предложил механизм.
|
||||
|
||||
2. **Два допника с одним parent_number, разными counterparty** — Opus рекомендует разделять. Я бы наоборот: группировать по номеру, игнорировать counterparty. Потому что один договор может иметь одного контрагента в базовом договоре, а в допнике он может быть написан иначе (сокращение, другая оргформа). Риск ложного разделения выше чем риск ложного объединения.
|
||||
|
||||
3. **Прогресс-бар в топбаре** — идея ок, но топбар уже содержит лого + заголовок + «О сервисе». 4 этапа + текст займут место. Возможно лучше сделать отдельную строку под топбаром или внутри карточки результатов. Opus не учёл текущую вёрстку.
|
||||
|
||||
4. **classify_raw vs parsed** — Opus предлагает хранить только `classify_raw`. Я бы хранил и то и другое: `classify_raw` (текст) + `classify_json` (jsonb). Потому что сырой ответ может быть невалидным JSON, а для отладки нужны оба. Но это увеличивает трудозатраты — ок, можно только raw для начала.
|
||||
|
||||
## Что я бы сделал иначе
|
||||
|
||||
1. **Порядок реализации:** #1 → #3 → #2. Потому что #1 (группировка) — самое востребованное заказчиком прямо сейчас. #3 (промежуточные результаты) даст данные для отладки #1 если что-то пойдёт не так. #2 (прогресс-бар) — вишенка, можно последней.
|
||||
|
||||
2. **Для #1:** вместо «виртуальной группы» — просто создавать реальный contract с флагом `is_virtual=true` или `number` из `parent_number`. Тогда `apply_groups()` не нужно менять вообще — contract уже существует, supplement просто привязывается. Меньше спецкейсов.
|
||||
|
||||
3. **Для #3:** вместо ALTER TABLE на лету — добавить колонку в `ensure_schema()` который вызывается при старте. Идемпотентно: `ADD COLUMN IF NOT EXISTS`. Уже есть прецедент с `_ensure_classify_prompt()`.
|
||||
|
||||
## Вердикт
|
||||
|
||||
План Opus — добротный, можно брать за основу. Три поправки выше (порядок, виртуальный contract через флаг, механизм миграции) — и можно делать.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Opus Prompt Improvement — Few-Shot + Domain Glossary
|
||||
|
||||
Дата: 2026-06-23 | Источник: Opus (раунд 3, старый чат)
|
||||
Связано: llm_prompt.py, prompt-strategy.md, provenance-columns.md
|
||||
|
||||
---
|
||||
|
||||
## Что попросили у Opus
|
||||
|
||||
Улучшить промпты для extract (первый документ) и diff (сравнение ДС). Ключевые требования:
|
||||
- Few-shot примеры на домене ЦОД/colocation
|
||||
- Доменный глоссарий (кВт, юнит, стойко-место, IP, каналы)
|
||||
- Edge-cases инструкция
|
||||
- JSON-формат не менять
|
||||
- Новые actions не добавлять
|
||||
|
||||
## Что Opus выдал
|
||||
|
||||
### EXTRACT
|
||||
- Добавлен доменный глоссарий
|
||||
- 1 few-shot пример (стойко-место + IP-адрес)
|
||||
- Усилены правила чисел и null
|
||||
|
||||
### DIFF
|
||||
- Добавлен доменный глоссарий
|
||||
- 4 few-shot примера:
|
||||
1. `partial`, UPDATE цены
|
||||
2. `partial`, ADD + UPDATE qty + UPDATE мощности (внутри name)
|
||||
3. `full_replace` (новая редакция приложения)
|
||||
4. `UNRESOLVED` (не с чем сопоставить)
|
||||
- Edge-cases: сопоставление по смыслу (не по символам), qty++ = UPDATE, мощность внутри name, приоритет full_replace, null при отсутствии данных
|
||||
|
||||
## Какие проблемы решает
|
||||
|
||||
| Проблема | Решение |
|
||||
|----------|---------|
|
||||
| Модель путает единицы (кВт vs шт.) | Глоссарий |
|
||||
| Не понимает что «Аренда стойко-места» = «Colocation» | Сопоставление по смыслу |
|
||||
| При увеличении qty создаёт ADD вместо UPDATE | Пример 2 |
|
||||
| full_replace: пишет UPDATE вместо ADD | Пример 3 |
|
||||
| Не создаёт UNRESOLVED для незнакомых услуг | Пример 4 |
|
||||
| «55 000,00 руб.» → строка вместо числа | Правило чисел |
|
||||
|
||||
## Размещение
|
||||
|
||||
Два варианта:
|
||||
1. Заменить `FALLBACK_EXTRACT` / `FALLBACK_DIFF` в `llm_prompt.py`
|
||||
2. Обновить активный промпт в БД через UI (вкладка «⚙ Промпты»)
|
||||
|
||||
**Рекомендация:** оба. Fallback в Python — защита если БД недоступна. БД — основной источник. Сделать одновременно.
|
||||
|
||||
## ⚠️ Важно: фигурные скобки в Python
|
||||
|
||||
В Python-строке (тройные кавычки) `{` и `}` НЕ требуют удвоения — они не являются f-string placeholder'ами. Удвоение (`{{`, `}}`) нужно ТОЛЬКО если используется `.format()`. В текущем коде промпты — обычные строки в тройных кавычках, подстановка через `.replace()`. Поэтому фигурные скобки оставляем одинарными.
|
||||
|
||||
## Статус
|
||||
|
||||
- [x] Opus выдал готовые промпты
|
||||
- [ ] Заменить в `llm_prompt.py`
|
||||
- [ ] Обновить в БД (сохранить как новую версию)
|
||||
- [ ] Bump + пуш + синк VM
|
||||
@@ -0,0 +1,47 @@
|
||||
# Вопросы Opus 4.8 — раунд 2: неизведанное для реального проекта
|
||||
|
||||
**Дата:** 2026-06-23
|
||||
**Контекст:** свой облачный LLM (gpt-oss-120b), бесплатный, без structural output. Проект для сверки договоров облачного провайдера и ЦОД (colocation, стойко-места, каналы связи, IP-адреса, облачные ресурсы). Не универсальный юрист — узкая доменная специфика. Документы: базовые договоры + допсоглашения + спецификации услуг.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Самое конкурентное из неизведанного
|
||||
|
||||
Ты дал 9 идей. У нас ограниченный ресурс (1 разработчик). Какую ОДНУ идею внедрить первой, чтобы получить максимальное конкурентное преимущество именно для сверки договоров? Не для галочки «у нас ES» — а чтобы юрист сказал «вау, без этого теперь не могу».
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Бесплатная LLM + confidence-gating
|
||||
|
||||
gpt-oss-120b — бесплатный, без ограничений по токенам. Self-consistency (N=3 прогона → консенсус) утроит время (80с → 240с), но токены бесплатны. Стоит ли? Или лучше N=1 + показывать confidence самой модели (logprobs)? Как проще всего вытащить confidence из gpt-oss-120b?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: Двухслойный лог без фанатизма
|
||||
|
||||
Твоя идея двухслойного лога правильна архитектурно. Но у нас Lucee CFML + PostgreSQL — не микросервисы с Kafka. Как сделать **минимальную** версию двухслойного лога, не переписывая всё? Может, просто добавить `event_type` в spec_events (`ingestion` vs `interpretation`) и `parent_event_id`?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: Эмбеддинги vs NFKC — что реально?
|
||||
|
||||
Мы уже сделали NFKC-нормализацию (normalize + translate тире + regexp). Эмбеддинги требуют pgvector + модель. Для 100-1000 услуг в спецификации — эмбеддинги реально дадут прирост точности матчинга строк по сравнению с NFKC? Или NFKC уже покрывает 95% случаев?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Temporal UI — дизайн для юриста
|
||||
|
||||
«Спецификация на дату» — killer-фича. Как должен выглядеть UI? Ползунок? Календарь с выбором допника? И главное — как визуализировать НЕ только конечную спецификацию, но и дифф между двумя датами («что изменилось между допником 2 и 4»)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: Provenance — минимально жизнеспособно
|
||||
|
||||
Что добавить в spec_events прямо сейчас (2-3 колонки) чтобы включить provenance, не раздувая таблицу? `llm_model`, `prompt_version`, `raw_llm_response` (JSONB)? Или что-то ещё?
|
||||
|
||||
---
|
||||
|
||||
## Важно
|
||||
- НЕ делай код. Только анализ и рекомендации.
|
||||
- Помни: стек Lucee CFML + PostgreSQL + Python ВМ. Не Kubernetes, не Kafka, не микросервисы.
|
||||
- LLM свой, облачный, бесплатный, 120B, без structural output.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Вопросы к Opus 4.8 по проекту Сверка договоров
|
||||
|
||||
**Дата:** 2026-06-23
|
||||
**Версия:** v1.0.107
|
||||
|
||||
---
|
||||
|
||||
## Что за проект
|
||||
|
||||
Сервис для сверки договоров облачного провайдера. Юзер загружает договоры/допники (docx/pdf), парсер извлекает текст, LLM сравнивает с накопленной спецификацией по цепочке (Event Sourcing). На выходе — какие услуги добавились/изменились/удалились.
|
||||
|
||||
**Стек:** Lucee CFML 6.0 (сервер приложений) + PostgreSQL 15 + Python 3 на отдельной ВМ (nginx + convert_server.py). LLM: gpt-oss-120b через api.aillm.ru (бесплатный, без структурного вывода). Фронт: CFML-страницы + JavaScript (EventSource для SSE).
|
||||
|
||||
**Масштаб:** сейчас 1 тестовый пользователь. В планах Keycloak, мульти-юзеры.
|
||||
|
||||
**Производительность:** 5 файлов (3-35 KB docx) × последовательные вызовы LLM = 80-100 секунд. Каждый вызов LLM: 6-50 секунд.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Параллельные вызовы LLM
|
||||
|
||||
**Текущая схема:** цикл по supplements последовательно. Для каждого: взять текущую spec_current → textify документа → вызвать LLM → получить ops → apply_events → следующий.
|
||||
|
||||
```python
|
||||
for s in supps:
|
||||
cur = get_current_spec() # из БД
|
||||
text = textify(elements_json) # текст документа
|
||||
ops = call_llm(cur, text) # HTTP к LLM, 6-50с
|
||||
apply_events(ops) # HTTP к Lucee
|
||||
# следующий видит обновлённую spec_current
|
||||
```
|
||||
|
||||
**Проблема:** 5 файлов × (12+20+38+7+6)с = 80-100с. LLM-вызовы занимают 95% времени.
|
||||
|
||||
**Вопрос:** Можно ли распараллелить вызовы LLM, если известно что первый файл = исходная спецификация (база), а остальные — допники к ней? То есть: файл1 → LLM → накопили. Затем файлы 2-5 запустить параллельно, каждый сравнивается с результатом файла1? Или есть архитектурный паттерн лучше? Что будет с seq в spec_events при параллельных apply_events?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Event Sourcing seq без гонки
|
||||
|
||||
**Текущая схема:** в apply_events.cfm (Lucee):
|
||||
```cfm
|
||||
<cfquery>SELECT COALESCE(MAX(seq),0) FROM spec_events WHERE contract_id=? FOR UPDATE</cfquery>
|
||||
<cfset seq = result + 1>
|
||||
<!-- цикл по ops, каждый seq++ -->
|
||||
```
|
||||
|
||||
FOR UPDATE не работает с агрегатной функцией MAX() в PostgreSQL 15 — падает с ошибкой. Убрали.
|
||||
|
||||
**Структура:** `spec_events(contract_id, seq)`, UNIQUE(contract_id, seq). Seq монотонный в рамках контракта.
|
||||
|
||||
**Вопрос:** Как правильно сделать атомарный инкремент seq без гонки? Варианты:
|
||||
1. `CREATE SEQUENCE spec_events_seq_{contract_id}` — но это динамический DDL на каждый контракт
|
||||
2. `LOCK TABLE spec_events IN EXCLUSIVE MODE` — грубо
|
||||
3. `INSERT ... SELECT COALESCE(MAX(seq),0)+1, ...` — атомарно ли?
|
||||
4. Отдельная таблица-счётчик с `UPDATE ... RETURNING`
|
||||
|
||||
Что рекомендовано для PostgreSQL + CFML?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: name_hash — стабильность идентификации строк
|
||||
|
||||
**Текущая схема:** строка спецификации идентифицируется хэшем:
|
||||
```sql
|
||||
md5(lower(trim(name)) || coalesce(date_start, ''))
|
||||
```
|
||||
Вычисляется в INSERT (БД) из данных, которые LLM вернула в new_row.
|
||||
|
||||
Для UPDATE/DELETE LLM возвращает target_hash — указывает какую строку менять/удалить.
|
||||
|
||||
**Проблема:** LLM может написать название услуги чуть иначе в разных файлах:
|
||||
- Файл1: "Аренда стойко-места, в составе: Номинальная мощность – 10 кВт"
|
||||
- Файл2: "Аренда стойко-места, в составе: Номинальная мощность - 10 кВт" (другое тире)
|
||||
- Хэши разные → LLM видит ADD/DELETE вместо UPDATE
|
||||
|
||||
**Вопрос:** Как стабилизировать? Варианты:
|
||||
1. Нормализовать name перед хэшированием (убрать пунктуацию, лишние пробелы)
|
||||
2. LLM возвращает не target_hash, а явно target_name + date_start, а хэш вычисляется сервером
|
||||
3. Fuzzy matching: если target_hash не найден, искать близкое имя через `levenshtein()` или `similarity()`
|
||||
4. Хранить исходный name из первого появления и использовать его как ключ
|
||||
|
||||
Что правильнее архитектурно?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: UPDATE/DELETE без имён в UI
|
||||
|
||||
**Проблема:** в результатах сравнения ADD-строки показывают все поля (услуга, цена, кол-во, сумма, дата). UPDATE и DELETE — пустые ячейки.
|
||||
|
||||
**Причина:** LLM для UPDATE/DELETE возвращает только target_hash и new_values (для UPDATE). Имени услуги нет — оно было в исходной спецификации. UI строит таблицу из d.ops, где для ADD есть new_row.name, а для UPDATE/DELETE — нет.
|
||||
|
||||
**Вопрос:** Где правильнее решать:
|
||||
1. В промпте LLM — требовать всегда возвращать name в new_values для UPDATE и в отдельном поле для DELETE
|
||||
2. В Python (convert_server.py) — перед отправкой ops в SSE, дополнить их именами из spec_current (доп. запрос к БД)
|
||||
3. В UI (JavaScript) — перед рендерингом запрашивать spec_current и резолвить имена
|
||||
|
||||
Что даст меньше запросов и меньше шансов рассинхронизации?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Мульти-юзеры и рост данных
|
||||
|
||||
**Планы:** Keycloak, каждый юзер имеет свои контракты и промпты.
|
||||
|
||||
**Таблицы:**
|
||||
- `documents`, `supplements`, `contracts` — растут с каждым файлом
|
||||
- `spec_events` — растёт с каждым сравнением (каждый ADD/UPDATE/DELETE — строка)
|
||||
- `spec_current` — текущая спецификация, O(услуг в контракте)
|
||||
- `prompts` — одна таблица на всех (в плане: `user_id + timestamp` как ID)
|
||||
|
||||
**Вопросы:**
|
||||
1. `prompts` — одна таблица с `user_id TEXT` норм для масштаба 10-100 юзеров? Или партиционировать?
|
||||
2. `spec_events` — для 100 контрактов по 20 услуг с 5 версиями каждый = 10K строк. Нужно ли партиционирование сейчас?
|
||||
3. `documents` хранит бинарные байты (`original_bytes BYTEA`). Для 1000 документов по 30KB = 30MB — норм в PostgreSQL или вынести в S3-совместимое хранилище?
|
||||
|
||||
---
|
||||
|
||||
## Важно
|
||||
- **НЕ делай код. Только анализ и рекомендации.**
|
||||
- Ответь кратко по каждому вопросу: рекомендуемое решение и почему.
|
||||
- Если нужно уточнение — спроси, но проект небольшой, контекста выше достаточно.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Opus 4.8 — Видение: LLM-driven Event Sourcing нового поколения
|
||||
|
||||
**Дата:** 2026-06-23
|
||||
|
||||
---
|
||||
|
||||
## Оценка текущей архитектуры
|
||||
|
||||
**Частично соответствует лучшим практикам ES. Нарушает две аксиомы:**
|
||||
|
||||
### 1. Событие должно быть фактом, а не интерпретацией
|
||||
Классика: событие — «что произошло» (PriceChanged), объективный факт.
|
||||
У нас: ADD/UPDATE/DELETE — интерпретация документа моделью, не факт.
|
||||
Настоящий факт: «загружен допник №3 с таким текстом».
|
||||
|
||||
### 2. События должны быть детерминированы
|
||||
Классика: реплей даёт то же состояние.
|
||||
У нас: та же модель + тот же документ = могут быть другие ops (стохастичность LLM).
|
||||
|
||||
**Вывод:** прагматичный гибрид. Не «неправильно», но неклассически.
|
||||
|
||||
---
|
||||
|
||||
## Двухслойный лог (ключевая идея)
|
||||
|
||||
- **Слой 1 — сырые факты:** `DocumentIngested(text_hash, bytes, order)`. Детерминированные, реплеятся идеально.
|
||||
- **Слой 2 — интерпретация:** `SpecChangeProposed` с метаданными (model_id, prompt_version, input_hash, raw_response, temperature).
|
||||
- `spec_current` — проекция слоя 2.
|
||||
|
||||
Даёт: перепрогнать слой 1 новой моделью без потери истории.
|
||||
|
||||
---
|
||||
|
||||
## Неизведанные идеи
|
||||
|
||||
### 1. Версионная ре-интерпретация
|
||||
Апгрейд модели → перепрогон всего потока → параллельная spec_current_v2 → сравнение дрейфа на истории. Версионирование *понимания* данных.
|
||||
|
||||
### 2. Provenance каждой строки
|
||||
«Эта услуга пришла из допника №3, вот исходный абзац, reasoning LLM, confidence 0.82». UI «почему здесь эта цифра».
|
||||
|
||||
### 3. Human-correction как событие → flywheel
|
||||
Правка юзера = `SpecChangeCorrected(by=user)` = regression-тест + размеченный пример. Бесплатный датасет качества.
|
||||
|
||||
### 4. «Prompt CI» — промпты как код с тестами
|
||||
Из human-corrections → golden dataset. Изменение промпта → eval-харнесс. Регрессионные тесты для промптов.
|
||||
|
||||
### 5. Confidence-gating + self-consistency
|
||||
N прогонов (или 2-3 модели) → консенсус. Согласные ops → авто. Расхождения → ручная проверка. Лечит нестохастичность.
|
||||
|
||||
### 6. Эмбеддинги вместо name_hash
|
||||
Векторный матчинг сущностей внутри event-проекции. «Аренда стойко-места…» с разными тире → один вектор.
|
||||
|
||||
### 7. Temporal queries — «спецификация на дату»
|
||||
UI-ползунок по времени: «как выглядел договор на 2025-03-01». Технически возможно уже сейчас.
|
||||
|
||||
### 8. Outbox/Saga + idempotency keys
|
||||
Exactly-once через HTTP-границы Lucee↔Python↔LLM. Для надёжности при мульти-юзерах.
|
||||
|
||||
### 9. Constrained decoding (GBNF-грамматика)
|
||||
Форсировать валидный JSON ops на уровне декодера LLM. Убирает ошибки парсинга.
|
||||
|
||||
---
|
||||
|
||||
## Приоритеты (от Opus)
|
||||
|
||||
| Приоритет | Идея | Зачем |
|
||||
|---|---|---|
|
||||
| 🔥 Высокий | Двухслойный лог | Чинит детерминизм, открывает всё |
|
||||
| 🔥 Высокий | Provenance-метаданные | Аудит, доверие, миграция моделей |
|
||||
| 🔥 Высокий | Human-correction как событие | Flywheel: бесплатный eval-датасет |
|
||||
| ⭐ Средний | Эмбеддинги для матчинга | Решает name_hash надёжнее |
|
||||
| ⭐ Средний | Temporal UI | Killer-фича почти даром |
|
||||
| ⭐ Средний | Confidence-gating | Убирает тихие ошибки |
|
||||
| 🧪 Исслед. | Версионная ре-интерпретация | Сравнение моделей на истории |
|
||||
@@ -0,0 +1,36 @@
|
||||
# Sonnet Analysis — 2026-06-23 — app.js bugs
|
||||
|
||||
## Q1: Что сломано в app.js
|
||||
Нужно убрать **ДВЕ** строки из app.js (не только `</script>`):
|
||||
- строка 644: `</script>` — SyntaxError
|
||||
- строка 845 старого index.cfm = `</body>` — вторая SyntaxError после первой
|
||||
|
||||
Обе попали из-за `sed -n '209,846p'`.
|
||||
|
||||
## Q2: lucide.createIcons()
|
||||
**НЕ потерян.** В старом index.cfm был на строке 845 (перед `</script>`), попал в app.js. Также вызывается ещё в 4 местах внутри JS.
|
||||
|
||||
## Q3: Дубликаты var
|
||||
`var UNZIP_URL` дублируется (стр. 6 и 7 app.js). Значения идентичны. `var` в JS допускает повторное объявление — не проблема.
|
||||
`UPLOAD_URL` и `CONVERT_URL` НЕ дублируются — они на строках 207-208 старого cfm, sed начат с 209.
|
||||
|
||||
## Q4: CORS
|
||||
`<script src>` **не требует CORS** — браузер грузит скрипты без проверки Origin.
|
||||
fetch/XHR из app.js → contracts.kube5s.ru требуют CORS — Python отвечает `Access-Control-Allow-Origin: *` (OK).
|
||||
|
||||
## Q5: Скрытый баг — parseInfo в модале
|
||||
Строки 484-499 app.js — правая панель "Распарсено" использует UPPERCASE ключи:
|
||||
```javascript
|
||||
d.ELEMENT_COUNT, d.PARAGRAPHS, d.TABLES, d.TABLE_ROWS,
|
||||
d.PAGES, d.PARSE_TIME_MS, d.TEXT_LENGTH, d.ERRORS
|
||||
```
|
||||
Python возвращает lowercase: `element_count`, `elements` (без PARAGRAPHS/TABLES).
|
||||
**Все числа в правой панели модала — undefined → 0.** Нужно исправить на lowercase.
|
||||
|
||||
## Q6: prompt.cfm и chat.cfm
|
||||
Ходят на Lucee-домен (relative URL). Ответы ожидаются в uppercase (`d.OK`, `d.BODY` и т.д.) — корректно для CFML. Работает, если prompt.cfm/chat.cfm есть на Lucee.
|
||||
|
||||
## Приоритеты исправлений
|
||||
1. Убрать `</script>` + `</body>` из app.js → JS выполнится
|
||||
2. `parseInfo` поля: UPPERCASE → lowercase для корректной работы модала
|
||||
3. prompt.cfm/chat.cfm — пока OK, потом перенести на VM
|
||||
@@ -0,0 +1,210 @@
|
||||
# Ответ: архитектура сервиса "Сверка договоров"
|
||||
|
||||
_Дата: 2026-06-13_
|
||||
|
||||
---
|
||||
|
||||
## 1. Правильно ли разбиты слои?
|
||||
|
||||
Разбивка в целом правильная. Принцип «один файл — одна ответственность» выдержан.
|
||||
Критических проблем нет, но есть два момента, которые стоит учесть.
|
||||
|
||||
### Что хорошо
|
||||
- `parser.py` — чисто I/O-слой: байты → JSON. Никакой логики.
|
||||
- `textify.py` — чисто форматирование: JSON → текст. Никакой логики.
|
||||
- `db.py` — чисто транспорт к БД. Не знает о бизнес-сущностях.
|
||||
- `test_routes.py` — отдельный Blueprint, не засоряет app.py.
|
||||
|
||||
### Что стоит скорректировать
|
||||
|
||||
**`db.py` — разделить на транспорт и схему.**
|
||||
Сейчас там `ensure_db()` — это уже «знание» о схеме. Когда появятся таблицы
|
||||
(`contracts`, `supplements`, `spec_rows`, `spec_history`), их создание (DDL)
|
||||
стоит вынести в отдельный `schema.py`. `db.py` остаётся просто `connect()` и `query()`.
|
||||
|
||||
**Слой LLM стоит разделить надвое:**
|
||||
- `llm_client.py` — HTTP-клиент к aillm.ru: отправить промпт → получить строку ответа.
|
||||
Не знает ни о договорах, ни о спецификациях.
|
||||
- `extractor.py` — бизнес-логика: взять текст договора, сформировать промпт,
|
||||
вызвать llm_client, распарсить ответ в строки спецификации.
|
||||
|
||||
Это важно: если поменяется LLM — меняем только `llm_client.py`.
|
||||
Если поменяется формат ответа — только `extractor.py`.
|
||||
|
||||
**Итоговый состав слоёв:**
|
||||
|
||||
```
|
||||
parser.py bytes → elements JSON (уже есть, не трогать)
|
||||
textify.py elements → текст для LLM (уже есть, не трогать)
|
||||
db.py connect() + query() (уже есть, убрать ensure_db)
|
||||
schema.py DDL: CREATE TABLE IF NOT EXISTS
|
||||
upload.py сохранить файл в БД (documents)
|
||||
llm_client.py HTTP к aillm.ru → строка ответа
|
||||
extractor.py текст → структурированные строки (промпт + парсинг ответа)
|
||||
differ.py сравнение строк между допниками → список изменений
|
||||
api.py Blueprint: /contracts, /supplements, /history
|
||||
app.py сборка слоёв, Flask-приложение
|
||||
test_routes.py Blueprint /test (уже есть)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. В каком порядке создавать
|
||||
|
||||
Каждый этап самодостаточен и проверяем до перехода к следующему.
|
||||
|
||||
### Этап 1 — основа хранения
|
||||
`schema.py` → DDL всех таблиц.
|
||||
Запустить `python schema.py` — таблицы созданы. Проверить через `/test sql`.
|
||||
|
||||
### Этап 2 — загрузка файлов
|
||||
`upload.py` → принять файл, вызвать `parser.parse()`, вызвать `textify.to_text()`,
|
||||
сохранить в `documents(original_bytes, parsed_text, mime, filename)`.
|
||||
Добавить POST `/upload` в `app.py`. Проверить curl-ом.
|
||||
|
||||
### Этап 3 — LLM клиент
|
||||
`llm_client.py` → POST к aillm.ru, вернуть строку.
|
||||
Проверить отдельно: `python llm_client.py` с тестовым промптом.
|
||||
|
||||
### Этап 4 — извлечение строк спецификации
|
||||
`extractor.py` → взять `parsed_text`, сформировать промпт, вызвать `llm_client`,
|
||||
распарсить ответ в список `spec_row`.
|
||||
Проверить на одном docx через тест-скрипт.
|
||||
|
||||
### Этап 5 — сравнение (diff)
|
||||
`differ.py` → взять два списка `spec_row`, вернуть изменения.
|
||||
Это чистая функция: `diff(rows_old, rows_new) → changes`.
|
||||
Проверить unit-тестом без БД.
|
||||
|
||||
### Этап 6 — API
|
||||
`api.py` → Blueprint с GET/POST для договоров, допников, истории.
|
||||
Подключить в `app.py`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Поток данных между слоями
|
||||
|
||||
Правило: слои передают данные через простые Python-структуры (dict, list).
|
||||
Никаких прямых вызовов «через слой» — только соседние слои.
|
||||
|
||||
```
|
||||
Файл (bytes)
|
||||
│
|
||||
▼
|
||||
parser.parse(bytes, mime) → {"elements": [...]}
|
||||
│
|
||||
▼
|
||||
textify.to_text(elements) → str
|
||||
│
|
||||
▼
|
||||
upload.py: сохранить в DB, получить document_id
|
||||
│
|
||||
▼
|
||||
extractor.extract(parsed_text) → [{"pos": 1, "name": "...", "qty": 10, ...}]
|
||||
│ (внутри вызывает llm_client.ask(prompt) → str)
|
||||
│
|
||||
▼
|
||||
schema: сохранить строки в spec_rows(document_id, pos, ...)
|
||||
│
|
||||
▼
|
||||
differ.diff(rows_v1, rows_v2) → [{"pos": 3, "field": "qty", "old": 5, "new": 10}]
|
||||
│
|
||||
▼
|
||||
schema: сохранить в spec_history
|
||||
```
|
||||
|
||||
Между слоями **нет импортов друг друга**, кроме:
|
||||
- `upload.py` импортирует `parser` и `textify` (это нормально — upload оркеструет парсинг)
|
||||
- `extractor.py` импортирует `llm_client` (клиент — зависимость экстрактора)
|
||||
- `app.py` и `api.py` импортируют всё — они и есть точки сборки
|
||||
|
||||
`db.py` никто не импортирует напрямую, кроме `upload.py`, `extractor.py` и `api.py`.
|
||||
`schema.py` вызывается только один раз при старте из `app.py`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Как должен выглядеть app.py
|
||||
|
||||
`app.py` — точка входа и сборки. Бизнес-логики ноль.
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
import db, schema
|
||||
from test_routes import test_bp
|
||||
from api import api_bp
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
# 1. Инициализация схемы при старте
|
||||
schema.ensure_schema()
|
||||
|
||||
# 2. Регистрация Blueprint-ов
|
||||
app.register_blueprint(test_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
# 3. Системные маршруты
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return "OK", 200
|
||||
|
||||
return app
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_app().run(host="0.0.0.0", port=5000)
|
||||
```
|
||||
|
||||
Правило: если в `app.py` появляется `if`, `for` или бизнес-слово — это уже лишнее.
|
||||
|
||||
---
|
||||
|
||||
## 5. Потенциальные проблемы
|
||||
|
||||
### LLM не гарантирует структуру ответа
|
||||
Самая острая проблема. Модель может вернуть текст в произвольном формате,
|
||||
сломать JSON, пропустить поля, придумать данные.
|
||||
|
||||
**Решение:**
|
||||
- В `extractor.py` — строгая схема промпта с примером ответа.
|
||||
- Парсинг ответа через `try/except` с явным возвратом `{"error": "parse_failed", "raw": ответ}`.
|
||||
- Никогда не падать — помечать строки как `unresolved`.
|
||||
|
||||
### Идентификация изменённой строки в допнике
|
||||
Самая неоднозначная задача: иногда в допнике новое полное состояние,
|
||||
иногда — дельта. Определить это автоматически сложно.
|
||||
|
||||
**Решение для `differ.py`:**
|
||||
- Сначала попробовать детерминированный diff по позиции/артикулу.
|
||||
- Если совпадение < порога — пометить как `ambiguous`, не фантазировать.
|
||||
- Заказчик потом разбирает вручную `ambiguous`-записи.
|
||||
|
||||
### Сопоставление артикула с каталогом
|
||||
Задача нетривиальная, код-имён в спецификациях нет, матч только по описанию.
|
||||
|
||||
**Решение:** вынести в отдельный `matcher.py`, реализовать как отдельный шаг после основного пайплайна. Пометить как `optional`, не блокировать основной поток.
|
||||
|
||||
### Размер документов vs контекст LLM
|
||||
Большая спецификация (100+ строк) может не влезть в контекст.
|
||||
|
||||
**Решение в `extractor.py`:** разбивать таблицы на чанки, обрабатывать частями,
|
||||
собирать результат. Это нужно заложить сразу — переделывать потом дороже.
|
||||
|
||||
### Транзакционность при загрузке
|
||||
Файл загружен → парсинг ок → LLM вызов → ошибка → документ в БД наполовину.
|
||||
|
||||
**Решение:** хранить в `documents` поле `status` (`uploaded` / `parsed` / `extracted` / `error`).
|
||||
Обновлять после каждого шага. Зависший `uploaded` — сигнал для повтора.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
| Вопрос | Ответ |
|
||||
|--------|-------|
|
||||
| Разбивка слоёв | Правильная. Добавить `schema.py`, разделить LLM на `llm_client` + `extractor` |
|
||||
| Порядок | schema → upload → llm_client → extractor → differ → api |
|
||||
| Поток данных | Через dict/list, нет перекрёстных импортов |
|
||||
| app.py | Только сборка: `ensure_schema()` + `register_blueprint()` |
|
||||
| Риски | LLM-нестабильность, diff-амбивалентность, чанкинг, транзакционность |
|
||||
|
||||
Всё, что не решается надёжно детерминированно — помечать `unresolved`, не фантазировать.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Запрос к Claude Sonnet: архитектура сервиса "Сверка договоров"
|
||||
|
||||
## Контекст
|
||||
|
||||
Мы строим Flask-сервис для автоматизированной обработки договоров и допников.
|
||||
|
||||
Исходная постановка задачи (от заказчика):
|
||||
1. Спецификации договоров разобрать до структурированного вида
|
||||
2. Собрать из **цепочки допников кумулятивный статус договора** — состояние во времени.
|
||||
Важно: иногда в допнике **новое состояние** договора целиком, иногда — **только изменения**,
|
||||
и нужно идентифицировать изменённую строку.
|
||||
3. (опционально, малый процент случаев) сопоставить артикул по описанию с каталогом услуг
|
||||
(кодов в спецификациях нет)
|
||||
|
||||
Критичные ограничения:
|
||||
- Данные строго конфиденциальны → LLM только своя (aillm.ru 120B), данные не покидают облако
|
||||
- На каждом этапе возможны исключения — **не фантазировать**, а отметить конкретную
|
||||
элементарную подзадачу как нерешаемую
|
||||
- Рассматриваем как задачку для ИИ
|
||||
|
||||
## Ключевое требование заказчика
|
||||
|
||||
**НЕ МОНОЛИТИТЬ.** Всё делать небольшими НЕЗАВИСИМЫМИ слоями.
|
||||
Каждый слой — отдельный Python-файл, своя зона ответственности.
|
||||
Слои не должны зависеть друг от друга (или минимально).
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
```
|
||||
site/
|
||||
├── app.py ← Flask-приложение, класс ContractsApp, только сборка слоёв
|
||||
├── db.py ← слой БД: connect(), query(), _pg_connect()
|
||||
├── test_routes.py ← Blueprint /test (статус БД, список таблиц, SQL, createdb)
|
||||
├── parser.py ← слой парсера: parse(bytes, mime) → полный слепок docx/pdf
|
||||
├── textify.py ← слой: elements JSON → линейный текст для LLM
|
||||
├── static/
|
||||
└── templates/
|
||||
```
|
||||
|
||||
База: PostgreSQL, всё внутри облачного кластера.
|
||||
LLM: aillm.ru (120B модель).
|
||||
|
||||
## Что предстоит сделать
|
||||
|
||||
1. **Слой загрузки** — приём файлов через API, сохранение в БД (original_bytes + parsed_text)
|
||||
2. **Слой LLM** — отправка текста → модель → структурированные строки спецификации
|
||||
3. **Слой хранения** — таблицы contracts, supplements, spec_rows, spec_history
|
||||
4. **Слой сравнения (diff)** — сравнение строк между допниками, выявление изменений
|
||||
5. **Слой API** — отдача истории договора, списка, etc.
|
||||
|
||||
## Вопрос
|
||||
|
||||
Разъясни план архитектуры:
|
||||
|
||||
1. Правильно ли разбиты слои? Может что-то объединить или наоборот — разделить?
|
||||
2. В каком порядке их создавать?
|
||||
3. Как организовать поток данных между слоями, чтобы они оставались независимыми?
|
||||
4. Как должен выглядеть основной оркестратор (app.py) — без бизнес-логики?
|
||||
5. Какие потенциальные проблемы ты видишь с таким подходом?
|
||||
|
||||
## Текущий код для ознакомления
|
||||
|
||||
### parser.py (bytes → JSON elements)
|
||||
Использует python-docx для docx, libreoffice для .doc, pdfplumber для PDF, zipfile для zip.
|
||||
Возвращает полный слепок: все абзацы (со стилями) + все таблицы (все строки).
|
||||
Ничего не фильтрует, не теряет.
|
||||
|
||||
### textify.py (JSON elements → текст)
|
||||
Форматирует elements в линейный текст: параграфы как `[Style] text`, таблицы как `| cell | cell |`.
|
||||
Тоже не фильтрует, только форматирует.
|
||||
|
||||
### db.py (БД)
|
||||
connect() в целевую БД, _pg_connect(dbname) в любую, query(sql) — выполнить произвольный запрос.
|
||||
|
||||
### test_routes.py (/test Blueprint)
|
||||
GET /test — статус БД, POST /test с действиями: status, tables, sql, createdb.
|
||||
Мост к БД извне для отладки.
|
||||
|
||||
## Ожидаемый формат ответа
|
||||
|
||||
Структурированный план архитектуры с пояснениями по каждому пункту вопроса.
|
||||
@@ -0,0 +1,258 @@
|
||||
# Аудит для Sonnet — пофайловый разбор
|
||||
|
||||
Ты должен прочитать КАЖДЫЙ файл из списка ниже и выдать по нему детальный отчёт.
|
||||
|
||||
## Формат отчёта ПОФАЙЛОВО
|
||||
|
||||
Для каждого файла:
|
||||
```
|
||||
### convert_server.py
|
||||
| Строка | Тип | Серьёзность | Что не так | Как исправить |
|
||||
|--------|-----|-------------|------------|---------------|
|
||||
| 242 | dead code | low | Дубликат _handle_cleanup | Удалить второй |
|
||||
```
|
||||
|
||||
После пофайлового разбора — СВОДНАЯ ТАБЛИЦА всех проблем по серьёзности: critical → high → medium → low.
|
||||
|
||||
---
|
||||
|
||||
## Файл 1: `contractor/deploy/convert_server.py`
|
||||
|
||||
**Что это:** HTTP роутер на http.server + ThreadingMixIn. Все endpoint'ы приложения.
|
||||
|
||||
**Что смотреть:**
|
||||
- ВСЕ `do_GET`, `do_POST`, `do_DELETE` — правильная диспетчеризация? Нет мёртвых путей?
|
||||
- `_handle_process_v2` — валидация UUID? SSE корректно закрывается при ошибке? Утечка соединений?
|
||||
- `_handle_upload` — размер тела? Content-Type проверка? Таумаут?
|
||||
- `_handle_cleanup` — ДВА определения (строки ~242 и ~255). Второй перекрывает первый. Dead code. Порядок DELETE правильный?
|
||||
- `_handle_api_sync` — читает JSON body. Что если тело пустое/битое? Что если keep_ids содержит 10000 id? Нет лимита.
|
||||
- `_handle_api_document` — doc_id из URL без валидации UUID → psycopg2.InvalidTextRepresentation на "fake-id".
|
||||
- `_handle_api_document_delete` — cascade порядок: spec_current → spec_events → supplements → document. Правильный?
|
||||
- `_handle_classify_batch` — batch_id из JSON без валидации UUID.
|
||||
- `_handle_apply_groups` — валидация структуры groups?
|
||||
- `_handle_api_prompts_*` — role из query params без валидации.
|
||||
- `_json()` — экранирует ли спецсимволы в данных?
|
||||
- `_sse()` — f-string с json.dumps. Данные от LLM могут содержать спецсимволы.
|
||||
- Импорт `execute` на строке 12 — не конфликтует с другими импортами?
|
||||
- `ALTER TABLE ADD COLUMN IF NOT EXISTS` — права на DDL? Идемпотентно?
|
||||
- CORS — `_send_cors()` вызывается везде? OPTIONS?
|
||||
|
||||
---
|
||||
|
||||
## Файл 2: `contractor/deploy/app.js`
|
||||
|
||||
**Что это:** Весь фронтенд (42KB). Загрузка, классификация, группы, сравнение, промпты.
|
||||
|
||||
**Что смотреть:**
|
||||
- `fileQueue`, `contractId`, `batchId` — глобальные. Где расходятся с сервером?
|
||||
- `renderTable()` — `innerHTML` из `fileQueue[].name`, `fileQueue[].status`. XSS?
|
||||
- `fileInput change` — гонка удаления старых + upload новых?
|
||||
- `syncDB()` — fire-and-forget, без await, без проверки ответа.
|
||||
- `runClassify()` — утечка таймеров при ошибке?
|
||||
- `loadGroups()` — ЕДИНСТВЕННОЕ место где diffBody. Больше никто не должен.
|
||||
- `runCompareForGroup()` — compareCard. Все ссылки compareBody/compareStatus?
|
||||
- `llmBtn` — compareCard. То же.
|
||||
- SSE handler'ы — ДВА почти идентичных. Дублирование.
|
||||
- `showText()` — `escHtml(classify_raw)` ок, но `counterparty`, `own_number` — НЕ экранированы! XSS.
|
||||
- `escHtml()` — экранирует `<>&"'`?
|
||||
- `stepDone/Active/resetStepper` — innerHTML через replace. Спецсимволы в id?
|
||||
- `window._groupsData` — устаревает после remove→re-classify. runCompareForGroup использует индекс.
|
||||
- `showClassifyBtn()` — идемпотентно?
|
||||
- `lucide.createIcons()` — не ломает onclick?
|
||||
|
||||
---
|
||||
|
||||
## Файл 3: `contractor/deploy/app_utils.js`
|
||||
|
||||
**Что это:** Утилиты: форматирование, removeFile, escHtml, модалки.
|
||||
|
||||
**Что смотреть:**
|
||||
- `removeFile()` — syncDB+resetStepper+showClassifyBtn через typeof. Если нет — молча.
|
||||
- `formatDate()`, `formatSize()` — null/undefined safe?
|
||||
- `escHtml()` — все опасные символы: `< > & " '`?
|
||||
- `moveUp/Down` — не вызывают syncDB (правильно, состав не меняется).
|
||||
|
||||
---
|
||||
|
||||
## Файл 4: `contractor/deploy/services/classify.py`
|
||||
|
||||
**Что это:** LLM-классификация. ThreadPoolExecutor, выжимка, вызов LLM.
|
||||
|
||||
**Что смотреть:**
|
||||
- `classify_batch()` — reset + list_pending. Гонка между ними?
|
||||
- `_classify_one()` — `_smart_extract` может упасть до LLM. Обрабатывается?
|
||||
- `_smart_extract()` — неожиданная структура elements_json?
|
||||
- `_call_llm_classify()` — httpx `verify=False`. MITM уязвимость.
|
||||
- `_safe_json_parse()` — edge cases: Null/None, числа без кавычек, пустой ответ.
|
||||
- `MAX_WORKERS=4` — 4 одновременных запроса создают каждый свой пул?
|
||||
- `LLM_KEY` из env — если не задан?
|
||||
- API key в коде? (берётся из env, ок)
|
||||
|
||||
---
|
||||
|
||||
## Файл 5: `contractor/deploy/services/grouping.py`
|
||||
|
||||
**Что это:** Группировка документов по контрактам + виртуальные группы.
|
||||
|
||||
**Что смотреть:**
|
||||
- `group_documents()` — виртуальные группы: что если parent_number и own_number оба null?
|
||||
- Коллизия normalize_number: разные номера → одинаковый нормализованный.
|
||||
- Сортировка по doc_date: None у всех → нестабильный порядок.
|
||||
- `apply_groups()` — нет проверки на существующий contract (дубликат при повторе).
|
||||
- `supplements_list.remove(s)` внутри цикла for — может пропускать элементы.
|
||||
|
||||
---
|
||||
|
||||
## Файл 6: `contractor/deploy/services/process.py`
|
||||
|
||||
**Что это:** SSE пайплайн сравнения.
|
||||
|
||||
**Что смотреть:**
|
||||
- `run_pipeline()` — битая ссылка supplement→document? timeout? retry?
|
||||
- `call_llm()` — таймаут?
|
||||
- SSE события — все ли обрабатываются на фронте?
|
||||
|
||||
---
|
||||
|
||||
## Файл 7: `contractor/deploy/services/parse.py`
|
||||
|
||||
**Что это:** Парсинг PDF/DOCX.
|
||||
|
||||
**Что смотреть:**
|
||||
- Расширение: .PDF uppercase? Без расширения?
|
||||
- PDF: битый/зашифрованный → исключение?
|
||||
- DOCX: .doc (OLE) → исключение?
|
||||
- Таблицы: пустые, объединённые ячейки?
|
||||
- file_data = None?
|
||||
|
||||
---
|
||||
|
||||
## Файл 8: `contractor/deploy/services/upload.py`
|
||||
|
||||
**Что это:** Multipart загрузка через cgi.FieldStorage.
|
||||
|
||||
**Что смотреть:**
|
||||
- cgi.FieldStorage deprecated. Большие файлы?
|
||||
- content_length — отрицательное/огромное → rfile.read?
|
||||
- filename — path traversal (`../../etc/passwd`)?
|
||||
- 100MB файл → весь в памяти.
|
||||
- contract_id без валидации → SQL.
|
||||
- `delete_by_document` до создания нового → исключение = потеря старого без создания нового.
|
||||
- base64 → +33% размер.
|
||||
|
||||
---
|
||||
|
||||
## Файл 9: `contractor/deploy/db/connection.py`
|
||||
|
||||
**Что это:** ThreadedConnectionPool.
|
||||
|
||||
**Что смотреть:**
|
||||
- `DB_PASS` из env, пустой → ошибка подключения.
|
||||
- `minconn=1, maxconn=10` — достаточно?
|
||||
- getconn/putconn всегда в finally?
|
||||
- `_connection` глобальная — сервер не стартует без БД.
|
||||
|
||||
---
|
||||
|
||||
## Файл 10: `contractor/deploy/db/documents.py`
|
||||
|
||||
**Что это:** CRUD documents + classify_raw.
|
||||
|
||||
**Что смотреть:**
|
||||
- `insert()` — все параметры через %s?
|
||||
- `set_classification()` — classify_raw=None → NULL.
|
||||
- `set_classify_failed()` — не чистит старые поля классификации.
|
||||
- `delete()` — без cascade!
|
||||
- `get()` — SELECT *, может вернуть base64 original_bytes.
|
||||
|
||||
---
|
||||
|
||||
## Файл 11: `contractor/deploy/db/supplements.py`
|
||||
|
||||
**Что это:** CRUD supplements + cascade.
|
||||
|
||||
**Что смотреть:**
|
||||
- `delete_by_document()` — spec_current WHERE contract_id удаляет ВСЁ для контракта. Если несколько supplements → остальные теряют spec_current.
|
||||
- `list_by_contract()` — orphan supplements игнорируются.
|
||||
- `insert()` — нет FK проверки.
|
||||
|
||||
---
|
||||
|
||||
## Файл 12: `contractor/deploy/db/contracts.py`
|
||||
|
||||
**Что это:** CRUD contracts.
|
||||
|
||||
**Что смотреть:**
|
||||
- `insert()` — нет уникальности, можно дубликат.
|
||||
- `delete_orphaned()` — вызывается? Где?
|
||||
|
||||
---
|
||||
|
||||
## Файл 13: `contractor/deploy/db/spec_events.py`
|
||||
|
||||
**Что это:** Event Sourcing.
|
||||
|
||||
**Что смотреть:**
|
||||
- `get_next_seq()` — `MAX(seq)+1`. Гонка! Два потока → одинаковый seq.
|
||||
- `reset()` — необратимо.
|
||||
- `apply_ops()` — валидация структуры?
|
||||
|
||||
---
|
||||
|
||||
## Файл 14: `contractor/deploy/db/spec_current.py`
|
||||
|
||||
**Что это:** Текущая спецификация.
|
||||
|
||||
**Что смотреть:**
|
||||
- Кто обновляет spec_current после удаления spec_events?
|
||||
- `get_elements_json()` — где используется?
|
||||
|
||||
---
|
||||
|
||||
## Файл 15: `contractor/deploy/db/prompts.py`
|
||||
|
||||
**Что это:** CRUD промптов с версионированием.
|
||||
|
||||
**Что смотреть:**
|
||||
- `seed_defaults()` — `if count>0: return` — если есть extract но нет classify → classify не создастся.
|
||||
- `_ensure_classify_prompt()` — отдельный механизм, почему?
|
||||
- `save_new_version()` — UPDATE+INSERT не атомарно. Гонка.
|
||||
- `activate()` — гонка.
|
||||
- `delete_prompt()` — проверка is_active потом DELETE. Гонка.
|
||||
- `_serialize()` — мутирует словарь.
|
||||
|
||||
---
|
||||
|
||||
## Файл 16: `contractor/deploy/llm_prompt.py`
|
||||
|
||||
**Что это:** Билдер промптов.
|
||||
|
||||
**Что смотреть:**
|
||||
- `build_prompt()` — f-string с данными парсинга. Безопасно?
|
||||
- `_fetch_prompt()` — HTTP к Lucee. Таймаут? Fallback при недоступности?
|
||||
- `build_classify_prompt()` — прямой доступ к БД, не через HTTP. Почему?
|
||||
- FALLBACK_* хардкод — дублирование с БД.
|
||||
|
||||
---
|
||||
|
||||
## Файл 17: `contractor/index.cfm`
|
||||
|
||||
**Что это:** HTML-оболочка.
|
||||
|
||||
**Что смотреть:**
|
||||
- `?v=1.0.175` хардкод — менять при каждом обновлении JS.
|
||||
- pipelineStepper — ○⏳✓ через replace. Надёжно?
|
||||
- XSS через prompt body в textarea/div?
|
||||
- Z-index конфликты модалок?
|
||||
- inline onclick — ломаются при перезагрузке JS?
|
||||
|
||||
---
|
||||
|
||||
## СВОДНАЯ ТАБЛИЦА (выдать после пофайлового разбора)
|
||||
|
||||
| # | Файл:строка | Серьёзность | Тип | Описание | Как исправить |
|
||||
|---|-------------|-------------|-----|----------|---------------|
|
||||
|
||||
## ТОП-5 (срочно)
|
||||
|
||||
5 проблем, которые надо чинить прямо сейчас.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Аудит Sonnet — результаты
|
||||
|
||||
## Ключевые цифры
|
||||
|
||||
- **48 проблем** найдено
|
||||
- **14 CRITICAL**, **15 HIGH**, **19 MEDIUM**
|
||||
- 17 файлов проанализировано
|
||||
|
||||
## ТОП-5 срочных
|
||||
|
||||
1. **upload.py:28** — Path Traversal: `filename` без `os.path.basename()` → `../../etc/passwd`
|
||||
2. **app.js:79,495-520,497,703,708** — XSS × 5 мест: `innerHTML` без `escHtml()` на данных от LLM
|
||||
3. **grouping.py:76** — `supplements_list.remove(s)` в итерации → пропуск элементов
|
||||
4. **spec_events.py:15** — `MAX(seq)+1` без блокировки → race condition на seq
|
||||
5. **prompts.py:40,73,86** — 3 race conditions: seed/save/activate без транзакций
|
||||
|
||||
## Что я понял
|
||||
|
||||
### Мои косяки (надо чинить)
|
||||
|
||||
1. **XSS в 5 местах** — `escHtml` не везде. `counterparty`, `own_number`, `filename`, `contract_number` — всё от LLM, всё в innerHTML без экранирования. Тупо пропустил.
|
||||
|
||||
2. **`supplements_list.remove(s)` в цикле** — реальный баг в grouping.py:76. При удалении элемента из списка во время итерации for пропускаются элементы. Может ломать группировку.
|
||||
|
||||
3. **`syncDB()` без await** — fire-and-forget. Если сервер не ответил — не узнаем. БД рассинхронится с таблицей.
|
||||
|
||||
4. **`delete_by_document` удаляет spec_current для ВСЕГО контракта** — если у контракта 3 supplements, удаление одного затирает spec_current для двух других. Серьёзный баг в supplements.py.
|
||||
|
||||
5. **`verify=False` в httpx** — отключена проверка SSL. MITM-уязвимость в classify.py и llm_prompt.py.
|
||||
|
||||
### Что НЕ надо чинить (не критично)
|
||||
|
||||
- Dead code (дубликат `_handle_cleanup`) — не влияет на работу.
|
||||
- `_serialize` мутирует словарь — косметика.
|
||||
- `v1.0.175` хардкод — пока сойдёт.
|
||||
- `.doc` без fallback — формат редкость.
|
||||
- `errors="ignore"` в парсинге — мелочь.
|
||||
|
||||
### Что Sonnet нашёл сверх моего анализа
|
||||
|
||||
- `MAX(seq)+1` race condition — я не подумал про параллельные запросы к spec_events.
|
||||
- `apply_groups()` без транзакций — я не проверил атомарность.
|
||||
- `_safe_json_parse()` возвращает None для `"null"` — edge case который я упустил.
|
||||
- DoS через `keep_ids` без лимита — не подумал про O(N²).
|
||||
- `get()` возвращает base64 original_bytes (133MB) — утечка памяти.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Sonnet Analysis — 2026-06-24 — Auto-classification
|
||||
|
||||
## 1. Архитектура: отдельный /api/classify
|
||||
Не встраивать в upload. Отдельный эндпоинт → фоновая классификация → SSE прогресс → UI подтверждение.
|
||||
|
||||
## 2. Промпт classify
|
||||
Только header (первые 50 элементов / 2000 символов). Экономия токенов в 5-10 раз.
|
||||
Поля: doc_type, contract_number, doc_date, counterparty, parent_contract_number.
|
||||
|
||||
## 3. Группировка
|
||||
Новая таблица doc_classifications (staging). Группировка по (contract_number, counterparty) + fuzzy match.
|
||||
Существующих contracts + supplements достаточно для хранения итога.
|
||||
|
||||
## 4. Схема БД
|
||||
- ALTER supplements ADD sort_order
|
||||
- CREATE TABLE doc_classifications (staging)
|
||||
|
||||
## 5. UI
|
||||
Карточки групп (contract), внутри — сортированный список документов.
|
||||
Действия: перенести, изменить тип, подтвердить, запустить сравнение.
|
||||
|
||||
## 6. Массовая загрузка
|
||||
ZIP → batch upload → 5 параллельных воркеров (threading.Thread + queue.Queue).
|
||||
2000 файлов × 6s / 5 воркеров ≈ 40 минут.
|
||||
Память: BYTEA в PG для пилота ОК, для прода нужен S3.
|
||||
|
||||
## 7. Приоритеты
|
||||
1. doc_classifications + промпт classify — низкая сложность, критично
|
||||
2. /api/classify (один doc_id) — низкая, критично
|
||||
3. Batch ZIP + workers + SSE — высокая, критично
|
||||
4. Алгоритм группировки — средняя, критично
|
||||
5. UI review — средняя, важно
|
||||
6. /process-v2 per group — низкая, важно
|
||||
7. Артикул→каталог — высокая, отложить
|
||||
|
||||
## Риски
|
||||
- threading в http.server: на пилоте ОК, для прода нужен gunicorn
|
||||
- Промпт classify надо обкатать на реальных документах ДО реализации
|
||||
@@ -0,0 +1,37 @@
|
||||
# Запрос: загрузка файлов с нуля
|
||||
|
||||
**Задача:** надёжная загрузка docx/pdf/zip (до 50MB) через веб-интерфейс.
|
||||
|
||||
## Ресурсы
|
||||
|
||||
- **Flask** (managed pythonk8s.services.ngcloud.ru, деплой = git push)
|
||||
- **PostgreSQL 17.6** (внутренний кластер `postgresqlk8s-master...svc.cluster.local`)
|
||||
- **Redis** (внутренний `redisk8s...svc.cluster.local`, admin/aLITloRefJEiCPqUc2xB)
|
||||
- **БД:** пул psycopg2 (2-10, keepalive), таблицы есть (`documents`, `contracts`, `supplements`)
|
||||
- **Парсеры:** python-docx (DOCX), pdfplumber (PDF), zipfile (ZIP), textify.to_text()
|
||||
- **LLM:** aillm.ru (gpt-oss-120b), HTTP/2
|
||||
- **Клиент:** браузер, XHR POST, JSON body
|
||||
|
||||
## Что НЕ работает (43 версии)
|
||||
|
||||
- POST с телом > 64KB → nginx/Ingress рвёт TCP (HTTP 000, ReadTimeout)
|
||||
- 10-50KB: 100% надёжно
|
||||
- FormData, JSON/base64, чанки, Redis, retry — ничего не помогло
|
||||
- 64KB — жёсткий предел платформы
|
||||
|
||||
## Что нужно
|
||||
|
||||
1. **Архитектура загрузки** — гарантированно обходящая лимит 64KB на тело запроса
|
||||
2. **Прогресс на клиенте** — интерактивно (имя файла, проценты, таймер)
|
||||
3. **Парсинг после загрузки** — извлечение параграфов, таблиц, стилей
|
||||
4. **ZIP** — показывать содержимое, парсить файлы внутри
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. Как обойти 64KB лимит тела запроса?
|
||||
2. Чанки — на клиенте или сервере?
|
||||
3. Redis — нужен или нет для этой задачи?
|
||||
4. Как гарантировать целостность файла при чанковой загрузке?
|
||||
5. Какая структура БД оптимальна для хранения результатов парсинга?
|
||||
|
||||
**Ответь кратко: архитектура (3-5 пунктов), потом отвечу на вопросы.**
|
||||
@@ -0,0 +1,79 @@
|
||||
# Sonnet Analysis — ZIP Upload CORS + Multipart
|
||||
|
||||
Дата: 2026-06-23 | Источник: Sonnet (новый чат)
|
||||
Связано: index.cfm, convert_server.py, nginx-contracts.conf
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
- JS на `contractor.luceek8s.dev.nubes.ru` шлёт FormData через fetch на `contracts.kube5s.ru/unzip-upload`
|
||||
- VM (Python, 8766) парсит multipart через `email.parser.BytesParser`
|
||||
- Nginx проксирует `/unzip-upload` → VM:8766
|
||||
- CURL работает, браузер — `Failed to fetch`
|
||||
|
||||
## Диагноз Sonnet
|
||||
|
||||
### 1. CORS в nginx — add_header внутри if не работает
|
||||
|
||||
`add_header` в родительском `location` не применяется к ответу из `if (...) { return 200; }` — это новый контекст.
|
||||
|
||||
**Исправление:** заголовки внутрь `if`:
|
||||
|
||||
```nginx
|
||||
location /unzip-upload {
|
||||
if ($request_method = OPTIONS) {
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "POST, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "*";
|
||||
add_header Content-Length 0;
|
||||
return 204;
|
||||
}
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
proxy_pass http://127.0.0.1:8766;
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Multipart парсинг — filename*= кодировка
|
||||
|
||||
Браузер для файлов с не-ASCII именами использует `filename*=UTF-8''...` (RFC 5987). `part.get_filename()` может вернуть None.
|
||||
|
||||
**Решение:** переход на raw binary (п.3) устраняет проблему полностью.
|
||||
|
||||
### 3. Raw binary — лучший вариант
|
||||
|
||||
**JS:**
|
||||
```javascript
|
||||
var zipResp = await fetch(UNZIP_URL, {
|
||||
method: 'POST',
|
||||
body: f,
|
||||
headers: { 'Content-Type': 'application/zip' }
|
||||
});
|
||||
```
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
if "multipart" in content_type:
|
||||
# fallback для curl
|
||||
...
|
||||
else:
|
||||
zip_data = self.rfile.read(length)
|
||||
```
|
||||
|
||||
**Плюсы:**
|
||||
- Нет multipart overhead
|
||||
- Нет проблем с filename-кодировкой
|
||||
- Нет проблем с boundary
|
||||
|
||||
**Минусы:**
|
||||
- Нужен правильный CORS preflight (Content-Type: application/zip — не simple)
|
||||
- Нужен `client_max_body_size` в nginx
|
||||
|
||||
## План реализации (когда «делай»)
|
||||
|
||||
1. nginx: перенести CORS-заголовки внутрь `if`-блока
|
||||
2. JS: `fetch(UNZIP_URL, { method: 'POST', body: f, headers: {'Content-Type': 'application/zip'} })`
|
||||
3. VM: raw binary как основной путь, multipart как fallback
|
||||
4. Убрать FormData из JS
|
||||
5. Bump, пуш, синк VM + nginx
|
||||
Reference in New Issue
Block a user