fix: auth join via direct URL (skip launcher), sequential audio queue, improve AI prompt (less floskel, stricter response rules)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-16 09:02:42 +01:00
parent efa648d6fe
commit a483aa6def
3 changed files with 82 additions and 19 deletions

View file

@ -20,6 +20,8 @@ export class AudioProcedure {
private _logger: Logger; private _logger: Logger;
private _audioContext: boolean = false; private _audioContext: boolean = false;
private _initScriptInjected: boolean = false; private _initScriptInjected: boolean = false;
private _audioQueue: Array<{ audioData: string; format: 'mp3' | 'wav' | 'pcm' }> = [];
private _isPlaying: boolean = false;
constructor(page: Page, logger: Logger) { constructor(page: Page, logger: Logger) {
this._page = page; this._page = page;
@ -113,13 +115,47 @@ export class AudioProcedure {
} }
/** /**
* Play audio in the browser. * Queue audio for sequential playback.
* Audio is piped into the MediaStreamDestination that Teams uses as mic input. * Audio is never played in parallel -- each clip waits for the previous one to finish.
* *
* @param audioData Base64 encoded audio data * @param audioData Base64 encoded audio data
* @param format Audio format (mp3, wav, pcm) * @param format Audio format (mp3, wav, pcm)
*/ */
async playAudio(audioData: string, format: 'mp3' | 'wav' | 'pcm'): Promise<void> { async playAudio(audioData: string, format: 'mp3' | 'wav' | 'pcm'): Promise<void> {
// Add to queue
this._audioQueue.push({ audioData, format });
this._logger.info(`Audio queued (queue size: ${this._audioQueue.length}, playing: ${this._isPlaying})`);
// If not currently playing, start processing the queue
if (!this._isPlaying) {
await this._processAudioQueue();
}
}
/**
* Process the audio queue sequentially.
*/
private async _processAudioQueue(): Promise<void> {
if (this._isPlaying) return;
this._isPlaying = true;
while (this._audioQueue.length > 0) {
const item = this._audioQueue.shift()!;
try {
await this._playAudioInternal(item.audioData, item.format);
} catch (error) {
this._logger.error('Error playing queued audio:', error);
}
}
this._isPlaying = false;
}
/**
* Internal: Play audio in the browser (single clip, no queuing).
* Audio is piped into the MediaStreamDestination that Teams uses as mic input.
*/
private async _playAudioInternal(audioData: string, format: 'mp3' | 'wav' | 'pcm'): Promise<void> {
if (!this._audioContext) { if (!this._audioContext) {
await this.initialize(); await this.initialize();
} }

View file

@ -54,6 +54,24 @@ export class JoinProcedure {
await this._handleLauncherDialog(); await this._handleLauncherDialog();
} }
/**
* Check if a launcher dialog is present and handle it.
* Used for authenticated joins where we navigate directly to the meeting URL
* but Teams may still show the launcher.
*/
async handleLauncherIfPresent(): Promise<void> {
try {
const launcherButton = await this._page.$('button[data-tid="joinOnWeb"]');
if (launcherButton) {
this._logger.info('Launcher dialog found after direct navigation, clicking "Continue on this browser"');
await launcherButton.click();
await this._page.waitForTimeout(2000);
}
} catch {
// No launcher - that's fine for authenticated joins
}
}
/** /**
* Handle the launcher dialog that asks how to join. * Handle the launcher dialog that asks how to join.
* Primary selector: button[data-tid="joinOnWeb"] (confirmed working in Recall.ai). * Primary selector: button[data-tid="joinOnWeb"] (confirmed working in Recall.ai).

View file

@ -168,25 +168,34 @@ export class BotOrchestrator {
this._setState('navigating'); this._setState('navigating');
// Navigate to meeting and handle launcher if (authenticate) {
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); // AUTHENTICATED JOIN: Navigate directly to the original meeting URL
// within the Teams v2 web app context. The launcher (/dl/launcher/) always
// redirects to the "light-meetings" anonymous experience regardless of auth.
// Instead, navigate directly to the original meeting URL -- Teams v2 will
// recognize the auth session and show the authenticated pre-join screen.
this._logger.info(`Authenticated: navigating directly to meeting URL: ${this._meetingUrl}`);
await this._page!.goto(this._meetingUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
// After navigation, check if Teams put us on the anonymous page despite auth // Teams may show the launcher dialog even for direct URLs -- handle it
if (authenticate && this._page) { await this._joinProcedure.handleLauncherIfPresent();
const currentUrl = this._page.url();
if (currentUrl.includes('anon=true') || currentUrl.includes('light-meetings/launch')) { // Wait for the pre-join page to stabilize
this._logger.warn(`Teams redirected to anonymous mode despite auth. URL: ${currentUrl}`); await this._page!.waitForTimeout(3000);
// Strip anon params and re-navigate
const cleanUrl = currentUrl // Verify we're on an authenticated page (no "Type your name" input)
.replace(/[&?]anon=true/gi, '') const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
.replace(/%26anon%3Dtrue/gi, ''); if (pageText.includes('Enter the name') || pageText.includes('Type your name')) {
this._logger.info(`Re-navigating without anon: ${cleanUrl}`); this._logger.warn('Still on anonymous page after auth navigation - auth session may not have transferred');
await this._page.goto(cleanUrl, { } else {
waitUntil: 'domcontentloaded', this._logger.info('On authenticated pre-join page (no name input required)');
timeout: 30000,
});
await this._page.waitForTimeout(3000);
} }
} else {
// ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params)
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
} }
// Set virtual background if configured (must be done on pre-join screen, before "Join now") // Set virtual background if configured (must be done on pre-join screen, before "Join now")