✅ Интеграция SSE + Redis Pub/Sub для real-time OCR результатов
🎯 Основные изменения: Backend: - Реализован SSE endpoint /events/{task_id} для real-time стриминга событий - Интеграция Redis Pub/Sub для получения событий от n8n - Исправлен путь к .env файлу (абсолютный путь) - Убран префикс /api/v1 для events router - Добавлено подробное логирование событий Frontend: - Переключён на Vite dev mode для работы proxy - Настроен proxy /events -> backend:8100 - Реализована модалка с крутилкой при загрузке файла - SSE клиент для получения OCR результатов в real-time - Отображение результатов AI анализа в модалке Docker: - Frontend: изменён на npm run dev (Vite dev server) - Добавлен host.docker.internal для доступа к backend - Настроен proxy в docker-compose Утилиты: - monitor_redis_direct.py - мониторинг Redis Pub/Sub - test_redis_publish_direct.py - тестирование публикации в Redis 🚀 Полная цепочка работает: Frontend → Backend SSE → Redis Pub/Sub ← n8n → OCR/AI → Result
This commit is contained in:
@@ -79,13 +79,14 @@ async def stream_events(task_id: str):
|
|||||||
Returns:
|
Returns:
|
||||||
StreamingResponse с событиями
|
StreamingResponse с событиями
|
||||||
"""
|
"""
|
||||||
|
logger.info(f"🚀 SSE connection requested for task_id: {task_id}")
|
||||||
|
|
||||||
async def event_generator():
|
async def event_generator():
|
||||||
"""Генератор событий из Redis Pub/Sub"""
|
"""Генератор событий из Redis Pub/Sub"""
|
||||||
channel = f"ocr_events:{task_id}"
|
channel = f"ocr_events:{task_id}"
|
||||||
|
|
||||||
# Подписываемся на канал Redis
|
# Подписываемся на канал Redis
|
||||||
pubsub = redis_service.redis.pubsub()
|
pubsub = redis_service.client.pubsub()
|
||||||
await pubsub.subscribe(channel)
|
await pubsub.subscribe(channel)
|
||||||
|
|
||||||
logger.info(f"📡 Client subscribed to {channel}")
|
logger.info(f"📡 Client subscribed to {channel}")
|
||||||
@@ -96,19 +97,37 @@ async def stream_events(task_id: str):
|
|||||||
try:
|
try:
|
||||||
# Слушаем события
|
# Слушаем события
|
||||||
while True:
|
while True:
|
||||||
|
logger.info(f"⏳ Waiting for message on {channel}...")
|
||||||
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=30.0)
|
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=30.0)
|
||||||
|
|
||||||
if message and message['type'] == 'message':
|
if message:
|
||||||
event_data = message['data'].decode('utf-8')
|
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 ноды (вложенный)
|
||||||
yield f"data: {event_data}\n\n"
|
# Формат: {"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"
|
||||||
|
|
||||||
# Если обработка завершена - закрываем соединение
|
# Если обработка завершена - закрываем соединение
|
||||||
event = json.loads(event_data)
|
if actual_event.get('status') in ['completed', 'error', 'success']:
|
||||||
if event.get('status') in ['completed', 'error']:
|
|
||||||
logger.info(f"✅ Task {task_id} finished, closing SSE")
|
logger.info(f"✅ Task {task_id} finished, closing SSE")
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
logger.info(f"⏰ Timeout waiting for message on {channel}")
|
||||||
|
|
||||||
# Пинг каждые 30 сек чтобы соединение не закрылось
|
# Пинг каждые 30 сек чтобы соединение не закрылось
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class Settings(BaseSettings):
|
|||||||
log_file: str = "/app/logs/erv_platform.log"
|
log_file: str = "/app/logs/erv_platform.log"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = "../.env"
|
env_file = "/var/www/fastuser/data/www/crm.clientright.ru/erv_platform/.env"
|
||||||
case_sensitive = False
|
case_sensitive = False
|
||||||
extra = "ignore" # Игнорируем лишние поля из .env
|
extra = "ignore" # Игнорируем лишние поля из .env
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ app.include_router(claims.router)
|
|||||||
app.include_router(policy.router)
|
app.include_router(policy.router)
|
||||||
app.include_router(upload.router)
|
app.include_router(upload.router)
|
||||||
app.include_router(draft.router)
|
app.include_router(draft.router)
|
||||||
app.include_router(events.router, prefix="/api/v1")
|
app.include_router(events.router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
- "5173:3000"
|
- "5173:3000"
|
||||||
environment:
|
environment:
|
||||||
- REACT_APP_API_URL=http://147.45.146.17:8100
|
- REACT_APP_API_URL=http://147.45.146.17:8100
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
networks:
|
networks:
|
||||||
- erv-network
|
- erv-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# React Frontend Dockerfile
|
# React Frontend Dockerfile (DEV MODE для Vite Proxy)
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
|
|
||||||
# Устанавливаем рабочую директорию
|
# Устанавливаем рабочую директорию
|
||||||
@@ -13,14 +13,8 @@ RUN npm install
|
|||||||
# Копируем исходный код
|
# Копируем исходный код
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Собираем приложение
|
# Открываем порт (Vite dev server на 5173, но внутри контейнера на 3000)
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Устанавливаем serve для статических файлов
|
|
||||||
RUN npm install -g serve
|
|
||||||
|
|
||||||
# Открываем порт
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Запускаем приложение
|
# Запускаем Vite dev server с proxy (изменяем порт на 3000)
|
||||||
CMD ["serve", "-s", "dist", "-l", "3000"]
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Form, Input, Button, message, Upload, Spin, Alert, Modal } from 'antd';
|
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 type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import { convertToPDF } from '../../utils/pdfConverter';
|
import { convertToPDF } from '../../utils/pdfConverter';
|
||||||
|
|
||||||
@@ -57,57 +57,86 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
|||||||
const [policyNotFound, setPolicyNotFound] = useState(false);
|
const [policyNotFound, setPolicyNotFound] = useState(false);
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [waitingForOcr, setWaitingForOcr] = useState(false); // ⬅️ НОВЫЙ state для ожидания SSE!
|
||||||
const [uploadProgress, setUploadProgress] = useState('');
|
const [uploadProgress, setUploadProgress] = useState('');
|
||||||
const [ocrResult, setOcrResult] = useState<any>(null);
|
const [, setOcrResult] = useState<any>(null);
|
||||||
|
const [ocrModalVisible, setOcrModalVisible] = useState(false); // ⬅️ Видимость модалки
|
||||||
|
const [ocrModalContent, setOcrModalContent] = useState<any>(null); // ⬅️ Контент модалки
|
||||||
const eventSourceRef = useRef<EventSource | null>(null);
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
|
|
||||||
// SSE подключение для получения результатов OCR/Vision
|
// SSE подключение для получения результатов OCR/Vision
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const claimId = formData.claim_id;
|
const claimId = formData.claim_id;
|
||||||
if (!claimId || !uploading) return;
|
if (!claimId || !waitingForOcr) {
|
||||||
|
console.log('🔍 SSE useEffect: условие не выполнено', { claimId, waitingForOcr });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Подключаемся к SSE для получения результатов OCR
|
console.log('🔌 SSE: Открываю соединение к', `/events/${claimId}`);
|
||||||
const eventSource = new EventSource(`http://147.45.189.234:8000/events/${claimId}`);
|
|
||||||
|
// Открываем модалку с крутилкой
|
||||||
|
setOcrModalVisible(true);
|
||||||
|
setOcrModalContent('loading');
|
||||||
|
|
||||||
|
// Подключаемся к SSE для получения результатов OCR (через Vite proxy)
|
||||||
|
const eventSource = new EventSource(`/events/${claimId}`);
|
||||||
eventSourceRef.current = eventSource;
|
eventSourceRef.current = eventSource;
|
||||||
|
|
||||||
|
console.log('✅ SSE: EventSource создан');
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log('📨 SSE event received:', data);
|
console.log('📨 SSE event received:', data);
|
||||||
|
|
||||||
if (data.event_type === 'ocr_completed') {
|
if (data.event_type === 'ocr_completed') {
|
||||||
setUploadProgress(''); // Убираем крутилку
|
console.log('✅ SSE: Получил событие ocr_completed!', data);
|
||||||
|
|
||||||
|
setUploadProgress('');
|
||||||
|
setUploading(false);
|
||||||
|
setWaitingForOcr(false); // Останавливаем ожидание
|
||||||
setOcrResult(data);
|
setOcrResult(data);
|
||||||
|
|
||||||
if (data.status === 'success' && data.data?.is_valid_document) {
|
// Обрабатываем формат от n8n: data.output.is_policy или data.is_valid_document
|
||||||
// ✅ Полис распознан успешно
|
const aiOutput = data.data?.output || data.data;
|
||||||
message.success(data.message || '✅ Полис успешно распознан!');
|
const isValidPolicy = aiOutput?.is_policy === 'yes' || data.data?.is_valid_document === true;
|
||||||
addDebugEvent?.('ocr', 'success', data.message, data.data);
|
|
||||||
} else {
|
|
||||||
// ❌ Документ не распознан или это не полис
|
|
||||||
const warnings = data.data?.ai_analysis?.warnings || ['Документ не распознан'];
|
|
||||||
|
|
||||||
Modal.error({
|
// Обновляем содержимое модалки на результат (вместо крутилки)
|
||||||
title: '❌ Документ не распознан',
|
setOcrModalContent({ success: isValidPolicy, data: aiOutput, message: data.message });
|
||||||
content: (
|
|
||||||
<div>
|
if (data.status === 'completed' || data.status === 'success') {
|
||||||
<p>{data.message}</p>
|
const policyNumber = aiOutput?.policy_number || 'неизвестно';
|
||||||
{warnings.length > 0 && (
|
const holderName = aiOutput?.policyholder_full_name || '';
|
||||||
<ul>
|
const insuredPersons = aiOutput?.insured_persons || [];
|
||||||
{warnings.map((w: string, i: number) => (
|
|
||||||
<li key={i}>{w}</li>
|
if (isValidPolicy) {
|
||||||
))}
|
// ✅ Полис распознан - логируем в Debug Panel
|
||||||
</ul>
|
addDebugEvent?.('ocr_ai_result', 'success', `✅ AI анализ завершён`, {
|
||||||
)}
|
policy_number: policyNumber,
|
||||||
<p style={{ marginTop: 12, color: '#666' }}>
|
holder: holderName,
|
||||||
Пожалуйста, загрузите скан страхового полиса ERV.
|
insured_persons: insuredPersons,
|
||||||
</p>
|
policy_period: aiOutput?.policy_period,
|
||||||
</div>
|
program_name: aiOutput?.program_name,
|
||||||
),
|
full_ai_output: aiOutput
|
||||||
});
|
});
|
||||||
|
|
||||||
addDebugEvent?.('ocr', 'error', data.message, data.data);
|
// Сохраняем извлечённые AI данные
|
||||||
setFileList([]); // Очищаем список файлов
|
updateFormData({
|
||||||
|
policyAiData: aiOutput,
|
||||||
|
policyNumber: policyNumber,
|
||||||
|
holderName: holderName
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ❌ Не полис
|
||||||
|
addDebugEvent?.('ocr', 'error', '❌ Документ не является полисом ERV', aiOutput);
|
||||||
|
setFileList([]);
|
||||||
|
setPolicyNotFound(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ошибка обработки
|
||||||
|
addDebugEvent?.('ocr', 'error', data.message || 'Ошибка OCR', data.data);
|
||||||
|
setFileList([]);
|
||||||
|
setPolicyNotFound(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -116,17 +145,24 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
|||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
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.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
eventSource.onopen = () => {
|
||||||
|
console.log('✅ SSE: Соединение открыто!');
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (eventSourceRef.current) {
|
if (eventSourceRef.current) {
|
||||||
eventSourceRef.current.close();
|
eventSourceRef.current.close();
|
||||||
eventSourceRef.current = null;
|
eventSourceRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [formData.claim_id, uploading]);
|
}, [formData.claim_id, waitingForOcr]);
|
||||||
|
|
||||||
// Обработчик изменения поля полиса с автозаменой и маской
|
// Обработчик изменения поля полиса с автозаменой и маской
|
||||||
const handleVoucherChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleVoucherChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -321,8 +357,15 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
|||||||
policyValidationWarning: '' // Silent validation
|
policyValidationWarning: '' // Silent validation
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success(`Загружено файлов: ${uploadResult.uploaded_count}`);
|
// ⏳ Включаем режим ожидания SSE результата!
|
||||||
onNext();
|
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 {
|
} else {
|
||||||
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки файлов`, { error: 'Upload failed' });
|
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки файлов`, { error: 'Upload failed' });
|
||||||
message.error('Ошибка загрузки файлов');
|
message.error('Ошибка загрузки файлов');
|
||||||
@@ -490,6 +533,70 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно ожидания OCR результата */}
|
||||||
|
<Modal
|
||||||
|
open={ocrModalVisible}
|
||||||
|
closable={ocrModalContent !== 'loading'}
|
||||||
|
maskClosable={false}
|
||||||
|
footer={ocrModalContent === 'loading' ? null : [
|
||||||
|
<Button key="close" type="primary" onClick={() => setOcrModalVisible(false)}>
|
||||||
|
Закрыть
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={700}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
{ocrModalContent === 'loading' ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px 20px' }}>
|
||||||
|
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
|
||||||
|
<h3 style={{ marginTop: 24, marginBottom: 12 }}>⏳ Обрабатываем документ</h3>
|
||||||
|
<p style={{ color: '#666', marginBottom: 8 }}>OCR распознавание текста...</p>
|
||||||
|
<p style={{ color: '#666', marginBottom: 8 }}>AI анализ содержимого...</p>
|
||||||
|
<p style={{ color: '#666' }}>Проверка валидности полиса...</p>
|
||||||
|
<p style={{ color: '#999', fontSize: 12, marginTop: 20 }}>
|
||||||
|
Это может занять 20-30 секунд. Пожалуйста, подождите...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : ocrModalContent ? (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ marginBottom: 16 }}>
|
||||||
|
{ocrModalContent.success ? '✅ Результат распознавания' : '❌ Ошибка распознавания'}
|
||||||
|
</h3>
|
||||||
|
{ocrModalContent.success ? (
|
||||||
|
<div>
|
||||||
|
<p><strong>Номер полиса:</strong> {ocrModalContent.data?.policy_number || 'н/д'}</p>
|
||||||
|
<p><strong>Владелец:</strong> {ocrModalContent.data?.policyholder_full_name || 'н/д'}</p>
|
||||||
|
{ocrModalContent.data?.insured_persons?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p><strong>Застрахованные лица:</strong></p>
|
||||||
|
<ul>
|
||||||
|
{ocrModalContent.data.insured_persons.map((person: any, i: number) => (
|
||||||
|
<li key={i}>{person.full_name} (ДР: {person.birth_date || 'н/д'})</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{ocrModalContent.data?.policy_period && (
|
||||||
|
<p><strong>Период:</strong> {ocrModalContent.data.policy_period.insured_from} - {ocrModalContent.data.policy_period.insured_to}</p>
|
||||||
|
)}
|
||||||
|
<p style={{ marginTop: 16 }}><strong>Полный ответ AI:</strong></p>
|
||||||
|
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, fontSize: 12, maxHeight: 400, overflow: 'auto' }}>
|
||||||
|
{JSON.stringify(ocrModalContent.data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<p>{ocrModalContent.message || 'Документ не распознан'}</p>
|
||||||
|
<p style={{ marginTop: 16 }}><strong>Полный ответ:</strong></p>
|
||||||
|
<pre style={{ background: '#fff3f3', padding: 12, borderRadius: 4, fontSize: 12, maxHeight: 400, overflow: 'auto' }}>
|
||||||
|
{JSON.stringify(ocrModalContent.data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8100',
|
target: 'http://host.docker.internal:8100',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/events': {
|
||||||
|
target: 'http://host.docker.internal:8100',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
monitor_redis_direct.py
Executable file
68
monitor_redis_direct.py
Executable file
@@ -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()
|
||||||
|
|
||||||
73
test_redis_publish_direct.py
Executable file
73
test_redis_publish_direct.py
Executable file
@@ -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')}")
|
||||||
|
|
||||||
Reference in New Issue
Block a user