mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(meet): recognize backend capacity gate and surface an actionable message (#4221)
This commit is contained in:
@@ -3,6 +3,7 @@ import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } fro
|
||||
import { type MascotFace, RiveMascot } from '../../features/human/Mascot';
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import {
|
||||
isCapacityGateMessage,
|
||||
joinMeetViaBackendBot,
|
||||
leaveBackendMeetBot,
|
||||
listMeetCalls,
|
||||
@@ -260,7 +261,12 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
|
||||
if (!hasSubmittedRef.current) return;
|
||||
if (meetStatus === 'error') {
|
||||
hasSubmittedRef.current = false;
|
||||
const message = meetError?.trim() || t('skills.meetingBots.failedToStart');
|
||||
const raw = meetError?.trim() || t('skills.meetingBots.failedToStart');
|
||||
// A capacity-gate error carries the backend's terse "…try again later."
|
||||
// wording; show the tailored, actionable (and localized) copy instead (#4151).
|
||||
const message = isCapacityGateMessage(raw)
|
||||
? t('skills.meetingBots.serverOverloaded')
|
||||
: raw;
|
||||
setError(message);
|
||||
setSubmitting(false);
|
||||
onToast?.({ type: 'error', title: t('skills.meetingBots.couldNotStartTitle'), message });
|
||||
@@ -288,7 +294,10 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps)
|
||||
listenOnly,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : t('skills.meetingBots.failedToStart');
|
||||
const raw = err instanceof Error ? err.message : t('skills.meetingBots.failedToStart');
|
||||
const message = isCapacityGateMessage(raw)
|
||||
? t('skills.meetingBots.serverOverloaded')
|
||||
: raw;
|
||||
setError(message);
|
||||
setSubmitting(false);
|
||||
hasSubmittedRef.current = false;
|
||||
|
||||
@@ -5047,6 +5047,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'تكبير',
|
||||
'skills.meetingBots.sendTo': 'إرسال إلى',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'يشهد OpenHuman ضغطًا كبيرًا في الوقت الحالي. يُرجى المحاولة مرة أخرى بعد بضع دقائق.',
|
||||
'skills.meetingBots.soonSuffix': 'قريبًا',
|
||||
'skills.meetingBots.starting': 'جارٍ البدء…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'مكالمات الاجتماعات الأخيرة',
|
||||
|
||||
@@ -5151,6 +5151,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'মাইক্রোসফট টিম',
|
||||
'skills.meetingBots.platforms.zoom': 'জুম',
|
||||
'skills.meetingBots.sendTo': 'পাঠান',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman এই মুহূর্তে অত্যধিক চাপের মধ্যে রয়েছে। অনুগ্রহ করে কয়েক মিনিট পরে আবার চেষ্টা করুন।',
|
||||
'skills.meetingBots.soonSuffix': 'শীঘ্রই',
|
||||
'skills.meetingBots.starting': 'শুরু হচ্ছে…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'সাম্প্রতিক মিটিং কল',
|
||||
|
||||
@@ -5283,6 +5283,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Senden an',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman ist gerade stark ausgelastet. Bitte versuche es in ein paar Minuten erneut.',
|
||||
'skills.meetingBots.soonSuffix': 'bald',
|
||||
'skills.meetingBots.starting': 'Beginnend mit …',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Letzte Meeting-Anrufe',
|
||||
|
||||
@@ -5798,6 +5798,8 @@ const en: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Send to {label}',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman is under heavy load right now. Please try again in a few minutes.',
|
||||
'skills.meetingBots.soonSuffix': 'soon',
|
||||
'skills.meetingBots.starting': 'Starting…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Recent meeting calls',
|
||||
|
||||
@@ -5247,6 +5247,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Equipos de Microsoft',
|
||||
'skills.meetingBots.platforms.zoom': 'Ampliar',
|
||||
'skills.meetingBots.sendTo': 'Enviar a',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman tiene una carga muy alta en este momento. Inténtalo de nuevo en unos minutos.',
|
||||
'skills.meetingBots.soonSuffix': 'pronto',
|
||||
'skills.meetingBots.starting': 'Iniciando…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Llamadas de reunión recientes',
|
||||
|
||||
@@ -5268,6 +5268,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Envoyer à',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman est actuellement très sollicité. Veuillez réessayer dans quelques minutes.',
|
||||
'skills.meetingBots.soonSuffix': 'bientôt',
|
||||
'skills.meetingBots.starting': 'Démarrage…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Appels de réunion récents',
|
||||
|
||||
@@ -5155,6 +5155,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'माइक्रोसॉफ्ट टीमें',
|
||||
'skills.meetingBots.platforms.zoom': 'ज़ूम करें',
|
||||
'skills.meetingBots.sendTo': 'भेजें',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman अभी भारी लोड में है। कृपया कुछ मिनटों में फिर से प्रयास करें।',
|
||||
'skills.meetingBots.soonSuffix': 'जल्द ही',
|
||||
'skills.meetingBots.starting': 'शुरू हो रहा है…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'हाल की मीटिंग कॉल',
|
||||
|
||||
@@ -5169,6 +5169,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Kirim ke',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman sedang menerima beban tinggi saat ini. Silakan coba lagi dalam beberapa menit.',
|
||||
'skills.meetingBots.soonSuffix': 'segera',
|
||||
'skills.meetingBots.starting': 'Memulai...',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Panggilan rapat terbaru',
|
||||
|
||||
@@ -5236,6 +5236,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Invia a',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman è sottoposto a un carico elevato in questo momento. Riprova tra qualche minuto.',
|
||||
'skills.meetingBots.soonSuffix': 'presto',
|
||||
'skills.meetingBots.starting': 'Avvio…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Chiamate recenti delle riunioni',
|
||||
|
||||
@@ -5103,6 +5103,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': '보내기',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'현재 OpenHuman의 부하가 매우 높습니다. 몇 분 후에 다시 시도해 주세요.',
|
||||
'skills.meetingBots.soonSuffix': '곧',
|
||||
'skills.meetingBots.starting': '시작 중…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': '최근 회의 통화',
|
||||
|
||||
@@ -5223,6 +5223,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Wyślij do',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman jest teraz mocno obciążony. Spróbuj ponownie za kilka minut.',
|
||||
'skills.meetingBots.soonSuffix': 'wkrótce',
|
||||
'skills.meetingBots.starting': 'Uruchamianie…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Ostatnie rozmowy na spotkaniach',
|
||||
|
||||
@@ -5239,6 +5239,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Enviar para',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'O OpenHuman está sob carga intensa no momento. Tente novamente em alguns minutos.',
|
||||
'skills.meetingBots.soonSuffix': 'em breve',
|
||||
'skills.meetingBots.starting': 'Iniciando…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Chamadas de reunião recentes',
|
||||
|
||||
@@ -5198,6 +5198,8 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': 'Microsoft Teams',
|
||||
'skills.meetingBots.platforms.zoom': 'Zoom',
|
||||
'skills.meetingBots.sendTo': 'Отправить',
|
||||
'skills.meetingBots.serverOverloaded':
|
||||
'OpenHuman сейчас сильно загружен. Пожалуйста, повторите попытку через несколько минут.',
|
||||
'skills.meetingBots.soonSuffix': 'скоро',
|
||||
'skills.meetingBots.starting': 'Запуск…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': 'Недавние звонки на встречах',
|
||||
|
||||
@@ -4897,6 +4897,7 @@ const messages: TranslationMap = {
|
||||
'skills.meetingBots.platforms.teams': '微软团队',
|
||||
'skills.meetingBots.platforms.zoom': '变焦',
|
||||
'skills.meetingBots.sendTo': '发送到会议',
|
||||
'skills.meetingBots.serverOverloaded': 'OpenHuman 当前负载过高,请几分钟后重试。',
|
||||
'skills.meetingBots.soonSuffix': '很快',
|
||||
'skills.meetingBots.starting': '启动中…',
|
||||
'skills.meetingBots.recentCallsAriaLabel': '最近的会议通话',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
isCapacityGateMessage,
|
||||
joinMeetingViaMascotBot,
|
||||
type MascotJoinMeetingError,
|
||||
SERVER_OVERLOADED_MESSAGE,
|
||||
@@ -49,8 +50,14 @@ describe('joinMeetingViaMascotBot', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('flags SERVER_OVERLOADED responses with isCapacityGated=true', async () => {
|
||||
postMock.mockRejectedValueOnce({ success: false, error: SERVER_OVERLOADED_MESSAGE });
|
||||
it('flags the real backend SERVER_OVERLOADED wording and shows the tailored copy (#4151)', async () => {
|
||||
// The VERBATIM backend message (backend src/utils/paidPlan.ts) — NOT the
|
||||
// frontend's friendly constant. Mocking the real wire string is what makes
|
||||
// this test catch the prior exact-equality mismatch that leaked the generic
|
||||
// "…try again later." text to users.
|
||||
const BACKEND_CAPACITY_MESSAGE =
|
||||
'Mascot streaming capacity is exhausted. Please try again later.';
|
||||
postMock.mockRejectedValueOnce({ success: false, error: BACKEND_CAPACITY_MESSAGE });
|
||||
let caught: MascotJoinMeetingError | undefined;
|
||||
try {
|
||||
await joinMeetingViaMascotBot({ platform: 'gmeet', meetUrl: 'https://meet.google.com/abc' });
|
||||
@@ -58,6 +65,7 @@ describe('joinMeetingViaMascotBot', () => {
|
||||
caught = e as MascotJoinMeetingError;
|
||||
}
|
||||
expect(caught?.isCapacityGated).toBe(true);
|
||||
// The tailored, actionable copy is surfaced — not the raw backend string.
|
||||
expect(caught?.message).toBe(SERVER_OVERLOADED_MESSAGE);
|
||||
});
|
||||
|
||||
@@ -75,3 +83,21 @@ describe('joinMeetingViaMascotBot', () => {
|
||||
).rejects.toMatchObject({ isCapacityGated: false, message: 'network down' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCapacityGateMessage', () => {
|
||||
it('matches the real backend capacity wording (and case/wording drift)', () => {
|
||||
expect(
|
||||
isCapacityGateMessage('Mascot streaming capacity is exhausted. Please try again later.')
|
||||
).toBe(true);
|
||||
expect(isCapacityGateMessage('MASCOT STREAMING CAPACITY IS EXHAUSTED.')).toBe(true);
|
||||
expect(isCapacityGateMessage('Streaming capacity reached — retry soon')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not match unrelated errors or empty input', () => {
|
||||
expect(isCapacityGateMessage('Bad Request')).toBe(false);
|
||||
expect(isCapacityGateMessage('network down')).toBe(false);
|
||||
expect(isCapacityGateMessage('')).toBe(false);
|
||||
expect(isCapacityGateMessage(null)).toBe(false);
|
||||
expect(isCapacityGateMessage(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -334,13 +334,34 @@ export interface MascotJoinMeetingResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* The 429 capacity-gate message the backend emits for free users. Treated
|
||||
* as the canonical user-facing copy so the UI can show a tailored notice
|
||||
* without leaking the underlying paid-plan rule.
|
||||
* Tailored, actionable user-facing copy shown when the backend's capacity gate
|
||||
* trips — replaces the backend's terse "…Please try again later." with retry
|
||||
* guidance, without leaking the underlying paid-plan rule.
|
||||
*/
|
||||
export const SERVER_OVERLOADED_MESSAGE =
|
||||
'OpenHuman is under heavy load right now. Please try again in a few minutes.';
|
||||
|
||||
/**
|
||||
* Recognize the backend's free-user capacity-gate response (`SERVER_OVERLOADED`,
|
||||
* backend `paidPlan.ts` → `"Mascot streaming capacity is exhausted. Please try
|
||||
* again later."`).
|
||||
*
|
||||
* The shared `apiClient` drops `errorCode` from error bodies (`apiClient.ts`
|
||||
* only forwards `error` + `message`), so the message text is the only signal
|
||||
* that survives. Detection therefore MUST key on the backend wording — it used
|
||||
* to be compared for exact equality against [`SERVER_OVERLOADED_MESSAGE`], but
|
||||
* that constant was changed to friendlier copy and no longer matches the
|
||||
* backend string, so the check silently never fired and the raw generic
|
||||
* "…try again later." leaked to the user instead of the tailored notice
|
||||
* (#4151). Match a stable substring, case-insensitively, so minor wording drift
|
||||
* on either side still resolves to the actionable message.
|
||||
*/
|
||||
export function isCapacityGateMessage(text: string | null | undefined): boolean {
|
||||
if (!text) return false;
|
||||
const t = text.toLowerCase();
|
||||
return t.includes('streaming capacity') || t.includes('capacity is exhausted');
|
||||
}
|
||||
|
||||
export interface MascotJoinMeetingError {
|
||||
/** User-safe error text. Falls back to a generic message. */
|
||||
message: string;
|
||||
@@ -379,8 +400,13 @@ export async function joinMeetingViaMascotBot(
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to start meeting bot.';
|
||||
const isCapacityGated = text === SERVER_OVERLOADED_MESSAGE;
|
||||
const wrapped: MascotJoinMeetingError = { message: text, isCapacityGated };
|
||||
const isCapacityGated = isCapacityGateMessage(text);
|
||||
// When capacity-gated, surface the tailored, actionable copy instead of the
|
||||
// backend's raw "…try again later." string (#4151).
|
||||
const wrapped: MascotJoinMeetingError = {
|
||||
message: isCapacityGated ? SERVER_OVERLOADED_MESSAGE : text,
|
||||
isCapacityGated,
|
||||
};
|
||||
throw wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user