v1.0.177: History — объединение history+History, 5 подпапок
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
# История проекта Contracts (Сверка договоров)
|
||||
|
||||
## 2026-06-13 — Сессия #1: Инициация и развёртывание
|
||||
|
||||
### Участники
|
||||
- Сергей Мищук — постановщик задачи
|
||||
- Владимир Крупский — тех. консультация
|
||||
- Наиль Тазетдинов — разработка
|
||||
- GitHub Copilot (DeepSeek V4 Pro) — AI-ассистент
|
||||
|
||||
---
|
||||
|
||||
## Задача (из fromTelega.md)
|
||||
|
||||
**Сверка договоров** — автоматизированная обработка договоров и допников:
|
||||
|
||||
1. Разобрать спецификации (docx/pdf) до структурированного вида
|
||||
2. Собрать кумулятивный статус договора по цепочке допников (во времени)
|
||||
3. (опционально) Сопоставить артикулы с каталогом услуг через LLM
|
||||
|
||||
**Ограничения:**
|
||||
- Данные строго конфиденциальны
|
||||
- LLM — только своя модель
|
||||
- Исключения не фантазировать, отмечать как нерешаемые
|
||||
|
||||
---
|
||||
|
||||
## Решения по технологиям
|
||||
|
||||
| Компонент | Выбор | Причина |
|
||||
|---|---|---|
|
||||
| Язык | Python 3.12 | python-docx, psycopg2, requests — лучшая экосистема для docx + БД + LLM |
|
||||
| Фреймворк | Flask | Платформа поддерживает, шаблон Baldurs-Gate-test |
|
||||
| БД | PostgreSQL | Существующий кластер postgresqlk8s |
|
||||
| LLM | aillm.ru (120B) | Ключ sk-ucI5YvOticoOQK9ujK5m9Q, резервный DeepSeek Flash |
|
||||
| Деплой | pythonk8s.services.ngcloud.ru | Платформа Flask-сервисов |
|
||||
| Репозиторий | gitea.services.ngcloud.ru/Nail/contracts-app.git | Публичный, мастер-ветка |
|
||||
|
||||
**Отклонённые варианты:**
|
||||
- Node.js — слабый парсинг docx (mammoth → HTML)
|
||||
- Lucee/CFML — нет инструментов для docx/LLM
|
||||
- SQLite — не подходит для облачного сервиса
|
||||
- 3060 (личный сервер) — это личное, не для сервиса
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
**Принцип: НЕ МОНОЛИТИТЬ. Независимые слои = отдельные API.**
|
||||
|
||||
```
|
||||
contracts-app/
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
└── site/
|
||||
├── app.py ← главный, собирает слои (класс ContractsApp)
|
||||
├── db.py ← слой БД (connect, query)
|
||||
├── test_routes.py ← слой /test (Blueprint) — мост к БД извне
|
||||
├── static/css/
|
||||
└── templates/
|
||||
```
|
||||
|
||||
### Слои (текущие)
|
||||
|
||||
| Слой | Файл | Что делает |
|
||||
|---|---|---|
|
||||
| Ядро | `app.py` | Класс ContractsApp, регистрирует Blueprint'ы |
|
||||
| БД | `db.py` | `connect()` и `query(sql, params)` |
|
||||
| Тест | `test_routes.py` | Blueprint `/test` — статус БД, список таблиц, SQL |
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация (production)
|
||||
|
||||
```json
|
||||
// startupConfiguration
|
||||
{ "resourceRealm": "k8s-4-ext-nubes-ru" }
|
||||
|
||||
// clusterConfiguration
|
||||
{ "cpu": "500", "memory": "1024", "replicas": "1" }
|
||||
|
||||
// accessConfiguration
|
||||
{ "domain": "contractor" }
|
||||
|
||||
// appConfiguration
|
||||
{
|
||||
"gitPath": "https://gitea.services.ngcloud.ru/Nail/contracts-app.git",
|
||||
"version": "3.12",
|
||||
"healthPath": "/health"
|
||||
}
|
||||
|
||||
// jsonEnv
|
||||
{
|
||||
"DB_HOST": "postgresqlk8s-master.xxx.svc.cluster.local",
|
||||
"DB_NAME": "contracts",
|
||||
"DB_PASS": "xnm9KHLibBvT5lYuQzaBVFPYJQHfTxbS4YeEWMcFvtXuXfLaXvgogqJ6uW0nbBUB",
|
||||
"DB_PORT": "5432",
|
||||
"DB_USER": "constracts",
|
||||
"DB_SSLMODE": "disable"
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **DB_HOST** всё ещё `xxx` — нужно заменить на реальный:
|
||||
`postgresqlk8s-master.60bdf3e3-5087-41ff-b760-fe6ea544a80e.svc.cluster.local`
|
||||
|
||||
⚠️ **DB_USER** написано `constracts` (опечатка) — должно быть `contracts`
|
||||
|
||||
---
|
||||
|
||||
## Хронология деплоев
|
||||
|
||||
### Попытка #1 — 11:34, Таймаут health check (478 сек)
|
||||
- **Причина:** `debug=True` в app.run(), жёсткие версии в requirements.txt
|
||||
- **Решение:** убрал debug, смягчил версии, health → plain text
|
||||
|
||||
### Попытка #2 — 12:12, Под(ы) не работают (ImportError)
|
||||
- **Причина:** `from . import db` — относительные импорты не работают при `python app.py` из папки site/
|
||||
- **Ошибка:** `ImportError: attempted relative import with no known parent package`
|
||||
- **Решение:** заменил на абсолютные: `import db`, `from test_routes import test_bp`
|
||||
|
||||
### Попытка #3 — 12:18, Успех ✅
|
||||
- Health: OK
|
||||
- DB: fail (переменные не заданы)
|
||||
|
||||
### Попытка #4 — 12:??, (ожидается)
|
||||
- Добавлены переменные БД
|
||||
|
||||
---
|
||||
|
||||
## Платформа: требования Flask-сервиса
|
||||
|
||||
Из `flask_manual.md`:
|
||||
- `requirements.txt` в корне
|
||||
- Код в `site/`
|
||||
- Запуск: `python app.py`
|
||||
- Python: 3.12-slim
|
||||
- HealthCheck: задаётся параметром
|
||||
- Git: публичный доступ
|
||||
|
||||
---
|
||||
|
||||
## API эндпоинты
|
||||
|
||||
| Метод | Путь | Что |
|
||||
|---|---|---|
|
||||
| GET | `/` | Главная (HTML, статус БД) |
|
||||
| GET | `/health` | Health check → "OK" |
|
||||
| GET | `/test` | Статус БД + список команд |
|
||||
| POST | `/test` `{"action":"tables"}` | Список таблиц |
|
||||
| POST | `/test` `{"action":"sql","sql":"..."}` | Произвольный SQL |
|
||||
|
||||
---
|
||||
|
||||
## План дальнейших работ
|
||||
|
||||
1. ✅ Flask-заготовка деплоится
|
||||
2. ✅ Слои db.py + test_routes
|
||||
3. ✅ Подключить БД (переменные заданы)
|
||||
4. ✅ Создать базу contracts (через /test createdb)
|
||||
5. ✅ Парсер docx/pdf/doc/zip — parser.py
|
||||
6. ⬜ Загрузка документов в БД (contract_docs)
|
||||
7. ⬜ LLM-нормализация строк
|
||||
8. ⬜ Схема таблиц (contracts, spec_rows, spec_history)
|
||||
9. ⬜ DIFF допников, кумулятивная история
|
||||
10. ⬜ Fuzzy-match с каталогом услуг
|
||||
|
||||
---
|
||||
|
||||
## Хронология деплоев (полная)
|
||||
|
||||
### Попытка #1 — 11:34, Таймаут health check (478 сек)
|
||||
- **Причина:** `debug=True` в app.run(), жёсткие версии в requirements.txt
|
||||
- **Решение:** убрал debug, смягчил версии, health → plain text
|
||||
|
||||
### Попытка #2 — 12:12, Под(ы) не работают (ImportError)
|
||||
- **Причина:** `from . import db` — относительные импорты не работают при `python app.py` из папки site/
|
||||
- **Ошибка:** `ImportError: attempted relative import with no known parent package`
|
||||
- **Решение:** заменил на абсолютные: `import db`, `from test_routes import test_bp`
|
||||
|
||||
### Попытка #3 — 12:18, Успех, но БД fail
|
||||
- Health: OK
|
||||
- DB: fail — переменные не заданы
|
||||
|
||||
### Попытка #4 — ~12:31, БД fail — password auth failed
|
||||
- Хост резолвится (10.102.125.70), порт доступен
|
||||
- Ошибка: `password authentication failed for user "contracts"`
|
||||
- Пароль от ipwhitelist не подходит для пользователя contracts
|
||||
|
||||
### Попытка #5 — 12:35, db.connect() возвращает ошибки
|
||||
- Улучшена диагностика — теперь /test показывает точную ошибку
|
||||
|
||||
### Попытка #6 — ~12:38, БД fail — database "contracts" does not exist
|
||||
- Аутентификация прошла (пароль исправлен)
|
||||
- База не существует
|
||||
|
||||
### Попытка #7 — 12:41, добавлен /test createdb
|
||||
- Создан endpoint для создания БД через API
|
||||
|
||||
### Попытка #8 — 12:49, БАЗА СОЗДАНА ✅
|
||||
- `POST /test {"action":"createdb"}` → `DB 'contracts' created`
|
||||
- `/test` → `db: "ok"`
|
||||
- Таблицы: только pg_stat_* (служебные)
|
||||
|
||||
---
|
||||
|
||||
## Решения по хранению (принято)
|
||||
|
||||
- ❌ S3 / приватная репа / файловая система — данные уходят за пределы облака
|
||||
- ❌ SQLite — не подходит для облачного сервиса
|
||||
- ✅ **PostgreSQL BYTEA + JSONB** — всё внутри кластера, без внешних зависимостей
|
||||
- `contract_docs` — исходные файлы (original_bytes BYTEA) + сырой парсинг (parsed_json JSONB)
|
||||
|
||||
---
|
||||
|
||||
## Парсер (parser.py)
|
||||
|
||||
Принцип: **ничего не фильтровать, не терять ни символа**.
|
||||
|
||||
```
|
||||
docx ──▶ python-docx ──▶ elements: [{paragraph, style, text}, {table, rows}, ...]
|
||||
.doc ──▶ libreoffice ──▶ docx ──▶ python-docx
|
||||
pdf ──▶ pdfplumber ──▶ страницы → текст + таблицы
|
||||
zip ──▶ zipfile ──▶ рекурсивно parse() каждый файл
|
||||
```
|
||||
|
||||
Результат: полный слепок документа → в БД → дальше LLM разбирает.
|
||||
|
||||
Протестирован на реальном примере (спецификация-XXX001-03700.docx):
|
||||
- 12 элементов (4 таблицы + 8 параграфов)
|
||||
- Все стили сохранены
|
||||
- Ничего не потеряно
|
||||
|
||||
---
|
||||
|
||||
## Текущая структура проекта
|
||||
|
||||
```
|
||||
contracts-app/
|
||||
├── Dockerfile
|
||||
├── requirements.txt ← flask, gunicorn, python-docx, requests, psycopg2-binary, python-dotenv, pdfplumber
|
||||
├── .env.example
|
||||
├── README.md
|
||||
└── site/
|
||||
├── __init__.py
|
||||
├── app.py ← класс ContractsApp, сборка слоёв
|
||||
├── db.py ← слой БД: connect(), query(), _pg_connect(), ensure_db()
|
||||
├── test_routes.py ← слой /test: статус, tables, sql, createdb
|
||||
├── parser.py ← слой парсера: parse() для docx/pdf/doc/zip
|
||||
├── static/css/
|
||||
└── templates/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация (актуальная)
|
||||
|
||||
```json
|
||||
// clusterConfiguration
|
||||
{ "cpu": "500", "memory": "1024", "replicas": "1" }
|
||||
|
||||
// accessConfiguration
|
||||
{ "domain": "contractor" }
|
||||
// URL: https://contractor.pythonk8s.services.ngcloud.ru
|
||||
|
||||
// appConfiguration
|
||||
{
|
||||
"gitPath": "https://gitea.services.ngcloud.ru/Nail/contracts-app.git",
|
||||
"version": "3.12",
|
||||
"healthPath": "/health"
|
||||
}
|
||||
|
||||
// jsonEnv
|
||||
{
|
||||
"DB_HOST": "postgresqlk8s-master.60bdf3e3-5087-41ff-b760-fe6ea544a80e.svc.cluster.local",
|
||||
"DB_NAME": "contracts",
|
||||
"DB_PASS": "****",
|
||||
"DB_PORT": "5432",
|
||||
"DB_USER": "contracts",
|
||||
"DB_SSLMODE": "disable",
|
||||
"LLM_API_KEY": "sk-ucI...",
|
||||
"LLM_API_URL": "https://api.aillm.ru/v1/chat/completions"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сага с LLM (aillm.ru + HTTP/2)
|
||||
|
||||
### Проблема
|
||||
Python-библиотеки (`requests`, `httpx`) не держат HTTP/2 из коробки.
|
||||
`api.aillm.ru` за ddos-guard, который **требует HTTP/2 от облачных IP**.
|
||||
С локальной машины (81.200.23.210) даже HTTP/1.1 работает — для этого IP ddos-guard делает исключение.
|
||||
|
||||
### Хронология попыток
|
||||
|
||||
| # | Подход | Результат |
|
||||
|---|---|---|
|
||||
| 1 | `requests` (HTTP/1.1) | ❌ 401 token_not_found_in_db (ложная ошибка от ddos-guard) |
|
||||
| 2 | `httpx` с `http2=True` | ❌ то же самое — `h2` пакет не установился в контейнере |
|
||||
| 3 | `httpx[http2]` + `h2` в requirements | ❌ `h2` не ставится через pip в slim-контейнере |
|
||||
| 4 | `subprocess.run(['curl', '-s', '--http2', ...])` | ⬜ ждёт редеплоя |
|
||||
|
||||
### Ключевые находки
|
||||
|
||||
1. **Хеш ключа не совпадал:** локально `5fad79ba...`, в облаке `c772c3e5...`.
|
||||
Оказалось — не разные ключи, а `ddos-guard` возвращает ложный 401 при HTTP/1.1,
|
||||
хешируя какой-то внутренний токен, а не наш ключ.
|
||||
|
||||
2. **Node.js `fetch()` работает** (проект `say`) — потому что undici (нативный fetch в Node)
|
||||
поддерживает HTTP/2 из коробки. Python — нет.
|
||||
|
||||
3. **`curl --http2`** — единственный гарантированный способ получить HTTP/2 в Python-контейнере,
|
||||
без дополнительных пакетов.
|
||||
|
||||
4. **Доступные модели:** `gpt-oss-120b`, `qwen3.6-27b-fp8`, `qwen3-6-27b-fp8-opt`, `whisper-large-v3-turbo`.
|
||||
|
||||
### Решение
|
||||
`llm_client.py` использует `subprocess.run(['curl', '-s', '--http2', ...])` вместо Python HTTP-библиотек.
|
||||
|
||||
---
|
||||
|
||||
## Хронология коммитов (14.06.2026)
|
||||
|
||||
### DB слой
|
||||
- `ba2e855` — `db.py`: `execute()` + `query_one()` для INSERT/UPDATE/DELETE и единичного SELECT
|
||||
- `4635ed4` — `test_routes`: action `exec` через `db.execute()` (DDL без fetch)
|
||||
- `9d01dba` — `schema.py`: `db.query → db.execute` для DDL
|
||||
|
||||
### Upload слой
|
||||
- `8db35e3` — `upload.py`: Blueprint `/upload`, поток bytes→parser→textify→DB
|
||||
- `db2f6cc` — fix MIME: приоритет расширения над content_type
|
||||
|
||||
### LLM слой
|
||||
- `44e53ed` — `llm_client.py`: HTTP-клиент (сначала `requests`)
|
||||
- `fe8a556` — `test_routes`: action `llm` для теста
|
||||
- `c580c93` — fix: модель `gpt-oss-120b` (не `deepseek-chat`)
|
||||
- `5904830` — fix: `httpx[http2]` вместо `requests`
|
||||
- `c97f26d` — fix: `h2` явно в requirements
|
||||
- `099edbc` — fix: `curl --http2` через `subprocess` вместо httpx
|
||||
|
||||
### Debug
|
||||
- `cc3203a` — `test_routes`: action `debug` для просмотра env vars
|
||||
- `0d0ba18` — debug показывает длину ключа
|
||||
- `b457de4` — debug показывает `h2_available` и `httpx_version`
|
||||
|
||||
---
|
||||
|
||||
## Развязка саги с LLM: опечатка в ключе
|
||||
|
||||
После 2 часов попыток (HTTP/2, h2, curl, httpx) причина оказалась простой:
|
||||
**в переменной `LLM_API_KEY` в платформе была опечатка**.
|
||||
|
||||
Сравнение хешей:
|
||||
- Правильный ключ: `sk-ucI5YvOticoOQ9Kuj5K9mQ` → hash `5fad79ba`
|
||||
- Ключ в облаке: `sk-ucI5YvOticoOQK9ujK5m9Q` → hash `c772c3e5` (ошибка 401)
|
||||
|
||||
Перепутаны местами `9K` → `K9` и `5K` → `K5`.
|
||||
|
||||
**Урок:** всегда первым делом проверять входные данные, а не гнаться за архитектурными гипотезами.
|
||||
|
||||
### Финальное решение LLM
|
||||
`httpx` + `h2` (отдельно в requirements) + `http2=True`. Работает.
|
||||
Модель: `gpt-oss-120b` (120B, reasoning).
|
||||
|
||||
---
|
||||
|
||||
## Extractor (extractor.py) — 14.06.2026
|
||||
|
||||
LLM-извлечение строк спецификации из распарсенного текста.
|
||||
|
||||
**Поток:** `parsed_text → промпт → LLM → JSON → [{row_num, name, price, qty, sum, date}]`
|
||||
|
||||
**Результат на реальном документе:** 14 строк из двух таблиц спецификации.
|
||||
LLM корректно определила изменение цены (213 905 → 250 000) и разные даты (01.04 → 25.04).
|
||||
|
||||
Промпт: строгий формат JSON, поддержка `unresolved` для нераспознанных значений.
|
||||
|
||||
---
|
||||
|
||||
## Differ (differ.py) — 14.06.2026
|
||||
|
||||
Сравнение строк спецификаций между допниками. Чистая функция без зависимостей.
|
||||
|
||||
**Вход:** `rows_old`, `rows_new` (списки dict)
|
||||
**Выход:** `{changes: [{row_num, change_type, old_values, new_values, changed_fields}], summary}`
|
||||
|
||||
**change_type:** `added` | `deleted` | `changed` | `unchanged`
|
||||
|
||||
**Тест:** row 1 (changed: price, date), row 2 (unchanged), row 3 (added) ✅
|
||||
|
||||
---
|
||||
|
||||
## Текущий статус (14.06.2026 08:15)
|
||||
|
||||
### Готовые слои (11 файлов)
|
||||
```
|
||||
site/
|
||||
├── app.py — сборка (ensure_schema + Blueprints)
|
||||
├── db.py — транспорт БД (connect, query, execute, query_one)
|
||||
├── schema.py — DDL 5 таблиц
|
||||
├── parser.py — bytes → elements (docx/pdf/doc/zip)
|
||||
├── textify.py — elements → текст
|
||||
├── upload.py — POST /upload (file → parse → textify → DB)
|
||||
├── llm_client.py — HTTP/2 к aillm.ru (httpx+h2)
|
||||
├── extractor.py — текст → LLM → строки спецификации
|
||||
├── differ.py — сравнение строк между версиями
|
||||
├── test_routes.py — /test (status, tables, sql, exec, llm, extract, debug, createdb)
|
||||
└── templates/
|
||||
```
|
||||
|
||||
### Осталось
|
||||
- `api.py` — Blueprint для договоров/допников/истории
|
||||
- Интеграция цепочки: upload → extract → save spec_rows → diff → spec_history
|
||||
@@ -0,0 +1,49 @@
|
||||
# Проблемы проекта Contracts App (15.06.2026)
|
||||
|
||||
_Дата: 15.06.2026_
|
||||
_Статус: Анализ и выученные уроки_
|
||||
|
||||
---
|
||||
|
||||
## 1. Технические ошибки и инструменты
|
||||
|
||||
### ⛔ replace_string_in_file портит файлы
|
||||
Инструмент заменяет подстроку, а не файл целиком. При повторных правках возникали дубликаты кода (например, два блока `if __name__ == "__main__"`) или обрезка функций.
|
||||
**Решение:** При больших правках использовать `create_file` для перезаписи файла целиком, либо очень внимательно выбирать контекст в `oldString`.
|
||||
|
||||
### ⛔ Опечатка в API-ключе (c772c3e5 vs 5fad79ba)
|
||||
Два часа отладки HTTP/2 и `httpx` из-за опечатки в `LLM_API_KEY` (перепутаны символы `9K` -> `K9`).
|
||||
**Урок:** Проверять валидность ключей и выводить первые/последние символы хеша в логи при ошибках 401.
|
||||
|
||||
### ⛔ Истёкший SSL-сертификат api.aillm.ru
|
||||
Сертификат протух 14 июня 2026. `httpx` блокировал запросы.
|
||||
**Временное решение:** `verify=False` в клиенте LLM.
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектура и Сеть
|
||||
|
||||
### ⛔ ERR_HTTP2_PROTOCOL_ERROR (Таймауты Ingress)
|
||||
LLM обрабатывает документ 30-40 секунд. Ingress в K8s обрывает соединение на 30-й секунде.
|
||||
**Решение:** Разделение процессов. Загрузка — мгновенно. Обработка LLM — по отдельному запросу с фронтенда с визуальным таймером, чтобы пользователь понимал, что процесс идет.
|
||||
|
||||
### ⛔ AJAX fetch() -> Failed to fetch
|
||||
Сложная логика на фронте (очереди, JSZip) ломалась.
|
||||
**Решение:** Упрощение до стандартных форм. Паттерн **PRG (Post-Redirect-Get)** для устранения проблемы «Подтвердите повторную отправку формы» при нажатии F5.
|
||||
|
||||
---
|
||||
|
||||
## 3. Фронтенд и Контент
|
||||
|
||||
### ⛔ Битый логотип
|
||||
`nubes-logo.svg` содержал текст "Forbidden" (ошибка копирования через `curl`).
|
||||
**Урок:** Проверять содержимое статических файлов после скачивания.
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (v1.0)
|
||||
|
||||
1. **Упрощение:** Форма `<input type="file" multiple>` + стандартный `submit`.
|
||||
2. **Надежность:** POST на `/` загружает и парсит файлы, затем делает `redirect` на страницу договора.
|
||||
3. **LLM:** Отдельная кнопка «Обработать (LLM)» внутри договора, чтобы избежать таймаутов при массовой загрузке.
|
||||
4. **Интерфейс:** Добавлен таймер обработки и версия **v1.0** в шапке.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Анализ ошибки ERR_HTTP2_PROTOCOL_ERROR (15.06.2026)
|
||||
|
||||
## Проблема
|
||||
При загрузке нескольких или тяжелых файлов (PDF/DOCX) через форму на главной странице, браузер через 30 секунд выдает ошибку `ERR_HTTP2_PROTOCOL_ERROR`.
|
||||
|
||||
## Причина
|
||||
В текущей реализации `app.py` функция `_upload_files` выполняет **синхронный парсинг** каждого файла в цикле:
|
||||
1. Сохранение байтов в БД.
|
||||
2. `parser_mod.parse(file_bytes, mime)` — тяжелая операция (особенно PDF через `pdfplumber` или DOC через `libreoffice`).
|
||||
3. Формирование текста через `textify`.
|
||||
4. Обновление БД.
|
||||
|
||||
Если суммарное время парсинга всех файлов превышает 30 секунд, K8s Ingress обрывает соединение, не дождавшись HTTP-ответа (Redirect) от Flask.
|
||||
|
||||
## План действий
|
||||
1. **Разделить загрузку и парсинг**:
|
||||
* В `_upload_files` (POST /) только сохранять файлы в БД со статусом `uploaded`.
|
||||
* Сразу делать редирект на страницу договора.
|
||||
2. **Перенести парсинг в процесс обработки**:
|
||||
* В `_process` (POST /process/<cid>) добавить шаг: если документ еще не распарсен (`status='uploaded'`), сначала вызвать `parser` и `textify`.
|
||||
3. **Визуализация**:
|
||||
* Пользователь на странице договора увидит кнопку «Обработать» с таймером. Теперь и парсинг, и LLM будут работать под этим таймером, не блокируя загрузку.
|
||||
|
||||
## Статус
|
||||
- [x] Оптимизировать `app.py` (убрать парсинг из загрузки).
|
||||
- [x] Обновить `_process` (добавить ленивый парсинг).
|
||||
- [x] Добавить таймер на загрузку.
|
||||
- [x] **РЕШЕНО**: Перевод обработки в Background Thread (threading) для обхода таймаутов Ingress.
|
||||
- [x] **ФИКС ZIP**: Добавлена логика обхода вложенных файлов в `_run_processing_task` (раньше ZIP возвращал пустой текст).
|
||||
|
||||
## План "Асинхронное спасение" (15.06.2026 16:00)
|
||||
1. **Threading**: В `app.py` запуск обработки в фоновом потоке, чтобы сразу вернуть ответ 200 OK.
|
||||
2. **Polling**: Фронтенд опрашивает статус обработки.
|
||||
3. **No More POST**: Замена классической формы на `fetch()`, чтобы убрать окно "Подтвердите отправку" при F5.
|
||||
|
||||
## Дополнение от 15:40
|
||||
Проблема `ERR_HTTP2_PROTOCOL_ERROR` на загрузке (даже без парсинга) указывает на то, что сама передача байтов в БД через `db.execute` для нескольких файлов занимает более 30 секунд.
|
||||
Решение:
|
||||
1. Визуальный таймер поможет понять реальное время «зависания».
|
||||
2. Если ошибка повторится — это жесткий лимит Ingress на размер тела запроса (Client Max Body Size) или таймаут записи в сетевую БД.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Исправление PRG-паттерна и окна повторной отправки (15.06.2026)
|
||||
|
||||
## Проблема
|
||||
При нажатии F5 (перезагрузка) браузер выдает окно «Подтвердите повторную отправку формы». Это происходит потому, что последним запросом был POST (на загрузку или на обработку), и страница отображается как результат этого POST-запроса.
|
||||
|
||||
## Решение: PRG (Post-Redirect-Get)
|
||||
Все POST-обработчики должны завершаться инструкцией `redirect(...)`.
|
||||
1. Пользователь отправляет POST.
|
||||
2. Сервер обрабатывает данные.
|
||||
3. Сервер возвращает статус 302 (Redirect).
|
||||
4. Браузер делает автоматический GET на новый URL.
|
||||
5. Теперь в истории браузера последний запрос — GET. При нажатии F5 просто обновится страница без всяких окон.
|
||||
|
||||
## Что сделано:
|
||||
- В `app.py` проверено, что `_upload_files` делает `redirect("/?id=" + contract_id)`.
|
||||
- В `app.py` проверено, что `_process` делает `redirect("/?id=" + cid)`.
|
||||
- Добавлена очистка параметров, если они могут вызвать зацикливание.
|
||||
|
||||
## Статус
|
||||
- [x] Реализован Redirect после всех POST-запросов.
|
||||
- [x] Окно подтверждения больше не должно появляться при F5 на странице договора.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Сессия #5 — SSE-парсинг, интерактивный прогресс, отказ от LLM
|
||||
|
||||
_Дата: 2026-06-16_
|
||||
_Версия: v1.0 → v1.8 (8 повышений)_
|
||||
_Коммиты: fa4c7fe → e0c58fc (8 шт.)_
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
Пользователь (Наиль) захотел:
|
||||
1. Видеть **интерактивный прогресс** при обработке файлов (имя + таймер в секундах)
|
||||
2. После обработки — **сводка**: общее время, байты
|
||||
3. **Никакого LLM** пока — только парсинг
|
||||
4. Вспомнил что очередь файлов (JS-слой) уже была реализована в коммите `17c4841`
|
||||
|
||||
---
|
||||
|
||||
## Шаг 1: SSE-поток для LLM (v1.1, fa4c7fe)
|
||||
|
||||
**Что сделано:**
|
||||
- `_process` в app.py переделан с блокирующего POST на SSE-поток (`text/event-stream`)
|
||||
- Маршрут `/process/<cid>` сменён на GET (нужно для `EventSource`)
|
||||
- События: `file_start` → `file_done`/`file_error` → `summary`
|
||||
- В шаблоне — JavaScript с `EventSource`, живые таймеры (каждые 500мс)
|
||||
|
||||
**Ошибка:** Назвал деплой «поды в Kubernetes» — пользователь поправил: это **managed service** на pythonk8s.services.ngcloud.ru, деплой = `git push master`.
|
||||
|
||||
**Ошибка:** Забыл повысить версию → v1.1 отдельным коммитом.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2: Полный отказ от LLM (v1.2, 3cffca7)
|
||||
|
||||
**Причина:** Пользователь сказал «НЕ НАДО ЛЛМ и на экране тоже».
|
||||
|
||||
**Что сделано:**
|
||||
- `_process` → `_parse` — только парсинг, без `extractor.extract()` и `differ.diff()`
|
||||
- `_upload_files` — только сохраняет файлы в БД, без парсинга на этом этапе
|
||||
- `POST /` возвращает JSON `{"contract_id": "..."}` вместо редиректа
|
||||
- Весь LLM (`extractor`, `differ`, `llm_client`) убран из основного потока
|
||||
|
||||
**Интерфейс:**
|
||||
- Файлы → сразу в таблицу (имя, дата изменения, размер)
|
||||
- ZIP — JSZip показывает содержимое в браузере
|
||||
- Кнопка **«Парсинг»** (не «Обработать LLM»)
|
||||
- Клиент: POST файлов через `fetch`, затем SSE `/parse/<cid>`
|
||||
- Прогресс в столбце «Статус»: ⏳ Xс → ✓ X.Xс
|
||||
- Итоговая строка: ✓ Готово: N файлов | размер | время
|
||||
|
||||
---
|
||||
|
||||
## Шаг 3: ZIP — файлы внутри не парсились (v1.3, 21f8c0b)
|
||||
|
||||
**Проблема:** ZIP-файл парсился как единое целое (✓ 0.2с), а файлы внутри него (`допник-1.doc`, `спецификация.docx`...) оставались с `—` в статусе.
|
||||
|
||||
**Причина:** В `_parse` парсер просто сливал все elements из ZIP в один документ. Файлы из JSZip-превью были только в браузере, сервер про них не знал.
|
||||
|
||||
**Решение:**
|
||||
- Сервер сам открывает ZIP (`zipfile.ZipFile`)
|
||||
- Для каждого файла внутри: создаёт отдельный `documents` + `supplements`, парсит, шлёт SSE-события
|
||||
- JSZip и сервер используют одинаковые имена → `findRowByName` находит строку
|
||||
- ZIP-строка показывает `↗ N файлов` вместо галочки
|
||||
|
||||
---
|
||||
|
||||
## Шаг 4: Подпапки в ZIP (v1.4, d5858d7)
|
||||
|
||||
**Проблема:** Если в ZIP есть подпапки, и в разных папках файлы с одинаковыми именами — конфликт.
|
||||
|
||||
**Решение:** Использовать полный относительный путь (`подпапка/файл.docx`) вместо basename.
|
||||
|
||||
Изменено в двух местах:
|
||||
- **Клиент**: `relativePath` вместо `relativePath.split('/').pop()`
|
||||
- **Сервер**: `zname` вместо `zbasename`
|
||||
|
||||
---
|
||||
|
||||
## Шаг 5: Только PDF и Word (v1.5, 435f617)
|
||||
|
||||
**Проблема:** Из ZIP парсились ВСЕ файлы (включая `.txt`, `.xlsx`), давая пустые результаты.
|
||||
|
||||
**Решение:** Фильтр по MIME-типу:
|
||||
- Сервер: `if zmime not in (pdf, docx, doc): continue`
|
||||
- Клиент (JSZip): `if ['docx','doc','pdf'].indexOf(ext) === -1: return`
|
||||
|
||||
---
|
||||
|
||||
## Архитектура (текущая)
|
||||
|
||||
```
|
||||
app.py:
|
||||
POST / → _upload_files() → сохранить файлы в БД, вернуть JSON {contract_id}
|
||||
GET / → _index() → render_template("upload.html")
|
||||
GET /parse/<cid> → _parse() → SSE-поток парсинга
|
||||
|
||||
_parse():
|
||||
1. Для каждого документа договора:
|
||||
- Обычный файл → parser_mod.parse() → сохранить parsed_text
|
||||
- ZIP → zipfile.ZipFile() → для каждого PDF/Word внутри:
|
||||
создать document + supplement → parser_mod.parse() → сохранить
|
||||
2. SSE-события: file_start → file_done/file_error → zip_expanded → summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Выученные уроки
|
||||
|
||||
1. **⛔ Не фантазировать про инфраструктуру.** Managed service ≠ Kubernetes. Деплой = `git push`.
|
||||
2. **Версия — всегда.** После каждого изменения кода — bump.
|
||||
3. **ZIP требует особой обработки.** Недостаточно просто распарсить — нужно создать документы для каждого файла внутри.
|
||||
4. **Имена должны совпадать** между клиентом (JSZip) и сервером (zipfile) — иначе `findRowByName` не находит строку.
|
||||
5. **`replace_string_in_file` портит файлы** — для больших правок использовать heredoc (`cat > file`).
|
||||
|
||||
---
|
||||
|
||||
## Шаг 6: Таймаут при загрузке (v1.6, 64b2295)
|
||||
|
||||
**Проблема:** Все файлы отправлялись одним POST-запросом → >30с → Ingress убивал соединение → «NetworkError when attempting to fetch resource». Никакого прогресса загрузки не было видно.
|
||||
|
||||
**Решение:**
|
||||
- `_upload_files` поддерживает `?cid=X` — первый файл создаёт договор, остальные добавляются
|
||||
- Клиент: загрузка по одному файлу через XHR с `upload.onprogress`
|
||||
- Прогресс: ↑ 0% → ↑ 45% → ↑ 100% → ✓ загружен
|
||||
- XHR таймаут: 60 секунд на файл
|
||||
- Размеры файлов из ZIP — через `zipEntry._data.uncompressedSize`
|
||||
- Размеры выровнены вправо (`.num-cell`)
|
||||
|
||||
---
|
||||
|
||||
## Шаг 7: Прогресс для файлов из ZIP не показывался (v1.7, c710a2d)
|
||||
|
||||
**Проблема:** `findRowByName` имел условие `&& !fileQueue[i].isZipChild`, поэтому
|
||||
SSE-события для файлов из ZIP не находили строку в таблице — статус оставался пустым.
|
||||
|
||||
**Причина:** в v1.6 `findRowByName` использовалась и для upload (realFiles)
|
||||
и для parse (все файлы). Фильтр защищал от нахождения zip-child при upload,
|
||||
но ломал parse.
|
||||
|
||||
**Решение:** убрать `!fileQueue[i].isZipChild`. Для upload это безопасно —
|
||||
`realFiles` и так фильтрует `!isZipChild` перед вызовом.
|
||||
|
||||
**БД:** проверена — все 5 файлов из ZIP созданы, 4 распарсены, 1 .doc с пустым parsed_text (нет LibreOffice).
|
||||
|
||||
---
|
||||
|
||||
## Шаг 8: Баги после полной проверки (v1.8, e0c58fc)
|
||||
|
||||
**Баг 1 — `zip_expanded` счётчик:** `zip_names.append(zname)` стоял ДО фильтра PDF/Word,
|
||||
поэтому «↗ N файлов» включал .txt, .xlsx и т.д. **Исправлено:** перенос после фильтра.
|
||||
|
||||
**Баг 2 — повторный `_parse` пересоздаст ZIP-файлы:** проверка `status='parsed'` пропускала
|
||||
только распарсенные, но ZIP имеют статус `expanded`. При повторном вызове `_parse`
|
||||
ZIP обрабатывался заново, создавая дубликаты. **Исправлено:** `status IN ('parsed','expanded')`.
|
||||
|
||||
**Другие проверки (без ошибок):**
|
||||
- `resetBtn()` — function declaration, hoisting работает, вызов до определения корректен
|
||||
- `removeFile` — удаление детей перед родителем, индексы не смещаются (дети всегда после родителя)
|
||||
- `fileTable.appendChild(summaryRow)` — повторные запуски создадут несколько строк-итогов (допустимо)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Сессия #6 — elements_json, структура документа в БД
|
||||
|
||||
_Дата: 2026-06-17_
|
||||
_Версия: v1.12_
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
После отладки парсинга выяснилось: `parsed_text` хранит плоский текст (таблицы слиты через `--- Таблица ---`), структура теряется. Для сравнения договоров нужна полная структура: где параграф, где таблица, какие стили, номера страниц.
|
||||
|
||||
## Решение
|
||||
|
||||
### schema.py
|
||||
- `documents.elements_json JSONB` — новая колонка
|
||||
- `ALTER TABLE ADD COLUMN IF NOT EXISTS` — миграция для существующих таблиц
|
||||
|
||||
### parser.py
|
||||
- PDF: каждый `element` теперь содержит `"page": N` (номер страницы из pdfplumber)
|
||||
|
||||
### app.py
|
||||
- `_parse`: `db.execute("... SET parsed_text=%s, elements_json=%s ...")` — сохраняется `json.dumps(elements)`
|
||||
|
||||
## Формат elements_json
|
||||
|
||||
```json
|
||||
[
|
||||
{"type":"paragraph","style":"Heading 1","text":"Спецификация"},
|
||||
{"type":"table","rows":[["Услуга","Цена"],["Хостинг","1000"]],"page":1},
|
||||
{"type":"paragraph","style":"Normal","text":"Итого:","page":1}
|
||||
]
|
||||
```
|
||||
|
||||
Для PDF: `page`, `style`="" (pdfplumber не даёт стилей).
|
||||
Для DOCX: `style` из Word, `page` нет (но есть разрывы страниц в XML — не извлечены пока).
|
||||
|
||||
## Что это даёт
|
||||
|
||||
| Запрос | SQL |
|
||||
|---|---|
|
||||
| Все таблицы документа | `jsonb_array_elements(elements_json) WHERE value->>'type'='table'` |
|
||||
| Ячейка [2][3] таблицы 1 | `elements_json->0->'rows'->2->3` |
|
||||
| Всё со страницы 5 | `WHERE value->>'page'='5'` |
|
||||
| Все Heading | `WHERE value->>'style' LIKE 'Heading%'` |
|
||||
|
||||
`parsed_text` сохранён для LLM/полнотекстового поиска.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Сессия #7 — чанковая загрузка, worker'ы, одно соединение
|
||||
|
||||
_Дата: 2026-06-17_
|
||||
_Версия: v1.15 → v1.18_
|
||||
|
||||
---
|
||||
|
||||
## Проблема
|
||||
|
||||
Загрузка PDF 153 KB падала с «✗ Сеть»:
|
||||
- v1.14: прямая загрузка (один POST) → Ingress режет `client_max_body_size`
|
||||
- v1.15-16: чанки 50KB в памяти `_chunk_store = {}` → worker'ы разные, чанки теряются
|
||||
- v1.17: чанки в PostgreSQL (таблица `chunks`) → всё равно «Сеть»
|
||||
- v1.18: ОДНО DB-соединение на сборку → ?
|
||||
|
||||
## Хронология
|
||||
|
||||
### v1.14: прямая загрузка
|
||||
`POST /` с файлом 153 KB → Ingress (nginx) обрывает → `XHR onerror` → «Сеть»
|
||||
|
||||
### v1.15: чанки 50KB в памяти
|
||||
`POST /chunk` × 3. `_chunk_store = {}` в памяти модуля.
|
||||
**Ошибка:** dict живёт в одном worker'е. Gunicorn с >1 worker → чанки на разных процессах → второй не видит первый → обрыв.
|
||||
|
||||
### v1.16: try/except + проверка doc_id
|
||||
Добавлена обработка ошибок при сборке. **Откачено** (не решило проблему worker'ов).
|
||||
|
||||
### v1.17: чанки в БД
|
||||
Таблица `chunks(upload_id, chunk_index, chunk_data, ...)`. `ON CONFLICT DO NOTHING`.
|
||||
**Всё равно «Сеть»** — worker'ы больше не проблема, но осталась другая.
|
||||
|
||||
### v1.18: одно DB-соединение (текущая)
|
||||
**Диагноз:** `_chunk_upload` при сборке делал до 7 отдельных `db.execute`/`db.query` —
|
||||
каждое = новый `psycopg2.connect`. На managed-платформе это медленно → таймаут Ingress → обрыв.
|
||||
|
||||
**Исправление:**
|
||||
- `_save_file_to_db(filename, file_bytes, mime, contract_id, conn=None)` — принимает соединение
|
||||
- `_chunk_upload` открывает ОДНО соединение на всю сборку:
|
||||
- SELECT chunks
|
||||
- INSERT contracts (если нужно)
|
||||
- INSERT documents
|
||||
- SELECT id документа
|
||||
- INSERT supplements
|
||||
- DELETE chunks
|
||||
- Все 6 запросов в одной транзакции, одно подключение.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура чанков (текущая)
|
||||
|
||||
```
|
||||
Клиент: file.slice(0,50KB) → XHR POST /chunk
|
||||
file.slice(50KB,100KB) → XHR POST /chunk
|
||||
file.slice(100KB,150KB) → XHR POST /chunk
|
||||
|
||||
Сервер /chunk:
|
||||
1. INSERT INTO chunks (...) ON CONFLICT DO NOTHING ← 1 conn
|
||||
2. SELECT COUNT(*) FROM chunks ← 1 conn
|
||||
3. Если received == total_chunks:
|
||||
ОДНО соединение:
|
||||
SELECT chunk_data ORDER BY chunk_index
|
||||
SELECT metadata
|
||||
INSERT INTO contracts (если нет cid)
|
||||
COMMIT
|
||||
→ _save_file_to_db(conn=...)
|
||||
INSERT INTO documents
|
||||
SELECT id
|
||||
INSERT INTO supplements
|
||||
COMMIT
|
||||
DELETE FROM chunks
|
||||
ОТВЕТ {"contract_id": "..."}
|
||||
```
|
||||
|
||||
## Выученные уроки
|
||||
|
||||
1. **`_chunk_store` в памяти не работает с несколькими worker'ами** — нужна общая БД.
|
||||
2. **Много отдельных `db.execute` = много `psycopg2.connect`** — на managed-платформе медленно.
|
||||
3. **Всегда одно соединение для зависимых операций** — сборка + сохранение должны быть атомарны.
|
||||
4. **Анализ агентом помог** — указал на `_save_file_to_db` как узкое место.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Сессия #8 — Upload Saga: 26 версий до рабочей загрузки
|
||||
|
||||
_Дата: 2026-06-17_
|
||||
_Версия: v1.12 → v1.26_
|
||||
|
||||
---
|
||||
|
||||
## Проблема
|
||||
|
||||
Загрузка файлов > 50-100 KB на managed-платформе pythonk8s.services.ngcloud.ru случайно обрывалась с `✗ Сеть` (XHR onerror, HTTP 000).
|
||||
|
||||
## Хронология попыток
|
||||
|
||||
| v | Что пробовали | Результат |
|
||||
|---|---|---|
|
||||
| 1.12-1.13 | FormData/multipart POST | ✗ Сеть > 100KB |
|
||||
| 1.14 | Promise fix (uploadFileXHR) | ✗ |
|
||||
| 1.15-1.17 | Чанки 50KB FormData → БД | Только чанк 0, остальные не доходят |
|
||||
| 1.18 | Одно DB-соединение вместо 7 | ✗ |
|
||||
| 1.19 | debug_log таблица | ✗ |
|
||||
| 1.20 | Пул соединений psycopg2 | ✗ |
|
||||
| 1.21 | base64 JSON вместо FormData | ✗ |
|
||||
| 1.22 | Пул: keepalive, reconnect | ✗ |
|
||||
| 1.23 | Всегда одно соединение (убрали if/cid) | ✗ |
|
||||
| 1.24-1.25 | base64 как TEXT (без decode) + логи | ✗ — `original_bytes NOT NULL` |
|
||||
| **1.26** | **`original_bytes DROP NOT NULL`** | **✓ РАБОТАЕТ** |
|
||||
|
||||
## Корневые причины (две)
|
||||
|
||||
### 1. `original_bytes BYTEA NOT NULL`
|
||||
Вставляли только `original_b64 TEXT`, а `original_bytes` требовал значения. PostgreSQL выдавал 500, но клиент не видел ответа из-за таймаута Ingress.
|
||||
|
||||
### 2. `base64.b64decode()` при загрузке
|
||||
Декодирование 200KB base64 → 150KB байт → BYTEA INSERT занимало >30 сек. Ingress обрывал соединение.
|
||||
|
||||
## Рабочий рецепт
|
||||
|
||||
```python
|
||||
# _upload_json (app.py)
|
||||
# 1. Принимаем base64 как строку
|
||||
# 2. INSERT TEXT (не BYTEA) — мгновенно
|
||||
# 3. Без base64.decode()
|
||||
# 4. Одно DB-соединение на весь запрос
|
||||
|
||||
cur.execute(
|
||||
"INSERT INTO documents (filename, mime_type, original_b64, status) VALUES (%s,%s,%s,'uploaded')",
|
||||
(filename, mime, b64), # b64 — строка как есть
|
||||
)
|
||||
```
|
||||
|
||||
```sql
|
||||
-- schema.py
|
||||
original_bytes BYTEA, -- было NOT NULL → теперь nullable
|
||||
original_b64 TEXT, -- новое: base64 строка
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Клиент: FileReader.readAsDataURL → base64 → XHR JSON POST /upload
|
||||
xhr.send(JSON.stringify({
|
||||
filename: file.name,
|
||||
data: b64, // base64 строка
|
||||
cid: contractId
|
||||
}));
|
||||
```
|
||||
|
||||
## Пул соединений (db.py)
|
||||
|
||||
```python
|
||||
ThreadedConnectionPool(
|
||||
minconn=2, maxconn=5,
|
||||
keepalives=1,
|
||||
keepalives_idle=30,
|
||||
keepalives_interval=10,
|
||||
keepalives_count=3,
|
||||
connect_timeout=10,
|
||||
)
|
||||
# Проверка живости: SELECT 1 при каждом getconn
|
||||
# Переподключение при OperationalError/InterfaceError
|
||||
```
|
||||
|
||||
## Как дебажили
|
||||
|
||||
1. `debug_log` таблица — 12 шагов лога в `_upload_json`
|
||||
2. curl тесты: 10×150KB — случайные HTTP 000
|
||||
3. `/test` endpoint: прямой SQL INSERT — работал, но только с малыми телами
|
||||
4. Исключение FormData → переход на JSON
|
||||
5. Исключение `base64.decode()` → TEXT как есть
|
||||
6. `ALTER TABLE ... DROP NOT NULL` — последний фикс
|
||||
|
||||
## Выученные уроки
|
||||
|
||||
1. **Managed-платформа ≠ свой сервер** — лимиты невидимы и неконтролируемы
|
||||
2. **TEXT INSERT быстрее BYTEA INSERT** — не гнать decode при загрузке
|
||||
3. **NOT NULL constraint** — проверять схему при добавлении колонок
|
||||
4. **debug_log в БД** — единственный способ увидеть что происходит внутри
|
||||
5. **Версия в шаблоне** — перед КАЖДЫМ коммитом
|
||||
6. **Пул соединений с keepalive** — обязательно для managed PostgreSQL
|
||||
@@ -0,0 +1,76 @@
|
||||
# Сессия 10 — Перенос Flask → Lucee: полный конвейер
|
||||
|
||||
Дата: 2026-06-18
|
||||
|
||||
## Итог
|
||||
|
||||
Весь функционал Flask (contracts-app, ветка vm) перенесён на Lucee (contractor, ветка master).
|
||||
|
||||
## Структура Lucee (contractor/)
|
||||
|
||||
| Файл | Назначение | Версия |
|
||||
|------|-----------|--------|
|
||||
| `index.cfm` | 🖥️ UI — загрузка, таблица, парсинг, LLM, сравнение, чат | v1.0.19 |
|
||||
| `upload.cfm` | 📤 Приём файлов (multipart, до 100MB) | v1.0.13 |
|
||||
| `parser.cfm` | 📄 PDF (PDFBox) + DOCX (ZIP/XML) + TXT + ZIP (подпапки) | v1.0.15 |
|
||||
| `extractor.cfm` | 🤖 LLM → spec_rows (cfhttp → api.aillm.ru) | v1.0.10 |
|
||||
| `differ.cfm` | 🔍 Сравнение допников (added/changed/deleted) | v1.0.11 |
|
||||
| `chat.cfm` | 💬 Чат с договором по spec_rows | v1.0.18 |
|
||||
| `api.cfm` | 🛠️ CRUD API (test, tables, schema, query, execute) | v1.0.1 |
|
||||
| `Application.cfc` | Конфигурация: datasource, requestTimeout=180 | v1.0.17 |
|
||||
| `favicon.png` | Nubes favicon | — |
|
||||
| `nubes-logo.svg` | Nubes логотип | — |
|
||||
|
||||
## Решённые проблемы
|
||||
|
||||
1. **BYTEA Large Objects** — PostgreSQL JDBC не передаёт сырые байты в BYTEA.
|
||||
Решение: `toBase64(fileBytes)` → `cf_sql_varchar`.
|
||||
|
||||
2. **auto-commit mode** — INSERT BYTEA требует транзакции.
|
||||
Решение: `<cftransaction>` вокруг всех INSERT.
|
||||
|
||||
3. **cfhttp.statusCode** — возвращает `"200 OK"` вместо `200`.
|
||||
Решение: `left(cfhttp.statusCode, 3) EQ "200"`.
|
||||
|
||||
4. **var scope** — `var` нельзя использовать вне функций в CFML.
|
||||
Решение: `var` только в `<cffunction>`, в MAIN без `var`.
|
||||
|
||||
5. **var в индексах циклов** — `index="var x"` невалидно.
|
||||
Решение: `var` объявлять до цикла, в индексе без `var`.
|
||||
|
||||
6. **Таймаут LLM** — 20с мало для cfhttp к api.aillm.ru.
|
||||
Решение: `requestTimeout=180`.
|
||||
|
||||
7. **CORS** — браузер блокировал fetch к upload.cfm.
|
||||
Решение: `Access-Control-Allow-Origin: *` + OPTIONS handler.
|
||||
|
||||
8. **HTTP/2 PROTOCOL_ERROR** — ddos-guard рвёт стримы на POST >~50KB.
|
||||
Решение: XHR вместо fetch, файлы договоров <40KB проходят.
|
||||
|
||||
## Результаты теста (5 файлов)
|
||||
|
||||
- ✅ 3 из 5 загрузились (2 — HTTP/2 сбой, ограничение ddos-guard)
|
||||
- Парсинг: 19 + 38 элементов
|
||||
- LLM: 1 + 6 строк spec_rows
|
||||
- Сравнение: changed: 1, added: 5, deleted: 0
|
||||
|
||||
## Отличия от Flask
|
||||
|
||||
| Функция | Flask | Lucee |
|
||||
|---------|-------|-------|
|
||||
| Парсер PDF | pdfplumber (таблицы!) | PDFBox (только текст) |
|
||||
| Парсер DOCX | python-docx | ZIP/XML (чистый CFML) |
|
||||
| .doc | libreoffice | ❌ |
|
||||
| Таблицы в PDF | ✅ | ❌ (только параграфы) |
|
||||
| SSE-стриминг | ✅ | ❌ (пока нет) |
|
||||
| HTTP/2 | httpx | cfhttp (работает) |
|
||||
| UI | 2 страницы | 1 страница |
|
||||
| ZIP-превью | JSZip в браузере | ❌ |
|
||||
|
||||
## Деплой
|
||||
|
||||
- Платформа: Lucee 6.0, k8s-4-sandbox-nubes-ru
|
||||
- Деплой: git push → автоматический
|
||||
- URL: https://contractor.luceek8s.dev.nubes.ru/
|
||||
- БД: PostgreSQL (внешний сервис), БД baza
|
||||
- Версия: v1.0.19
|
||||
@@ -0,0 +1,52 @@
|
||||
# Сессия 11 — process.cfm: один запрос вместо N+1
|
||||
|
||||
Дата: 2026-06-19
|
||||
|
||||
## Проблема
|
||||
|
||||
Сравнение на Lucee занимало 264.6с. Причина:
|
||||
|
||||
1. **Браузер делал N+1 HTTP-запросов** — по одному `fetch('/extractor.cfm')` на каждый допник + отдельный `fetch('/differ.cfm')`. Каждый экстрактор вызывал `cfhttp timeout=120` к LLM.
|
||||
2. **Не было проверки на уже извлечённые** — каждый запуск заново гонял LLM для всех допников.
|
||||
3. **Не было DELETE перед INSERT** — плодились дубликаты.
|
||||
4. **DEFAULT_PROMPT различался** — в Python `{{` (двойные скобки), в Lucee `{` (одинарные).
|
||||
|
||||
## Решение
|
||||
|
||||
### process.cfm (новый)
|
||||
|
||||
Один эндпоинт, аналог Python `/llm/process/<cid>`:
|
||||
|
||||
- `GET /process.cfm?contract_id=X`
|
||||
- Сервер внутри обходит все допники:
|
||||
- Проверяет `spec_rows` → если есть, skip
|
||||
- textify → LLM → parse → DELETE + INSERT
|
||||
- Затем differ на свежих данных
|
||||
- `requesttimeout=600`
|
||||
- Все ключи JSON в UPPERCASE (как ждёт JS)
|
||||
|
||||
### index.cfm (JS)
|
||||
|
||||
Вместо:
|
||||
```javascript
|
||||
for (supplements) { await fetch('/extractor.cfm?...'); }
|
||||
await fetch('/differ.cfm?...');
|
||||
```
|
||||
|
||||
Стало:
|
||||
```javascript
|
||||
await fetch('/process.cfm?contract_id=...');
|
||||
// один ответ: {OK, EXTRACTED, ALL_CHANGES}
|
||||
```
|
||||
|
||||
### extractor.cfm
|
||||
|
||||
DEFAULT_PROMPT: `{{"rows":[...]}}` вместо `{"rows":[...]}` — как в Python.
|
||||
|
||||
## Версия
|
||||
|
||||
v1.0.47 → v1.0.49
|
||||
|
||||
## Деплой
|
||||
|
||||
git push origin master → Lucee k8s
|
||||
@@ -0,0 +1,83 @@
|
||||
# Резюме для нового чата — contracts (contractor/Lucee)
|
||||
|
||||
Дата: 2026-06-23 | Версия в git: v1.0.152 | Версия на Lucee: v1.0.152 (после редеплоя)
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Браузер (contractor.luceek8s.dev.nubes.ru)
|
||||
│
|
||||
├── загрузка файлов → VM (contracts.kube5s.ru/upload)
|
||||
│ Python convert_server.py, чанки 10KB → chunk.cfm → Lucee DB
|
||||
│
|
||||
├── парсинг: PDF→VM (PyPDF2), DOCX/DOC→Lucee (Apache POI через parser.cfm)
|
||||
│
|
||||
├── сравнение → VM SSE /process-v2 → LLM (gpt-oss-120b)
|
||||
│
|
||||
└── промпты → Lucee prompt.cfm (версионные, extract/diff)
|
||||
```
|
||||
|
||||
**Компоненты:**
|
||||
- Lucee 6.0 (CFML) на k8s — `contractor.luceek8s.dev.nubes.ru`
|
||||
- VM Python 3 — `contracts.kube5s.ru` (5.172.178.213)
|
||||
- PostgreSQL 15 — внутренний, БД `baza`
|
||||
- LLM: gpt-oss-120b через api.aillm.ru (бесплатно, 8000 токенов)
|
||||
- Nginx: `/upload`, `/process-v2`, `/unzip-upload`, `/convert-doc` → VM:8766
|
||||
|
||||
## Что сделано (v1.0.109 → v1.0.152)
|
||||
|
||||
### Успешно работает
|
||||
- Upload через VM чанками (10KB) — обходит лимит Lucee k8s (~30KB)
|
||||
- ZIP через /unzip-upload (VM)
|
||||
- PDF парсинг через PyPDF2 на VM
|
||||
- Prompts в БД с версионированием, редактор в UI
|
||||
- Opus-улучшенные промпты (few-shot, глоссарий ЦОД)
|
||||
- Provenance: prompt_version, source_document_id, raw_llm_response в spec_events
|
||||
- ↕ стрелки для порядка файлов
|
||||
- О сервисе — модальное окно
|
||||
|
||||
### Критические баги исправлены
|
||||
- JS: UPPERCASE ключи Lucee (OK, BODY, VERSIONS, IS_ACTIVE...)
|
||||
- JS: лишний `});` ломавший весь JS
|
||||
- CORS: дубликаты заголовков на /upload и /unzip-upload
|
||||
- chunk.cfm: `decode(base64)` для правильного хранения файлов
|
||||
- parser.cfm: откачен к рабочей версии (PDF→VM, DOCX→Lucee POI)
|
||||
|
||||
## Текущее состояние (v1.0.152)
|
||||
|
||||
- VM: Python, чанки base64, PDF парсинг ✅
|
||||
- Lucee: v1.0.152, parser.cfm рабочий, chunk.cfm с base64 ✅
|
||||
- PDF: загрузка + парсинг работают ✅
|
||||
- DOCX/DOC: должно работать после v1.0.152 (base64 fix) ✅
|
||||
- Prompts: работают ✅
|
||||
|
||||
## Проблемы решённые сегодня
|
||||
|
||||
1. PDF upload: "Network Error" → chunked upload через VM (10KB чанки)
|
||||
2. PDF parsing: PDFBox сломан → PyPDF2 на VM
|
||||
3. DOCX storage: hex вместо base64 → base64 в chunk.cfm
|
||||
4. CORS дубликаты → nginx только OPTIONS, VM — POST
|
||||
5. JS uppercase keys → исправлено
|
||||
|
||||
## Правила (важно!)
|
||||
|
||||
- ⛔ НИКОГДА ничего не делать без «делай»
|
||||
- После каждой правки: проверка синтаксиса (node --check для JS, py_compile для Python)
|
||||
- После каждого изменения: bump версии в index.cfm
|
||||
- Перед push: git diff, проверить всё
|
||||
- Lucee требует ручного редеплоя через Nubes UI
|
||||
- VM: scp + pkill + nohup для перезапуска
|
||||
- curl к *.ngcloud.ru всегда с --http2 и --max-time
|
||||
- Lucee serializeJSON → UPPERCASE ключи
|
||||
- Lucee k8s лимит тела запроса ~30KB
|
||||
- Документация в History/*.md
|
||||
|
||||
## Если что-то сломалось
|
||||
|
||||
1. Проверить git status в /home/naeel/nubes/contracts/contractor
|
||||
2. Проверить версию на Lucee: curl --http2 --max-time 5 -s 'https://contractor.luceek8s.dev.nubes.ru/' | grep 'v1.0.'
|
||||
3. Проверить VM: ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 'ps aux | grep convert'
|
||||
4. Логи nginx: ssh ... 'sudo tail -20 /var/log/nginx/error.log'
|
||||
5. Тест загрузки: curl --http2 -X POST 'https://contracts.kube5s.ru/upload' -F 'files=@file.pdf'
|
||||
@@ -0,0 +1,124 @@
|
||||
# Сессия 12 — Архитектура Event Sourcing v2
|
||||
|
||||
Дата: 2026-06-20
|
||||
|
||||
## Контекст
|
||||
|
||||
Обсуждение новой архитектуры сравнения договоров: переход от pairwise diff по `row_num`
|
||||
к Event Sourcing с LLM-интерпретацией допсоглашений (ДС).
|
||||
|
||||
Участники: DeepSeek (этот агент), Gemini, Sonnet.
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
### 1. Разделение труда
|
||||
- **LLM** переводит текст ДС → список операций (ADD/UPDATE/DELETE/UNRESOLVED)
|
||||
- **Lucee** исполняет операции как лог, собирает кумулятивный статус
|
||||
- LLM не участвует в математике сравнения
|
||||
|
||||
### 2. Хэш вместо row_num
|
||||
- `md5(lower(trim(name)) || coalesce(date_start,''))` — только неизменяемые идентификаторы
|
||||
- `qty`, `price`, `sum` НЕ входят в хэш — это изменяемые атрибуты
|
||||
- Если `qty` в хэше: ДС меняет количество → новый хэш → ADD вместо UPDATE → дубликат
|
||||
- Уникальность: имя + дата достаточно (две строки с одинаковым именем и датой в одном договоре — крайний случай, решается уточнением name)
|
||||
|
||||
### 3. Старый код
|
||||
- `spec_rows` + старый differ остаются параллельно (prod не трогаем)
|
||||
- Первичный договор: старый экстрактор строк
|
||||
- Допники: новый движок с операциями
|
||||
- После обкатки — старый отключаем
|
||||
|
||||
### 4. ВМ-прокси как async-буфер для LLM
|
||||
- Lucee через cfhttp висит синхронно до 120с
|
||||
- ВМ (Flask + httpx) забирает задачу, вызывает LLM, отдаёт готовый JSON
|
||||
- Новый endpoint `/llm-ops` в том же `convert_server.py`
|
||||
- Допники обрабатываются по одному (SSE прогресс)
|
||||
|
||||
### 5. LLM-промпт (новый, для ДС)
|
||||
- Контекст: полная текущая спецификация (hash + все поля) + текст ДС
|
||||
- 100 строк ~10KB — для 120B модели не проблема
|
||||
- LLM сам определяет full_replace vs изменения построчно
|
||||
- Возвращает частичные изменения (только изменённые поля), Lucee делает COALESCE
|
||||
|
||||
### 6. БД: Event Sourcing
|
||||
- `spec_events`: лог операций (contract_id, supplement_id, seq INTEGER per-contract, action, target_hash, new_values JSONB, comment)
|
||||
- `spec_current`: текущий статус (таблица, обновляется при apply)
|
||||
- full_replace → явные DELETE на каждую строку (аудит)
|
||||
- Весь ДС — одна транзакция (атомарность)
|
||||
- Откат целиком по supplement_id
|
||||
|
||||
### 7. UNRESOLVED
|
||||
- Тип операции уже определён LLM (ADD/UPDATE/DELETE)
|
||||
- UNRESOLVED — LLM не смог найти хэш для привязки
|
||||
- Оператор в `resolve.cfm`: выпадайка с существующими услугами → привязать хэш
|
||||
- Применяется как UPDATE к выбранной услуге
|
||||
|
||||
### 8. UI
|
||||
- `resolve.cfm` — отдельная страница для ручного разрешения
|
||||
- Список UNRESOLVED строк, дропдаун с услугами, кнопка «Подтвердить»
|
||||
- Текущий `view.cfm` — предпросмотр документа (первые 15 строк)
|
||||
|
||||
### 9. Триггер пайплайна
|
||||
- `process.cfm?contract_id=X&v=2` — новая версия
|
||||
- Старая кнопка = v1, новая кнопка = v2 (рядом)
|
||||
- Никаких флагов в БД, никакого auto-detect
|
||||
- Когда обкатаем → переключаем дефолтную кнопку
|
||||
|
||||
### 10. parsed_text для ДС
|
||||
- `process.cfm` v2 переиспользует ту же логику textify (elements_json → текст), что и v1
|
||||
- `parser.cfm` НЕ трогать
|
||||
- Текст формируется в process.cfm и передаётся на ВМ
|
||||
|
||||
### 11. Первичное заполнение spec_current
|
||||
- `process.cfm?v=2` обрабатывает все supplements с нуля, по порядку
|
||||
- Initial: `current_spec = []` (пустой) → LLM возвращает всё как ADD → spec_current заполнен
|
||||
- Каждый следующий ДС: `current_spec` из уже заполненного spec_current
|
||||
- `spec_rows` не трогаем — старый пайплайн независим
|
||||
- Для существующих контрактов: v2 пересчитывает с нуля через LLM, миграция не нужна
|
||||
|
||||
## Технические детали реализации
|
||||
|
||||
### Таблицы (новые)
|
||||
```sql
|
||||
spec_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
contract_id UUID REFERENCES contracts(id),
|
||||
supplement_id UUID REFERENCES supplements(id),
|
||||
seq INTEGER NOT NULL, -- per-contract счётчик
|
||||
action TEXT NOT NULL, -- ADD, UPDATE, DELETE, UNRESOLVED
|
||||
target_hash TEXT, -- хэш услуги
|
||||
new_values JSONB, -- для ADD/UPDATE
|
||||
comment TEXT,
|
||||
resolved_by TEXT, -- кто разрешил UNRESOLVED
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(contract_id, seq)
|
||||
)
|
||||
|
||||
spec_current (
|
||||
contract_id UUID REFERENCES contracts(id),
|
||||
name_hash TEXT NOT NULL, -- md5(lower(trim(name)) || coalesce(date_start,''))
|
||||
name TEXT,
|
||||
price NUMERIC,
|
||||
qty NUMERIC,
|
||||
sum NUMERIC,
|
||||
date_start TEXT,
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (contract_id, position_hash)
|
||||
)
|
||||
```
|
||||
|
||||
### API ВМ `/llm-ops`
|
||||
```
|
||||
POST /llm-ops
|
||||
body: {contract_id, supplement_id, current_spec: [{hash, name, price, qty, sum, date_start}], doc_text: "..."}
|
||||
returns: {operations: [{action, target_hash, new_values, comment}], mode: "partial"|"full_replace"}
|
||||
```
|
||||
|
||||
## Связанные файлы
|
||||
- Запрос Sonnet: `contractor/Files/sonnet-v2-request.md`
|
||||
- Старый код парсинга: `contractor/parser.cfm`
|
||||
- Старый код сравнения: `contractor/differ.cfm`
|
||||
- Старый код экстракции: `contractor/extractor.cfm`
|
||||
- Текущий process: `contractor/process.cfm`
|
||||
- ВМ-прокси: `contractor/deploy/convert_server.py`
|
||||
- ВМ-nginx: `contractor/deploy/nginx-contracts.conf`
|
||||
@@ -0,0 +1,31 @@
|
||||
# Session 12 — 2026-06-23: v1.0.158 — Python lowercase + /convert-doc
|
||||
|
||||
## Баги исправлены
|
||||
|
||||
### 1. `resp.parsed` undefined → «Неизвестная ошибка»
|
||||
**Причина:** index.cfm строка 438: `r.OK` / `r.ERROR` (Lucee uppercase), Python возвращает `r.ok` / `r.error` (lowercase).
|
||||
Promise всегда падал в `reject`, `resp` становился Error-объектом, `resp.parsed` → undefined.
|
||||
**Исправление:** `r.OK` → `r.ok`, `r.ERROR` → `r.error`.
|
||||
|
||||
### 2. `.doc` → «Конвертер»
|
||||
**Причина:** `/convert-doc` был в nginx но отсутствовал в новом convert_server.py.
|
||||
**Исправление:** Добавлен `_handle_convert_doc()` — логика из convert_doc.py (libreoffice headless).
|
||||
|
||||
## Архитектура (v1.0.158)
|
||||
|
||||
```
|
||||
Браузер → contracts.kube5s.ru (nginx) → VM :8766 Python
|
||||
├── /upload (multipart → DB + parse)
|
||||
├── /convert-doc (DOC→DOCX)
|
||||
├── /api/supplements?contract_id=X
|
||||
├── /api/documents/<id>
|
||||
├── /process-v2 (SSE)
|
||||
└── /health
|
||||
→ contractor.luceek8s.dev.nubes.ru (Lucee — только index.cfm SPA)
|
||||
```
|
||||
|
||||
## Ключевые правила
|
||||
- Python API: ВСЕ ключи lowercase (`ok`, `error`, `doc_id`, `parsed`)
|
||||
- Lucee serializeJSON: UPPERCASE (`OK`, `ERROR`) — больше не используется в JS
|
||||
- index.cfm: все запросы через `VM_API = 'https://contracts.kube5s.ru'`
|
||||
- БД: VM PostgreSQL 127.0.0.1:5432/baza, пользователь super
|
||||
@@ -0,0 +1,36 @@
|
||||
# Session 13 — 2026-06-24 — Auto-classification planning
|
||||
|
||||
## Требование Мищука (из Telegram, 23.06.2026)
|
||||
|
||||
> «Должна быть массовая загрузка, система сама должна разбираться, кто к кому (там в документе вся эта инфа есть)»
|
||||
> «Я не должен распознавать ничего. Система должна сделать все.»
|
||||
> «я ей дам 2000 документов»
|
||||
|
||||
## Текущее состояние (v1.0.161)
|
||||
|
||||
- Загрузка по одному файлу
|
||||
- Ручная сортировка стрелками ↕
|
||||
- Первый в списке = базовый договор (жёстко)
|
||||
- LLM-сравнение: каждый следующий против spec_current
|
||||
- Нет автоопределения типа, нет группировки
|
||||
|
||||
## Что нужно
|
||||
|
||||
1. Массовая загрузка (папка, 2000+ файлов)
|
||||
2. LLM-классификация каждого файла: тип, номер, дата, контрагент
|
||||
3. Автогруппировка по контрактам
|
||||
4. Автопорядок внутри группы (хронология)
|
||||
5. UI подтверждения
|
||||
6. Пайплайн сравнения по группам
|
||||
|
||||
## Куда смотреть Соннету
|
||||
|
||||
- `contractor/deploy/convert_server.py` — роутер
|
||||
- `contractor/deploy/db/` — PostgreSQL CRUD
|
||||
- `contractor/deploy/services/` — бизнес-логика
|
||||
- `contractor/deploy/app.js` — фронтенд JS
|
||||
- `contractor/index.cfm` — Lucee HTML (191 строка)
|
||||
- VM: `/home/naeel/contracts/` — рабочий код
|
||||
- VM: `/etc/nginx/sites-enabled/nginx-contracts.conf`
|
||||
- VM: `/etc/systemd/system/contracts.service`
|
||||
- БД: VM `127.0.0.1:5432/baza`, схема в `db/*.py`
|
||||
@@ -0,0 +1,58 @@
|
||||
# Архитектура v2 — Event Sourcing через ВМ
|
||||
|
||||
Дата: 2026-06-20
|
||||
|
||||
## Почему ВМ
|
||||
|
||||
- k8s Ingress буферизирует SSE → соединение рвётся
|
||||
- На ВМ nginx с `proxy_buffering off` + `proxy_read_timeout 600s` — всё работает
|
||||
- ВМ уже проксирует загрузку файлов и конвертацию .doc
|
||||
- ВМ — единственное место где можно делать долгие стриминговые запросы
|
||||
|
||||
## Схема
|
||||
|
||||
```
|
||||
Браузер (index.cfm)
|
||||
│
|
||||
├─ загрузка файлов: contracts.kube5s.ru/lucee/upload.cfm (ВМ → Lucee)
|
||||
├─ парсинг: contract.luceek8s.dev.nubes.ru/parser.cfm (Lucee напрямую)
|
||||
│
|
||||
└─ v2 сравнение: contracts.kube5s.ru/process-v2?contract_id=X (SSE через ВМ)
|
||||
│
|
||||
├─ GET Lucee: api.cfm → список supplements
|
||||
├─ для каждого supplement:
|
||||
│ ├─ GET Lucee: api.cfm → current_spec из spec_current
|
||||
│ ├─ GET Lucee: api.cfm → elements_json
|
||||
│ ├─ POST локально: /llm-ops (httpx → api.aillm.ru)
|
||||
│ ├─ POST Lucee: apply_events.cfm
|
||||
│ └─ flush SSE: прогресс
|
||||
└─ flush SSE: done
|
||||
```
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
| Эндпоинт | Где | Что делает |
|
||||
|----------|-----|------------|
|
||||
| `/llm-ops` | ВМ:8766 | Принимает {current_spec, doc_text} → LLM → {mode, ops} |
|
||||
| `/process-v2` | ВМ:8766 | SSE-стриминг: оркеструет весь v2 пайплайн |
|
||||
| `apply_events.cfm` | Lucee | Принимает {supplement_id, ops} → транзакционно применяет |
|
||||
| `process_v2.cfm` | Lucee | Удалить (логика перенесена на ВМ) |
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Действие |
|
||||
|------|---------|
|
||||
| `contractor/deploy/convert_server.py` | Добавить `/process-v2` SSE endpoint |
|
||||
| `contractor/deploy/llm_prompt.py` | Уже готов (build_prompt) |
|
||||
| `contractor/deploy/nginx-contracts.conf` | Добавить `location /process-v2` |
|
||||
| `contractor/process.cfm` | Убрать include process_v2.cfm |
|
||||
| `contractor/process_v2.cfm` | Удалить |
|
||||
| `contractor/apply_events.cfm` | Уже готов |
|
||||
| `contractor/index.cfm` | Кнопка v2 → ВМ EventSource |
|
||||
|
||||
## Проверка
|
||||
|
||||
1. `/llm-ops` на ВМ — работает (проверено)
|
||||
2. nginx с `/process-v2` → 8766 — после деплоя
|
||||
3. `apply_events.cfm` на Lucee — работает (проверено)
|
||||
4. Кнопка v2 в UI → SSE через ВМ
|
||||
Reference in New Issue
Block a user