43 KiB
📋 Лог сессии: Исправление 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 работал корректно!
Причина ошибки:
- Backend отправляет событие OCR клиенту
- Backend закрывает SSE соединение (это нормально)
- Браузер получает событие закрытия SSE
- Браузер триггерит
eventSource.onerror - 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
✅ Результат
Что работает:
- ✅ Backend запущен (PID 25931) на порту 8100
- ✅ Frontend пересобран и работает на http://147.45.146.17:5173
- ✅ SSE подключение устанавливается корректно
- ✅ События OCR получаются из Redis через backend
- ✅ Результат распознавания отображается в модальном окне
- ✅ Ошибка "Ошибка подключения к серверу" больше не появляется
- ✅ 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
🔗 Ссылки
- Frontend: http://147.45.146.17:5173
- Backend API: http://localhost:8100
- Backend Health: http://localhost:8100/health
- Gitea: http://147.45.146.17:3002/negodiy/erv-platform
- n8n: http://147.45.146.17:5678
📝 Важные заметки
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)
-
Посадочный талон ИЛИ Билет (обязательно)
file_type: flight_delay_boarding_or_ticketevent_type: flight_delay_boarding_or_ticket_processed- AI извлекает: номер рейса, дату, маршрут, ФИО, время вылета
-
Подтверждение задержки (обязательно, до 3 файлов)
file_type: flight_delay_confirmationevent_type: flight_delay_confirmation_processed- Справка от АК, email/SMS, ИЛИ фото табло
- AI извлекает: время задержки, причину, фактическое время вылета
Отмена рейса (cancel_flight)
-
Билет (обязательно)
file_type: flight_cancel_ticketevent_type: flight_cancel_ticket_processed
-
Уведомление об отмене (обязательно, до 3 файлов)
file_type: flight_cancel_noticeevent_type: flight_cancel_notice_processed- Письмо/SMS от АК, фото табло
Пропуск стыковки (missed_connection)
-
Рейс отправления: Посадочный талон ИЛИ Билет (обязательно)
file_type: missed_connection_first_boarding_or_ticketevent_type: missed_connection_first_boarding_or_ticket_processed
-
Рейс прибытия: Билет на пропущенный рейс (обязательно)
file_type: missed_connection_second_ticketevent_type: missed_connection_second_ticket_processed
-
Подтверждение задержки первого рейса (опционально, до 3 файлов)
file_type: missed_connection_delay_proofevent_type: missed_connection_delay_proof_processed
Задержка/отмена поезда (delay_train, cancel_train)
-
Билет (обязательно)
file_type: train_delay_ticket/train_cancel_ticket
-
Справка о задержке/отмене (обязательно, до 3 файлов)
file_type: train_delay_certificate/train_cancel_certificate- Справка от ЖД, фото табло
Задержка/отмена парома/круиза (delay_ferry, cancel_ferry)
-
Билет/Бронь (обязательно)
file_type: ferry_delay_ticket/ferry_cancel_ticket
-
Подтверждение задержки/отмены (обязательно, до 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
Новая структура:
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: [...],
// ... остальные типы событий
};
- Пошаговая загрузка документов:
const [currentDocIndex, setCurrentDocIndex] = useState(0);
const currentDoc = requiredDocs[currentDocIndex];
// После успешной загрузки
if (currentDocIndex < requiredDocs.length - 1) {
setCurrentDocIndex(prev => prev + 1);
} else {
// Все документы загружены
onNext();
}
- Модалка обработки для каждого документа:
<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>
- 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: Синтаксические ошибки на фронте
Симптом:
чета шляпа у нас на фронте
Диагностика:
- Пользователь сообщил "что то не того"
- Проверка файлов показала дублирующийся код после закрывающих тегов компонентов
Найденные проблемы:
-
Step1Policy.tsx(строки 659-820):- Дублирован весь DEV MODE блок после
</div>компонента - Код был просто скопирован повторно
- Дублирован весь DEV MODE блок после
-
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.tsx1207222- 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}]}\""
}
}
Причина:
INSERT INTO claim_filesне вернулfile_idиs3_url- Выяснилось: запись в
claimsс даннымclaim_numberне существует - Foreign key
claim_idне может быть установлен → INSERT падает 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 кнопки:
- "📸 Загрузить документ лучшего качества"
- "✍️ Ввести недостающие данные вручную"
- "Продолжить с доступными данными"
FAIL: Неправильный тип документа
- ❌ Ошибка
- 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
🔗 Ссылки
- 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