Files
crm.clientright.ru/ticket_form/SESSION_LOG_2025-10-28.md
Fedor de011efba9 fix: исправлен конфликт имён переменных в loadDraft (claimId -> finalClaimId)
- Исправлена ошибка ReferenceError при загрузке черновиков
- Переименована локальная переменная claimId в finalClaimId для избежания конфликта с параметром функции
- Обновлена логика извлечения claim_id из разных источников (claim.claim_id, payload.claim_id, body.claim_id, claim.id)
- Добавлен fallback на параметр claimId функции для надёжности
2025-11-19 23:33:52 +03:00

43 KiB
Raw Permalink Blame History

📋 Лог сессии: Исправление SSE error handling

Дата: 28 октября 2025 (00:00 - 01:00 MSK)
Задача: Исправление ошибки "Ошибка подключения к серверу" при успешном распознавании полиса
Статус: Успешно завершено


🎯 Проблема

После успешного распознавания полиса через OCR/Vision, пользователь видел модальное окно с ошибкой:

❌ Ошибка распознавания
Ошибка подключения к серверу

Полный ответ: null

Хотя в логах backend видно, что:

  • SSE подключение установлено
  • Событие OCR получено из Redis
  • Данные отправлены клиенту
  • SSE соединение закрыто корректно

🔍 Диагностика

Backend логи показывали успешную работу:

