From 496268e93647f5afc78a3b8f99cc2eecc808a58c Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 15 Feb 2026 11:06:00 +0100 Subject: [PATCH] feat: HTTP fallback for transcript delivery, updated Teams language selectors - Auto-detect WebSocket failure and switch to HTTP POST for transcripts/status - Derive HTTP base URL from WebSocket URL (wss->https, ws->http) - Updated Teams caption language menu selectors for current UI - Added: Captions & transcripts, Untertitel und Transkripte, Gesprochene Sprache Co-authored-by: Cursor --- src/bot/captionsProcedure.ts | 15 +++++- src/bot/orchestrator.ts | 88 +++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/bot/captionsProcedure.ts b/src/bot/captionsProcedure.ts index bba1629..e1f6ca9 100644 --- a/src/bot/captionsProcedure.ts +++ b/src/bot/captionsProcedure.ts @@ -227,13 +227,20 @@ export class CaptionsProcedure { await this._openMoreMenu(); await this._page.waitForTimeout(500); - // Look for "Language and speech" or "Spoken language" menu item + // Look for "Language and speech" or "Captions & transcripts" menu items + // Teams has renamed this menu multiple times across versions const languageMenuSelectors = [ + '[data-tid="captions-and-transcripts-button"]', + ':has-text("Captions & transcripts")', + ':has-text("Captions and transcripts")', + ':has-text("Untertitel und Transkripte")', ':has-text("Language and speech")', ':has-text("Spoken language")', ':has-text("Sprache und Spracheingabe")', + ':has-text("Gesprochene Sprache")', '[data-tid="language-and-speech-button"]', 'button:has-text("Language")', + 'button:has-text("Sprache")', ]; for (const selector of languageMenuSelectors) { @@ -257,11 +264,15 @@ export class CaptionsProcedure { return; } - // Now look for the "Language settings" sub-option if needed + // Now look for the "Language settings" / "Change spoken language" sub-option if needed const langSettingsSelectors = [ + ':has-text("Change spoken language")', + ':has-text("Gesprochene Sprache ändern")', ':has-text("Language settings")', ':has-text("Spracheinstellungen")', 'button:has-text("Language settings")', + 'button:has-text("Spoken language")', + 'button:has-text("Gesprochene Sprache")', ]; for (const selector of langSettingsSelectors) { diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 5ed387a..90c3ce9 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -47,6 +47,8 @@ export class BotOrchestrator { private _context: BrowserContext | null = null; private _page: Page | null = null; private _gatewayWs: WebSocket | null = null; + private _useHttpFallback: boolean = false; + private _httpBaseUrl: string = ''; private _joinProcedure: JoinProcedure | null = null; private _captionsProcedure: CaptionsProcedure | null = null; @@ -139,11 +141,29 @@ export class BotOrchestrator { const wsUrl = this._options.gatewayWsUrl; this._logger.info(`Connecting to Gateway: ${wsUrl}`); + // Derive HTTP base URL from WebSocket URL for fallback + this._httpBaseUrl = wsUrl + .replace('wss://', 'https://') + .replace('ws://', 'http://') + .replace(/\/bot\/ws\/.*$/, ''); + return new Promise((resolve, reject) => { this._gatewayWs = new WebSocket(wsUrl); + const wsTimeout = setTimeout(() => { + if (this._gatewayWs?.readyState !== WebSocket.OPEN) { + this._logger.warn('WebSocket connection timeout - switching to HTTP fallback'); + this._useHttpFallback = true; + this._gatewayWs?.close(); + this._gatewayWs = null; + resolve(); // Continue with HTTP fallback instead of failing + } + }, 10000); + this._gatewayWs.on('open', () => { - this._logger.info('Connected to Gateway'); + clearTimeout(wsTimeout); + this._logger.info('Connected to Gateway via WebSocket'); + this._useHttpFallback = false; resolve(); }); @@ -152,23 +172,21 @@ export class BotOrchestrator { }); this._gatewayWs.on('close', (code, reason) => { - this._logger.warn(`Gateway connection closed: ${code} - ${reason}`); - if (!this._isShuttingDown) { - this._setState('error', 'Gateway connection lost'); + this._logger.warn(`Gateway WebSocket closed: ${code} - ${reason}`); + if (!this._isShuttingDown && !this._useHttpFallback) { + this._logger.info('Switching to HTTP fallback for transcript delivery'); + this._useHttpFallback = true; } }); this._gatewayWs.on('error', (error) => { + clearTimeout(wsTimeout); this._logger.error('Gateway WebSocket error:', error); - reject(error); + this._logger.info('Switching to HTTP fallback for transcript delivery'); + this._useHttpFallback = true; + this._gatewayWs = null; + resolve(); // Continue with HTTP fallback }); - - // Timeout after 10 seconds - setTimeout(() => { - if (this._gatewayWs?.readyState !== WebSocket.OPEN) { - reject(new Error('Gateway connection timeout')); - } - }, 10000); }); } @@ -198,18 +216,54 @@ export class BotOrchestrator { } /** - * Send a message to the Gateway. + * Send a message to the Gateway (WebSocket or HTTP fallback). */ private _sendToGateway(message: object): void { - if (!this._gatewayWs || this._gatewayWs.readyState !== WebSocket.OPEN) { - this._logger.warn('Cannot send to Gateway - not connected'); + if (this._gatewayWs && this._gatewayWs.readyState === WebSocket.OPEN) { + try { + this._gatewayWs.send(JSON.stringify(message)); + return; + } catch (error) { + this._logger.error('WebSocket send error, falling back to HTTP:', error); + this._useHttpFallback = true; + } + } + + // HTTP fallback + if (this._useHttpFallback) { + this._sendViaHttp(message); + } else { + this._logger.warn('Cannot send to Gateway - no WebSocket and no HTTP fallback'); + } + } + + /** + * Send a message via HTTP POST (fallback when WebSocket unavailable). + */ + private async _sendViaHttp(message: any): Promise { + const msgType = message.type; + let url = ''; + + if (msgType === 'transcript') { + url = `${this._httpBaseUrl}/bot/transcript/${this._sessionId}`; + } else if (msgType === 'status') { + url = `${this._httpBaseUrl}/bot/status/${this._sessionId}`; + } else { + this._logger.debug(`HTTP fallback: unsupported message type ${msgType}`); return; } try { - this._gatewayWs.send(JSON.stringify(message)); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(message), + }); + if (!response.ok) { + this._logger.warn(`HTTP fallback response: ${response.status} ${response.statusText}`); + } } catch (error) { - this._logger.error('Error sending to Gateway:', error); + this._logger.error(`HTTP fallback error for ${msgType}:`, error); } }