feat: State-Info nach Klick/Screenshot, kein extra Warten
- browser_computer left_click: afterState (url, urlChanged, readyState, activeTag, activeLabel, dialogText) direkt im Ergebnis — kein sleep, kein Polling, kein Blockieren. - browser_computer screenshot: page (url, title, readyState, activeTag) im Ergebnis — ein einziger evaluateInMainWorld-Call. - browser_click (semantisch): snapshotAfterClick() nach Klick — gleiche Felder, gleiche Logik, kein extra Warten. - browser_navigate: gibt loading:true + hint zurück statt zu blockieren. Die KI entscheidet selbst ob sie wartet. - Keine adaptive Pace, kein waitForTabLoad, kein Polling — das war der Grund warum alles eingefroren ist.
This commit is contained in:
@@ -574,6 +574,19 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
|
||||
s.imgH = Number.isFinite(shot.height) && shot.height > 0 ? shot.height : 0;
|
||||
|
||||
const [cx, cy] = toModel(s, s.x, s.y);
|
||||
|
||||
// Seitenstatus mitliefern — das Modell sieht sofort ob die Seite noch lädt
|
||||
// und kann selbst entscheiden ob es warten will. Kein extra Warten hier.
|
||||
let page: Record<string, unknown> | undefined;
|
||||
try {
|
||||
page = await evaluateInMainWorld(tabId, `({
|
||||
url: location.href,
|
||||
title: document.title,
|
||||
readyState: document.readyState,
|
||||
activeTag: (document.activeElement||{}).tagName?.toLowerCase() || null,
|
||||
})`, { returnByValue: true }) as Record<string, unknown>;
|
||||
} catch { /* chrome:// etc — egal */ }
|
||||
|
||||
return {
|
||||
action,
|
||||
image: shot.dataUrl,
|
||||
@@ -582,6 +595,7 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
|
||||
scale: s.scale,
|
||||
fullPage,
|
||||
cursor: { x: cx, y: cy },
|
||||
...(page ? { page } : {}),
|
||||
_method: 'cdp',
|
||||
};
|
||||
}
|
||||
@@ -633,7 +647,25 @@ async function execute(tabId: number, action: string, input: Record<string, unkn
|
||||
s.x = p.x;
|
||||
s.y = p.y;
|
||||
await paintCursor(tabId, p.x, p.y, clicks > 1 ? 'double' : button === 'right' ? 'right' : 'left');
|
||||
return mouseResult(action, s, p, { button, clickCount: clicks, modifiers });
|
||||
|
||||
// State nach Klick abfragen — kein extra Warten, nur schnell schauen was
|
||||
// sich geändert hat. Das Modell entscheidet selbst ob es einen Screenshot
|
||||
// braucht oder ob die Info hier reicht.
|
||||
const result = mouseResult(action, s, p, { button, clickCount: clicks, modifiers });
|
||||
try {
|
||||
const post = await evaluateInMainWorld(tabId, `({
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
activeTag: (document.activeElement||{}).tagName?.toLowerCase() || null,
|
||||
activeLabel: document.activeElement?.getAttribute?.('aria-label')
|
||||
|| document.activeElement?.getAttribute?.('placeholder') || null,
|
||||
hasDialog: !!(document.querySelector('dialog[open],[role="dialog"],[role="alertdialog"]')),
|
||||
})`, { returnByValue: true }) as Record<string, unknown>;
|
||||
if (post) result.afterState = post;
|
||||
} catch { /* Seite navigiert — das ist selbst ein Signal */
|
||||
result.afterState = { navigating: true };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'left_mouse_down':
|
||||
|
||||
+37
-6
@@ -420,6 +420,10 @@ async function wantsTrusted(tabId: number): Promise<boolean> {
|
||||
async function clickElement(tabId: number, selector: string, forceTrusted: boolean): Promise<any> {
|
||||
const trusted = forceTrusted || await wantsTrusted(tabId);
|
||||
|
||||
// URL vor dem Klick merken — für den State-Vergleich danach
|
||||
let preUrl = '';
|
||||
try { preUrl = await execInTab(tabId, () => location.href); } catch { /* ok */ }
|
||||
|
||||
if (trusted) {
|
||||
const box = await elementBox(tabId, selector);
|
||||
if (!box) throw new ToolError('ELEMENT_NOT_FOUND', `Element nicht gefunden oder unsichtbar: ${selector}`, true);
|
||||
@@ -427,9 +431,9 @@ async function clickElement(tabId: number, selector: string, forceTrusted: boole
|
||||
await cdp.attach(tabId);
|
||||
await cdp.mouseClick(tabId, box.x, box.y);
|
||||
notifyCursor(tabId, box.x, box.y, 'left');
|
||||
return { clicked: selector, tag: box.tag, text: box.text, _method: 'cdp' };
|
||||
const after = await snapshotAfterClick(tabId, preUrl);
|
||||
return { clicked: selector, tag: box.tag, text: box.text, ...after, _method: 'cdp' };
|
||||
} catch (e: any) {
|
||||
// Kein harter Fehler: lieber synthetisch klicken als gar nicht handeln.
|
||||
console.warn('[Nexus] CDP-Klick fehlgeschlagen, weiche auf synthetisch aus:', e?.message);
|
||||
}
|
||||
}
|
||||
@@ -458,7 +462,6 @@ async function clickElement(tabId: number, selector: string, forceTrusted: boole
|
||||
if (!el) return { error: 'NOT_FOUND' };
|
||||
el.scrollIntoView({ block: 'center', behavior: 'instant' as ScrollBehavior });
|
||||
|
||||
// Vollständige Maus-Event-Sequenz für maximale Kompatibilität
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
@@ -483,7 +486,35 @@ async function clickElement(tabId: number, selector: string, forceTrusted: boole
|
||||
if (!result || (result as any).error === 'NOT_FOUND') {
|
||||
throw new ToolError('ELEMENT_NOT_FOUND', `Element nicht gefunden: ${selector}`, true);
|
||||
}
|
||||
return { ...result, _method: 'synthetic' };
|
||||
const after = await snapshotAfterClick(tabId, preUrl);
|
||||
return { ...result, ...after, _method: 'synthetic' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest nach einem Klick schnell den Seitenstatus — ohne extra Warten.
|
||||
* Gibt dem Modell: hat sich die URL geändert? Lädt die Seite noch?
|
||||
* Welches Element hat jetzt Fokus? Ist ein Dialog aufgegangen?
|
||||
* Das Modell entscheidet selbst ob es einen Screenshot braucht.
|
||||
*/
|
||||
async function snapshotAfterClick(tabId: number, preUrl: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const s = await execInTab(tabId, (before: string) => {
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
const dialog = document.querySelector('dialog[open],[role="dialog"],[role="alertdialog"]') as HTMLElement | null;
|
||||
return {
|
||||
url: location.href,
|
||||
urlChanged: location.href !== before,
|
||||
readyState: document.readyState,
|
||||
activeTag: active?.tagName?.toLowerCase() || null,
|
||||
activeLabel: active?.getAttribute('aria-label') || active?.getAttribute('placeholder') || null,
|
||||
dialogText: dialog ? dialog.innerText.slice(0, 120) : null,
|
||||
};
|
||||
}, [preUrl]);
|
||||
return { afterState: s };
|
||||
} catch {
|
||||
// execInTab wirft wenn die Seite gerade navigiert — das ist selbst ein Signal
|
||||
return { afterState: { navigating: true } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -726,11 +757,11 @@ async function dispatchTool(name: string, input: Record<string, unknown>): Promi
|
||||
const newTab = await chrome.tabs.create({ url, active: false });
|
||||
targetTabId = newTab.id!;
|
||||
notifyTargetTab();
|
||||
return { tabId: newTab.id, url, _method: 'synthetic' };
|
||||
return { tabId: newTab.id, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' };
|
||||
}
|
||||
await chrome.tabs.update(tabId!, { url });
|
||||
notifyTargetTab();
|
||||
return { tabId, url, _method: 'synthetic' };
|
||||
return { tabId, url, loading: true, hint: 'Seite lädt — browser_wait oder screenshot nach kurzer Pause.', _method: 'synthetic' };
|
||||
}
|
||||
|
||||
case 'browser_go_back':
|
||||
|
||||
Reference in New Issue
Block a user