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:
parent
efa648d6fe
commit
a483aa6def
3 changed files with 82 additions and 19 deletions
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// After navigation, check if Teams put us on the anonymous page despite auth
|
// redirects to the "light-meetings" anonymous experience regardless of auth.
|
||||||
if (authenticate && this._page) {
|
// Instead, navigate directly to the original meeting URL -- Teams v2 will
|
||||||
const currentUrl = this._page.url();
|
// recognize the auth session and show the authenticated pre-join screen.
|
||||||
if (currentUrl.includes('anon=true') || currentUrl.includes('light-meetings/launch')) {
|
this._logger.info(`Authenticated: navigating directly to meeting URL: ${this._meetingUrl}`);
|
||||||
this._logger.warn(`Teams redirected to anonymous mode despite auth. URL: ${currentUrl}`);
|
await this._page!.goto(this._meetingUrl, {
|
||||||
// Strip anon params and re-navigate
|
|
||||||
const cleanUrl = currentUrl
|
|
||||||
.replace(/[&?]anon=true/gi, '')
|
|
||||||
.replace(/%26anon%3Dtrue/gi, '');
|
|
||||||
this._logger.info(`Re-navigating without anon: ${cleanUrl}`);
|
|
||||||
await this._page.goto(cleanUrl, {
|
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
await this._page.waitForTimeout(3000);
|
|
||||||
|
// Teams may show the launcher dialog even for direct URLs -- handle it
|
||||||
|
await this._joinProcedure.handleLauncherIfPresent();
|
||||||
|
|
||||||
|
// Wait for the pre-join page to stabilize
|
||||||
|
await this._page!.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Verify we're on an authenticated page (no "Type your name" input)
|
||||||
|
const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
|
||||||
|
if (pageText.includes('Enter the name') || pageText.includes('Type your name')) {
|
||||||
|
this._logger.warn('Still on anonymous page after auth navigation - auth session may not have transferred');
|
||||||
|
} else {
|
||||||
|
this._logger.info('On authenticated pre-join page (no name input required)');
|
||||||
}
|
}
|
||||||
|
} 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")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue