Реализован SSE + Redis Pub/Sub для AI Drawer

- Добавлен SSE endpoint (aiassist/ai_sse.php) для real-time получения ответов от n8n
- Обновлен n8n_proxy.php: убран callback, добавлена передача Redis параметров в n8n
- Обновлен ai-drawer-simple.js: переход с polling на SSE с fallback через Redis
- Добавлен check_redis_response.php для прямого чтения из Redis кэша
- Добавлена документация: N8N_REDIS_SETUP.md, N8N_REDIS_FIX.md, AI_DRAWER_REDIS_SSE.md
- Поддержка plain text ответов от n8n (автоматическое определение формата)
- Кэширование ответов в Redis для надежности (TTL 5 минут)
This commit is contained in:
Fedor
2025-11-11 15:16:27 +03:00
parent f770bd0e43
commit 1a4653298d
6 changed files with 768 additions and 70 deletions

130
AI_DRAWER_REDIS_SSE.md Normal file
View File

@@ -0,0 +1,130 @@
# AI Drawer: Redis Pub/Sub вместо Polling
## ✅ Что сделано
### Проблема
Раньше использовался polling - браузер каждые 2 секунды спрашивал сервер "готово ли?". Это создавало:
- Лишние запросы к серверу
- Задержку до 2 секунд перед получением ответа
- Нагрузку на БД
- Дублирование истории (БД + n8n)
### Решение
Теперь используется **Redis Pub/Sub + SSE (Server-Sent Events)** с прямой публикацией из n8n:
- ✅ Мгновенная доставка ответов (без задержек)
- ✅ Нет лишних запросов (одно SSE соединение)
- ✅ Меньше нагрузка на сервер
- ✅ Нет дублирования - история только в n8n
- ✅ Упрощенная архитектура - без БД и callback
- ✅ Fallback на polling если SSE не работает
## 📋 Архитектура
```
1. AI Drawer → n8n_proxy.php → возвращает task_id
2. n8n обрабатывает → публикует ответ НАПРЯМУЮ в Redis
3. Redis PUBLISH → канал "ai:response:{task_id}"
4. ai_sse.php → подписан на Redis → отправляет через SSE в браузер
5. Браузер → EventSource → получает ответ мгновенно! ⚡
```
**История диалога:** Сохраняется в n8n автоматически (не дублируется в БД)
## 📁 Измененные файлы
### 1. `/aiassist/ai_sse.php` (новый)
SSE endpoint для подписки на Redis события
### 2. `/aiassist/n8n_proxy.php` (упрощен)
- ❌ Убрано сохранение в БД
- ❌ Убран callback URL
- ✅ Добавлены параметры Redis для n8n
### 3. `/callback_ai_response.php` (больше не используется)
Можно удалить - n8n публикует напрямую в Redis
### 4. `/layouts/v7/resources/js/ai-drawer-simple.js` (обновлен)
- Заменен `startPolling()` на `startSSEListener()`
- Добавлен fallback на polling если SSE не работает
- Добавлено поле `currentEventSource` для управления SSE соединением
## 🔧 Как работает
### Отправка запроса:
```javascript
// Пользователь отправляет сообщение
sendToN8N(message) получает task_id startSSEListener(task_id)
```
### Получение ответа:
```javascript
// SSE соединение открывается один раз
EventSource('/aiassist/ai_sse.php?task_id=123')
// n8n обрабатывает и публикует НАПРЯМУЮ в Redis:
Redis PUBLISH "ai:response:123" {
task_id: "123",
response: "...",
status: "completed"
}
// SSE endpoint получает событие и отправляет в браузер
// Браузер получает ответ мгновенно!
```
### Настройка n8n:
См. подробную инструкцию: `N8N_REDIS_SETUP.md`
## 🛡️ Fallback механизм
Если SSE не работает (старые браузеры, проблемы с сетью):
1. Через 5 секунд автоматически переключается на polling
2. Использует старый метод `startPollingFallback()`
3. Проверяет БД каждые 2 секунды
## ⚙️ Настройки Redis
- **Host**: `crm.clientright.ru`
- **Port**: `6379`
- **Password**: `CRM_Redis_Pass_2025_Secure!`
- **Канал**: `ai:response:{task_id}`
## 🧪 Тестирование
1. Откройте AI Drawer в CRM
2. Отправьте сообщение
3. Проверьте консоль браузера:
- `AI Drawer: SSE connection opened`
- `AI Drawer: Received response via SSE`
4. Ответ должен прийти мгновенно после обработки n8n
## 📊 Преимущества
| Параметр | Polling (старое) | Redis Pub/Sub (новое) |
|----------|------------------|----------------------|
| Скорость | До 2 сек задержки | Мгновенно ⚡ |
| Запросы | Каждые 2 сек | Одно соединение |
| Нагрузка | Высокая | Низкая |
| Надежность | ✅ | ✅ + fallback |
## 🔍 Отладка
### Проверить Redis публикацию:
```bash
redis-cli -h crm.clientright.ru -a 'CRM_Redis_Pass_2025_Secure!' \
PUBLISH "ai:response:test-task" '{"task_id":"test-task","response":"test"}'
```
### Проверить SSE endpoint:
```bash
curl -N "https://crm.clientright.ru/aiassist/ai_sse.php?task_id=test-task"
```
### Логи:
- PHP error_log: `/var/log/php/error.log`
- Ищите: `[AI SSE]` и `[Callback]`
## ✅ Результат
Теперь AI Drawer получает ответы **мгновенно** через Redis Pub/Sub вместо ожидания polling каждые 2 секунды!

