diff --git a/backend/app/api/events.py b/backend/app/api/events.py
index 450349a..d7ce8ab 100644
--- a/backend/app/api/events.py
+++ b/backend/app/api/events.py
@@ -79,13 +79,14 @@ async def stream_events(task_id: str):
Returns:
StreamingResponse с событиями
"""
+ logger.info(f"🚀 SSE connection requested for task_id: {task_id}")
async def event_generator():
"""Генератор событий из Redis Pub/Sub"""
channel = f"ocr_events:{task_id}"
# Подписываемся на канал Redis
- pubsub = redis_service.redis.pubsub()
+ pubsub = redis_service.client.pubsub()
await pubsub.subscribe(channel)
logger.info(f"📡 Client subscribed to {channel}")
@@ -96,19 +97,37 @@ async def stream_events(task_id: str):
try:
# Слушаем события
while True:
+ logger.info(f"⏳ Waiting for message on {channel}...")
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=30.0)
- if message and message['type'] == 'message':
- event_data = message['data'].decode('utf-8')
-
- # Отправляем событие клиенту
- yield f"data: {event_data}\n\n"
-
- # Если обработка завершена - закрываем соединение
- event = json.loads(event_data)
- if event.get('status') in ['completed', 'error']:
- logger.info(f"✅ Task {task_id} finished, closing SSE")
- break
+ if message:
+ logger.info(f"📥 Received message type: {message['type']}")
+ if message['type'] == 'message':
+ event_data = message['data'] # Уже строка (decode_responses=True)
+ logger.info(f"📦 Raw event data: {event_data[:200]}...")
+ event = json.loads(event_data)
+
+ # Обработка формата от n8n Redis ноды (вложенный)
+ # Формат: {"claim_id": "...", "event": {...}}
+ if 'event' in event and isinstance(event['event'], dict):
+ # Извлекаем вложенное событие
+ actual_event = event['event']
+ logger.info(f"📦 Unwrapped n8n Redis format for {task_id}")
+ else:
+ # Формат уже плоский (от backend API или старых источников)
+ actual_event = event
+
+ # Отправляем событие клиенту (плоский формат)
+ event_json = json.dumps(actual_event, ensure_ascii=False)
+ logger.info(f"📤 Sending event to client: {actual_event.get('status', 'unknown')}")
+ yield f"data: {event_json}\n\n"
+
+ # Если обработка завершена - закрываем соединение
+ if actual_event.get('status') in ['completed', 'error', 'success']:
+ logger.info(f"✅ Task {task_id} finished, closing SSE")
+ break
+ else:
+ logger.info(f"⏰ Timeout waiting for message on {channel}")
# Пинг каждые 30 сек чтобы соединение не закрылось
await asyncio.sleep(0.1)
diff --git a/backend/app/config.py b/backend/app/config.py
index eaa64a3..31bae2b 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -163,7 +163,7 @@ class Settings(BaseSettings):
log_file: str = "/app/logs/erv_platform.log"
class Config:
- env_file = "../.env"
+ env_file = "/var/www/fastuser/data/www/crm.clientright.ru/erv_platform/.env"
case_sensitive = False
extra = "ignore" # Игнорируем лишние поля из .env
diff --git a/backend/app/main.py b/backend/app/main.py
index 0d8c471..e24f4ca 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -98,7 +98,7 @@ app.include_router(claims.router)
app.include_router(policy.router)
app.include_router(upload.router)
app.include_router(draft.router)
-app.include_router(events.router, prefix="/api/v1")
+app.include_router(events.router)
@app.get("/")
diff --git a/docker-compose.yml b/docker-compose.yml
index fb2c41d..ed83372 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,6 +8,8 @@ services:
- "5173:3000"
environment:
- REACT_APP_API_URL=http://147.45.146.17:8100
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
networks:
- erv-network
restart: unless-stopped
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 8e0020f..17b8d34 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -1,4 +1,4 @@
-# React Frontend Dockerfile
+# React Frontend Dockerfile (DEV MODE для Vite Proxy)
FROM node:18-alpine
# Устанавливаем рабочую директорию
@@ -13,14 +13,8 @@ RUN npm install
# Копируем исходный код
COPY . .
-# Собираем приложение
-RUN npm run build
-
-# Устанавливаем serve для статических файлов
-RUN npm install -g serve
-
-# Открываем порт
+# Открываем порт (Vite dev server на 5173, но внутри контейнера на 3000)
EXPOSE 3000
-# Запускаем приложение
-CMD ["serve", "-s", "dist", "-l", "3000"]
+# Запускаем Vite dev server с proxy (изменяем порт на 3000)
+CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]
diff --git a/frontend/src/components/form/Step1Policy.tsx b/frontend/src/components/form/Step1Policy.tsx
index d2ecf1c..0f3e67b 100644
--- a/frontend/src/components/form/Step1Policy.tsx
+++ b/frontend/src/components/form/Step1Policy.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { Form, Input, Button, message, Upload, Spin, Alert, Modal } from 'antd';
-import { FileProtectOutlined, UploadOutlined, LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
+import { FileProtectOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import { convertToPDF } from '../../utils/pdfConverter';
@@ -57,18 +57,32 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
const [policyNotFound, setPolicyNotFound] = useState(false);
const [fileList, setFileList] = useState([]);
const [uploading, setUploading] = useState(false);
+ const [waitingForOcr, setWaitingForOcr] = useState(false); // ⬅️ НОВЫЙ state для ожидания SSE!
const [uploadProgress, setUploadProgress] = useState('');
- const [ocrResult, setOcrResult] = useState(null);
+ const [, setOcrResult] = useState(null);
+ const [ocrModalVisible, setOcrModalVisible] = useState(false); // ⬅️ Видимость модалки
+ const [ocrModalContent, setOcrModalContent] = useState(null); // ⬅️ Контент модалки
const eventSourceRef = useRef(null);
// SSE подключение для получения результатов OCR/Vision
useEffect(() => {
const claimId = formData.claim_id;
- if (!claimId || !uploading) return;
+ if (!claimId || !waitingForOcr) {
+ console.log('🔍 SSE useEffect: условие не выполнено', { claimId, waitingForOcr });
+ return;
+ }
- // Подключаемся к SSE для получения результатов OCR
- const eventSource = new EventSource(`http://147.45.189.234:8000/events/${claimId}`);
+ console.log('🔌 SSE: Открываю соединение к', `/events/${claimId}`);
+
+ // Открываем модалку с крутилкой
+ setOcrModalVisible(true);
+ setOcrModalContent('loading');
+
+ // Подключаемся к SSE для получения результатов OCR (через Vite proxy)
+ const eventSource = new EventSource(`/events/${claimId}`);
eventSourceRef.current = eventSource;
+
+ console.log('✅ SSE: EventSource создан');
eventSource.onmessage = (event) => {
try {
@@ -76,38 +90,53 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
console.log('📨 SSE event received:', data);
if (data.event_type === 'ocr_completed') {
- setUploadProgress(''); // Убираем крутилку
+ console.log('✅ SSE: Получил событие ocr_completed!', data);
+
+ setUploadProgress('');
+ setUploading(false);
+ setWaitingForOcr(false); // Останавливаем ожидание
setOcrResult(data);
- if (data.status === 'success' && data.data?.is_valid_document) {
- // ✅ Полис распознан успешно
- message.success(data.message || '✅ Полис успешно распознан!');
- addDebugEvent?.('ocr', 'success', data.message, data.data);
+ // Обрабатываем формат от n8n: data.output.is_policy или data.is_valid_document
+ const aiOutput = data.data?.output || data.data;
+ const isValidPolicy = aiOutput?.is_policy === 'yes' || data.data?.is_valid_document === true;
+
+ // Обновляем содержимое модалки на результат (вместо крутилки)
+ setOcrModalContent({ success: isValidPolicy, data: aiOutput, message: data.message });
+
+ if (data.status === 'completed' || data.status === 'success') {
+ const policyNumber = aiOutput?.policy_number || 'неизвестно';
+ const holderName = aiOutput?.policyholder_full_name || '';
+ const insuredPersons = aiOutput?.insured_persons || [];
+
+ if (isValidPolicy) {
+ // ✅ Полис распознан - логируем в Debug Panel
+ addDebugEvent?.('ocr_ai_result', 'success', `✅ AI анализ завершён`, {
+ policy_number: policyNumber,
+ holder: holderName,
+ insured_persons: insuredPersons,
+ policy_period: aiOutput?.policy_period,
+ program_name: aiOutput?.program_name,
+ full_ai_output: aiOutput
+ });
+
+ // Сохраняем извлечённые AI данные
+ updateFormData({
+ policyAiData: aiOutput,
+ policyNumber: policyNumber,
+ holderName: holderName
+ });
+ } else {
+ // ❌ Не полис
+ addDebugEvent?.('ocr', 'error', '❌ Документ не является полисом ERV', aiOutput);
+ setFileList([]);
+ setPolicyNotFound(true);
+ }
} else {
- // ❌ Документ не распознан или это не полис
- const warnings = data.data?.ai_analysis?.warnings || ['Документ не распознан'];
-
- Modal.error({
- title: '❌ Документ не распознан',
- content: (
-
-
{data.message}
- {warnings.length > 0 && (
-
- {warnings.map((w: string, i: number) => (
- - {w}
- ))}
-
- )}
-
- Пожалуйста, загрузите скан страхового полиса ERV.
-
-
- ),
- });
-
- addDebugEvent?.('ocr', 'error', data.message, data.data);
- setFileList([]); // Очищаем список файлов
+ // Ошибка обработки
+ addDebugEvent?.('ocr', 'error', data.message || 'Ошибка OCR', data.data);
+ setFileList([]);
+ setPolicyNotFound(true);
}
}
} catch (error) {
@@ -116,9 +145,16 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
};
eventSource.onerror = (error) => {
- console.error('SSE connection error:', error);
+ console.error('❌ SSE connection error:', error);
+ console.error('SSE readyState:', eventSource.readyState);
+ setOcrModalContent({ success: false, data: null, message: 'Ошибка подключения к серверу' });
+ setWaitingForOcr(false);
eventSource.close();
};
+
+ eventSource.onopen = () => {
+ console.log('✅ SSE: Соединение открыто!');
+ };
return () => {
if (eventSourceRef.current) {
@@ -126,7 +162,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
eventSourceRef.current = null;
}
};
- }, [formData.claim_id, uploading]);
+ }, [formData.claim_id, waitingForOcr]);
// Обработчик изменения поля полиса с автозаменой и маской
const handleVoucherChange = (e: React.ChangeEvent) => {
@@ -321,8 +357,15 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
policyValidationWarning: '' // Silent validation
});
- message.success(`Загружено файлов: ${uploadResult.uploaded_count}`);
- onNext();
+ // ⏳ Включаем режим ожидания SSE результата!
+ console.log('🔄 Устанавливаю waitingForOcr=true для claim_id:', claimId);
+ setWaitingForOcr(true); // ⬅️ Это откроет SSE соединение в useEffect!
+ setUploadProgress('⏳ Ждём результат распознавания полиса...');
+ message.info('Файл загружен. Ожидаем результат OCR и AI анализа...');
+ console.log('📡 waitingForOcr установлен в true, useEffect должен сработать!');
+
+ // SSE событие обработается в useEffect и покажет модалку
+ // НЕ вызываем onNext() здесь!
} else {
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки файлов`, { error: 'Upload failed' });
message.error('Ошибка загрузки файлов');
@@ -490,6 +533,70 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
)}
+
+ {/* Модальное окно ожидания OCR результата */}
+ setOcrModalVisible(false)}>
+ Закрыть
+
+ ]}
+ width={700}
+ centered
+ >
+ {ocrModalContent === 'loading' ? (
+
+
} />
+
⏳ Обрабатываем документ
+
OCR распознавание текста...
+
AI анализ содержимого...
+
Проверка валидности полиса...
+
+ Это может занять 20-30 секунд. Пожалуйста, подождите...
+
+
+ ) : ocrModalContent ? (
+
+
+ {ocrModalContent.success ? '✅ Результат распознавания' : '❌ Ошибка распознавания'}
+
+ {ocrModalContent.success ? (
+
+
Номер полиса: {ocrModalContent.data?.policy_number || 'н/д'}
+
Владелец: {ocrModalContent.data?.policyholder_full_name || 'н/д'}
+ {ocrModalContent.data?.insured_persons?.length > 0 && (
+ <>
+
Застрахованные лица:
+
+ {ocrModalContent.data.insured_persons.map((person: any, i: number) => (
+ - {person.full_name} (ДР: {person.birth_date || 'н/д'})
+ ))}
+
+ >
+ )}
+ {ocrModalContent.data?.policy_period && (
+
Период: {ocrModalContent.data.policy_period.insured_from} - {ocrModalContent.data.policy_period.insured_to}
+ )}
+
Полный ответ AI:
+
+ {JSON.stringify(ocrModalContent.data, null, 2)}
+
+
+ ) : (
+
+
{ocrModalContent.message || 'Документ не распознан'}
+
Полный ответ:
+
+ {JSON.stringify(ocrModalContent.data, null, 2)}
+
+
+ )}
+
+ ) : null}
+
);
}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 2121808..d68e11b 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -5,10 +5,14 @@ export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
- port: 5173,
+ port: 3000,
proxy: {
'/api': {
- target: 'http://localhost:8100',
+ target: 'http://host.docker.internal:8100',
+ changeOrigin: true
+ },
+ '/events': {
+ target: 'http://host.docker.internal:8100',
changeOrigin: true
}
}
diff --git a/monitor_redis_direct.py b/monitor_redis_direct.py
new file mode 100755
index 0000000..b9b1f20
--- /dev/null
+++ b/monitor_redis_direct.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""
+Мониторинг Redis Pub/Sub для проверки прямой публикации из n8n
+"""
+import redis
+import json
+from datetime import datetime
+
+print("=" * 60)
+print("🎧 МОНИТОРИНГ REDIS PUB/SUB")
+print("=" * 60)
+
+# Подключение к Redis
+r = redis.Redis(
+ host='crm.clientright.ru',
+ port=6379,
+ password='CRM_Redis_Pass_2025_Secure!',
+ decode_responses=True
+)
+
+# Проверка подключения
+try:
+ r.ping()
+ print("✅ Redis подключен!")
+except Exception as e:
+ print(f"❌ Ошибка подключения: {e}")
+ exit(1)
+
+# Подписка на все каналы ocr_events:*
+pubsub = r.pubsub()
+pubsub.psubscribe('ocr_events:*')
+
+print(f"📡 Слушаем каналы: ocr_events:*")
+print(f"⏰ Запущено: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+print("-" * 60)
+print("\n⏳ Ожидаю события... (Ctrl+C для выхода)\n")
+
+# Счётчик событий
+event_count = 0
+
+try:
+ for message in pubsub.listen():
+ if message['type'] == 'pmessage':
+ event_count += 1
+ print(f"\n{'='*60}")
+ print(f"📢 СОБЫТИЕ #{event_count}")
+ print(f"⏰ Время: {datetime.now().strftime('%H:%M:%S')}")
+ print(f"📺 Канал: {message['channel']}")
+ print(f"📦 Данные:")
+ print("-" * 60)
+
+ try:
+ # Пытаемся распарсить как JSON
+ data = json.loads(message['data'])
+ print(json.dumps(data, indent=2, ensure_ascii=False))
+ except json.JSONDecodeError:
+ # Если не JSON - показываем как есть
+ print(message['data'])
+
+ print(f"{'='*60}\n")
+
+except KeyboardInterrupt:
+ print(f"\n\n✅ Остановлено. Получено событий: {event_count}")
+ print(f"⏰ Завершено: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+finally:
+ pubsub.close()
+ r.close()
+
diff --git a/test_redis_publish_direct.py b/test_redis_publish_direct.py
new file mode 100755
index 0000000..1980a64
--- /dev/null
+++ b/test_redis_publish_direct.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""
+Тест прямой публикации в Redis (имитация n8n Redis ноды)
+"""
+import redis
+import json
+from datetime import datetime
+
+print("=" * 60)
+print("🧪 ТЕСТ ПРЯМОЙ ПУБЛИКАЦИИ В REDIS")
+print("=" * 60)
+
+# Подключение к Redis
+r = redis.Redis(
+ host='crm.clientright.ru',
+ port=6379,
+ password='CRM_Redis_Pass_2025_Secure!',
+ decode_responses=True
+)
+
+# Проверка подключения
+try:
+ r.ping()
+ print("✅ Redis подключен!")
+except Exception as e:
+ print(f"❌ Ошибка подключения: {e}")
+ exit(1)
+
+# Тестовые данные
+claim_id = "CLM-TEST-DIRECT-123"
+channel = f"ocr_events:{claim_id}"
+
+event_data = {
+ "event_type": "ocr_completed",
+ "status": "success",
+ "message": "✅ Тест прямой публикации из Python (имитация n8n)",
+ "data": {
+ "file_id": "test-file-123",
+ "is_valid_document": True,
+ "test_mode": True,
+ "source": "direct_redis_publish"
+ },
+ "timestamp": datetime.now().isoformat()
+}
+
+message = json.dumps(event_data, ensure_ascii=False)
+
+print(f"\n📺 Канал: {channel}")
+print(f"📦 Сообщение:")
+print(json.dumps(event_data, indent=2, ensure_ascii=False))
+print("\n" + "=" * 60)
+
+# Публикация
+try:
+ num_subscribers = r.publish(channel, message)
+ print(f"\n✅ Сообщение опубликовано!")
+ print(f"👥 Количество подписчиков: {num_subscribers}")
+
+ if num_subscribers == 0:
+ print("\n⚠️ ВНИМАНИЕ: Нет активных подписчиков!")
+ print(" Это нормально, если никто не слушает канал.")
+ print(" Запусти monitor_redis_direct.py в другом терминале.")
+ else:
+ print(f"\n🎉 {num_subscribers} подписчик(ов) получили сообщение!")
+
+except Exception as e:
+ print(f"\n❌ Ошибка публикации: {e}")
+finally:
+ r.close()
+
+print("\n" + "=" * 60)
+print(f"⏰ Завершено: {datetime.now().strftime('%H:%M:%S')}")
+