diff --git a/SESSION_LOG_2025-10-28.md b/SESSION_LOG_2025-10-28.md index 77c2e14..1979614 100644 --- a/SESSION_LOG_2025-10-28.md +++ b/SESSION_LOG_2025-10-28.md @@ -354,3 +354,710 @@ Channels: ocr_events:{claim_id} **Автор:** AI Assistant (Claude Sonnet 4.5) **Дата:** 28 октября 2025, 01:00 MSK +--- +--- + +# 📋 Лог сессии: Умная форма Step 2 с AI-обработкой документов + +**Дата:** 28 октября 2025 (13:00 - 17:00 MSK) +**Задача:** Рефакторинг Step 2 в интеллектуальную форму с пошаговой загрузкой и AI-обработкой документов +**Статус:** ✅ Успешно завершено + +--- + +## 🎯 Основные задачи + +### 1. ✅ Улучшение UX на Step 1 (Policy) +- Добавлены динамические кнопки в модалке OCR: + - **"Продолжить →"** при успешном распознавании → переход на Step 2 + - **"Загрузить другой файл"** при ошибке → очистка и повтор +- Добавлен **DEV MODE** панель с кнопкой быстрого перехода на Step 2 без валидации + +### 2. ✅ Рефакторинг Step 2 (Details) +**Было:** +- Ручной ввод всех полей (тип события, дата, номер рейса/поезда/парома) +- Загрузка документов как дополнение к ручному вводу + +**Стало:** +- **"Интеллектуальная форма"** — AI извлекает данные из документов +- **Пошаговая загрузка** каждого документа с индивидуальной обработкой +- **Модалка обработки** для каждого документа с результатами извлечения +- Ручной ввод только при необходимости (fallback) + +### 3. ✅ Определение требований к документам + +#### Задержка рейса (`delay_flight`) +1. **Посадочный талон ИЛИ Билет** (обязательно) + - `file_type: flight_delay_boarding_or_ticket` + - `event_type: flight_delay_boarding_or_ticket_processed` + - AI извлекает: номер рейса, дату, маршрут, ФИО, время вылета + +2. **Подтверждение задержки** (обязательно, до 3 файлов) + - `file_type: flight_delay_confirmation` + - `event_type: flight_delay_confirmation_processed` + - Справка от АК, email/SMS, ИЛИ фото табло + - AI извлекает: время задержки, причину, фактическое время вылета + +#### Отмена рейса (`cancel_flight`) +1. **Билет** (обязательно) + - `file_type: flight_cancel_ticket` + - `event_type: flight_cancel_ticket_processed` + +2. **Уведомление об отмене** (обязательно, до 3 файлов) + - `file_type: flight_cancel_notice` + - `event_type: flight_cancel_notice_processed` + - Письмо/SMS от АК, фото табло + +#### Пропуск стыковки (`missed_connection`) +1. **Рейс отправления: Посадочный талон ИЛИ Билет** (обязательно) + - `file_type: missed_connection_first_boarding_or_ticket` + - `event_type: missed_connection_first_boarding_or_ticket_processed` + +2. **Рейс прибытия: Билет на пропущенный рейс** (обязательно) + - `file_type: missed_connection_second_ticket` + - `event_type: missed_connection_second_ticket_processed` + +3. **Подтверждение задержки первого рейса** (опционально, до 3 файлов) + - `file_type: missed_connection_delay_proof` + - `event_type: missed_connection_delay_proof_processed` + +#### Задержка/отмена поезда (`delay_train`, `cancel_train`) +1. **Билет** (обязательно) + - `file_type: train_delay_ticket` / `train_cancel_ticket` + +2. **Справка о задержке/отмене** (обязательно, до 3 файлов) + - `file_type: train_delay_certificate` / `train_cancel_certificate` + - Справка от ЖД, фото табло + +#### Задержка/отмена парома/круиза (`delay_ferry`, `cancel_ferry`) +1. **Билет/Бронь** (обязательно) + - `file_type: ferry_delay_ticket` / `ferry_cancel_ticket` + +2. **Подтверждение задержки/отмены** (обязательно, до 3 файлов) + - `file_type: ferry_delay_confirmation` / `ferry_cancel_confirmation` + - Справка, email, фото расписания + +### 4. ✅ Уникальные `file_type` для каждого документа + +**Принцип:** Каждый тип документа → уникальный `file_type` → уникальный `event_type` в Redis + +```typescript +// Пример для отмены рейса +{ + file_type: "flight_cancel_ticket" // → S3, n8n, DB + event_type: "flight_cancel_ticket_processed" // → Redis pub/sub +} + +{ + file_type: "flight_cancel_notice" + event_type: "flight_cancel_notice_processed" +} +``` + +**Почему это важно:** +- n8n разделяет потоки обработки по `file_type` +- Разные AI промпты для каждого типа документа +- Frontend слушает уникальный `event_type` для каждого документа + +### 5. ✅ Добавлены DEV MODE кнопки во все 3 шага + +**Step 1 (Policy):** +- "Далее → (Step 2) [пропустить]" — авто-заполнение voucher и claim_id + +**Step 2 (Details):** +- "← Назад (Step 1)" — возврат назад +- "Далее → (Step 3) [пропустить]" — авто-заполнение eventType, incidentDate, transportNumber + +**Step 3 (Payment):** +- "← Назад (Step 2)" — возврат назад +- "✅ Автоподтверждение телефона [dev]" — автозаполнение всех полей + setIsPhoneVerified(true) +- "🚀 Отправить [пропустить]" — автозаполнение + submit + +--- + +## 🛠️ Технические изменения + +### Файл: `frontend/src/components/form/Step1Policy.tsx` + +#### Изменение 1: Динамические кнопки в модалке OCR + +**Было:** +```typescript +footer={[ + +]} +``` + +**Стало:** +```typescript +footer={ocrModalContent === 'loading' ? null : + ocrModalContent?.success ? [ + + ] : [ + + ] +} +``` + +#### Изменение 2: DEV MODE панель + +```typescript +
+
+ 🔧 DEV MODE - Быстрая навигация (без валидации) +
+ +
+``` + +### Файл: `frontend/src/components/form/Step2Details.tsx` + +#### Полный рефакторинг! + +**Бэкап старой версии:** `Step2Details.OLD_MANUAL_INPUT.tsx` + +**Новая структура:** + +1. **`DOCUMENT_CONFIGS`** — конфигурация документов для каждого типа события: +```typescript +const DOCUMENT_CONFIGS = { + delay_flight: [ + { + name: "Посадочный талон ИЛИ Билет", + field: "boarding_or_ticket", + file_type: "flight_delay_boarding_or_ticket", + required: true, + maxFiles: 1, + description: "Посадочный талон (boarding pass) или билет (ticket/booking)", + aiPromptFocus: "Извлеки: номер рейса, дату, маршрут, ФИО пассажира, время вылета" + }, + // ... остальные документы + ], + cancel_flight: [...], + // ... остальные типы событий +}; +``` + +2. **Пошаговая загрузка документов:** +```typescript +const [currentDocIndex, setCurrentDocIndex] = useState(0); +const currentDoc = requiredDocs[currentDocIndex]; + +// После успешной загрузки +if (currentDocIndex < requiredDocs.length - 1) { + setCurrentDocIndex(prev => prev + 1); +} else { + // Все документы загружены + onNext(); +} +``` + +3. **Модалка обработки для каждого документа:** +```typescript + + {currentDocIndex < requiredDocs.length - 1 + ? 'Продолжить к следующему документу →' + : 'Далее (Step 3) →' + } + + ]} +> + {processingModalContent === 'loading' ? ( +
+ +

Обрабатываем документ...

+
+ ) : ( +
+ +
{JSON.stringify(processingModalContent, null, 2)}
+
+ )} +
+``` + +4. **SSE для каждого документа с уникальным `event_type`:** +```typescript +const eventSource = new EventSource( + `${API_BASE_URL}/events/${claimId}?event_type=${currentDoc.file_type}_processed` +); + +eventSource.onmessage = (event) => { + const result = JSON.parse(event.data); + if (result.event_type === `${currentDoc.file_type}_processed`) { + setProcessingModalContent(result.data); + } +}; +``` + +#### DEV MODE панель: +```typescript +
+ + +
+``` + +### Файл: `frontend/src/components/form/Step3Payment.tsx` + +#### DEV MODE панель (3 кнопки): +```typescript + + + + + +``` + +--- + +## 🐛 Проблемы и их решения + +### Проблема 1: Синтаксические ошибки на фронте + +**Симптом:** +``` +чета шляпа у нас на фронте +``` + +**Диагностика:** +- Пользователь сообщил "что то не того" +- Проверка файлов показала **дублирующийся код** после закрывающих тегов компонентов + +**Найденные проблемы:** + +1. **`Step1Policy.tsx`** (строки 659-820): + - Дублирован весь DEV MODE блок после `` компонента + - Код был просто скопирован повторно + +2. **`Step3Payment.tsx`** (после строки 381): + - Дублирован обрезанный фрагмент DEV панели + - Неполный JSX + +**Решение:** +```bash +# Удалены дублирующиеся блоки +# Step1Policy.tsx: строки 659-820 удалены +# Step3Payment.tsx: строки после 381 удалены + +# Rebuild frontend +docker-compose build frontend +docker-compose up -d frontend +``` + +**Коммиты:** +- `2999951` - fix: Удалён дублирующийся код в Step1Policy.tsx +- `1207222` - fix: Удалён дублирующийся код в Step3Payment.tsx + +### Проблема 2: PostgreSQL INSERT не возвращает данные в n8n + +**Симптом:** +```json +{ + "s3_url": null, + "file_id": null, + "error": { + "message": "422 - \"{\\\"detail\\\":[{\\\"type\\\":\\\"string_type\\\",\\\"loc\\\":[\\\"body\\\",\\\"file_url\\\"],\\\"msg\\\":\\\"Input should be a valid string\\\",\\\"input\\\":null}]}\"" + } +} +``` + +**Причина:** +1. `INSERT INTO claim_files` не вернул `file_id` и `s3_url` +2. Выяснилось: запись в `claims` с данным `claim_number` не существует +3. Foreign key `claim_id` не может быть установлен → INSERT падает +4. `file_size` передан как `"4.47 MB"` вместо числа в байтах + +**Решение:** +Создан UPSERT запрос с CTE (Common Table Expression): + +```sql +WITH upserted_claim AS ( + INSERT INTO claims ( + claim_number, voucher, session_id, status, created_at, updated_at + ) VALUES ( + $1, $2, $3, 'draft', NOW(), NOW() + ) + ON CONFLICT (claim_number) + DO UPDATE SET + updated_at = NOW(), + voucher = COALESCE(EXCLUDED.voucher, claims.voucher) + RETURNING id as claim_id +) +INSERT INTO claim_files ( + claim_id, file_type, original_name, s3_key, s3_url, + file_size, mime_type, ocr_status, uploaded_at +) +SELECT + upserted_claim.claim_id, + $4, $5, $6, $7, $8, $9, 'pending', NOW() +FROM upserted_claim +RETURNING id as file_id, s3_url, ocr_status; +``` + +**Параметры:** +```javascript +[ + claim_number, // $1 + voucher, // $2 + session_id, // $3 + file_type, // $4 + original_name, // $5 + s3_key, // $6 + s3_url, // $7 + file_size, // $8 (число в байтах!) + mime_type // $9 +] +``` + +**Преимущества:** +- ✅ Атомарная операция +- ✅ Идемпотентность (можно запускать повторно) +- ✅ Всегда создаёт `claims` если его нет +- ✅ Обновляет `updated_at` если уже есть +- ✅ Возвращает `file_id` и `s3_url` для следующих шагов + +--- + +## ✅ Тестирование + +### Тест 1: Загрузка билета на отмену рейса + +**Файл:** "Билет Романова.pdf" +**Claim ID:** CLM-2025-10-28-33ID32 +**file_type:** `flight_cancel_ticket` + +**Результат Redis:** +```json +{ + "claim_id": "CLM-2025-10-28-33ID32", + "event": { + "event_type": "flight_cancel_ticket_processed", + "status": "completed", + "message": "✅ Документ обработан: flight_cancel_ticket", + "data": { + "output": { + "is_flight_doc": "yes", + "document_type": "e-ticket", + "ticket_number": "2222411714956", + "passengers": [{ + "full_name": "ROMANOVA ANASTASIIA", + "doc_number": "774099576" + }], + "itinerary": [{ + "flight_number": "A4-6025", + "departure": { + "airport_iata": "MRV", + "date_local": "2025-09-30", + "time_local": "16:25" + }, + "arrival": { + "airport_iata": "TLV", + "time_local": "20:00" + } + }] + } + } + } +} +``` + +**Backend лог:** +``` +16:46:29 - 📥 Received message type: message +16:46:29 - 📦 Raw event data: {"claim_id":"CLM-2025-10-28-33ID32",...} +16:46:29 - 📦 Unwrapped n8n Redis format for CLM-2025-10-28-33ID32 +16:46:29 - 📤 Sending event to client: completed +16:46:29 - ✅ Task CLM-2025-10-28-33ID32 finished, closing SSE +``` + +**Результат:** ✅ Полный успех! +- S3 upload ✅ +- PostgreSQL UPSERT ✅ +- OCR/AI обработка ✅ +- Redis publish ✅ +- Backend SSE ✅ +- Frontend получил данные ✅ + +--- + +## 📊 Архитектура Step 2 (новая) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Step 2: Details (NEW) │ +│ │ +│ 1. Выбор типа события (eventType) │ +│ ↓ │ +│ 2. DOCUMENT_CONFIGS определяет список документов │ +│ ↓ │ +│ 3. Пошаговая загрузка каждого документа: │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Документ 1: Посадочный талон │ │ +│ │ - Upload компонент │ │ +│ │ - POST /upload → n8n webhook │ │ +│ │ - file_type: "flight_delay_boarding_or_ticket" │ │ +│ │ - SSE: event_type = "..._processed" │ │ +│ │ - Модалка: "Обрабатываем..." │ │ +│ │ - Результат: extracted data │ │ +│ │ - Кнопка: "Продолжить к следующему" │ │ +│ └────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Документ 2: Подтверждение задержки │ │ +│ │ - (аналогично) │ │ +│ │ - file_type: "flight_delay_confirmation" │ │ +│ │ - Может быть до 3 файлов │ │ +│ │ - Кнопка: "Далее (Step 3)" │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ 4. После загрузки всех документов → Step 3 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Data Flow для одного документа: + +``` +Frontend n8n Backend Redis + │ │ │ │ + │ POST /upload │ │ │ + ├────────────────────>│ │ │ + │ {claim_id, │ │ │ + │ file_type, │ │ │ + │ file} │ │ │ + │ │ │ │ + │ SSE connect │ │ │ + ├────────────────────────────────────────────>│ │ + │ /events/CLM-XXX? │ │ │ + │ event_type= │ │ │ + │ flight_..._processed│ │ │ + │ │ │ │ + │ │ 1. Upload to S3 │ │ + │ │ 2. UPSERT claims │ │ + │ │ 3. INSERT claim_files │ │ + │ │ 4. OCR Service │ │ + │ │ 5. AI Vision │ │ + │ │ 6. PUBLISH │ │ + │ ├────────────────────────────────────────────>│ + │ │ {event_type: │ │ + │ │ "..._processed", │ │ + │ │ data: {...}} │ │ + │ │ │ │ + │ │ │<──────────────────┤ + │ │ │ SUBSCRIBE │ + │ │ │ ocr_events:CLM-XXX │ + │<────────────────────────────────────────────┤ │ + │ SSE: data: {event_type, data} │ │ + │ │ │ │ + │ Show modal: │ │ │ + │ "✅ Обработано" │ │ │ + │ Display data │ │ │ + │ │ │ │ + │ User clicks │ │ │ + │ "Continue" → │ │ │ + │ next document │ │ │ + │ (or Step 3) │ │ │ +``` + +--- + +## 🎯 Логика обработки результатов AI (спроектирована) + +### Предложенная структура валидации: + +```typescript +const handleOcrResult = (event) => { + const { output } = event.data; + + // Проверка 1: Это правильный тип документа? + if (output.is_flight_doc !== "yes") { + return { success: false, message: "❌ Это не авиадокумент" }; + } + + // Проверка 2: Извлечены ли критичные данные? + const firstFlight = output.itinerary?.[0]; + const criticalFields = { + flightNumber: firstFlight?.flight_number, + departureDate: firstFlight?.departure?.date_local, + departureAirport: firstFlight?.departure?.airport_iata, + arrivalAirport: firstFlight?.arrival?.airport_iata, + passengerName: output.passengers?.[0]?.full_name + }; + + const missing = Object.entries(criticalFields) + .filter(([_, value]) => !value) + .map(([key]) => key); + + if (missing.length === 0) { + return { success: true, message: "✅ Билет распознан успешно!" }; + } else { + return { + success: "partial", + message: "⚠️ Билет распознан, но не хватает данных", + missingFields: missing + }; + } +}; +``` + +### 3 сценария UI: + +**SUCCESS:** Все данные извлечены +- ✅ Показать извлечённые данные +- Кнопка: "Продолжить к следующему документу →" + +**PARTIAL:** Документ валидный, но данные неполные +- ⚠️ Показать что извлечено + что отсутствует +- 3 кнопки: + 1. "📸 Загрузить документ лучшего качества" + 2. "✍️ Ввести недостающие данные вручную" + 3. "Продолжить с доступными данными" + +**FAIL:** Неправильный тип документа +- ❌ Ошибка +- 2 кнопки: + 1. "Загрузить другой файл" + 2. "Ввести данные вручную" + +--- + +## 📝 Git Commits + +```bash +# Commit history (от старого к новому) +6fe1459 - backup: Сохранён старый Step2Details с ручным вводом полей +122af07 - feat: Умная форма Step2 с автоматическим распознаванием документов +9084d75 - feat: Пошаговая загрузка документов с модалкой на Step 2 +2999951 - fix: Удалён дублирующийся код в Step1Policy.tsx +1207222 - fix: Удалён дублирующийся код в Step3Payment.tsx +``` + +**Push:** ✅ `origin/main` (все коммиты) + +--- + +## 📈 Метрики + +**Время выполнения сессии:** ~4 часа +**Количество коммитов:** 5 +**Изменённых файлов:** 4 (Step1Policy, Step2Details, Step2Details.OLD, Step3Payment) +**Строк добавлено:** ~800 +**Строк удалено:** ~200 (дубликаты) + ~400 (рефакторинг Step2) + +**Frontend rebuilds:** 3 +**Тестовых загрузок:** 3 +**Redis событий обработано:** 3 + +--- + +## 🔗 Ссылки + +- Frontend: http://147.45.146.17:5173 +- Backend API: http://localhost:8100 +- Gitea: http://147.45.146.17:3002/negodiy/erv-platform +- n8n: http://147.45.146.17:5678 +- N8N SQL Queries: `/erv_platform/N8N_SQL_QUERIES.md` + +--- + +## 📝 Важные заметки + +### Redis Password (обновлено) +``` +Host: crm.clientright.ru +Port: 6379 +Password: CRM_Redis_Pass_2025_Secure! +(из /etc/redis/redis.conf) +``` + +### PostgreSQL UPSERT для n8n +Сохранён в `N8N_SQL_QUERIES.md` для использования в webhook nodes. + +### Структура `file_type` → `event_type` +``` +file_type: "flight_cancel_ticket" +event_type: "flight_cancel_ticket_processed" + +Формула: event_type = file_type + "_processed" +``` + +### DEV MODE +Все три шага имеют панель для быстрой навигации без заполнения форм — ускоряет разработку и тестирование. + +--- + +**Статус:** ✅ Успешно завершено +**Автор:** AI Assistant (Claude Sonnet 4.5) +**Дата:** 28 октября 2025, 17:00 MSK +