fix: chat queue to prevent interleaved text, retry+selectors for greeting

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-02-28 16:12:56 +01:00
parent ee2dcd61f1
commit dbcc53bed8
2 changed files with 94 additions and 48 deletions

View file

@ -408,50 +408,67 @@ export class ChatProcedure {
/**
* Send a chat message in the meeting.
* Finds the chat input, types the message, and sends it.
* Finds the chat input (with retry), types the message, and sends it.
*/
async sendChatMessage(text: string): Promise<boolean> {
this._logger.info(`Sending chat message: ${text.substring(0, 60)}...`);
try {
// Find chat input
const inputSelectors = [
'[data-tid="ckeditor-replyConversation"]',
'div[role="textbox"][data-tid*="chat"]',
'div[role="textbox"][aria-label*="message" i]',
'div[role="textbox"][aria-label*="Nachricht" i]',
'div[contenteditable="true"][data-tid*="message"]',
'div[contenteditable="true"][aria-label*="message" i]',
];
const inputSelectors = [
'[data-tid="ckeditor-replyConversation"]',
'div[role="textbox"][data-tid*="chat"]',
'div[role="textbox"][data-tid*="message"]',
'div[role="textbox"][aria-label*="message" i]',
'div[role="textbox"][aria-label*="Nachricht" i]',
'div[contenteditable="true"][data-tid*="message"]',
'div[contenteditable="true"][data-tid*="chat"]',
'div[contenteditable="true"][aria-label*="message" i]',
'[aria-label*="Type a new message" i]',
'[aria-label*="Neue Nachricht eingeben" i]',
];
let input: any = null;
for (const selector of inputSelectors) {
input = await this._page.$(selector);
if (input) break;
const maxAttempts = 5;
const retryDelayMs = 600;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
let input: any = null;
for (const selector of inputSelectors) {
input = await this._page.$(selector);
if (input) {
const isVisible = await input.isVisible().catch(() => false);
if (isVisible) break;
await input.dispose();
input = null;
}
}
if (input) {
await input.click();
await this._page.waitForTimeout(200);
await this._page.keyboard.type(text, { delay: 10 });
await this._page.waitForTimeout(200);
await this._page.keyboard.press('Enter');
this._logger.info('Chat message sent');
return true;
}
if (attempt < maxAttempts) {
this._logger.info(`Chat input not found, retry ${attempt}/${maxAttempts} in ${retryDelayMs}ms`);
await this._page.waitForTimeout(retryDelayMs);
}
} catch (error) {
this._logger.error(`Error sending chat message (attempt ${attempt}):`, error);
if (attempt < maxAttempts) {
await this._page.waitForTimeout(retryDelayMs);
} else {
return false;
}
}
if (!input) {
this._logger.warn('Could not find chat input field');
return false;
}
// Click to focus
await input.click();
await this._page.waitForTimeout(200);
// Type the message
await this._page.keyboard.type(text, { delay: 10 });
await this._page.waitForTimeout(200);
// Press Enter to send
await this._page.keyboard.press('Enter');
this._logger.info('Chat message sent');
return true;
} catch (error) {
this._logger.error('Error sending chat message:', error);
return false;
}
this._logger.warn('Could not find chat input field');
return false;
}
/**

View file

@ -72,6 +72,8 @@ export class BotOrchestrator {
private _isShuttingDown: boolean = false;
private _isDebugMode: boolean = false;
private _keepAliveInterval: NodeJS.Timeout | null = null;
private _chatMessageQueue: string[] = [];
private _chatQueueProcessing: boolean = false;
constructor(
sessionId: string,
@ -699,10 +701,8 @@ export class BotOrchestrator {
case 'sendChatMessage':
const chatMsg = message as SendChatMessage;
this._logger.info(`Gateway sendChatMessage received: ${chatMsg.text?.substring(0, 60)}...`);
try {
await this.sendChatMessageToMeeting(chatMsg.text);
} catch (chatErr) {
this._logger.error(`Failed to send chat message to meeting: ${chatErr}`);
if (chatMsg.text) {
this._enqueueChatMessage(chatMsg.text);
}
break;
@ -1234,7 +1234,10 @@ export class BotOrchestrator {
this._logger.info(`Sending join greeting (chat + voice): ${greeting}`);
// Chat greeting
// Brief delay so chat input is ready after panel open (Teams DOM can lag)
await new Promise((r) => setTimeout(r, 800));
// Chat greeting (queued; retries if input not found)
await this.sendChatMessageToMeeting(greeting);
// Voice greeting — ask Gateway to generate TTS and send back playAudio
@ -1294,14 +1297,40 @@ export class BotOrchestrator {
}
/**
* Send a text message to the meeting chat.
* Enqueue a chat message for serialized sending (prevents interleaved text when
* multiple messages arrive in quick succession).
*/
private _enqueueChatMessage(text: string): void {
this._chatMessageQueue.push(text);
this._processChatQueue();
}
/**
* Process chat queue one message at a time to avoid interleaved typing.
*/
private async _processChatQueue(): Promise<void> {
if (this._chatQueueProcessing || this._chatMessageQueue.length === 0) return;
this._chatQueueProcessing = true;
while (this._chatMessageQueue.length > 0) {
const text = this._chatMessageQueue.shift()!;
if (this._isShuttingDown || this._state !== 'in_meeting' || !this._chatProcedure) {
this._logger.warn('Cannot send chat message - not in meeting, skipping queued message');
continue;
}
try {
await this._chatProcedure.sendChatMessage(text);
} catch (chatErr) {
this._logger.error(`Failed to send chat message to meeting: ${chatErr}`);
}
}
this._chatQueueProcessing = false;
}
/**
* Send a text message to the meeting chat (enqueued for serialized delivery).
*/
async sendChatMessageToMeeting(text: string): Promise<void> {
if (this._isShuttingDown || this._state !== 'in_meeting' || !this._chatProcedure) {
this._logger.warn('Cannot send chat message - not in meeting');
return;
}
await this._chatProcedure.sendChatMessage(text);
this._enqueueChatMessage(text);
}
/**