155
N8N_REDIS_FIX.md Normal file
View File

@@ -0,0 +1,155 @@
# 🔧 Исправление конфигурации n8n для Redis публикации
## ❌ Проблема в текущей конфигурации
```json
{
"channel": "=ai:response:{{ $('Edit Fields').item.json.taskId }}",
"messageData": "={{ JSON.stringify($json.output) }}"
}
```
**Проблемы:**
1. ❌ Канал использует `$('Edit Fields').item.json.taskId` - неправильный путь
2.`messageData` содержит `$json.output` - неправильный формат
3. ❌ Нет сохранения в Redis ключ для fallback
## ✅ Правильная конфигурация
### Вариант 1: Если taskId в корне webhook body
**Channel:**
```
ai:response:{{ $json.taskId }}
```
**Message (JSON объект):**
```json
{
"task_id": "{{ $json.taskId }}",
"response": "{{ $json.output }}",
"status": "completed"
}
```
### Вариант 2: Если taskId в webhook.body
**Channel:**
```
ai:response:{{ $json.webhook.body.taskId }}
```
**Message (JSON объект):**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json.output }}",
"status": "completed"
}
```
### Вариант 3: Если ответ в другой ноде (например, AI Chat)
**Channel:**
```
{{ $json.webhook.body.redisChannel }}
```
**Message (JSON объект):**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json['AI Chat'].json.response }}",
"status": "completed"
}
```
## 📋 Полная настройка n8n workflow
### Шаг 1: Redis SET (сохранить в ключ для fallback)
**Operation:** `Set`
**Key:** `ai:response:cache:{{ $json.webhook.body.taskId }}`
**Value:**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json['AI Chat'].json.response }}",
"status": "completed",
"timestamp": "{{ $now.toISO() }}"
}
```
**TTL:** `300` секунд
### Шаг 2: Redis PUBLISH (опубликовать в канал для SSE)
**Operation:** `Publish`
**Channel:** `{{ $json.webhook.body.redisChannel }}`
**Message:**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json['AI Chat'].json.response }}",
"status": "completed"
}
```
## 🔍 Как найти правильный путь к данным
1. **Добавьте ноду "Set" перед Redis:**
- Сохраните все данные из предыдущих нод
- Посмотрите структуру данных в n8n
2. **Используйте Expression Editor в n8n:**
- Нажмите на поле "Channel" или "Message"
- Выберите "Expression"
- Начните вводить `$json.` - увидите доступные поля
3. **Проверьте webhook body:**
- В ноде Webhook посмотрите что приходит
- `taskId` и `redisChannel` должны быть в `$json.webhook.body`
## ✅ Проверка
После настройки проверьте:
1. **В n8n:**
- Запустите workflow
- Проверьте что Redis ноды выполнились успешно
- Посмотрите что именно публикуется в канал
2. **В Redis:**
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
MONITOR
```
Должны видеть:
- `SET ai:response:cache:task-xxx ...`
- `PUBLISH ai:response:task-xxx ...`
3. **В браузере:**
- Откройте консоль (F12)
- Должны видеть: `AI Drawer: SSE connection opened`
- Должны видеть: `AI Drawer: Received response via SSE`
## 🐛 Отладка
Если не работает:
1. **Проверьте формат сообщения:**
- Должен быть валидный JSON
- Должно быть поле `response` или `task_id`
2. **Проверьте канал:**
- Должен совпадать с `redisChannel` из `n8n_proxy.php`
- Формат: `ai:response:task-xxx`
3. **Проверьте логи PHP:**
```bash
tail -f /var/log/apache2/error.log | grep "AI SSE"
```
4. **Проверьте что SSE endpoint доступен:**
- Откройте в браузере: `/aiassist/ai_sse.php?task_id=test-123`
- Должен открыться поток SSE (не ошибка)

185
N8N_REDIS_SETUP.md Normal file
View File

@@ -0,0 +1,185 @@
# Настройка n8n для прямой публикации в Redis
## 🎯 Цель
Настроить n8n workflow так, чтобы после обработки AI ответа он публиковал результат **напрямую в Redis**, без промежуточного callback.
## 📋 Архитектура
```
AI Drawer → n8n_proxy.php → n8n webhook
[Обработка AI]
Redis PUBLISH → ai:response:{taskId}
SSE → браузер получает мгновенно! ⚡
```
## 🔧 Настройка n8n Workflow
### Шаг 1: Добавить Redis ноду после обработки AI
В вашем n8n workflow после ноды обработки AI добавьте **Redis ноду**:
1. **Тип ноды**: `Redis`
2. **Operation**: `Publish`
### Шаг 2: Настройки Redis ноды
**Connection:**
```
Host: crm.clientright.ru
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
Database: 0
```
**Operation Settings:**
```
Operation: Publish
Channel: {{ $json.redisChannel }}
```
**Message (вариант 1 - JSON объект, рекомендуется):**
```json
{
"task_id": "{{ $json.taskId }}",
"status": "completed",
"response": "{{ $json.aiResponse }}",
"timestamp": "{{ $now.toISO() }}"
}
```
**Message (вариант 2 - просто текст, тоже работает):**
```
{{ $json.aiResponse }}
```
⚠️ **Важно:** SSE endpoint поддерживает оба формата:
- JSON объект с полем `response` - предпочтительно
- Просто текст ответа - тоже работает (автоматически обрабатывается)
### Шаг 2.5: Сохранение в Redis ключ (ВАЖНО для fallback)
⚠️ **КРИТИЧНО:** Сохраняйте ответ в Redis ключ **ПЕРЕД** публикацией в канал!
**Порядок действий в n8n:**
1. Обработка AI → получен ответ
2. **Сначала:** Redis SET → сохранить в ключ `ai:response:cache:{taskId}` (TTL 300 сек)
3. **Потом:** Redis PUBLISH → опубликовать в канал `ai:response:{taskId}`
**Добавьте Redis ноду для SET (перед PUBLISH):**
**Operation:** `Set`
**Key:** `ai:response:cache:{{ $json.taskId }}`
**Value:**
```json
{
"task_id": "{{ $json.taskId }}",
"response": "{{ $json.aiResponse }}",
"status": "completed",
"timestamp": "{{ $now.toISO() }}"
}
```
**TTL:** `300` секунд (5 минут)
**Зачем это нужно:**
- Если SSE не подписался вовремя → fallback найдет ответ в ключе
- Если браузер перезагрузился → ответ все еще доступен
- Надежность: двойное сохранение (канал + ключ)
### Шаг 3: Канал Redis
Канал формируется автоматически из `taskId`:
```
ai:response:{{ $json.taskId }}
```
Или используйте значение из входящего запроса:
```
{{ $json.redisChannel }}
```
## 📝 Пример workflow
```
[Webhook] → [AI обработка] → [Redis SET] → [Redis PUBLISH] → [End]
↓ ↓
[Сохранить историю в n8n] [Ответ в ключе + канале]
```
**Порядок:**
1. SET в ключ `ai:response:cache:{taskId}` (для fallback)
2. PUBLISH в канал `ai:response:{taskId}` (для SSE)
3. Сохранение истории в n8n
### Детали Redis ноды:
**Input:**
- `taskId` - из входящего webhook запроса
- `aiResponse` - результат обработки AI
- `redisChannel` - канал из входящего запроса (`ai:response:{taskId}`)
**Output:**
- Публикация в Redis канал
- Браузер получает через SSE мгновенно
## ✅ Проверка
### Тест публикации из командной строки:
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
PUBLISH "ai:response:test-task" '{"task_id":"test-task","response":"Тест","status":"completed"}'
```
### Проверка в n8n:
1. Запустите workflow с тестовым запросом
2. Проверьте логи Redis ноды - должна быть успешная публикация
3. В браузере откройте AI Drawer и отправьте сообщение
4. Ответ должен прийти мгновенно через SSE
## 🔍 Отладка
### Если ответ не приходит:
1. **Проверьте канал Redis:**
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
MONITOR
```
Должны видеть PUBLISH команды
2. **Проверьте формат сообщения:**
Сообщение должно быть валидным JSON:
```json
{
"task_id": "task-123",
"response": "Ответ от AI",
"status": "completed"
}
```
3. **Проверьте SSE endpoint:**
```bash
curl -N "https://crm.clientright.ru/aiassist/ai_sse.php?task_id=test-task"
```
## 📊 Преимущества новой архитектуры
✅ **Проще** - нет промежуточного callback
✅ **Быстрее** - прямая публикация в Redis
✅ **Надежнее** - меньше точек отказа
✅ **Меньше кода** - убрали БД и callback
## 🚨 Важно
- История диалога сохраняется в n8n автоматически (не нужно дублировать в БД)
- Если Redis недоступен, браузер автоматически переключится на fallback (polling)
- Канал Redis уникален для каждого запроса: `ai:response:{taskId}`
## 📁 Связанные файлы
- `/aiassist/n8n_proxy.php` - отправляет запрос в n8n с параметрами Redis
- `/aiassist/ai_sse.php` - SSE endpoint для получения ответов из Redis
- `/layouts/v7/resources/js/ai-drawer-simple.js` - JavaScript клиент с SSE

View File

@@ -0,0 +1,107 @@
<?php
/**
* check_redis_response.php
* Проверка ответа в Redis (fallback если SSE не работает)
* Проверяет ключ Redis где хранится последний ответ
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
try {
$taskId = $_GET['task_id'] ?? null;
if (!$taskId) {
throw new Exception('Missing task_id parameter');
}
$redisKey = "ai:response:cache:{$taskId}";
// Подключаемся к Redis
if (class_exists('Redis')) {
$redis = new Redis();
if (!$redis->connect('crm.clientright.ru', 6379)) {
throw new Exception('Redis connection failed');
}
$redis->auth('CRM_Redis_Pass_2025_Secure!');
// Пробуем получить ответ из кеша
$cachedResponse = $redis->get($redisKey);
if ($cachedResponse) {
$responseData = json_decode($cachedResponse, true);
if ($responseData && isset($responseData['response'])) {
echo json_encode([
'found' => true,
'response' => $responseData['response'],
'status' => $responseData['status'] ?? 'completed',
'timestamp' => $responseData['timestamp'] ?? null
]);
} else {
// Если это просто строка
echo json_encode([
'found' => true,
'response' => $cachedResponse
]);
}
} else {
echo json_encode([
'found' => false,
'message' => 'Ответ еще не готов или истек TTL'
]);
}
$redis->close();
} else {
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!',
]);
$cachedResponse = $redis->get($redisKey);
if ($cachedResponse) {
$responseData = json_decode($cachedResponse, true);
if ($responseData && isset($responseData['response'])) {
echo json_encode([
'found' => true,
'response' => $responseData['response'],
'status' => $responseData['status'] ?? 'completed',
'timestamp' => $responseData['timestamp'] ?? null
]);
} else {
echo json_encode([
'found' => true,
'response' => $cachedResponse
]);
}
} else {
echo json_encode([
'found' => false,
'message' => 'Ответ еще не готов или истек TTL'
]);
}
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'found' => false,
'error' => $e->getMessage()
]);
}
?>

View File

@@ -1,6 +1,7 @@
<?php <?php
// aiassist/n8n_proxy.php // aiassist/n8n_proxy.php
// Proxy между AI Drawer и n8n с асинхронной обработкой // Proxy между AI Drawer и n8n
// n8n обрабатывает запрос и публикует ответ напрямую в Redis
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Origin: *');
@@ -22,17 +23,6 @@ if (!function_exists('curl_init')) {
} }
try { try {
// Подключаемся к БД
include_once('../config.inc.php');
$conn = new mysqli($dbconfig['db_server'], $dbconfig['db_username'], $dbconfig['db_password'], $dbconfig['db_name']);
if ($conn->connect_error) {
throw new Exception("DB connection failed: " . $conn->connect_error);
}
$conn->set_charset('utf8mb4');
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
if (!$input) { if (!$input) {
@@ -48,22 +38,8 @@ try {
error_log("N8N Proxy: New task {$taskId} for session {$sessionId}"); error_log("N8N Proxy: New task {$taskId} for session {$sessionId}");
// Сохраняем начальный статус в БД
$requestData = json_encode(['message' => $message, 'context' => $context], JSON_UNESCAPED_UNICODE);
$stmt = $conn->prepare("INSERT INTO ai_responses (task_id, session_id, status, request_data) VALUES (?, ?, 'processing', ?)");
$stmt->bind_param('sss', $taskId, $sessionId, $requestData);
if (!$stmt->execute()) {
throw new Exception("Failed to save task: " . $stmt->error);
}
$stmt->close();
$conn->close();
// Формируем callback URL
$callbackUrl = 'https://crm.clientright.ru/callback_ai_response.php';
// Отправляем запрос в n8n // Отправляем запрос в n8n
// n8n обработает и опубликует ответ напрямую в Redis: ai:response:{taskId}
$n8nWebhookUrl = 'https://n8n.clientright.pro/webhook/0b20bf1e-7cda-4dc8-899e-a7c3be4096c0'; $n8nWebhookUrl = 'https://n8n.clientright.pro/webhook/0b20bf1e-7cda-4dc8-899e-a7c3be4096c0';
$payload = [ $payload = [
@@ -71,12 +47,15 @@ try {
'context' => $context, 'context' => $context,
'sessionId' => $sessionId, 'sessionId' => $sessionId,
'taskId' => $taskId, 'taskId' => $taskId,
'callbackUrl' => $callbackUrl, 'redisChannel' => "ai:response:{$taskId}", // Канал для публикации ответа
'redisHost' => 'crm.clientright.ru',
'redisPort' => 6379,
'redisPassword' => 'CRM_Redis_Pass_2025_Secure!',
'timestamp' => date('Y-m-d H:i:s'), 'timestamp' => date('Y-m-d H:i:s'),
'source' => 'crm-client' 'source' => 'crm-client'
]; ];
error_log("N8N Proxy: Sending to n8n - Task: {$taskId}"); error_log("N8N Proxy: Sending to n8n - Task: {$taskId}, Redis channel: ai:response:{$taskId}");
$ch = curl_init(); $ch = curl_init();
curl_setopt_array($ch, [ curl_setopt_array($ch, [
@@ -105,11 +84,13 @@ try {
} }
// Возвращаем task_id клиенту // Возвращаем task_id клиенту
// Клиент подпишется на SSE и получит ответ когда n8n опубликует в Redis
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'task_id' => $taskId, 'task_id' => $taskId,
'status' => 'accepted', 'status' => 'accepted',
'message' => 'Запрос принят в обработку' 'message' => 'Запрос принят в обработку',
'redisChannel' => "ai:response:{$taskId}" // Для информации
]); ]);
} catch (Exception $e) { } catch (Exception $e) {

View File

@@ -4,6 +4,7 @@ class AIDrawer {
this.fontSize = 'normal'; this.fontSize = 'normal';
this.avatarType = 'default'; this.avatarType = 'default';
this.sessionId = null; this.sessionId = null;
this.currentEventSource = null; // Для SSE соединения
this.init(); this.init();
// Загружаем историю сразу при инициализации (при загрузке страницы) // Загружаем историю сразу при инициализации (при загрузке страницы)
@@ -425,9 +426,9 @@ class AIDrawer {
console.log('AI Drawer: data.success =', data.success, 'type:', typeof data.success); console.log('AI Drawer: data.success =', data.success, 'type:', typeof data.success);
if (data.success && data.task_id) { if (data.success && data.task_id) {
// Запрос принят, начинаем polling по task_id // Запрос принят, подписываемся на SSE события через Redis
console.log('AI Drawer: Request accepted, task_id:', data.task_id); console.log('AI Drawer: Request accepted, task_id:', data.task_id);
this.startPolling(data.task_id); this.startSSEListener(data.task_id);
} else { } else {
throw new Error(data.message || 'Unknown error'); throw new Error(data.message || 'Unknown error');
} }
@@ -440,18 +441,189 @@ class AIDrawer {
} }
} }
// Метод для polling результатов // Метод для подписки на SSE события через Redis Pub/Sub
async startPolling(taskId) { startSSEListener(taskId) {
console.log('AI Drawer: Starting polling for task:', taskId); console.log('AI Drawer: Starting SSE listener for task:', taskId);
// Закрываем предыдущее соединение если есть
if (this.currentEventSource) {
this.currentEventSource.close();
}
// Флаг для отслеживания получения ответа
let responseReceived = false;
// Создаем новое SSE соединение
const sseUrl = `/aiassist/ai_sse.php?task_id=${encodeURIComponent(taskId)}`;
this.currentEventSource = new EventSource(sseUrl);
// Обработчик подключения
this.currentEventSource.onopen = () => {
console.log('AI Drawer: SSE connection opened');
};
// Обработчик получения ответа
this.currentEventSource.addEventListener('response', (event) => {
try {
const data = JSON.parse(event.data);
console.log('AI Drawer: Received response via SSE:', data);
if (data.data && data.data.response) {
responseReceived = true; // Отмечаем что получили ответ
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.data.response, false, 25);
this.currentEventSource.close();
this.currentEventSource = null;
}
} catch (error) {
console.error('AI Drawer: SSE response parse error:', error);
}
});
// Обработчик ошибок SSE (стандартное событие)
this.currentEventSource.onerror = (event) => {
console.error('AI Drawer: SSE connection error:', event);
console.log('AI Drawer: SSE readyState:', this.currentEventSource?.readyState);
// НЕ вызываем fallback если уже получили ответ (SSE закрывается после отправки)
if (responseReceived) {
console.log('AI Drawer: Response already received, ignoring SSE close error');
return;
}
// Fallback на polling только если SSE действительно не работает
if (this.currentEventSource && this.currentEventSource.readyState === EventSource.CLOSED) {
console.log('AI Drawer: SSE closed without response, falling back to Redis check');
this.currentEventSource.close();
this.currentEventSource = null;
// Вместо polling БД проверяем Redis напрямую
this.checkRedisDirectly(taskId);
}
};
// Обработчик кастомных ошибок от сервера
this.currentEventSource.addEventListener('error', (event) => {
try {
const data = JSON.parse(event.data);
console.error('AI Drawer: SSE error event from server:', data);
if (data.data && data.data.error) {
responseReceived = true; // Отмечаем что получили ответ (даже если ошибка)
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.data.error || 'Произошла ошибка при обработке запроса.', false, 25);
this.currentEventSource.close();
this.currentEventSource = null;
}
} catch (error) {
// Игнорируем ошибки парсинга
}
});
// Обработчик heartbeat (поддержание соединения)
this.currentEventSource.addEventListener('heartbeat', (event) => {
console.log('AI Drawer: SSE heartbeat received');
});
// Обработчик подключения
this.currentEventSource.addEventListener('connected', (event) => {
console.log('AI Drawer: SSE connected:', event.data);
});
// Таймаут на случай если SSE не работает (fallback на Redis check)
setTimeout(() => {
if (this.currentEventSource && this.currentEventSource.readyState === EventSource.CONNECTING && !responseReceived) {
console.log('AI Drawer: SSE timeout, checking Redis directly');
this.currentEventSource.close();
this.currentEventSource = null;
this.checkRedisDirectly(taskId);
}
}, 5000); // 5 секунд на подключение
// Общий таймаут ожидания ответа (5 минут)
setTimeout(() => {
if (this.currentEventSource && !responseReceived) {
this.currentEventSource.close();
this.currentEventSource = null;
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Время ожидания истекло. Попробуйте еще раз.', false, 25);
}
}, 300000);
}
// Метод для прямой проверки Redis (если SSE не работает)
async checkRedisDirectly(taskId) {
console.log('AI Drawer: Checking Redis directly for task:', taskId);
// Проверяем несколько раз с интервалом (на случай если ответ еще обрабатывается)
let attempts = 0;
const maxAttempts = 30; // 30 попыток = 1 минута (каждые 2 секунды)
const checkInterval = setInterval(async () => {
attempts++;
console.log(`AI Drawer: Redis check attempt ${attempts}/${maxAttempts}`);
try {
const response = await fetch(`/aiassist/check_redis_response.php?task_id=${encodeURIComponent(taskId)}`);
if (!response.ok) {
throw new Error(`Redis check failed: ${response.status}`);
}
const data = await response.json();
if (data.found && data.response) {
clearInterval(checkInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.response, false, 25);
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Ответ не получен. Попробуйте отправить запрос еще раз.', false, 25);
}
} catch (error) {
console.error('AI Drawer: Redis direct check error:', error);
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Ошибка при получении ответа. Попробуйте еще раз.', false, 25);
}
}
}, 2000); // Проверяем каждые 2 секунды
}
// Fallback метод для polling (если SSE не работает)
async startPollingFallback(taskId) {
console.log('AI Drawer: Starting polling fallback for task:', taskId);
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
try { try {
const completed = await this.checkAIResult(taskId); const response = await fetch(`/get_ai_result.php?task_id=${taskId}`);
if (completed) {
if (!response.ok) {
throw new Error(`Result check failed: ${response.status}`);
}
const data = await response.json();
if (data.status === 'completed') {
clearInterval(pollInterval); clearInterval(pollInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.response, false, 25);
} else if (data.status === 'error') {
clearInterval(pollInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.error || 'Произошла ошибка при обработке запроса.', false, 25);
} }
} catch (error) { } catch (error) {
console.error('AI Drawer: Polling error:', error); console.error('AI Drawer: Polling fallback error:', error);
clearInterval(pollInterval); clearInterval(pollInterval);
this.hideLoading(); this.hideLoading();
this.hideTypingIndicator(); this.hideTypingIndicator();
@@ -468,38 +640,6 @@ class AIDrawer {
}, 300000); }, 300000);
} }
// Метод для проверки результата
async checkAIResult(taskId) {
console.log('AI Drawer: Checking result for task:', taskId);
const response = await fetch(`/get_ai_result.php?task_id=${taskId}`);
if (!response.ok) {
throw new Error(`Result check failed: ${response.status}`);
}
const data = await response.json();
console.log('AI Drawer: Result check response:', data);
if (data.status === 'completed') {
// Задача завершена
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.response, false, 25);
return true; // Остановить polling
} else if (data.status === 'error') {
// Ошибка обработки
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.error || 'Произошла ошибка при обработке запроса.', false, 25);
return true; // Остановить polling
} else {
// Задача еще обрабатывается
console.log('AI Drawer: Still processing...');
return false; // Продолжить polling
}
}
getCurrentContext() { getCurrentContext() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('record') || ''; const projectId = urlParams.get('record') || '';