2025-10-28 00:41:15,187 - 🚀 SSE connection requested for task_id: CLM-2025-10-27-Y1KWA1
2025-10-28 00:41:15,202 - 📡 Client subscribed to ocr_events:CLM-2025-10-27-Y1KWA1
2025-10-28 00:41:15,203 - ⏳ Waiting for message on ocr_events:CLM-2025-10-27-Y1KWA1...
2025-10-28 00:41:49,729 - 📥 Received message type: message
2025-10-28 00:41:49,729 - 📦 Raw event data: {"claim_id":"CLM-2025-10-27-Y1KWA1","event":{"event_type":"ocr_completed","status":"completed","message":"OCR обработка завершена","data":{"output":{"is_policy":"yes","policy_number":"E1000-302545808"...
2025-10-28 00:41:49,730 - 📦 Unwrapped n8n Redis format for CLM-2025-10-27-Y1KWA1
2025-10-28 00:41:49,730 - 📤 Sending event to client: completed
2025-10-28 00:41:49,730 - ✅ Task CLM-2025-10-27-Y1KWA1 finished, closing SSE

Вывод: Backend работал корректно!

Причина ошибки:

  1. Backend отправляет событие OCR клиенту
  2. Backend закрывает SSE соединение (это нормально)
  3. Браузер получает событие закрытия SSE
  4. Браузер триггерит eventSource.onerror
  5. Frontend в onerror перезаписывает успешный результат ошибкой:
// ❌ СТАРЫЙ КОД (неправильный)
eventSource.onerror = (error) => {
  console.error('❌ SSE connection error:', error);
  setOcrModalContent({ 
    success: false, 
    data: null, 
    message: 'Ошибка подключения к серверу' 
  });
  setWaitingForOcr(false);
  eventSource.close();
};

Проблема: onerror вызывается после получения результата, когда backend закрывает SSE, и затирает успешный результат.


🛠️ Решение

Добавил проверку в eventSource.onerror — если уже получили результат OCR, не затираем его сообщением об ошибке:

// ✅ НОВЫЙ КОД (правильный)
eventSource.onerror = (error) => {
  console.error('❌ SSE connection error:', error);
  console.error('SSE readyState:', eventSource.readyState);
  
  // Не показываем ошибку если уже получили результат (backend закрыл SSE после успешной отправки)
  setOcrModalContent((prev) => {
    if (prev && prev !== 'loading') {
      console.log('✅ SSE закрыто после получения результата, не показываем ошибку');
      return prev; // Оставляем текущий результат
    }
    return { success: false, data: null, message: 'Ошибка подключения к серверу' };
  });
  
  setWaitingForOcr(false);
  eventSource.close();
};

Логика:

  • Если prev !== 'loading' → значит уже получили результат → не затираем его
  • Если prev === 'loading' → реальная ошибка подключения → показываем ошибку

📝 Изменённые файлы

/frontend/src/components/form/Step1Policy.tsx

Изменение: Обработка eventSource.onerror с проверкой наличия результата

Строки: 147-162

Было:

eventSource.onerror = (error) => {
  console.error('❌ SSE connection error:', error);
  setOcrModalContent({ success: false, data: null, message: 'Ошибка подключения к серверу' });
  setWaitingForOcr(false);
  eventSource.close();
};

Стало:

eventSource.onerror = (error) => {
  console.error('❌ SSE connection error:', error);
  console.error('SSE readyState:', eventSource.readyState);
  
  setOcrModalContent((prev) => {
    if (prev && prev !== 'loading') {
      console.log('✅ SSE закрыто после получения результата, не показываем ошибку');
      return prev;
    }
    return { success: false, data: null, message: 'Ошибка подключения к серверу' };
  });
  
  setWaitingForOcr(false);
  eventSource.close();
};

🐛 Проблемы в процессе исправления

Проблема 1: Backend завис после kill -HUP

Симптом:

ERROR: [Errno 98] Address already in use

Причина: kill -HUP не перезапустил uvicorn корректно, порт 8100 остался занят зависшим процессом.

Решение:

# Убили все процессы на порту 8100
sudo lsof -ti :8100 | xargs -r kill -9

# Перезапустили backend
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
source venv/bin/activate
python -m uvicorn app.main:app --host 0.0.0.0 --port 8100 --reload > ../backend.log 2>&1 &

Проблема 2: Изменения не применились во frontend

Симптом: После docker-compose restart frontend старый код всё ещё работал.

Причина: Frontend работает в Docker без volume mount — код встроен в образ при сборке.

Решение:

# Пересборка образа с новым кодом
docker-compose build frontend

# Пересоздание контейнера
docker-compose up -d frontend

Проверка применения изменений:

docker exec erv_platform_frontend_1 grep -A8 "eventSource.onerror" /app/src/components/form/Step1Policy.tsx

🚀 Git Commit

Commit: 0b75e01
Message: "fix: Не затираем результат OCR при закрытии SSE соединения"

Полное описание:

Проблема: Backend закрывает SSE после отправки события, браузер триггерит onerror, 
фронтенд перезаписывал успешный результат сообщением 'Ошибка подключения к серверу'.

Решение: Проверяем в onerror что если уже получили результат (prev !== 'loading'), 
не затираем его ошибкой.

Push: origin/main


Результат

Что работает:

  1. Backend запущен (PID 25931) на порту 8100
  2. Frontend пересобран и работает на http://147.45.146.17:5173
  3. SSE подключение устанавливается корректно
  4. События OCR получаются из Redis через backend
  5. Результат распознавания отображается в модальном окне
  6. Ошибка "Ошибка подключения к серверу" больше не появляется
  7. Git репозиторий синхронизирован

Тестирование:

Сценарий 1: Успешное распознавание полиса

  • Загрузка файла полиса →
  • SSE подключение →
  • OCR/Vision обработка →
  • Отображение результата → "Полис распознан: E1000-302545808"
  • Нет ошибки при закрытии SSE →

Сценарий 2: Загрузка неподходящего документа

  • Загрузка не-полиса →
  • SSE подключение →
  • OCR/Vision обработка →
  • Отображение: "Документ не является полисом ERV" →

Сценарий 3: Реальная ошибка подключения

  • Если backend недоступен → "Ошибка подключения к серверу" (корректная ошибка)

📊 Архитектура (финальная)

┌─────────────────────────────────────────────────────────────────────┐
│                           USER BROWSER                               │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  React Frontend (Vite Dev Server, port 3000)                 │  │
│  │  - Step1Policy.tsx (SSE Client)                              │  │
│  │  - Модалка с результатом OCR                                 │  │
│  │  - EventSource(`/events/${claimId}`)                         │  │
│  │  - ✅ Защита от затирания результата в onerror               │  │
│  └────────────┬─────────────────────────────────────────────────┘  │
│               │ Vite Proxy (/events → host:8100)                   │
└───────────────┼─────────────────────────────────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      BACKEND (FastAPI, port 8100)                    │
│                      PID: 25931                                      │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  SSE Endpoint: GET /events/{task_id}                         │  │
│  │  - Подписка на Redis: ocr_events:{task_id}                   │  │
│  │  - Стриминг событий через SSE                                │  │
│  │  - Закрытие SSE после отправки результата                    │  │
│  └────────────┬─────────────────────────────────────────────────┘  │
└───────────────┼──────────────────────────────────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                Redis Pub/Sub (crm.clientright.ru:6379)              │
│                                                                      │
│  Channel: ocr_events:CLM-2025-10-27-XXXXX                          │
│  Format: {                                                          │
│    "claim_id": "CLM-...",                                          │
│    "event": {                                                       │
│      "event_type": "ocr_completed",                                │
│      "status": "completed",                                         │
│      "data": { "output": { "is_policy": "yes", ... } }            │
│    }                                                                │
│  }                                                                  │
└────────────────▲────────────────────────────────────────────────────┘
                 │
                 │ PUBLISH
                 │
┌────────────────┴────────────────────────────────────────────────────┐
│                    n8n Workflow (OCR Processing)                     │
│                                                                      │
│  1. Webhook trigger (file upload)                                   │
│  2. Upload to S3                                                    │
│  3. OCR Service (147.45.146.17:8001)                               │
│  4. AI Vision (OpenRouter Gemini 2.0 Flash)                        │
│  5. Redis Publish Node → ocr_events:{claim_id}                     │
└─────────────────────────────────────────────────────────────────────┘

📈 Метрики

Время выполнения сессии: ~1 час
Количество коммитов: 1
Изменённых файлов: 1
Строк изменено: +10 / -1
Перезапусков backend: 2
Rebuild frontend: 1

Проблемы решены:

  • Затирание результата OCR при закрытии SSE
  • Backend завис после kill -HUP
  • Изменения не применялись без rebuild

🔗 Ссылки


📝 Важные заметки

Backend запущен вне Docker:

# Процесс
PID: 25931
Command: python -m uvicorn app.main:app --host 0.0.0.0 --port 8100 --reload

# Логи
tail -f /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend.log

# Перезапуск
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
source venv/bin/activate
python -m uvicorn app.main:app --host 0.0.0.0 --port 8100 --reload > ../backend.log 2>&1 &

Frontend требует rebuild при изменениях:

# Применение изменений
docker-compose build frontend
docker-compose up -d frontend

# Проверка кода в контейнере
docker exec erv_platform_frontend_1 cat /app/src/components/form/Step1Policy.tsx

Redis credentials:

Host: crm.clientright.ru
Port: 6379
Password: cKSq8M11ZQIRi59OuUXb
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

// Пример для отмены рейса
{
  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

Было:

footer={[
  <Button key="close" onClick={() => setOcrModalVisible(false)}>
    Закрыть
  </Button>
]}

Стало:

footer={ocrModalContent === 'loading' ? null : 
  ocrModalContent?.success ? [
    <Button key="next" type="primary" onClick={() => {
      setOcrModalVisible(false);
      onNext(); // Переход на следующий шаг
    }}>
      Продолжить 
    </Button>
  ] : [
    <Button key="retry" type="primary" onClick={() => {
      setOcrModalVisible(false);
      setFileList([]); // Очищаем список файлов
      setPolicyNotFound(true); // Показываем форму загрузки снова
    }}>
      Загрузить другой файл
    </Button>
  ]
}

Изменение 2: DEV MODE панель

<div style={{ 
  marginTop: 24, 
  padding: 16, 
  background: '#f0f0f0', 
  borderRadius: 8,
  border: '2px dashed #999'
}}>
  <div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
    🔧 DEV MODE - Быстрая навигация (без валидации)
  </div>
  <Button 
    type="dashed"
    onClick={() => {
      const devData = {
        voucher: 'E1000-123456789',
        claim_id: `CLM-DEV-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
      };
      updateFormData(devData);
      onNext();
    }}
  >
    Далее  (Step 2) [пропустить]
  </Button>
</div>

Файл: frontend/src/components/form/Step2Details.tsx

Полный рефакторинг!

Бэкап старой версии: Step2Details.OLD_MANUAL_INPUT.tsx

Новая структура:

  1. DOCUMENT_CONFIGS — конфигурация документов для каждого типа события:
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: [...],
  // ... остальные типы событий
};
  1. Пошаговая загрузка документов:
const [currentDocIndex, setCurrentDocIndex] = useState(0);
const currentDoc = requiredDocs[currentDocIndex];

// После успешной загрузки
if (currentDocIndex < requiredDocs.length - 1) {
  setCurrentDocIndex(prev => prev + 1);
} else {
  // Все документы загружены
  onNext();
}
  1. Модалка обработки для каждого документа:
<Modal
  title="Обработка документа"
  visible={processingModalVisible}
  footer={processingModalContent === 'loading' ? null : [
    <Button type="primary" onClick={handleContinueAfterProcessing}>
      {currentDocIndex < requiredDocs.length - 1 
        ? 'Продолжить к следующему документу →'
        : 'Далее (Step 3) →'
      }
    </Button>
  ]}
>
  {processingModalContent === 'loading' ? (
    <div style={{textAlign: 'center', padding: 24}}>
      <Spin size="large" />
      <p>Обрабатываем документ...</p>
    </div>
  ) : (
    <div>
      <Alert type="success" message="✅ Документ обработан" />
      <pre>{JSON.stringify(processingModalContent, null, 2)}</pre>
    </div>
  )}
</Modal>
  1. SSE для каждого документа с уникальным event_type:
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 панель:

<div style={{ marginTop: 24, padding: 16, background: '#f0f0f0' }}>
  <Button onClick={onPrev}> Назад (Step 1)</Button>
  <Button 
    type="dashed"
    onClick={() => {
      const devData = { eventType: 'delay_flight', incidentDate: dayjs(), transportNumber: 'TEST123' };
      updateFormData(devData);
      onNext();
    }}
  >
    Далее  (Step 3) [пропустить]
  </Button>
</div>

Файл: frontend/src/components/form/Step3Payment.tsx

DEV MODE панель (3 кнопки):

<Button onClick={onPrev}> Назад (Step 2)</Button>

<Button 
  type="dashed"
  onClick={() => {
    setIsPhoneVerified(true);
    const devData = {
      fullName: 'Тест Тестов',
      email: 'test@test.ru',
      phone: '+79991234567',
      paymentMethod: 'sbp',
      bankName: 'sberbank',
    };
    updateFormData(devData);
    message.success('DEV: Телефон автоматически подтверждён');
  }}
>
   Автоподтверждение телефона [dev]
</Button>

<Button 
  type="primary"
  onClick={() => {
    setIsPhoneVerified(true);
    const devData = {...};
    updateFormData(devData);
    onSubmit();
  }}
>
  🚀 Отправить [пропустить]
</Button>

🐛 Проблемы и их решения

Проблема 1: Синтаксические ошибки на фронте

Симптом:

чета шляпа у нас на фронте

Диагностика:

  • Пользователь сообщил "что то не того"
  • Проверка файлов показала дублирующийся код после закрывающих тегов компонентов

Найденные проблемы:

  1. Step1Policy.tsx (строки 659-820):

    • Дублирован весь DEV MODE блок после </div> компонента
    • Код был просто скопирован повторно
  2. Step3Payment.tsx (после строки 381):

    • Дублирован обрезанный фрагмент DEV панели
    • Неполный JSX

Решение:

# Удалены дублирующиеся блоки
# 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

Симптом:

{
  "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):

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;

Параметры:

[
  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:

{
  "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 (спроектирована)

Предложенная структура валидации:

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

# 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


🔗 Ссылки


📝 Важные заметки

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_typeevent_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