perf(vpn-bot): фоновая подготовка подписки, оптимизация запросов, анимация кнопок

- validateTelemostRoom: таймаут 3 сек + кэш 10 мин
- createVpnTask: транзакция + batch insert field values
- prepareVpnSubscription: валидация и создание подписки в фоне при получении ссылки
- sendVpnBotMessage: кэш botId, Promise.all для enrich/members/orgMap
- processPlatformSelection: одно сообщение с файлом и подпиской вместо двух
- Chat.tsx: нажатая кнопка становится зелёной галочкой, остальные disabled
This commit is contained in:
2026-07-17 12:38:33 +03:00
parent 6bb89b4cf8
commit 9f1c2e5b45
4 changed files with 224 additions and 103 deletions

View File

@@ -72,7 +72,14 @@ export function extractRoomId(roomUrl: string): string {
return roomUrl.slice(idx + 3);
}
const telemostValidationCache = new Map<string, { valid: boolean; expiresAt: number }>();
export async function validateTelemostRoom(roomUrl: string): Promise<boolean> {
const cached = telemostValidationCache.get(roomUrl);
if (cached && cached.expiresAt > Date.now()) {
return cached.valid;
}
try {
const encoded = encodeURIComponent(roomUrl);
const url = `${VPN_TELEMOST_API_BASE}/conferences/${encoded}/connection?next_gen_media_platform_allowed=true&display_name=VPN-Bot&waiting_room_supported=true`;
@@ -87,20 +94,49 @@ export async function validateTelemostRoom(roomUrl: string): Promise<boolean> {
Origin: 'https://telemost.yandex.ru',
Referer: 'https://telemost.yandex.ru/',
},
signal: AbortSignal.timeout(3000),
});
return resp.status === 200;
const valid = resp.status === 200;
telemostValidationCache.set(roomUrl, { valid, expiresAt: Date.now() + 10 * 60 * 1000 });
return valid;
} catch (err) {
console.error('[VPN] Telemost validation error:', err);
telemostValidationCache.set(roomUrl, { valid: false, expiresAt: Date.now() + 60 * 1000 });
return false;
}
}
export async function getOrCreateVpnSubscription(
export interface PreparedSubscription {
subscriptionUrl: string;
isNew: boolean;
taskId?: number;
roomId: string;
cryptoKey?: string;
}
export async function getExistingVpnSubscription(
organizationId: number,
roomUrl: string,
): Promise<PreparedSubscription | null> {
const existing = await findVpnTaskByRoomUrl(roomUrl, organizationId);
if (!existing) return null;
const token = existing.values.subscription_token as string | undefined;
if (!token) return null;
return {
subscriptionUrl: buildSubscriptionUrl(token),
isNew: false,
taskId: existing.task.id,
roomId: extractRoomId(roomUrl),
cryptoKey: existing.values.crypto_key as string | undefined,
};
}
export async function prepareVpnSubscription(
organizationId: number,
userId: number,
userName: string,
roomUrl: string,
): Promise<{ subscriptionUrl: string; isNew: boolean }> {
): Promise<PreparedSubscription> {
const existing = await findVpnTaskByRoomUrl(roomUrl, organizationId);
if (existing && existing.task.createdBy !== userId) {
throw new VpnError('Эта комната уже используется другим сотрудником', 'ROOM_IN_USE');
@@ -108,7 +144,13 @@ export async function getOrCreateVpnSubscription(
if (existing) {
const token = existing.values.subscription_token as string | undefined;
if (!token) throw new VpnError('Подписка повреждена, обратитесь к администратору', 'BROKEN');
return { subscriptionUrl: buildSubscriptionUrl(token), isNew: false };
return {
subscriptionUrl: buildSubscriptionUrl(token),
isNew: false,
taskId: existing.task.id,
roomId: extractRoomId(roomUrl),
cryptoKey: existing.values.crypto_key as string | undefined,
};
}
const roomId = extractRoomId(roomUrl);
@@ -125,7 +167,7 @@ export async function getOrCreateVpnSubscription(
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + DEFAULT_SUBSCRIPTION_DAYS);
await createVpnTask({
const task = await createVpnTask({
organizationId,
createdBy: userId,
title: `VPN ${userName || `#${userId}`} / ${roomId}`,
@@ -144,7 +186,24 @@ export async function getOrCreateVpnSubscription(
},
});
return { subscriptionUrl: buildSubscriptionUrl(token), isNew: true };
return {
subscriptionUrl: buildSubscriptionUrl(token),
isNew: true,
taskId: task.id,
roomId,
cryptoKey,
};
}
/** @deprecated Use prepareVpnSubscription instead */
export async function getOrCreateVpnSubscription(
organizationId: number,
userId: number,
userName: string,
roomUrl: string,
): Promise<{ subscriptionUrl: string; isNew: boolean }> {
const result = await prepareVpnSubscription(organizationId, userId, userName, roomUrl);
return { subscriptionUrl: result.subscriptionUrl, isNew: result.isNew };
}
export async function fetchSubscription(token: string, hwid: string | undefined): Promise<string> {