mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(google-meet): reuse create tabs on retry
This commit is contained in:
@@ -96,7 +96,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Group chats/silent replies: tighten `NO_REPLY` prompt guidance so groups stay quiet without narrating silence or emitting fallback chatter when silence is the intended outcome. (#70954, #71209) Thanks @Takhoffman.
|
||||
- WhatsApp/groups+direct: setting `systemPrompt: ""` on a specific `groups.<id>` or `direct.<peerId>` entry now suppresses the wildcard system prompt instead of falling through to it, so users can silence the global prompt for a specific group or peer. (#70381) Thanks @Bluetegu.
|
||||
- Browser/tool: tell agents not to pass per-call `timeoutMs` on existing-session type, evaluate, and other Chrome MCP actions that reject timeout overrides. Thanks @steipete.
|
||||
- Plugins/Google Meet: use browser automation to classify and clear Meet's microphone-choice interstitial during browser meeting creation instead of reporting a false Google login failure. Thanks @steipete.
|
||||
- Browser/tool: use Playwright's current AI aria snapshot API for `refs="aria"` and fall back to role refs when a node browser cannot provide aria refs, so agents can still inspect and click controls such as Google Meet admission buttons. Thanks @steipete.
|
||||
- Plugins/Google Meet: use browser automation to classify and clear Meet's microphone-choice interstitial during browser meeting creation, and reuse in-progress create tabs on retry instead of opening duplicates. Thanks @steipete.
|
||||
- Codex/GPT-5.4: harden fallback, auth-profile, tool-schema, and replay edge cases across native and embedded runtime paths. (#70743) Thanks @100yenadmin.
|
||||
- Models/fallback: resolve bare fallback model provider ids before model switching, so configured fallback chains keep working when a fallback is named without an explicit provider prefix. Thanks @steipete.
|
||||
- Voice-call/Telnyx: preserve inbound/outbound callback metadata and read transcription text from Telnyx's current `transcription_data` payload. Thanks @steipete.
|
||||
|
||||
@@ -305,7 +305,9 @@ Common failure checks:
|
||||
config points at the profile you want, for example
|
||||
`browser.defaultProfile: "user"` or a named existing-session profile.
|
||||
- Duplicate Meet tabs: leave `chrome.reuseExistingTab: true` enabled. OpenClaw
|
||||
activates an existing tab for the same Meet URL before opening a new one.
|
||||
activates an existing tab for the same Meet URL before opening a new one, and
|
||||
browser meeting creation reuses an in-progress `https://meet.google.com/new`
|
||||
or Google account prompt tab before opening another one.
|
||||
- No audio: in Meet, route microphone/speaker through the virtual audio device
|
||||
path used by OpenClaw; use separate virtual devices or Loopback-style routing
|
||||
for clean duplex audio.
|
||||
@@ -820,6 +822,9 @@ to the pinned Chrome node browser. Confirm:
|
||||
`googlemeet.chrome`.
|
||||
- For browser fallback: the OpenClaw Chrome profile on that node is signed in
|
||||
to Google and can open `https://meet.google.com/new`.
|
||||
- For browser fallback: retries reuse an existing `https://meet.google.com/new`
|
||||
or Google account prompt tab before opening a new tab. If an agent times out,
|
||||
retry the tool call rather than manually opening another Meet tab.
|
||||
- For browser fallback: if Meet shows "Do you want people to hear you in the
|
||||
meeting?", leave the tab open. OpenClaw should click **Use microphone** or, for
|
||||
create-only fallback, **Continue without microphone** through browser
|
||||
|
||||
@@ -820,6 +820,9 @@ describe("google-meet plugin", () => {
|
||||
{
|
||||
nodesInvokeHandler: async (params) => {
|
||||
const proxy = params.params as { path?: string; body?: { url?: string } };
|
||||
if (proxy.path === "/tabs") {
|
||||
return { payload: { result: { tabs: [] } } };
|
||||
}
|
||||
if (proxy.path === "/tabs/open") {
|
||||
return {
|
||||
payload: {
|
||||
@@ -877,6 +880,83 @@ describe("google-meet plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reuses an existing browser create tab instead of opening duplicates", async () => {
|
||||
const { methods, nodesInvoke } = setup(
|
||||
{
|
||||
defaultTransport: "chrome-node",
|
||||
chromeNode: { node: "parallels-macos" },
|
||||
},
|
||||
{
|
||||
nodesInvokeHandler: async (params) => {
|
||||
const proxy = params.params as { path?: string; body?: { targetId?: string } };
|
||||
if (proxy.path === "/tabs") {
|
||||
return {
|
||||
payload: {
|
||||
result: {
|
||||
tabs: [
|
||||
{
|
||||
targetId: "existing-create-tab",
|
||||
title: "Meet",
|
||||
url: "https://meet.google.com/new",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (proxy.path === "/tabs/focus") {
|
||||
return { payload: { result: { ok: true } } };
|
||||
}
|
||||
if (proxy.path === "/act") {
|
||||
return {
|
||||
payload: {
|
||||
result: {
|
||||
ok: true,
|
||||
targetId: proxy.body?.targetId ?? "existing-create-tab",
|
||||
result: {
|
||||
meetingUri: "https://meet.google.com/reu-sedx-tab",
|
||||
browserUrl: "https://meet.google.com/reu-sedx-tab",
|
||||
browserTitle: "Meet",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected browser proxy path ${proxy.path}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
const handler = methods.get("googlemeet.create") as
|
||||
| ((ctx: {
|
||||
params: Record<string, unknown>;
|
||||
respond: ReturnType<typeof vi.fn>;
|
||||
}) => Promise<void>)
|
||||
| undefined;
|
||||
const respond = vi.fn();
|
||||
|
||||
await handler?.({ params: {}, respond });
|
||||
|
||||
expect(respond.mock.calls[0]?.[0]).toBe(true);
|
||||
expect(respond.mock.calls[0]?.[1]).toMatchObject({
|
||||
source: "browser",
|
||||
meetingUri: "https://meet.google.com/reu-sedx-tab",
|
||||
browser: { nodeId: "node-1", targetId: "existing-create-tab" },
|
||||
});
|
||||
expect(nodesInvoke).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
path: "/tabs/focus",
|
||||
body: { targetId: "existing-create-tab" },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(nodesInvoke).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({ path: "/tabs/open" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Use microphone", "Accepted Meet microphone prompt with browser automation."],
|
||||
[
|
||||
|
||||
@@ -231,6 +231,12 @@ type BrowserTab = {
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const GOOGLE_MEET_NEW_URL = "https://meet.google.com/new";
|
||||
const GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS = 60_000;
|
||||
const GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS = 10_000;
|
||||
const GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS = 1_000;
|
||||
const GOOGLE_MEET_BROWSER_POLL_MS = 500;
|
||||
|
||||
type BrowserCreateStepResult = {
|
||||
meetingUri?: string;
|
||||
browserUrl?: string;
|
||||
@@ -324,6 +330,50 @@ function readBrowserTab(result: unknown): BrowserTab | undefined {
|
||||
return result && typeof result === "object" ? (result as BrowserTab) : undefined;
|
||||
}
|
||||
|
||||
function isGoogleMeetCreateTab(tab: BrowserTab): boolean {
|
||||
const url = tab.url ?? "";
|
||||
if (/^https:\/\/meet\.google\.com\/(?:new|[a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.test(url)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
url.startsWith("https://accounts.google.com/") &&
|
||||
/sign in|google accounts|meet/i.test(tab.title ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
async function findGoogleMeetCreateTab(params: {
|
||||
runtime: PluginRuntime;
|
||||
nodeId: string;
|
||||
timeoutMs: number;
|
||||
}): Promise<BrowserTab | undefined> {
|
||||
const tabs = asBrowserTabs(
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
method: "GET",
|
||||
path: "/tabs",
|
||||
timeoutMs: params.timeoutMs,
|
||||
}),
|
||||
);
|
||||
return tabs.find(isGoogleMeetCreateTab);
|
||||
}
|
||||
|
||||
async function focusBrowserTab(params: {
|
||||
runtime: PluginRuntime;
|
||||
nodeId: string;
|
||||
targetId: string;
|
||||
timeoutMs: number;
|
||||
}): Promise<void> {
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId: params.nodeId,
|
||||
method: "POST",
|
||||
path: "/tabs/focus",
|
||||
body: { targetId: params.targetId },
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
function readStringArray(value: unknown): string[] | undefined {
|
||||
return Array.isArray(value)
|
||||
? value.filter((entry): entry is string => typeof entry === "string")
|
||||
@@ -454,17 +504,35 @@ export async function createMeetWithBrowserProxyOnNode(params: {
|
||||
runtime: params.runtime,
|
||||
requestedNode: params.config.chromeNode.node,
|
||||
});
|
||||
const timeoutMs = Math.max(15_000, params.config.chrome.joinTimeoutMs);
|
||||
const tab = readBrowserTab(
|
||||
await callBrowserProxyOnNode({
|
||||
const timeoutMs = Math.max(
|
||||
GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS,
|
||||
params.config.chrome.joinTimeoutMs,
|
||||
);
|
||||
const stepTimeoutMs = Math.min(timeoutMs, GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS);
|
||||
let tab = await findGoogleMeetCreateTab({
|
||||
runtime: params.runtime,
|
||||
nodeId,
|
||||
timeoutMs: stepTimeoutMs,
|
||||
});
|
||||
if (tab?.targetId) {
|
||||
await focusBrowserTab({
|
||||
runtime: params.runtime,
|
||||
nodeId,
|
||||
method: "POST",
|
||||
path: "/tabs/open",
|
||||
body: { url: "https://meet.google.com/new" },
|
||||
timeoutMs,
|
||||
}),
|
||||
);
|
||||
targetId: tab.targetId,
|
||||
timeoutMs: stepTimeoutMs,
|
||||
});
|
||||
} else {
|
||||
tab = readBrowserTab(
|
||||
await callBrowserProxyOnNode({
|
||||
runtime: params.runtime,
|
||||
nodeId,
|
||||
method: "POST",
|
||||
path: "/tabs/open",
|
||||
body: { url: GOOGLE_MEET_NEW_URL },
|
||||
timeoutMs: stepTimeoutMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const targetId = tab?.targetId;
|
||||
if (!targetId) {
|
||||
throw new Error("Browser fallback opened Google Meet but did not return a targetId.");
|
||||
@@ -485,7 +553,7 @@ export async function createMeetWithBrowserProxyOnNode(params: {
|
||||
targetId,
|
||||
fn: CREATE_MEET_FROM_BROWSER_SCRIPT,
|
||||
},
|
||||
timeoutMs: Math.min(timeoutMs, 10_000),
|
||||
timeoutMs: stepTimeoutMs,
|
||||
});
|
||||
const result = readBrowserCreateResult(evaluated);
|
||||
lastResult = result;
|
||||
@@ -509,13 +577,13 @@ export async function createMeetWithBrowserProxyOnNode(params: {
|
||||
}
|
||||
throw new Error(result.manualAction);
|
||||
}
|
||||
await sleep(result.retryAfterMs ?? 500);
|
||||
await sleep(result.retryAfterMs ?? GOOGLE_MEET_BROWSER_POLL_MS);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!isBrowserNavigationInterruption(error)) {
|
||||
throw error;
|
||||
}
|
||||
await sleep(1_000);
|
||||
await sleep(GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user