chat: robustere Panel-Erkennung, erweiterte Input-Selektoren, DOM-Diagnostik, periodische Debug-Screenshots
Made-with: Cursor
This commit is contained in:
parent
b552ccd547
commit
92f96bbc3e
2 changed files with 135 additions and 38 deletions
|
|
@ -58,33 +58,64 @@ export class ChatProcedure {
|
|||
this._logger.info('Chat panel opened');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the chat panel is currently visible by probing for known
|
||||
* UI elements (chat input, message list, or aria-pressed toggle).
|
||||
*/
|
||||
private async _isChatPanelOpen(): Promise<boolean> {
|
||||
return this._page.evaluate(() => {
|
||||
// 1. Chat button toggle state
|
||||
const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null;
|
||||
if (chatBtn?.getAttribute('aria-pressed') === 'true') return true;
|
||||
|
||||
// 2. Chat input visible (multiple Teams UI variants)
|
||||
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]',
|
||||
'[contenteditable="true"][aria-label*="message" i]',
|
||||
'[contenteditable="true"][aria-label*="Nachricht" i]',
|
||||
'[placeholder*="message" i]',
|
||||
'[placeholder*="Nachricht" i]',
|
||||
'div[aria-label="Type a message"]',
|
||||
];
|
||||
for (const sel of inputSelectors) {
|
||||
const el = document.querySelector(sel) as HTMLElement | null;
|
||||
if (el && el.offsetHeight > 0) return true;
|
||||
}
|
||||
|
||||
// 3. Message list container visible
|
||||
const listSelectors = [
|
||||
'[data-tid="message-pane-list"]',
|
||||
'[data-tid="chat-pane-list"]',
|
||||
'[data-tid="chat-pane"]',
|
||||
'.ts-message-list-container',
|
||||
'[role="log"]',
|
||||
];
|
||||
for (const sel of listSelectors) {
|
||||
const el = document.querySelector(sel) as HTMLElement | null;
|
||||
if (el && el.offsetHeight > 50) return true;
|
||||
}
|
||||
|
||||
// 4. "Meeting chat" heading visible
|
||||
const headings = document.querySelectorAll('h2, h3, [role="heading"]');
|
||||
for (const h of Array.from(headings)) {
|
||||
const txt = (h as HTMLElement).innerText?.toLowerCase() || '';
|
||||
if (txt.includes('meeting chat') || txt.includes('besprechungschat')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the chat panel by clicking the chat button.
|
||||
* In authenticated Teams, the chat panel may already be open (meeting loads
|
||||
* from a chat thread). Clicking again would TOGGLE it closed.
|
||||
*/
|
||||
private async _openChatPanel(): Promise<void> {
|
||||
// Check if chat panel is already open before clicking (avoid toggle-off)
|
||||
const alreadyOpen = await this._page.evaluate(() => {
|
||||
const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null;
|
||||
if (chatBtn) {
|
||||
// Teams toggle buttons use aria-pressed="true" when active
|
||||
if (chatBtn.getAttribute('aria-pressed') === 'true') return true;
|
||||
}
|
||||
// Also check if chat input or message list is visible (panel is open)
|
||||
const chatInput = document.querySelector(
|
||||
'[data-tid="ckeditor-replyConversation"], div[role="textbox"][data-tid*="chat"], div[role="textbox"][aria-label*="message" i]'
|
||||
) as HTMLElement | null;
|
||||
if (chatInput && chatInput.offsetHeight > 0) return true;
|
||||
// Check if message list container is visible
|
||||
const messageList = document.querySelector(
|
||||
'[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]'
|
||||
) as HTMLElement | null;
|
||||
if (messageList && messageList.offsetHeight > 50) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (alreadyOpen) {
|
||||
if (await this._isChatPanelOpen()) {
|
||||
this._logger.info('Chat panel already open - skipping toggle');
|
||||
return;
|
||||
}
|
||||
|
|
@ -369,27 +400,23 @@ export class ChatProcedure {
|
|||
*/
|
||||
private _startPeriodicChatScan(): void {
|
||||
if (this._scanIntervalId) return;
|
||||
const intervalMs = 2500;
|
||||
const intervalMs = 5000;
|
||||
this._scanIntervalId = setInterval(async () => {
|
||||
if (!this._isSubscribed || !this._page) return;
|
||||
try {
|
||||
const chatPanelOpen = await this._page.evaluate(() => {
|
||||
const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null;
|
||||
if (chatBtn?.getAttribute('aria-pressed') === 'true') return true;
|
||||
const messageList = document.querySelector('[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]') as HTMLElement | null;
|
||||
return !!(messageList && messageList.offsetHeight > 50);
|
||||
});
|
||||
if (!chatPanelOpen) {
|
||||
if (!(await this._isChatPanelOpen())) {
|
||||
this._logger.info('Chat panel closed, reopening');
|
||||
await this._openChatPanel();
|
||||
await this._page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
const knownKeys = Array.from(this._recentMessageKeyTimestamps.keys());
|
||||
const messages = await this._page.evaluate((knownKeysArr: string[]) => {
|
||||
const scanResult = await this._page.evaluate((knownKeysArr: string[]) => {
|
||||
const known = new Set(knownKeysArr);
|
||||
const noisePatterns = [
|
||||
'meeting ended', 'meeting started', 'was invited', 'left the chat',
|
||||
'joined the meeting', 'left the meeting', 'doesn\'t have a teams account',
|
||||
'verify their identity',
|
||||
];
|
||||
function isNoise(t: string) {
|
||||
const l = t.toLowerCase();
|
||||
|
|
@ -397,15 +424,45 @@ export class ChatProcedure {
|
|||
}
|
||||
const results: Array<{ speaker: string; text: string; timestamp: string; teamsTimestamp?: string; messageKey: string }> = [];
|
||||
const seenThisScan = new Set<string>();
|
||||
const container = document.querySelector('[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]') || document.body;
|
||||
const candidates = container.querySelectorAll('[data-tid="chat-message"], .fui-ChatMessage, [data-tid*="chat-pane-message"]');
|
||||
|
||||
// Strategy 1: known selectors
|
||||
const containerSelectors = [
|
||||
'[data-tid="message-pane-list"]', '[data-tid="chat-pane-list"]',
|
||||
'[data-tid="chat-pane"]', '[role="log"]', '.ts-message-list-container',
|
||||
];
|
||||
let container: Element | null = null;
|
||||
for (const sel of containerSelectors) {
|
||||
container = document.querySelector(sel);
|
||||
if (container) break;
|
||||
}
|
||||
|
||||
const messageSelectors = [
|
||||
'[data-tid="chat-message"]', '.fui-ChatMessage',
|
||||
'[data-tid*="chat-pane-message"]', '[data-tid*="message-body"]',
|
||||
];
|
||||
const target = container || document.body;
|
||||
const candidates = target.querySelectorAll(messageSelectors.join(', '));
|
||||
|
||||
for (const el of Array.from(candidates) as HTMLElement[]) {
|
||||
const messageEl = el.closest?.('[data-tid="chat-message"], .fui-ChatMessage') || el;
|
||||
let author = 'Unknown';
|
||||
const authorEl = messageEl.querySelector('[data-tid="message-author"], [data-tid="message-author-name"], .fui-ChatMessage__author, [data-tid*="author"]');
|
||||
if (authorEl?.textContent) author = authorEl.textContent.trim();
|
||||
const bodyEl = messageEl.querySelector('[data-tid="message-body"], .fui-ChatMessage__body, [data-tid*="message-body"]');
|
||||
const text = (bodyEl as HTMLElement)?.innerText?.trim() || '';
|
||||
const authorSels = [
|
||||
'[data-tid="message-author"]', '[data-tid="message-author-name"]',
|
||||
'.fui-ChatMessage__author', '[data-tid*="author"]',
|
||||
];
|
||||
for (const sel of authorSels) {
|
||||
const authorEl = messageEl.querySelector(sel) || el.querySelector(sel);
|
||||
if (authorEl?.textContent) { author = authorEl.textContent.trim(); break; }
|
||||
}
|
||||
const bodySels = [
|
||||
'[data-tid="message-body"]', '.fui-ChatMessage__body',
|
||||
'[data-tid="chat-message-text"]', '[data-tid*="message-body"]',
|
||||
];
|
||||
let text = '';
|
||||
for (const sel of bodySels) {
|
||||
const bodyEl = messageEl.querySelector(sel) || el.querySelector(sel);
|
||||
if (bodyEl) { text = (bodyEl as HTMLElement).innerText?.trim() || ''; break; }
|
||||
}
|
||||
if (!text || text.length < 2 || isNoise(text)) continue;
|
||||
const key = `${author}::${text}`;
|
||||
if (known.has(key) || seenThisScan.has(key)) continue;
|
||||
|
|
@ -414,9 +471,35 @@ export class ChatProcedure {
|
|||
const ts = timeEl?.getAttribute?.('datetime') || new Date().toISOString();
|
||||
results.push({ speaker: author, text, timestamp: ts, teamsTimestamp: ts, messageKey: key });
|
||||
}
|
||||
return results;
|
||||
|
||||
// Diagnostics (once per 20s, controlled by caller)
|
||||
let diag: string | undefined;
|
||||
if ((window as any).__chatScanDiagCounter === undefined) (window as any).__chatScanDiagCounter = 0;
|
||||
(window as any).__chatScanDiagCounter++;
|
||||
if ((window as any).__chatScanDiagCounter % 4 === 1) {
|
||||
const info: string[] = [];
|
||||
info.push(`container=${container?.tagName || 'body'}[${container?.getAttribute?.('data-tid') || ''}]`);
|
||||
info.push(`candidates=${candidates.length}`);
|
||||
// Dump all chat-ish elements for debugging
|
||||
const allChat = document.querySelectorAll('[data-tid*="chat"], [data-tid*="message"], [role="log"], .fui-Chat');
|
||||
const tags: string[] = [];
|
||||
for (const c of Array.from(allChat).slice(0, 15)) {
|
||||
const tid = c.getAttribute('data-tid') || '';
|
||||
const cls = c.className?.toString?.()?.substring(0, 40) || '';
|
||||
const h = (c as HTMLElement).offsetHeight || 0;
|
||||
tags.push(`<${c.tagName} tid="${tid}" cls="${cls}" h=${h}>`);
|
||||
}
|
||||
info.push(`domElements=[${tags.join(', ')}]`);
|
||||
diag = info.join(' | ');
|
||||
}
|
||||
|
||||
return { messages: results, diag };
|
||||
}, knownKeys);
|
||||
for (const msg of messages) {
|
||||
|
||||
if (scanResult.diag) {
|
||||
this._logger.info(`[ChatScan] ${scanResult.diag}`);
|
||||
}
|
||||
for (const msg of scanResult.messages) {
|
||||
this._handleChatMessage(msg);
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -490,7 +573,11 @@ export class ChatProcedure {
|
|||
'div[contenteditable="true"][data-tid*="chat"]',
|
||||
'div[contenteditable="true"][aria-label*="message" i]',
|
||||
'[aria-label*="Type a new message" i]',
|
||||
'[aria-label="Type a message"]',
|
||||
'[aria-label*="Neue Nachricht eingeben" i]',
|
||||
'[placeholder*="Type a message" i]',
|
||||
'[placeholder*="Nachricht" i]',
|
||||
'div[contenteditable="true"][role="textbox"]',
|
||||
];
|
||||
|
||||
const maxAttempts = 5;
|
||||
|
|
|
|||
|
|
@ -589,12 +589,17 @@ export class BotOrchestrator {
|
|||
* a WebSocket ping. Prevents Teams from detecting the bot as idle
|
||||
* and kicking it from the meeting.
|
||||
*/
|
||||
private _keepAliveCounter: number = 0;
|
||||
|
||||
private _startKeepAlive(): void {
|
||||
if (this._keepAliveInterval) return;
|
||||
|
||||
this._keepAliveCounter = 0;
|
||||
this._keepAliveInterval = setInterval(async () => {
|
||||
if (this._isShuttingDown || !this._page) return;
|
||||
|
||||
this._keepAliveCounter++;
|
||||
|
||||
try {
|
||||
// Small random mouse movement to simulate user activity
|
||||
const x = 640 + Math.floor(Math.random() * 20 - 10);
|
||||
|
|
@ -612,6 +617,11 @@ export class BotOrchestrator {
|
|||
// Connection might be closing
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic debug screenshot every ~45s (every 3rd keepalive cycle)
|
||||
if (this._isDebugMode && this._keepAliveCounter % 3 === 0) {
|
||||
await this._takeScreenshot(`debug-periodic-${this._keepAliveCounter}`);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
this._logger.info('Keepalive started (15s interval)');
|
||||
|
|
|
|||
Loading…
Reference in a new issue