feat(chat): typing indicator для ботов, кнопки ботов в мессенджере, VPN-бот с кнопками

- Добавлены SSE-события conv_typing_started/stopped и их обработка в Chat.tsx
- AI-бот публикует typing events перед/после генерации ответа
- Боты могут отправлять сообщения с кнопками через /api/bot/conversations/:id/messages
- Добавлен callback /api/messenger/messages/:id/callback для нажатий кнопок
- VPN-бот использует кнопки Android/iOS вместо текстового prompt
- Оптимистичное обновление unread count, ускоренный reportActiveChat, polling fallback,
  логирование SSE, push suppression для bot_direct
This commit is contained in:
2026-07-17 11:25:25 +03:00
parent 586a591773
commit 6bb89b4cf8
16 changed files with 492 additions and 60 deletions

View File

@@ -114,6 +114,15 @@ export class EventBus {
timestamp: Date.now(),
};
// Логируем chat-события для диагностики задержек/пропусков
const isChatEvent = event.type === 'conv_message_created' || event.type === 'conv_message_updated' ||
event.type === 'conv_message_deleted' || event.type === 'conv_read' || event.type === 'message_created' ||
event.type === 'message_read';
if (isChatEvent) {
const target = event.userId ? `user=${event.userId}` : `org=${event.organizationId}`;
console.log(`[SSE] publish ${event.type} ${target} conn=${this.connections.size} id=${bufferedEvent.id}`);
}
// Буферизуем событие для восстановления после reconnect
if (event.userId) {
this.pushToBuffer(this.userBuffers, event.userId, bufferedEvent);
@@ -121,6 +130,7 @@ export class EventBus {
this.pushToBuffer(this.orgBuffers, event.organizationId, bufferedEvent);
}
let delivered = 0;
this.connections.forEach((connection, id) => {
// Проверяем tenant isolation
if (event.organizationId && connection.organizationId !== event.organizationId) {
@@ -132,8 +142,13 @@ export class EventBus {
return;
}
this.writeEventToConnection(connection, bufferedEvent);
const ok = this.writeEventToConnection(connection, bufferedEvent);
if (ok) delivered++;
});
if (isChatEvent) {
console.log(`[SSE] delivered ${event.type} to ${delivered}/${this.connections.size} connection(s)`);
}
}
/**