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

@@ -506,6 +506,7 @@ export function registerBotApiRoutes(app: import("express").Express): void {
replyToId: conversationMessages.replyToId,
mentionedUserIds: conversationMessages.mentionedUserIds,
attachments: conversationMessages.attachments,
botButtons: conversationMessages.botButtons,
createdAt: conversationMessages.createdAt,
updatedAt: conversationMessages.updatedAt,
isDeleted: conversationMessages.isDeleted,
@@ -587,9 +588,10 @@ export function registerBotApiRoutes(app: import("express").Express): void {
return res.status(400).json({ success: false, error: 'Некорректный ID чата' });
}
const { message, attachments } = req.body;
if (!message?.trim() && (!attachments || attachments.length === 0)) {
return res.status(400).json({ success: false, error: 'Сообщение или вложения обязательны' });
const { message, attachments, buttons } = req.body;
const hasButtons = Array.isArray(buttons) && buttons.length > 0;
if (!message?.trim() && (!attachments || attachments.length === 0) && !hasButtons) {
return res.status(400).json({ success: false, error: 'Сообщение, вложения или кнопки обязательны' });
}
const [conv] = await db
@@ -616,6 +618,7 @@ export function registerBotApiRoutes(app: import("express").Express): void {
replyToId: null,
mentionedUserIds: null,
attachments: attachments?.length ? attachments : null,
botButtons: hasButtons ? buttons : null,
})
.returning();